diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 971c8964..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,1386 +0,0 @@ -# Brandolini’s Reference - -> *A little learning is a dang’rous thing;* -> -> *Drink deep, or taste not the Pierian spring.* -> -> *There shallow draughts intoxicate the brain,* -> -> *And drinking largely sobers us again.* -> -> -> - Alexander Pope, 1711 -> - -This chapter serves as a helpful reference for developers, users and community members living in an Age of Bullshit Manufacture equivalent to the Cambrian Explosion. - -As we are surely only at the on-ramp of this trajectory, the chapter will continue to evolve, expand and be updated based on community feedback as events occur. - -## What The Reticulum License Is - -*To understand the legal reality relating to this section, see* [Void Grants](#void-grants). - -The [Reticulum License](./LICENSE) is the license under which RNS, the Reticulum reference implementation, has been published since **April 15, 2025**, in versioning effective from **0.9.4** and every release since. In practical terms, it covers all of “modern Reticulum”: The current wire format, the cryptographic machinery as it exists today (including the AES-256-based link encryption, ratchets, et cetera), and all protocol additions that distinguish the modern implementation from the earliest pre-1.0.0 beta formats. If an implementation speaks today’s Reticulum protocol, it is, in all relevant respects, built on Reticulum-licensed work. - -The license is a permissive license. It grants the full range of rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, free of charge; subject to three conditions, which are reproduced here verbatim: - -> * The Software shall not be used in any kind of system which includes amongst its -> functions the ability to purposefully do harm to human beings. -> * The Software shall not be used, directly or indirectly, in the creation of an artificial -> intelligence, machine learning or language model training dataset, including but not -> limited to any use that contributes to the training or development of such a model or -> algorithm. -> * The above copyright notice and this permission notice shall be included in all copies or -> substantial portions of the Software. - -### Why This License Exists - -Reticulum was not moved to a custom license casually or quickly. It was licensed under the MIT license for the greater part of its existence, and that served well for a long time. The change came when it became clear that no existing license expressed what the project needed, and that the two most commonly proposed alternatives would either say the wrong thing or say nothing useful at all. Three factors had primary influence on the situation: - -**The harm problem** - -Reticulum is a tool that was built, deliberately and at great cost, to empower people; to make communication something you own rather than something you are temporarily granted. It has also been, at various times, exactly what certain kinds of organisations would like to use to control people. This is the duality of any powerful tool. A permissive license could not, and cannot, say anything about that; MIT is silent on the question of whether the software may be used to build systems designed to harm human beings. The Reticulum License is not silent. It does not pretend that a license can stop an individual madman; it does assert, however, plainly and in enforceable terms, that corporations and organisations integrated into the legal system cannot consume this software for harm and then claim they never knew. The line is drawn by the creators of the tool, not by an external authority, and it is drawn deliberately. This is covered in more depth in the discussion of the ethics of the tool in the *Zen of Reticulum*. - -**The appropriation problem** - -The second factor was the machine-learning appropriation of the open-source commons. By 2025 that was no longer hypothetical: The commons was already being systematically ingested by corporate training pipelines, and the code, words, and creativity of the people who built it were being converted into synthetic systems designed to replace them. The license responds to this with an explicit condition: The software shall not be used in the creation of an AI, machine-learning, or language-model training dataset, nor in any use that contributes to the training or development of such a model or algorithm. - -It is important to understand what this condition does and does not do, which matters for what follows in this chapter. **The AI-training condition is not the legal basis for protecting the code from machine copying**. That protection *already exists*, in the oldest and most well-tested form of copyright law, and it applies regardless of any license (see [Copyright & The Two Legal Layers](#brandolinis-copyright-and-layers)). - -The AI-training condition addresses something *additional*: the appropriation of the code *into the models themselves*; the ingestion of human-made work as raw material for a synthetic replacement, and the use of that ingestion to fabricate “prior work” minefields, patent-troll material, and a wall of slop meant to bury and devalue the actual ecosystem. That danger was foreseeable; the licensing decision was made with full awareness of it. The events of 2026 have since demonstrated, in public and in detail, that the concern was not theoretical. - -**The sustainability problem** - -Reticulum has been developed and maintained for over a decade by a single person, supported by a small group of contributors and network-building users, without corporate backing, without VC funding, through several serious attempts at commercialization and takeover from both the corporate and the ideological side. The license is part of what makes that sustainable, and possible at all. - -It ensures that the specific, painstaking labor of the reference implementation cannot be hijacked to undermine the intent of the project, while leaving the protocol itself, the mathematical rules of how Reticulum works, dedicated to the public domain, where it belongs to humanity and can never be owned. Anyone can implement the protocol, provided they themselves invest the *actual* work and effort. What the license protects is the work, and the intent embodied in it. - -### On Freedom - -The most persistent mischaracterisation of the Reticulum License is that it is “more restrictive” than the licenses it replaced or than the GPL family. This is backwards, and I’ll state the comparison plainly, because the confusion is the foundation of a great deal of deliberately manufactured controversy examined later in this chapter. - -The GPL and AGPL are constructed around a single, self-referential axiom: that “free” is *defined by the terms of the license itself*, and that any use falling outside those terms is not free. In practice, this means the GPL restricts the freedom of **everyone** - you may not write, distribute, link, or (in some cases) even communicate with GPL-covered software without the entire arrangement being pulled under the same terms, which the licensor then controls. The restriction applies to 100% of users, including the ones building purely constructive things. - -The Reticulum License restricts the freedom of almost nobody. Its restrictions apply only to: - -* Those who wish to use the software in systems that purposefully harm human beings, and -* Those who wish to ingest the software into AI training pipelines. - -That is a restriction on a vanishingly small fraction of would-be users - the 0.0000001% - and it is a restriction they can characterise however they like, forever. The bickering is itself a signal that the licensing decision hit exactly the right target. - -Everyone else retains the full breadth of use, modification, distribution, and sale, with none of the viral encumbrance of copyleft. - -Both licenses contain moral axioms. The difference is that the Reticulum License states its axioms out loud: - -* *Intentionally restricting the way that millions of people use and build upon a foundational - communications technology* - **The mechanism of copyleft** -* *Intentionally restricting a small number of organisations from using the technology to - violently harm human beings* - **The mechanism of the Reticulum License** - -Which of these is more free? The answer is not a matter of committee approval. It is a matter of what the word “free” means to the people *whose freedom is at stake*. The question of whether “open source” requires the blessing of a particular licensing orthodoxy is examined in [Open Source Means Open Source](#brandolinis-opensource). - -### The Prediction, And What Happened - -When the license change was made and publicly discussed in 2025, the following was said, in public, about the machine-learning threat: - -> A generative system could be trained on the Reticulum codebase, and for extremely low cost -> be set to work spewing out millions of source repositories for all kinds of ridiculous -> “Reticulum-based programs”. This is already happening. You can find several examples of this -> on GitHub already. … the point of this kind of trolling is to establish as wide a base for -> “prior work” as possible. By doing that, a patent troll or IP abuser can create an immense -> minefield for real developers. - -This was written when the phenomenon was just beginning. It was not a guess; it was a description of a mechanism already in motion, in the specific context of Reticulum, by people who had already begun to appear around the project. - -Less than a year and a half later, the prediction has now been demonstrated, in public, exhaustively and to the letter. There are now roughly a dozen machine-generated “Reticulum implementations” of the same shape: fluent, marketing-heavy, some without attribution, and - in several cases - actively harmful to the network. The [Prns case](#brandolinis-prns) is an example documented in this chapter. The pattern that produced it (and the people, mechanisms, and the recycling of falsehoods behind the *wider* phenomenon) is examined in [A Movement Of A Dozen](#brandolinis-movement-of-a-dozen). - -The license was written, in part, so that when this happened, the community would not be defenceless. - -### What This Means For Other Implementations - -Two simple things must be kept separate: - -1. **The protocol is public domain.** The mathematical rules of Reticulum - addresses, - signatures, routing, the wire format as an idea - belong to humanity. A *genuine* clean-room - implementation of the protocol, written by human beings who reasoned it out themselves, is - free and welcome, and owes nothing to the Reticulum License. Nothing in this chapter is an - argument against implementing the protocol. On the contrary: More good implementations, - honestly made, are good for the network. -2. **The implementation is licensed.** Copying the reference implementation - by machine or - by silk-screen press - and modifying and redistributing the result, is - exercising rights granted by the license, and is therefore subject to its conditions: The attribution - notice, the harm condition, the AI-training condition. A copy that does not follow those - conditions is **not** licensed, and it is not “open source in spirit”; it is an unauthorized - derivative work, and therefore a copyright violation. - -The distinction is the same one the license itself makes, and it must be held firmly: An implementation that *speaks* Reticulum is exercising the public domain protocol, **if** it was truly created independently. An implementation that was *generated from* RNS, reproduced by a copy process, is a derivative of licensed work. - -### Copyright & The Two Legal Layers - -The exact legal structure is frequently muddied in public discussion; and in several cases deliberately. - -**Layer A - Copyright (license-independent)** - -Copyright does not depend on any license. A primarily machine-generated copy of a copyrighted work - a copy not accompanied by demonstrable, substantial, new human creative input - is a derivative work over which the copier holds no copyright whatsoever, and the reproduction of which, without the original author’s permission, is an infringement of the original author’s copyright. This is standard, well-tested law, and it applies with equal force under the MIT license, the GPL, or any other license, because it is not about the license at all; it is about who created what. A process that copies the reference implementation into a machine, produces a transformed derivative, and then claims “Copyright, [name of the process operator]” - with no human able to account for the creative decisions in the result - **is not** asserting a valid copyright over the result. They are openly asserting the absence of one, and reproducing someone else’s work without authorisation. This is probably the cleanest cut copyright violation one could imagine. - -**Layer B - License Conditions (Reticulum License specific)** - -Where a copy *is* a licensed derivative, the conditions of the Reticulum License apply: The copyright notice and permission notice must be included in all copies and substantial portions; the harm condition must be honoured; the AI-training condition must be honoured. A derivative that follows none of these is unlicensed *a second way over*. - -The two layers are often intentionally confused in public discussion - the AI-training condition is waved around as if the entire question of machine copying rested on it, when in fact machine copying of copyrighted work is a copyright violation that would exist even under the most permissive license ever written. The purpose of keeping the layers separate is not rhetorical flourish; it is that Layer A is old, settled law, and does not rely on any novel legal theory, while Layer B is specific to this license. Both matter. Neither is a substitute for the other. - -Layer B is the required mechanism for closing the loophole where the work is *first* ingested during model training into the weights of a model itself, which would make any resulting violations almost impossible to detect and enforce. - -### The “Clean-Room” Delusion - -I regularly receive mail or LXMF messages from people who proudly and happily proclaim that they have created a “clean-room” implementation of one of the software systems I maintain; RNS, Sideband, LXMF or others. The declaration is delivered with the flat certainty of someone who believes they have found the legal lock-pick that settles everything at once: Their production is independent, uncontaminated, and therefore free of any obligation to the work they copied, its license, or its author. The proclamation usually concludes with a request for blessing, or at least acknowledgment. The proclamation is, usually, also generated by an LLM. - -This is a delusion, and I’ll cut through it surgically. The word they here invoke as magic is attempting to do a great deal of work it was never able to bear. - -**What a clean room actually was** - -The legal “clean room” is not folklore, incantation, witchcraft or a claim a person gets to make about their own output. It is a very specific, institutionalized procedure that grew out of the hardware and software litigations of the 1980s, and it existed for exactly one purpose: **Convincingly proving the absence of copying in a court of law**. - -The canonical case was Phoenix Technologies and its 1984 reimplementation of the IBM PC BIOS. IBM’s BIOS was copyrighted, which meant that the entire PC-compatible industry depended on being able to show that a compatible BIOS had been written independently, and not from IBM’s code. Phoenix’s answer was a physical and procedural firewall: - -* One team, the “dirty room”, read the original code and produced written *functional specifications*: descriptions of what the BIOS did, not how IBM’s code did it. -* A second team, the “clean room”, consisted of engineers who had *never seen the original code at all*, and they wrote the new BIOS from those specifications alone. - -This obsessively documented, legally attested separation was not a preference or a willy-nilly show for a kindergarten; it was the entire legal point, and it had to be *solid*. If even one clean-room engineer had *glanced* at IBM’s code, the clone would have been contaminated, and the defense would shatter. - -The other landmark case was *Computer Associates v. Altai* (1992), in which Altai had copied CA’s scheduler code into its own product, and (upon being caught) hired outside counsel to run a genuine clean-room procedure: A programmer with no exposure to the original was brought in to rewrite the infringing modules from functional specifications prepared by the team that had seen the code. The court credited the good-faith effort precisely because it was done, as the carefully documented record showed, to the letter of the procedure. - -Bear in mind what this procedure entailed in practice, because this is the part the modern claimants never seem to have even *heard* of: Physical separation of teams, legal counsel managing the process, signed and notarised declarations of non-exposure, controlled specification documents that described *behaviour* and never *code*, months of work, and testing against the specification - not against the original, which the implementers were forbidden to consult. - -The people on the clean-room side of the wall did not just *claim* they had not read the code. Their non-exposure was institutionally guaranteed, obsessively documented, and legally attestable through every fart and sneeze of the process, because the entire point was that a court - hostile, adversarial, and entitled to discovery - would believe it. - -*That* is the process. “Clean-room implementation” is the name of a *certified procedure for proving independent authorship*. It is not a mood, a marketing label, or a property a person gets to declare about their own work. - -**The moronic inversion** - -Now consider what is actually being described when someone says: *“I created a clean-room implementation of Reticulum by feeding the source code into a machine.”* - -That sentence is the opposite of a clean room. Feeding a copyrighted work into a machine so that it reproduces and transforms the work is **the definition of copying**, and a clean room is the one procedure whose entire purpose is the demonstrated *absence* of copying. This process cannot demonstrate the absence of copying, because the process is *composed of* copying. The cleanest possible room has been replaced by the dirtiest possible room, and the dirt has been installed as the base feature. What these claimants have actually performed is a photocopy with extra steps, and the extra steps, far from cleaning anything, are simply steps that forge a certificate they believe the output has earned. - -**Why the word is deployed** - -The word “clean-room” is deployed because it sounds like what the speaker wishes were true. It carries the accumulated legal prestige of the 1980s and 1990s - the BIOS clones, the Altai credit - and it sounds like a certificate already issued: *Independent, original, unencumbered*, the work of a person who owes nothing. The claimants want that halo without the procedure: No law firm, no physical separation, no written specifications, no signed declarations, no months of process - just a laptop, a loop, and the word “clean-room” typed into a README. The word must do the work of the process it replaces, and the delusional replacement is the entire point. - -**The irony runs deep** - -The Reticulum protocol is public domain. Any human being can implement it - from the mathematics, from the documentation, from their own reasoning - and owe nothing whatsoever to the license, to the reference implementation, or to me. - -An honestly written implementation needs no incantations, no legal theater, and no special vocabulary; it simply *is* what it claims. It’s plain for everyone to see. **It’s also plain for everyone to see when you’re faking it**. The people who reach for the word “clean-room” are, without exception, the people who need it to create a fantasy - and the people who need that are precisely the people who cannot have it. If what you wrote were genuinely independent work on the public-domain protocol, you would not need to call it anything. The word is not a seal of independence. In this context it is a confession of copying, filed by the copier, in the confident belief that it says the opposite. - -## Open Source Means Open Source - -An objection regarding the “open source status” of RNS appears occasionally in variations, but always reduces to the same syllogism: - -1. “Open source” means *licensed under one of the licenses approved by the Open Source - Initiative (or, in other variants, compatible with GPL orthodoxy)*. -2. The Reticulum License is not on that list. -3. Therefore Reticulum is not open source, and anything built on it is illegitimate. - -The entire argument rests on the first premise, and the first premise is false. - -Open source is not a certification conferred by a committee. It is a description of a fact: The source code is available, and the rights to use, copy, modify, merge, publish, distribute, sublicense, and sell it are granted to everyone, free of charge. - -The Reticulum License does all of those things. It is, in every substantive respect that concerns the person who wants to use, study, modify, or build upon the software, **an open license**. What it adds - and what disqualifies it in the eyes of the orthodoxy - is two conditions on *who may do what with the original work*, not on what the *user* is allowed to do with it. - -The definition being wielded against Reticulum is not even a young one. It is, in essence, the Debian Free Software Guidelines written in 1997, adopted by the OSI in 1998, and treated ever since by certain people as if it had descended from a mountain on tablets. It is an approach that is close to three decades old. Being old is not itself a flaw - but being old is also not a proof of correctness, and the suggestion that the meaning of a word like “open” was settled for all time by a committee in the 1990s, and may never be revisited, is not a position rooted in reason. It is rooted in institutional inertia dressed as principle, maintained by those whose ideologies it serves. - -### What The Argument Is Actually Doing - -I would like to go into some detail about the rhetorical mechanism, because it is roughly the same mechanism that powers the rest of the campaign described later in this chapter. - -The orthodoxy’s argument is self-referential: *“open source means what we say it means, because we are the ones who define it.”* When the Reticulum License is measured against that definition and found wanting, the conclusion is presented as a property of Reticulum - when in fact it is *a property of the definition*. The definition is treated as neutral and authoritative - as some base fact of raw cosmology - and the license is treated as deviant. But the definition is no more neutral than the license is. It encodes a moral position, and that moral position is exactly the one that this project declines to take. - -Consider what the orthodoxy’s definition requires, in the case at hand. The OSI-based definition contains, in its ancestry, the Debian Free Software Guideline of “No discrimination against fields of endeavor”: The software must not restrict who may use it or for what purpose. Taken to its logical end, this means that open source, properly “certified”, must be equally available to those who build hospitals and those who build weapons, those who connect people and those who control them, those who create and those who ingest and replace. - -The orthodoxy presents this as neutrality. It is not neutrality; it is a moral position - one that says, in effect, that *harming human beings is a field of endeavor like any other, and software must not discriminate against it*. - -The Reticulum License takes a different position, and it has the honesty to state it in the license text: this software is for life, not for death; it is for people, not for the systems that would replace them. That is not a restriction on openness, nor on freedom. It is a restriction on *who may consume the work*, and it is a restriction the orthodoxy’s definition cannot express - which is why the orthodoxy must always attack the license as “not open source” rather than engage with what it actually does. - -### Who Is Restricted By Which License? - -The arithmetic is simple, so let’s do it out loud, because the orthodoxy’s framing inverts it every time. - -The GPL family imposes its conditions on **everyone**: You may not distribute, link, or - in the AGPL case - even *communicate with* GPL-covered software without your own work being pulled under the same terms. “Free” in this system is defined (self-referentially) *by the license’s own terms*; everything outside those terms is, by definition, unfree. The restriction applies to 100% of users. It applies to the hospital, the mesh community, the school, and the hobbyist with the same force it applies to anyone else. That is not freedom for everyone; it is a *specific moral architecture applied to everyone*, presented *as if it were the absence of one*. - -The Reticulum License imposes its conditions on a fraction of would-be users so small as to be practically unmeasurable: - -* Those whose intended use is to build systems that purposefully harm human beings, and -* Those whose intended use is to ingest the work into AI training pipelines. - -That is the entire list. Everyone else - the overwhelming majority, the community, the developers, the users, the downstream projects - receives the full, unrestricted rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, with none of the viral encumbrance of copyleft. - -So when the claim is made that the Reticulum License “is more restrictive” than the GPL, the claim is not merely wrong; it is the exact inverse of the truth. The GPL restricts the freedom of everyone, in the name of a specific ideology. - -The Reticulum License restricts the freedom of the harm-doer and the appropriator, and leaves everyone else free. The two approaches do not differ in whether they contain moral axioms - **all licenses do**. They differ in whether the axioms are stated openly, and in *whose* freedom they restrict. - -### The Consequences Of The “Not Open Source” Claim - -This would be a footnote debate if the mischaracterisation had no practical consequences. It has very large ones, and therefore this section exists. - -The claim that “Reticulum is not open source” is the foundation of the takeover narrative it so often accompanies. The sequence is always the same: - -* First, declare the reference implementation illegitimate (“not really open source”, “owned by one man”, “not a real community project”) -* Second, conclude that it is therefore acceptable - even necessary - to replace it. -* Third, present the replacement, which is (conveniently) often the speaker’s own machine-generated clone, as the legitimate heir. - -This is not speculation about motives; it is the documented structure of the campaign examined in [A Movement Of A Dozen](#brandolinis-movement-of-a-dozen). - -The “not open source” claim is the legal-ideological battering ram of that campaign. It is recycled, in article after article, in forum after forum, because it works on people who assume that a committee’s definition is the same thing as the truth. - -It is also worth stating the other obvious fact: None of these critics have ever shown that the Reticulum License restricts *them*. They do not build weapons. They are not training models on the codebase. They are free, in every practical sense, to use Reticulum exactly as they would under MIT or GPL. Their objection is not to a restriction on themselves; it is to the *mere existence* of a license that states a moral position they disagree with, or which they can use as leverage for a different goal entirely, and to the institutional fact that it does so outside *their* system of approval and control. The complaint is not that the license makes them unfree. The complaint is that the license is *free of them*. - -### The Distribution Argument - -A recurring companion claim is that RNS is not properly “distributed”, usually deployed as “it’s not packaged as a .deb” or “it’s not on package manager X”, with the implication that it is somehow unavailable or inaccessible, or hard to install on a given system. - -This is simply false, and it has been false for years. The reference implementation is available on practically every platform and architecture with a single command (`pip install rns`). It is packaged in the Arch User Repository, in NixOS, and in a range of other distribution channels, community-maintained and official. It can be installed, updated, and kept current *over Reticulum itself* - the network transports its own software updates. It is used on more than a hundred thousand devices worldwide, is embedded in over a hundred other packages and applications, and carries terabytes of traffic every day. - -The claim that Reticulum needs a `.deb` to be “really distributed” mistakes the mechanism for the goal. The goal is that people can obtain and update the software easily, everywhere, **on their own terms**, and distribute it themselves, both offline, online and over Reticulum with full cryptographic verification of the install packages, without any central infrastructure. - -That goal is met to an excellence that few software packages can speak of, and met in a way that does not pin the project’s release cadence to the review cycles of distribution maintainers, which is an achievement, not a bug. - -A project like RNS that updates continuously, sometimes daily, and has a track-record of shipping security updates mere hours after issues are discovered, would be crippled by packaging that makes every change wait months for approval. The distribution complaint is a strawman generated for the occasion. It has been answered, in public, many times; it is repeated anyway, because the people repeating it are not counting on facts - they are counting on the reader never having heard the answer. - -### On The Institutions, In Fairness - -None of the above is hostility toward the Open Source Initiative or the Free Software Foundation as institutions, nor toward the individuals who have dedicated decades of work to them. Their contribution to the world of software is **enormous**, and this document does not dispute it. What is disputed is a specific claim of authority: That “open source” is a term whose meaning is reserved to a committee’s approval, and that a license which does not submit itself to that approval is therefore not open. - -That claim of authority is not in Reticulum’s interest, and it is not in the interest of the people the movement claims to serve. The current legal and technological landscape - the machine-appropriation of the commons, the weaponisation of “prior work” generation, the erosion of human agency by synthetic systems - was not anticipated by the DFSG of 1997, and the institutions have not, as of this writing, produced *any* meaningful answer to it. The choice, as it was put at the time of the license change, is whether developers should sit back and wait for a solution from a committee that “may or may not ever materialize, before the entire foundation we stand on has been eaten away”. - -**Reticulum’s answer was to act**. If we had waited, it would have been too late. - -If an OSI-approved license existed that expressed the same constraints - and did not impose the far more severe restrictions of copyleft - the project would have used it. None such exists. That is not Reticulum’s failure. It is the measure of what the definition is missing. - -### What “Open” Means Positively - -For anyone who wants the positive statement rather than the rebuttal, Open Source, in this project’s understanding, means: - -* The source code is available, to everyone, without charge -* Everyone may use, copy, modify, merge, publish, distribute, sublicense, and sell the - software -* No one is discriminated against as a person - developer, user, community, or project -* The protocol itself is dedicated to the public domain, where no one can own it -* And the single, plainly stated condition of the license is that the work shall not be - consumed to harm human beings, or ingested to replace the humans who made it - -If that is not “open” by a definition written in the 1990s, then the definition is the thing -that has failed - not the software, and not the people who use it. - -**Open source means open source**. It was true before the committees existed to certify it, and it will remain true after they are gone. - -## Assistance Versus Machine-Substitution - -### False Dichotomy - -The public discussion around machine learning and software development has been derailed, by the loudest voices on both sides, into a farcical fight between “AI enthusiasts” and “AI haters”. - -> **Both positions are absolute caricatures.** - -> **The fight is beside the point in the ways that matters most.** - -It is not an accident, but a very conscious choice that the initial analysis of the Prns case examined herein, and this very chapter, were themself produced with the assistance of a machine-learning based generative system. - -Specifically, it was generated using `lc` managing a `Q8_K_XL` quant of `DeepSeek v4 Flash` running in the `llama.cpp` inference engine. The main context base used to stabilize, ground and inform the generative environment consisted of approximately 100,000 tokens of text material that I personally thought out and wrote (by hand), then archived and organized, over the last eight years. - -If you know my writing, and have a hard time discerning where this chapter is me directly, typing manually, and where it is the machine output, it is because of two things: Almost every line in this chapter was either directly edited by my hands on the keyboard or typed by me, and the text corpus that informed the process *is* genuinely me - my thoughts, written by me, over a period of years. - -The chapter was produced this way *deliberately and openly*, under a software framework I designed and wrote, with the human directing the inquiry points, flow of investigation, structure of the outputs, contents of every section, making the editorial decisions, reviewing the conclusions, painstakingly adjusting every line, and taking responsibility for the final claims presented, with the goal of reaching a higher level of synthesis and symbolic density than I could have achieved without that assistance. - -The tool was *used as tool*, both to structure and produce a complex chapter, and to inspect a repository so preposterous in size, commit velocity and nonsensical wording that it barred any purely human inquiry. - -As such, the position is hereby, by personal example and demonstration, stated clearly: - -* **Technological assistance is not the problem** - -* **It was never the problem** - -* **It is the opposite of the problem** - -The problems are *substitution* and *usurpation*: The perverse process in which the machine becomes the author of choices, and the human becomes the facade of software, while expecting “reciprocity” or recognition for deeds they never performed. - -### The Line - -The exact distinction can be reduced neither to how many lines of code a machine produced, nor who pressed the keys. The final distinction has to rest on **who, or what, the real agent of the work is**. - -* **Assistance** is when the human retains the full seat of agency: The human decides what to build, judges - what is correct, rejects what is wrong, understands what is shipped, and can answer for every single line of it. - The machine writes, suggests, drafts, accelerates *meaningfully* and within reason - but every choice that survives into the - final work is a choice the human made *consciously*, and understood - or grew to understand - the consequences of. - -* **Substitution** is when the machine *becomes* the agency: the machine decides what to build, - decides what is correct, iterates on its own errors, generates at a scale and speed no single - human can follow - and the human’s role is reduced to providing the name, the account, the - signature, and the marketing launch. - -> The Line can be stated as a single test, and this is the same test that has always separated authorship from couriership: **Can the person whose name is on the work answer for it fully?** Not “can they describe what it does” - can they account for why it is the way it is, line by line and decision by decision, and the history of how it came to be that way, under questioning, without machine assistance, including by people who know more than they do? - -If they cannot, they did not author the work. They wore it, or in more severe cases, they stole it. And a courier who claims authorship is not a writer; they are a fraud. - -### What assistance looks like - -Assisted work is recognisable, and recognisability is itself part of its value. It has the following observable properties: - -1. **The human can explain it.** Under examination, the author can say why this choice was - made, what was rejected, and what they verified. The ability to explain is not a luxury; - it is the very demonstration of authorship. On a large project with many contributors this is partial in - practice - no one can explain every line of a large codebase - but the *full capacity* must - exist between the authors and the *habit* must be visible. -2. **The human can, and does, reject machine output.** An author who has never rejected the machine’s output is not - an author, but an open drain. Rejection is not a stylistic flourish; it is the - mechanism by which any work attains Quality. A record of decisions, including reversals and rejections, - is a sign of agency. A record without rejection is a record of assent-by-default. -3. **The human’s judgment is directional.** The work contains the human’s actual choices; - things the machine would not have decided on its own, which the human can defend on their - merits. This directionality has many names in engineering: Design, architecture, taste, - judgment. Where it is absent, the work has the uniform, self-consistent texture of - something that was never doubted, and doubt is exactly what anything important requires. -4. **Provenance is not concealed.** Disclosure of how the work was made is not a legal - requirement in every jurisdiction; it is not even always a practical requirement. But it - is a *statement* that matters. A project that discloses its process - - including its use of machines - is telling the truth about the relationship between the - human and the work, and that is a property users *are* entitled to know. Concealment of - provenance is not a stylistic choice; it is deception, made by someone who knows what the - record would otherwise show. -5. **The human stands behind the failures.** When assisted work fails, the human can say: Yes, I - chose this, under these assumptions, here is why, here is what I got wrong, here is the fix. - That sentence is the entire contract between an author and the people who depend on their work. - A human who cannot say it - because the choices were never theirs - *has nothing to offer anyone*, and - is by definition entirely replaceable; anyone else could have pressed the same button. - -### What substitution looks like - -Substitution is equally recognisable, once the pattern is known: - -1. **Scope that no human could have authored.** Output that - structurally cannot have been the product of human authorship, review, and judgment, for - example when the volume exceeds any reasonable human capacity within the span of the project’s life. - When the review capacity and the output volume are incommensurable, the output was not - reviewed. It was generated, then forgotten, and the record was signed blindly. -2. **Uniform generation.** The work carries no trace of a human’s changing mind. No style - drift, no contested decisions, no “we tried X and it was wrong, so we did Y”. That is, *no - signs of actual development, growth and learning*. Instead, endless, self-referential - refactoring, renamed abstractions, and the same fluent register in every commit, - every document, every reply. A human’s history shows the wear of judgment. - Pure generation shows the superficial polish of its absence. -3. **Discovery by recursive regeneration.** When errors are found, they are found by the machine’s own - subsequent runs, not by human scrutiny - and the findings are recorded as planned - “reviews” and “alignments” rather than as the corrections they are. This is the tell: A - machine editing its own previous output, while the edit is recorded as if it were a - scheduled milestone. That’s not review. That’s a continuous generation loop revising itself, - with the attempt of an illusion-preserving voice-over. -4. **Laundered provenance.** Theatrical concealment machinery: Commit hooks that refuse - attribution of machine generation, contribution policies that declare machine output - “yours”, processes that scrub personal traces from the history, fake identities in the - contributor list. All of these have one function: To make the record say what the “author” - *wished* had happened, rather than what happened. A project that builds such machinery is - not making a record of its work; it is using automated software to manufacture one. -5. **Authorship by assignment.** The human’s name is on the work, but the human cannot - answer for it - because the human did not choose any of it, and knows it. The name is not a - signature of authorship; it is a *fraudulent claim of ownership* over something the claimant did not - make and cannot explain. In every field that has ever dealt with such claims - art, - literature, invention, journalism - *this has a name*. That **forgery** is now cheap in software - makes it no more forgivable. - -### Infrastructure Matters - -For a hobby project, the distinction between assistance and substitution is a personal matter. For infrastructure - code that other people’s communications, safety, and freedom run on - it is a matter of life and safety, and any statement about this must be entirely unapologetic. - -Infrastructure has one property that distinguishes it from almost all other software: While most of the people who depend on it cannot verify it themselves, **it must be verifiable, auditable and open to human inspection**, and be engineered in such a way that this is as frictionless and straight-forward as possible. - -The reference implementation of Reticulum was designed this way, and continues to make that goal a primary one. While it *is* a complex piece of software, with an intricate array of interlocking and moving parts, the core parts of RNS, the entire Reticulum protocol implementation, the transport logic, the cryptographic primitive interfaces, **is less than 15,000 lines of code**. That line count even includes the rather extensive comments, incredibly detailed logging facilities and other choices that makes the code easy to read and understand for humans, and easy to operate, verify and debug. It took real dedication, and *a lot* of hard work to achieve that. - -Exactly because not *everyone* can do this, it is a fundamental necessity for infrastructure software that it is designed and authored so that *as many as possible can*, and that the *chain of accountability* - who made it, who reviewed it, who answers for it - is held intact at every link. That chain is not decoration. It is the entire mechanism by which normal users decide to trust a system they cannot fully verify. The transparent foundations and the chain combined *is* the verification for most users. - -Substitution breaks the chain both at the foundations, and at its load-bearing link: The human at the top can no longer answer, because they were never the maker. Unfortunately, the chain does not always fail visibly; often it fails silently, at the exact moment a question goes deep, or when routing fails subtly at a critical moment. - -For the user, the result is not a project with a normal and humanly flawed author - it is a project *with no author at all*; a signature with no substance behind it, a name that cannot be held to account, and a body of work that nobody on earth can (or wants to) explain, defend, or fix in a crisis. - -### The Fluency Trap - -It is worth understanding, briefly, why substitution is so attractive and so dangerous at once. Machine-generated output is fluent. It is confident, well-structured, and terminologically *self-consistent*. At the surface, and from the perspective of someone outside a given craft, it is indistinguishable from the output of a knowledgeable human. Fluency of this kind can be a useful property: It makes assisted work fast. But fluency is *not* understanding. It is the *minimal conformal surface* of understanding. - -The danger is that the surface is exclusively where the substituter invests; for the simple reason that the surface is where most of the desired audience looks, and what yields attention to the project. - -A human who can tell the difference uses the tool well. A human who cannot is being deceived through means they cannot detect from inside, namely through their own inability to account for the limits imposed by what they do not know. - -This is why the most important quality for anyone working with systems like Reticulum is not speed, and not vocabulary, but the oldest engineering virtue there is: The honest, uncomfortable, terrorizing knowledge of **what you do not know**. The substitute *never* has it. The substitute’s operator, *if* they ever had it, loses it precisely the moment they accept the fantasy of the substitution process. - -The tool is not the hazard, plain old human nature is. The tool is a mirror, and mirrors - as has been observed for rather longer than software has existed - show the truth only to those willing and yearning to see their own shortcomings. - -### Disclosed Assistance Or Laundered Generation - -The contrast at the centre of this entire discussion can be made clearer with two examples, both of which are publicly accessible, both produced in the same year, by people using the same class of tools. - -The first is this chapter. It was produced with machine assistance, openly: the structure was directed by the human, the material was assembled with the tool’s help, every conclusion, every line, every slight nuance of meaning and semantics was examined, edited, and accepted or rejected by the human, and the process described publicly - including the precise characteristics of the technological assistance framework used. The human can answer for every claim in these documents, because the human made the decisions the claims rest on. - -The second is the project examined in [the Prns case](#brandolinis-prns). It was produced by a machine, at a volume no human could review, and the authorship record was engineered to erase the machine’s role: A commit hook that refuses attribution of machine generation, a contribution policy declaring machine output the human’s own, a fictitious contributor identity, 637,000 lines of unreviewed code. The human cannot answer for the code, because the choices were never made by a human - only the conformal surface of the presentation was. - -The same tool class. The same technology. Two diametrically opposed relationships to truth and responsibility. **The difference is not the tool**. The difference is the presence or absence of the human as the agent of the work - and the honesty or concealment with which that relationship is recorded. - -Assistance leaves the human as the agent and the commons as the source from which the human works and can grow. Substitution removes the human from the loop and converts the commons into fuel for the thing that seeks to replace them, or usurp their work for the profit of the few, human atrophying in the process. - -### The License In This Context - -The Reticulum License, examined in [What The Reticulum License Is](#brandolinis-what-the-license-is), draws the same line, at the level of the law. Its AI condition does not forbid the use of machine-learning technology to *assist* development - no reading of the license could support that, and no one associated with this project has ever claimed so. What the condition forbids is the *appropriation of the work into the machine itself*: The ingestion of the commons as raw material for synthetic replacement, and the concealed fabrication of “prior work” that would bury the real thing. - -As also noted in [Copyright & The Two Legal Layers](#brandolinis-copyright-and-layers), plain old copyright law already protects against the case where a process copies the reference implementation into a machine, produces a transformed derivative, and then claims “Copyright, [name of the process operator]”, with no human able to account for the creative decisions in the result. That is simple, petty theft, as it always has been. - -### The General Principle - -In these regards, the position of this project can be codensed to something relatively concise: - -* Machine assistance in human learning, development, and creation is accepted and useful. - It is not a shameful secret; but it *is* a tool that needs knowledge, training, understanding, - and great care to use. It should not make anything easier or more fun, it should make the - results better, more carefully considered, and arrive at *higher levels of synthesis*, not - produce endless streams of code churn. -* The line is crossed when the human stops being the agent of the work. When *any* output is - accepted that the human cannot explain, produced at a scale the human cannot review, or filed under a record - where the human has arranged to conceal the fact. -* The line is crossed, when a human claims authorship of work they cannot - answer for or has usurped. Credit follows the ability to answer. It has always worked that way, and - machines do not change the rule - they only make it cheaper and more tempting to break. -* The people who depend on infrastructure have the right to know which side of the line - the code they run was made on. That knowledge is the only verification most of - them will ever be able to perform. - -Use assistance, by all means, *and learn*, but don’t substitute. And never sign what you cannot answer for. - -Somewhere, within the general shape of the conceptual framework outlined in this section, or realistically, a more refined variant of it, I do believe there is the *potential* of the *beginning* of a constructive outcome to the immense challenge we are faced with as a species in regards to these problems. - -At the same time, I reserve my right to revert that position to Butlerian Jihad without prior notice. We are on a knife’s edge, and you know it just as well as I do. - -## Void Grants: Legal Foundations of Machine-Generated Code - -A grant of rights is only as real as the rights behind it. A license conveys what its issuer holds, and nothing more. A derivative may be distributed only under whatever license covers what it took. And over a work that a machine originated, no human holds anything at all. - -Each of these statements is a matter of settled statute and case law, and this section sets out the authorities. The conclusions they support are: - -1. A software implementation generated from a licensed work by a machine, published under its operator’s claimed copyright and license, **conveys rights the operator never possessed**. -2. **Its grant is void**. The projects, products, and promises built upon such grants stand on nothing. A field of void grants is a minefield for everyone who crosses it. - -This section is a general examination of copyright law as it applies to machine-generated code. It is not legal advice. - -### I. The Foundational Statute: Translation Is Copying - -The starting point is the statutory definition of the class of works at issue. Title 17, United States Code, section 101 provides the definition of a derivative work: - -> A “derivative work” is a work based upon one or more preexisting works, such as a *translation*, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or *any other form in which a work may be recast, transformed, or adapted*. - -Section 101 further provides that *“the terms ‘including’ and ‘such as’ are illustrative and not limitative”*. As such, the list is a list of *examples*, and the very first example is translation. The definition also reaches, in its own words, *“any other form in which a work may be recast, transformed, or adapted”*. - -The exclusive right at issue is defined in section 106: - -> Subject to sections 107 through 122, the owner of copyright under this title has the exclusive rights to do and to authorize any of the following: -> (1) to reproduce the copyrighted work in copies or phonorecords; -> (2) to prepare derivative works based upon the copyrighted work; -> (3) to distribute copies or phonorecords of the copyrighted work to the public by sale or other transfer of ownership, or by rental, lease, or lending; - -Violation of any of these rights is infringement. 17 U.S.C. § 501(a). - -Taken together, the provisions yield a construction that is difficult to improve upon and impossible to escape: - -1. **A port is a translation.** A work that takes a program written in one language and from it produces a program written in another language, implementing the same behavior, is a translation of the original - whatever mechanism performed it, and however the surface presentation differs. -2. **A translation is a derivative work.** Section 101 says so, explicitly. -3. **Preparing a derivative work is the copyright owner’s exclusive right.** Section 106(2) says so explicitly. A derivative work prepared without authorization, or outside the scope of any authorization, infringes that right. - -The common objection - “the code is in a different language, so it is completely different” - is not a legal argument; it is the definition of what the statute already classifies. - -Translation *is the paradigm* of derivative authorship, not an exception to it. If translating a work without permission was a defense, the derivative-work right would be meaningless; every infringing adaptation would simply need to be “different enough” to count as a translation of its source. - -Nor does the identity of the translator change the analysis. The statute asks whether the work is “based upon” a preexisting work. It does not ask who or what performed the translation. A machine that ingests a licensed work and reproduces a translated transformation of it has performed mechanical translation; the output is a derivative of the ingested work, and the law governing derivative works applies to it exactly as it applies to a translation made by hand. - -### II. Ideas Are Free, Expressions Protected - -The second foundation is the distinction between what a work *is* - its ideas, its processes, its systems - and the *expression* in which those ideas are embodied. The distinction is codified in section 102(b): - -> In no case does copyright protection for an original work of authorship extend to any idea, procedure, process, system, method of operation, concept, principle, or discovery, regardless of the form in which it is described, explained, illustrated, or embodied in such work. - -The principle is older than the statute, and its application to software is settled. An author may own the particular code in which a method is expressed; the method itself, the process, the algorithm, belongs to no one - because *“the sine qua non of copyright is originality”* (*Feist Publications, Inc. v. Rural Telephone Service Co.*, 499 U.S. 340, 345. 1991). And ideas are not original to anyone in particular, *Baker v. Selden*, 101 U.S. 99 (1879), established the line: Copyright in a book describing a system does not prevent others from practicing the system. - -Applied to a communications protocol, the structure is clean: - ---- - -**An implementation of a public-domain protocol, honestly reasoned and written by humans, is welcome and unencumbered, and owes nothing to any license. An implementation generated from a licensed implementation is a derivative of licensed work, subject to its conditions and to the ordinary copyright of its source.** - ---- - -The public-domain status of the protocol’s ideas changes nothing about the copyright in an implementation’s expression. A person who copies an implementation - by machine or by hand - cannot defend the copying by pointing to the protocol, because the protocol covers the ideas, not the symbols in which someone expressed them. - -A further misconception can be dismissed here; the recently popularized “clean-room” claim about the absence of copying. A [clean-room](#brandolinis-cleanroom-delusion) is a not a magic label achievable by LLM, but a specific, institutionalized procedure for proving to a court that copying did not occur: Physically separated teams, written functional specifications, attestable non-exposure, and no access to the original by the implementers (*Altai*, 982 F.2d at 709–11). - - *“I created a clean-room implementation by feeding the original source code into a machine”* attempts to *claim* the certified, legally established procedure for proving the absence of copying by describing, in the same breath, a process **composed entirely of copying**. The word is deployed because the procedure it names is what the claimant would *need to have happened*. - -### III. Machine Output Has No Author - -The third foundation is the human authorship requirement. It is settled at both levels of the federal courts and in the Copyright Office’s administering practice. - -In *Thaler v. Perlmutter*, the district court held that *“copyright law protects only works of human creation,”* and that *“human authorship is a bedrock requirement of copyright”* (687 F. Supp. 3d 140, 146, D.D.C. 2023). On appeal, the United States Court of Appeals for the District of Columbia Circuit affirmed, holding that the Copyright Act *“requires all eligible works to be authored in the first instance by a human being”* (No. 23-5233, D.C. Cir. Mar. 18, 2025). - -The requirement traces to the ordinary meaning of the statutory term. In *Burrow-Giles Lithographic Co. v. Sarony*, the Supreme Court defined an “author” as *“he to whom anything owes its origin; originator; maker; one who completes a work of science or literature”* (111 U.S. 53, 57–58, 1884), and more recently described the author as *“the person who translates an idea into a fixed, tangible expression entitled to copyright protection”* (*Community for Creative Non-Violence v. Reid*, 490 U.S. 730, 737, 1989). - ---- - -**A machine does not owe expression its origin; it reproduces it. No court has recognized copyright in material created by non-humans, and the courts that have addressed the question have rejected it. If you create by machine generation, you do not own the output.** - ---- - -The Copyright Office’s registration guidance applies the requirement to generative systems in terms that are directly on point. The Office asks: - -> whether the “work” is basically one of human authorship, with the computer [or other device] merely being an assisting instrument, or whether the traditional elements of authorship in the work (literary, artistic, or musical expression or elements of selection, arrangement, etc.) were actually conceived and executed not by man but by a machine. -* *Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence*, 88 Fed. Reg. 16190, 16192 (Mar. 16, 2023) - -And it states the consequence: *“If a work’s traditional elements of authorship were produced by a machine, the work lacks human authorship and the Office will not register it”* (*Id.*). - -On the role of prompts, the Office is equally explicit: when a system receives a prompt and produces a work in response, *“these prompts function more like instructions to a commissioned artist - they identify what the prompter wishes to have depicted, but the machine determines how those instructions are implemented in its output”* (*Id.*). - -The agency applied this standard and excluded machine-originated material from registration in the leading administrative case. In *Zarya of the Dawn* (Cancellation Decision, Feb. 21, 2023, Registration No. VAu001480196), the Office cancelled registration of images generated by the Midjourney service, holding that *“it was Midjourney-not [the author]-that originated the ‘traditional elements of authorship’ in the images”*, while preserving protection for the human-authored text and the human selection and arrangement of the work. - -The Copyright Office’s 2025 report on AI copyrightability reaches the same conclusion: - -> Based on the functioning of current generally available technology, prompts do not alone provide sufficient control. -* U.S. Copyright Office, *Copyright and Artificial Intelligence, Part 2: Copyrightability*, at iii (Jan. 29, 2025) - -The report’s analysis is explicit: *“The Office concludes that, given current generally available technology, prompts alone do not provide sufficient human control to make users of an AI system the authors of the output. Prompts essentially function as instructions that convey unprotectible ideas”* (*Id.* at 18). - -Its guiding distinction is stated plainly: *“The use of AI tools to assist rather than stand in for human creativity does not affect the availability of copyright protection for the output”* while *“Copyright does not extend to purely AI-generated material, or material where there is insufficient human control over the expressive elements”* (*Id.* at iii). - -The boundary is therefore not “AI or no AI”. It is *assistance or substitution*: Whether a human exercised sufficient creative control over the expressive elements and can answer for them, or whether the machine originated them and the human merely supplied instructions and a signature. That is a question of fact in each case, determined from the record of how the work was made. - -Two facts follow, and stated together, they form the base legal reality: - -1. **A work whose expressive elements were originated by a machine contains no valid new copyright: It has no human author. To the extent that the same work reproduces a preexisting copyrighted work without explicit license, it is at the same time an unauthorized derivative. It is unowned for anything new it contains, and unlicensed for everything taken.** -2. **Its operator therefore holds no copyright in it and no grant over it - and a person who holds neither copyright nor grant can convey neither.** - -The first fact makes the work **unownable** by its operator. The second makes it infringing against its source. Neither replaces the other: - -* The absence of copyright in machine output does not make the output free for anyone to take; it makes it unowned. -* This is a different thing entirely, especially when what it contains is someone else’s expression. - -### IV. License Conditions Bound the Grant - -The fourth foundation governs what a license *is*. A copyright owner may grant permission to use the work on conditions. The conditions define the scope of the permission. Use outside the scope is use that was never authorized. - -In *Jacobsen v. Katzer*, 535 F.3d 1373 (Fed. Cir. 2008), the Federal Circuit held that the conditions of an open-source license are *conditions of the copyright license itself*, not mere contractual promises, and that a licensee operating outside them is an infringer: - -> Copyright holders who engage in open source licensing have the right to control the modification and distribution of copyrighted material. - -> The choice to exact consideration in the form of compliance with the open source requirements of disclosure and explanation of changes, rather than as a dollar-denominated fee, is entitled to no less legal recognition. - -> It is outside the scope of the [license] to modify and distribute the copyrighted materials without copyright notices and a tracking of modifications from the original computer files. -* *Id.* at 1381–82 - -The doctrinal mechanism is settled: *“A copyright owner who grants a nonexclusive license to use his copyrighted material waives his right to sue the licensee for copyright infringement”* within the scope of the license. If, however *“the licensee acts outside the scope, the licensor can bring an action for copyright infringement”*: An act outside the granted scope **is infringement** (*Sun Microsystems, Inc. v. Microsoft Corp.*, 188 F.3d 1115, 1121, 9th Cir. 1999; *Graham v. James*, 144 F.3d 229, 236, 2d Cir. 1998; *S.O.S., Inc. v. Payday, Inc.*, 886 F.2d 1081, 1087, 9th Cir. 1989). - -Whether a given license term is a condition of the grant or a mere covenant is determined from the license’s own language; conditions that limit what may be copied, modified, and distributed - the attribution terms, the notice terms, the modification-tracking terms, the field-of-use terms - are the boundaries of the copyright grant (*MDY Industries, LLC v. Blizzard Entertainment, Inc.*, 629 F.3d 928, 940, 9th Cir. 2010). - -Three consequences follow for the analysis at hand. They are not hypothetical. - -**First, the conditions are measured at the moment of copying** - -A work is licensed under whatever conditions applied when the reproduction, adaptation, and distribution occurred. There is **no** period in which copying a licensed work is “free” because the copy was made by a machine; the machine is the *licensee’s tool*, and the licensee is bound by the conditions regardless. A derivative generated without satisfying the conditions - without the attribution, without authorization for the manner of use - was never within the scope of the license, from the first instant of its existence. - -**Second, breach does not restore rights that never attached** - -The district court in *Artifex Software, Inc. v. Hancom, Inc.*, No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017), analyzing the GPL’s termination provisions, observed that *“the language of the GPL suggests that [the licensee’s] obligations persisted beyond termination of its rights to propagate software using Ghostscript,”* because the source-code obligation recurs each time a covered work is conveyed. The same structure operates a fortiori where a derivative was produced *outside* the license entirely: No grant ever attached, and no subsequent event creates one. - -**Third, the settled conditions are the enforceable core** - -The attribution and notice conditions rest on the settled doctrine above. Novel conditions (restrictions on field of use, restrictions on how the work may be consumed) are enforceable as conditions of the grant when clearly drafted, but their enforcement history is thinner. The argument that a derivative is unlicensed does *not* depend on the novel conditions being vindicated; it needs **only** the settled ones, and the ordinary law of derivative works, to stand. - -### V. The Void Grant - -The preceding sections assemble into a single structure. Let us apply it to an example situation: - -* A project presented as an independent implementation of a protocol; “ground-up”, “from scratch” or “clean-room” work. -* The actual history shows continuous, machine-mediated ingestion of a licensed reference implementation. -* The output is generated at a scale no human could produce or review, with the provenance of the process obscured. - -**Step One: The derivative** - -The project’s output is a translation of the licensed implementation: A derivative work, produced by a copy process. 17 U.S.C. §§ 101, 106(2). Preparing it without authorization infringed from the first moment of its existence. *Id.* § 501(a). - -**Step Two: The license** - -The derivative was produced outside the conditions of the license; without the notices, without the attribution. The license never attached (*Jacobsen*, 535 F.3d at 1381–82). - -**Step Three: Authorship** - -The expression was originated by a machine. No human can account for its creative decisions, and no human authored it. It contains no valid new copyright (*Thaler*, 687 F. Supp. 3d at 146; No. 23-5233, D.C. Cir. 2025; 88 Fed. Reg. at 16192). - -**Consequence: Void Grant** - -The statute supplies the consequence. Section 103(a) provides: - -> The subject matter of copyright as specified by section 102 includes compilations and derivative works, but protection for a work employing preexisting material in which copyright subsists does not extend to any part of the work in which such material has been used unlawfully. - -And section 103(b) provides that copyright in a derivative work *“extends only to the material contributed by the author of such work.”* The legislative history states the point without ambiguity: *“An unauthorized translation of a novel could not be copyrighted at all”* (H.R. Rep. No. 94-1476, at 57, 1976). - -In our example, the operator of the derivative’s copy process decides to assert copyright over the output, and licenses it to the world under terms of the operator’s choosing, for example the Apache License. - -But the operator holds no copyright in machine-originated content (section III) and no license over the source from which it was derived (sections I and IV). - -For the derived material, the operator’s copyright claim **is an assertion of a right that never existed**. The material was used unlawfully, and section 103(a) withholds protection from *every* part of the work in which it appears. For the machine-originated remainder, the operator holds no copyright either, for lack of any human author. - -The operator’s license to the world is a promise the operator had no power to make. Every person who receives the work believing they received a valid grant has received **a void grant**, and stands, as a matter of settled law, in the position of a downstream user of an unauthorized derivative, whatever they were told at the time. - -This is a cascading consequence, and it is why the pattern is a hazard. No combination of self-granted licenses - “MIT,” “Apache,” “public domain,” anything at all - can transfer rights the operator never held. Every downstream project, product, or deployment built on the derivative is built on unlicensed reproduction of a third party’s work, whether or not it knew, whether or not it was told, and whether or not the marketing was plausible. - -The longer the derivative circulates unexamined, the more it becomes embedded, forked, vendored, depended upon, and the more expensive its correction becomes for everyone who trusted it. A field of superficially real projects, repositories, and licenses that **in reality confer nothing on anyone who relies on them is a minefield**, in the most precise sense the law allows: The rights they purport to grant *were never possessed*. - -The law defines what may be asserted, not what will be. The copyright owner may choose whether and how to assert these rights, and at what point in time. The exposure exists regardless, and for *every* downstream that trusted the void grant. This is the scope of the hazard that the conduct exemplified here produces. - -### VI. Fair Use Does Not Rescue This Pattern - -Every defendant raises fair use. It is an essential and important limitation in the service of free speech, accountability, criticism, education and other important domains. Therefore, it must be considered, and in the case exemplified here, it fails. - -Fair use is a limitation on the exclusive rights, evaluated case by case under four factors: The purpose and character of the use; the nature of the copyrighted work; the amount and substantiality of the portion used; and the effect of the use on the potential market for or value of the work (17 U.S.C. § 107). - -On the first factor, the governing standard is *Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith*, 598 U.S. 508 (2023), in which the Supreme Court held that: - -> In sum, the first fair use factor considers whether the use of a copyrighted work has a further purpose or different character, which is a matter of degree, and the degree of difference must be balanced against the commercial nature of the use - -The question is whether the use serves a further purpose, not whether the surface changed. A derivative that exists to serve as the same thing as the original, marketed to the people who would otherwise use the original, has not acquired a further purpose by being translated; the surface changed, the function did not, and the use is commercial. - -The remaining factors resolve the same way. Source code is expressive creative work, not bare fact. The pattern under examination copies the whole; a complete implementation, ingested and translated in its entirety. And the market effect is the decisive factor, because it is engineered: A derivative marketed as a drop-in replacement for the work it was copied from is not a complement to the original’s market, it is a substitute for it. The Supreme Court has long held that use of a work to satisfy the market demand for the original is the paradigm of an *unfair* use (*Harper & Row, Publishers, Inc. v. Nation Enterprises*, 471 U.S. 539, 566–67, 1985). - -The one case that is always raised in response, *Google LLC v. Oracle America, Inc.*, 593 U.S. 1 (2021), holds less than it is believed to. The Court there assumed *“for argument’s sake, that the material was copyrightable”* and held *“that the copying here at issue nonetheless constituted a fair use”* (*Id.* at 11). - -The material in question was a thin slice of *declaring code*, which the Court found sits, *“if copyrightable at all, further than are most computer programs (such as the implementing code) from the core of copyright”* (*id.*), in a use that built a new platform for a new environment, with the copier **having written the vast majority of its own code**. By comparison: A wholesale translation of an entire implementation, marketed as a substitute for it, is **not** that case; it is the other end of every factor. - -The valuable zones of fair use remain, and they are important. **Commentary and criticism**, quoting from a work to review, discuss or criticize it. **Teaching and scholarship**, explaining concepts and quoting excerpts, is its traditional beneficiary. - -### Conclusion - -The law is neither obscure nor new. The conclusion it yields can now be stated: - -* A machine translation of a protected implementation **is a derivative work**, and an unauthorized derivative **is an infringement**, whatever language it speaks and whatever machinery produced it. -* A work whose expression a machine originated has no human author, no valid new copyright, and without valid copyright **no license can be granted on it**. -* § 103 withholds protection from every part of a derivative in which protected material was used unlawfully. A license is a grant with conditions, and a derivative made outside the conditions **never received a grant**. -* A person who holds no copyright and no grant **can convey neither**, and *every* downstream consumer who received from them received a **void grant**. - -This is a *catastrophic* failure state for all who want to create things good and useful: The projects that inherit only void grants and liability believing they have inherited foundations, the users who depend on code that no one on earth can answer for, the honest builders whose work is buried under a field of plausible-looking impostors. - -### Sources and Authorities - -Statutory references are to Title 17, United States Code. Case citations are to the official or generally cited reporters. Online links are for reference only. Section numbers refer to the sections in which each authority is principally relied upon. - -**Statutes** - -* [17 U.S.C. § 101 (definitions: “derivative work,” “including,” “publication”) - I, II](https://www.law.cornell.edu/uscode/text/17/101) -* [17 U.S.C. § 102(b) (idea/expression) - II](https://www.law.cornell.edu/uscode/text/17/102) -* [17 U.S.C. § 103(a)–(b) (derivative works employing unlawfully used preexisting material) - V](https://www.law.cornell.edu/uscode/text/17/103) -* [17 U.S.C. § 106(1)–(3) (exclusive rights: reproduction, derivative works, distribution) - I](https://www.law.cornell.edu/uscode/text/17/106) -* [17 U.S.C. § 107 (fair use) - VI](https://www.law.cornell.edu/uscode/text/17/107) -* [17 U.S.C. § 501(a) (infringement) - I, V](https://www.law.cornell.edu/uscode/text/17/501) - -**Judicial decisions** - -* [\*Jacobsen v. Katzer\*, 535 F.3d 1373 (Fed. Cir. 2008) - IV, V](https://www.casemine.com/judgement/us/5914b290add7b04934760a54) -* [\*Sun Microsystems, Inc. v. Microsoft Corp.\*, 188 F.3d 1115 (9th Cir. 1999) - IV](https://law.justia.com/cases/federal/appellate-courts/F3/188/1115/629501/) -* [\*Graham v. James\*, 144 F.3d 229 (2d Cir. 1998) - IV](https://caselaw.findlaw.com/court/us-2nd-circuit/1011522.html) -* [\*S.O.S., Inc. v. Payday, Inc.\*, 886 F.2d 1081 (9th Cir. 1989) - IV](https://law.justia.com/cases/federal/appellate-courts/F2/886/1081/19091/) -* [\*MDY Industries, LLC v. Blizzard Entertainment, Inc.\*, 629 F.3d 928 (9th Cir. 2010) - IV](https://law.justia.com/cases/federal/appellate-courts/ca9/09-15932/09-15932-2011-02-25.html) -* [\*Artifex Software, Inc. v. Hancom, Inc.\*, No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017) (settled; dismissed with prejudice Jan. 2018) - IV](https://storage.courtlistener.com/recap/gov.uscourts.cand.305835/gov.uscourts.cand.305835.54.0_1.pdf) -* [\*Thaler v. Perlmutter\*, 687 F. Supp. 3d 140 (D.D.C. 2023), aff’d, No. 23-5233 (D.C. Cir. Mar. 18, 2025) - III, V](https://www.bitlaw.com/source/cases/copyright/Thaler-Perlmutter-Dct.html) -* [\*Burrow-Giles Lithographic Co. v. Sarony\*, 111 U.S. 53 (1884) - III](https://supreme.justia.com/cases/federal/us/111/53/) -* [\*Community for Creative Non-Violence v. Reid\*, 490 U.S. 730 (1989) - III](https://supreme.justia.com/cases/federal/us/490/730/) -* [\*Feist Publications, Inc. v. Rural Telephone Service Co.\*, 499 U.S. 340 (1991) - II](https://supreme.justia.com/cases/federal/us/499/340/) -* [\*Baker v. Selden\*, 101 U.S. 99 (1879) - II](https://supreme.justia.com/cases/federal/us/101/99/) -* [\*Computer Associates International, Inc. v. Altai, Inc.\*, 982 F.2d 693 (2d Cir. 1992) - II](https://law.justia.com/cases/federal/appellate-courts/F2/982/693/137252/) -* [\*Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith\*, 598 U.S. 508 (2023) - VI](https://www.supremecourt.gov/opinions/22pdf/21-869_87ad.pdf) -* [\*Google LLC v. Oracle America, Inc.\*, 593 U.S. 1 (2021) - VI](https://supreme.justia.com/cases/federal/us/593/18-956/) -* [\*Harper & Row, Publishers, Inc. v. Nation Enterprises\*, 471 U.S. 539 (1985) - VI](https://supreme.justia.com/cases/federal/us/471/539/) - -**Administrative and policy authorities** - -* [\*Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence\*, 88 Fed. Reg. 16190 (Mar. 16, 2023) - III](https://www.federalregister.gov/documents/2023/03/16/2023-05321/copyright-registration-guidance-works-containing-material-generated-by-artificial-intelligence) -* [\*Zarya of the Dawn\*, U.S. Copyright Office (Cancellation Decision, Feb. 21, 2023), Registration No. VAu001480196 - III](https://www.copyright.gov/docs/zarya-of-the-dawn.pdf) -* [U.S. Copyright Office, \*Copyright and Artificial Intelligence, Part 2: Copyrightability\* (Jan. 29, 2025) - III](https://www.copyright.gov/ai/Copyright-and-Artificial-Intelligence-Part-2-Copyrightability-Report.pdf) -* [H.R. Rep. No. 94-1476 (1976) - V](https://law.resource.org/pub/us/works/aba/ibr/H.Rep.94-1476.pdf) - -## Evaluating a Reticulum Implementation - -### The Intention Of This Guide - -Very few user of network infrastructure can verify the software they run. This is true of the reference implementation as well, and it is precisely why it was engineered the way it was: the entire Reticulum protocol implementation - the transport logic, the cryptographic interfaces, the routing - is less than 15,000 lines of code, including its extensive comments and detailed logging. The reference is *designed* to be readable by a human in a reasonable sitting. That is a deliberate decision, and it is the standard against which every other implementation should be measured: An implementation that asks for trust while being structurally unreadable is asking for the absurd. - -Because most users cannot verify the code directly, the evaluation of the *project* is the evaluation of the software. The framework below exists so that this evaluation can be performed in minutes, by anyone, without expertise - and so that each new “implementation” does not require the kind of full investigation that would otherwise be if not impossible, prohibitevly expensive. - -### Two Questions That Resolve Most Cases - -Other considerations in this guide is further corroboration. These two questions allow you to decide the case quickly in most instances, and they should be asked first. - -**Question 1: Does this speak the protocol, or was it just generated from the reference?** - -The copyright and license realities, described in [What The Reticulum License Is](#brandolinis-what-the-license-is), is the legal foundation of this distinction. The protocol is public domain: A *genuine* [clean-room](#brandolinis-cleanroom-delusion) implementation, reasoned out and written by humans, is free and welcome, and owes nothing to any license. But an implementation that was *directly generated from* the reference - reproduced by a copy process, whether manually or through a machine or software - is a derivative of the licensed work, and is subject to both the license’s conditions, and to the ordinary copyright law examined in [Copyright & The Two Legal Layers](#brandolinis-copyright-and-layers). A primarily machine-generated copy without substantial human creative input is not merely unlicensed; it is a copyright violation that would exist under any license at all. - -**Ask**: Does the project say where its protocol knowledge came from? Does it credit the reference implementation, or claim “ground-up” status while its own history shows continuous ingestion of the reference’s source? Does its license carry the original copyright notice? The answers tell you, within minutes, which of two very different kinds of project you are dealing with. - -**Question 2: Who, or what, is the agent of the work?** - -This distinction, established in [Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution), is the trust foundation of everything else. Is there a human who decided what to build, judged what was correct, rejected what was wrong, understands what was shipped - and can answer for it, line by line and decision by decision, *without machine assistance*? Or is the machine the agent, with the human’s role reduced to the name, the account, and the signature? - -> This test has a sharper form: **would the project be meaningfully different if someone else had pressed the button?** If the answer is no - if any person could have been the signature and nothing would change - then the “author” is not the agent of the work. They are its facade, and the project is, in the most literal sense, worthless and unowned. - -A project with no owner is a project no one can answer for, and infrastructure with no one to answer for it has no place carrying other people’s traffic. - -### Other Signals To Look For - -Once the first two questions are answered, the following signals determine the verdict. None is alone decisive; together they form a pattern that is difficult to fake in the directions that matter. - -#### Provenance and attribution - -* **Credit to the reference.** Does the project name Reticulum’s reference implementation - in its README, its website, its licenses, and the code? Does it present itself as a port - or as a “ground-up” origin story? Absence of credit, in a field where every implementation - owes its existence to the reference, is not an oversight; it is a decision. - -* **License compliance.** For a derivative, the license conditions apply: the copyright - notice and permission notice in all copies and substantial portions, the harm condition, - the AI-training condition. A project whose license file claims a fresh copyright in the - operator’s name, over work that was generated from the reference, has failed the minimum - bar of honesty before any code is read. - -* **Disclosed provenance.** Is the use of machine assistance stated openly, or does the - project maintain machinery to conceal it - commit hooks that refuse attribution of - generation, contribution policies that declare machine output the human’s own, scrubbers - for personal traces, fictitious contributor identities? The first is the mark of a human - using a tool. The second is the mark of a record being manufactured. - -* **The author’s history.** Is there a prior record - projects, contributions, a body of - work that a person can look at and reasonably assess? A sudden, fully-formed “decade of experience” - appearing from nowhere, attached to a project of unprecedented scope, is a story that - contradicts itself. - -#### Agency and answerability - -* **Who answers the questions?** When technical questions are asked, does a human answer - with substance - going deeper, explaining trade-offs, admitting uncertainty - or do - answers deflect to process (“the CI verifies it”, “the proofs prove it”, “the audit - covered it”)? Are the answers themself machine generated? Deflection to process is - the substitute’s answer pattern: It names the machinery instead of the reasoning. - -* **Velocity commensurate with authorship.** Is the output volume compatible with human - authorship, review, and judgment? An implementation ten (or 42!) times the size of the reference, - produced in a fraction of the time by a single person, is not evidence of brilliance; it - is evidence of unchecked generation. Churn is part of the same signal: A project that - deletes half of everything it adds is not building; it is looping. - -* **Signs of development, growth and learning.** Do reversals exist? Contested decisions? - “We tried X and it was wrong, so we did Y”? Humans change their minds, admit mistakes, and - leave the wear of judgment in their history. Pure generation shows “the polish of its - absence”: uniform register, endless renaming, and errors discovered by the generator’s own - later runs, recorded as if they were planned milestones. - -* **Standing behind failures.** When something is wrong, does the human say “I chose this, - here is what I got wrong, here is the fix”? Or do corrections arrive as process language, - with nobody accepting that a decision was even made? A project whose history contains no - first-person acceptance of error contains no author. - -#### Engineering reality - -* **Honest benchmarks, or none.** If performance is claimed, is the baseline real - stock, - current, interpreted RNS, on the modern interface, at equal policy, with methodology - published in advance and reproducible by anyone? - -* **Interop with reality.** Is compatibility verified against the *current* reference - implementation - on real hardware, including the radio and serial interfaces this - protocol family lives on - in both directions, over long transfers, uptimes and edge cases? - -* **The readability standard.** Can a competent human read the core of this implementation - in a reasonable sitting, the way the 15,000-line reference can be read? Was human - readability a stated design goal, or an afterthought? A project that cannot be inspected - is a project asking for trust it has not earned and will never attend to. - -* **Nonstandard inventions.** Does the project “improve” on the protocol with its own - modes - new channel behaviour on shared spectrum, new rendezvous mechanisms, new crypto, - “post-quantum” replacements, “turbo” variants? Inventions that change behaviour on a - shared network are not features; they are forks of the airwaves, imposed on every other - node that shares the medium. A project that invents on top of a protocol it has not yet - correctly implemented is not advanced; it is dangerous to its neighbours. - -* **The public record.** The commit history is public. Read it. - -#### Network citizenship - -* **Honest presence.** Does the project identify itself honestly on the network, or does - it masquerade as something it is not? - -* **Coexistence.** Does it respect the shared medium and network conventions? A node that - is a bad neighbour on the spectrum is a hazard regardless of how well its code works in isolation. - -* **Community participation.** Does the author participate in the community as a member - - answering questions, contributing, learning - or only appear to advertise? The second is - not participation; it is exploiting the community as a broadcasting target. - -#### Monetary and influence signals - -* **Money under the claim.** Donations, funding, or commercial offers premised on claims - like “89× faster” or “formally proven” convert false marketing into a different category - of problem. When money changes hands on the strength of fabricated measurements, the - stakes are no longer theoretical. - -* **Tokens and “investment” vehicles.** Any implementation that surrounds itself with a - crypto token, a “coin”, or an investment pitch should be treated as what it is: A project - whose primary purpose is to earn the controller a profit, not open networking. - -* **Marketing-first ordering.** A polished website on day three, a twelve-language marketing - presence, a “benchmarks” page and a “get started” funnel existing before the protocol works - is a massive red flag. Real builders ship, then talk. The inverse order is the signature of - a project whose goal is attention. - -### The Conformal Surface - -One warning before the synthesis. When you evaluate any project, assume - as a working hypothesis - that its surface is an investment. The marketing, the design, the badge collection, the “proof records”, the fluent and confident prose: This is exactly where substitution invests, for the simple reason that it is where the audience looks first. Fluency is the *minimal conformal surface* of understanding, and the surface is not evidence of anything except that the author *wanted to look convincing*. If you find yourself impressed by the surface before you have asked Question 1 and Question 2, you are doing the evaluation in the order the substitute operator hoped you would. - -### Possible Verdicts - -The framework proposed here can yield three different verdicts: - -1. **Meets the standard.** A named human who can answer for the work and demonstrably does; - provenance disclosed; attribution given; honest benchmarks; interop - demonstrated against current, real RNS on real media; readable code; a maintenance - history that shows growth and learning. This standard is not high. It is what the - reference implementation has met, in public, for over a decade, and it is what any - project that wants other people’s traffic should be willing to meet in turn. -2. **Experimental, but honest.** A project that clearly labels itself as in-progress and - experimental (“do not use this yet”, in plain words, as the first thing a user sees) - discloses its provenance, credits the reference, and is being developed in the open by - humans who answer questions. Such projects should be encouraged. They are how the - ecosystem grows, new ideas are tested, and the framework above is designed to make an - honest experimental project *easy* to recognize, not hard. -3. **Does not meet the standard.** Unanswerable authorship, concealed provenance, fabricated - measurements, invented protocol behaviour, absent interop, marketing ahead of substance. - Such projects should be treated as unproven regardless of their surface, and as hazards - if they invite users onto shared networks. The community owes them exactly the - skepticism they have earned, and not a moment of engagement beyond that; the network’s - time is better spent elsewhere. - -### These Standards Are Reasonable - -I’ll close this guide by stating what these standards are *not*: They are neither a demand for perfection, nor a ban on machine assistance. Assistance is useful, when it is used to arrive at *higher levels of synthesis* than unaided work. It is not a requirement that an implementation be a clone of the reference; neither is it a requirement that it invents everything from scratch or aspires to be a [“clean-room”](#brandolinis-cleanroom-delusion) implementation. Good implementations of the protocol are very welcome, *however they come*, but working on one requires *serious* work, considerable skill, and **deep competence**. - -The reference implementation has met this standard for years, and its users know what that looks like. - -## The Movement Of A Dozen - -### Presentation, Elongated - -Whenever Reticulum defends itself, its license, or its community - or simply continues to exist - a familiar chorus appears. The register is always similar: *concern*. Concern about the direction of the project. Concern about its license. Concern about its governance. Concern about its “bullying” of “good-faith contributors”. Concern about the future of the ecosystem at the hands of a single maintainer. The chorus speaks in the plural, presents itself as a body of independent voices, and insists, with some frequency, that it is… *not alone*. - -The implicit claim under examination in this section is that there exists a *movement* of concerned individuals, spread across the ecosystem, all worried about Reticulum, and therefore (for some reason) deserving of a place at the table of decisions. - -A resolution of this claim requires one step: Count them. - -### Headcount - -This “movement” does have a headcount. It is not in the hundreds, and it is not in the dozens. It is a small number of people who appear, in various combinations and under various names, in the following roles: - -* Creators of the machine-generated “Reticulum implementations” examined in this chapter. -* Creators of articles praising those implementations and lamenting the state of the - reference, its moral shortcomings, its unavailability or lack of something it already has. -* Commenters “defending” the implementations when they are evaluated reasonably and critically, - most often by attempting simply to derail the discussion. -* Critics of the Reticulum License on “open source” grounds. -* “Concerned community members” objecting to the maintainer’s conduct. -* Campaigners on online platforms; and -* In some cases, a single individual performing *several* of these roles at once, under - different names, in the same week. - -The overlap is well documented at this point, across time and different platforms. The same phrasing, the same arguments, the same links, the same reactive reflexes, the same handful of accounts appearing in the same threads at the same moments. When the sockpuppets are unmasked (and they frequently unmask themselves) the cast does not grow, but rapidly shrinks. - -This is the most interesting single fact about the “movement”: **It isn’t one. It is a synthetic cast**, held together mostly by LLM-glue, reddit handles and hysteria. A cast is not a constituency. It performs; it does not represent. The difference matters quite a bit, because a great deal of this “movement’s” imagined power comes from the assumption that the *plural form* is actual evidence of plurality. If that sentence seemed opaque, notice how many times a statement or article uses “we” instead of “I”, and it will be less so. - -### Recycled Wine, Fermenting - -The cast is not only small, it is also, by any evidentiary standard, *forgetful*. Every major claim this “movement” advances has been advanced before, answered in public, with evidence, and often years ago - and is then advanced again, unchanged and completely ignoring any rationale or evidence presented previously (to the same people), as if the answer had never been written. - -* **The license argument.** Reticulum is “not really open source” because its license is not - approved by a particular orthodoxy. This is answered (now in a new and updated version, for the - fifth time) in full in [Open Source Means Open Source](#brandolinis-opensource). It was also - answered in public, at length, at the time of the license change - including the - arithmetic of who is actually restricted by which license. It is repeated *ad infinitum* anyway. - Quite a conversation starter, apparently. - -* **The distribution argument.** Reticulum is “not properly distributed” because it is not - packaged as a `.deb` or on some favoured catalogue. Reality: It is available on practically every - platform with a single command, packaged by the community in a wide range of channels, and - capable of updating itself over its own network. The argument has been answered; it is repeated. - -* **The “stepped back” narrative.** When the primary mirror of the project moved away from - a public internet platform, it was spun as the founder leaving for good, or now taking on dictatorial - powers, or abandoning his creation, or any variation or nonsensical combination thereof. - Those spins always center around justifying that *a vaccuum now needs to be filled*. - There was no such vaccuum. The narrative is repeated in various forms whenever convenient. - -* **The enclosure narrative.** The license is reframed as a “power grab” or an “enclosure” - of the commons - as if a license whose protocol is public domain and whose conditions - restrict only harm and appropriation were a form of enclosure. The [most recent](https://gaggl.com/blogs/2026-09-05-enclosure-by-good-intentions/) (and mostly incoherent) - expression of this narrative is, once again, fluent, polite, and factually hollow. The - bizarre arguments supporting the “article” include: Defunct and abandoned forks presented - as living, community forks bearing the very Reticulum License mislabeled as AGPL license vehicles - (created by “concerned users”), companion app forks correctly retaining the Creative Commons - licensing used as further evidence of the same and completely abandoned projects propped up - as being “the ecosystem”. Another case of LLM generated nonsense that the “author” didn’t bother checking. - The intent is clear here, simply produce more *mass* of the bullshit, so it can be further - referenced, regurgitated and linked to. - -* **The “bullying” narrative.** Whenever the community declines to treat manufactured - controversies as legitimate, or calmly documents the falsity of claims, or (Lord forbid) - moves a forum post for violating clearly stated forum rules, - the response is the same: The documentation is “bullying”, the community is “excluding” people, the - maintainer is “unprofessional”. In the most recent example, those accusations were made by someone who, in the - same week, publically called for the Reticulum maintainer’s “removal from the equation”, forged a moderator’s - username, attempted to drag unrelated individuals into the campaign, and presented a new - round of outright falsehoods. Contrary to popular belief, the accusation of incivility - is not the last resort of the wronged; it is the standard reflex of the exposed. - -None of the “arguments” engages the answers given repeatedly, and extensively. That is the point, and the strategy. The arguments are not produced for the purpose of being engaged; they are produced for the purpose of *being repeated* - the same wine, poured into new bottles, on a cycle. Each cycle is short, cheap to produce, and (thanks to freely available fluent text generation) now nearly effortless. The intended audience is not the community, which has heard it all before. The intended audience is the newcomer, who has not. - -### The Method of Madness - -The method behind the pattern is consistent and simple enough to be described precisely: - -**Bullshit asymmetry** - -Producing a fluent, plausibly-sourced smear is now the work of minutes. Refuting it to the standard this manual demands is the work of days, and the refuted smear is replaced by the next one within the week. This asymmetry isn’t just a fine point of the campaign; **it is the campaign**. The goal is exhaustion - the slow conversion of the community’s time and attention from building to defending. - -**Weaponized concern** - -The register of the campaign is almost always the register of solicitude: “we are worried about the ecosystem”, “we just want what is best for the community”, “this is about the future of the project”. The concern is directed, without exception, at the things the campaign wants changed, and the change it wants is always the same: **The replacement of the reference’s authority with its own**. Concern is the *costume*; capture is the choreography. It is the oldest form of cheap power grab in the book. - -**Event seizure** - -Any event - a license change, a platform move, a forum moderation decision, a subreddit closure, a security notice, even the most banal messages from maintainers or known contributors - are seized, stripped of their actual context, and re-narrated as evidence of the same pre-written story. The subreddit that was closed after its moderator vanished and ceased moderating (according to *prior agreement with said moderator*) becomes “the maintainer shutting down dissent”. Moving away from a highly centralized online platform becomes “the founder retreating”. The license becomes “the enclosure”. The pattern is so reliable that the story could be pre-written by a lightly mechanized quill; only the date changes. The actual real-world context of these events is documented, in public, in each case; the re-narration proceeds regardless. - -**The legitimacy circuit** - -The campaign invests heavily in the places where legitimacy is minted: Conference talks, blog platforms, social threads, and forums - presented as if these appearances constituted some sort of ecosystem-wide groundswell rather than the same dozen people moving through the same circuit. The LLM-generated articles cite the LLM-generated ports; the ports are praised in the articles; the managers of the ports spread the articles, and echo them on the forums and internet platforms; the conference presentations cite the forums. Random bypassers with the need for a cause watch the conference presentations and take on the holy duty of Internet Soldier. The circuit is closed, self-referential, and (to a reader encountering it fresh) convincing. It is also, entirely, **a loop of bullshit**. - -**The prior-work minefield** - -The machine-generated implementations have a consequence beyond attention: They flood the space with fake “prior work”. The effect, as the license discussion predicted in the beginning of 2025, is the creation of a minefield of sloppy repositories that can later be matched against any genuine project, and attempt to claim prior art, or even worse: Dissuade actual, honest developers or builders from taking up a real project they might have succeeded with, because they now believe it has already been done. Just as importantly, it also buries real efforts under machine-generated noise. The machine-ports are not failed attempts to contribute. They are (disregarding intentions, but effectively) the campaign’s long game, deployed as inventory. - -### A Persistent, But Fragile Triad - -This chapter as a whole has documented three fronts: The legal-ideological attack on the license ([Open Source Means Open Source](#brandolinis-opensource)), the substitution and usurpation pattern in machine-generated implementations ([Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution)), and the takeover narratives examined in this section. They are three fronts of the same operation, run by the same cast. - -* The license attack and other urgency-engineered narratives are the *cover*: They supply - the ideological justification for treating the reference as illegitimate, and needing replacement. -* The ports are the *inventory*: they supply the “replacements” that the narrative claims - the ecosystem needs. -* The takeover narrative is the *objective function*: the repeated, persistent, escalating claim that - the reference should be replaced, and that the speakers themself are the natural heirs. - -Each front depends on the others, and all three depend on a single, fragile assumption: That their audience cannot count. Restore the count, and the charade collapses into what it is: - -**A small group of people, obsessively orbiting a technology they did not build, synthetically producing the -appearance of a constituency they do not have, in service of a takeover they cannot justify.** - -### What The Pattern Is Not - -It is important, to also say what this pattern is *not*, exactly because the campaign depends on being mistaken for such things. - -**It is not criticism** - -The Reticulum community has always had lively, rigorous criticism - of decisions, of priorities, of direction - and it has always engaged that criticism on the merits, and changed course and development priorities many times. The license discussions at the time of the change were extensive, public, and answered in full, including with the critics themselves. Core protocol decisions were always discussed and evaluated in the community. The complex coordination and execution of several protocol upgrades between a loosely organised group of developers, node operators and users have succeeded surprisingly fluidly. All of these are success stories that the “critics” **always** ignore. Real criticism engages the content and accepts evidence. The campaign engages nothing. - -**It is not a community** - -Communities *build things*, maintain them, and have skin in the game; long-term users, operators of nodes, authors of real applications, people whose traffic flows through the network every day. A community has a stake. The “movement” only has an agenda. - -**It is not a debate** - -A debate has parties who evaluate arguments and evidence, and as a function of that, the debate moves forward, perpetually. The campaign’s arguments, as shown above, are not designed to survive contact with answers, but to be repeated past them. You cannot debate a loop. You can decline to perform for it, though. - -### Intent & Consequence - -While it would provide for interesting speculation, we do not need to know what the individuals in the cast *intend*, and this document will not speculate about their inner lives. People may believe their own narratives entirely, or partly, or not at all; **the pattern is the same in any case**, and the pattern is what matters here, since it is the exact thing that has been so insidiously destructive and resource wasting for the actual Reticulum project and community, ever since Reticulum was more widely discovered, popularized, and became a prime target for all sorts of grift known to man. - -The consequences are very real: - -* **Time**. The community’s scarcest resource is the attention of skilled people who actually build - and maintain the network. The campaign converts that attention into reactive defence, on a cycle - with no end. - -* **Harm**. Hastily machine-generated implementations, marketed as drop-in replacements and carrying - traffic onto shared networks, mislead users and can damage the network and its neighbours. - We, the actual reticulum community, all rely on these networks every day, and it is my belief - that we need to defend them. The fabricated measurements and laundered authorship examined in this chapter are not - editorial embellishments; they are the mechanism by which real users are induced to run - unverified, and often destructive software. - -* **Erosion of the currency of trust**. Every manufactured scandal, every fluently polite smear, every - fabricated “movement” debases the currency of trust and degrades it for **everyone**; - including, most of all, the honest projects that come after. This is deep harm, and - it is the primary reason this chapter exists. - -Whatever the intent - profit, control, attention, or a simple, obsessive inability to stop - the mechanism is the same, and the response should be *calibrated to the mechanism as a whole*, not to the individual person acting as the current avatar for it. - -### Recognition & Response - -For anyone who encounters the pattern fresh, here is a proposal on how to recognise it, and what to do about it. - -**Recognition:** The pattern is recognisable in minutes, once the signs are known: - -* Claims that have been answered before, repeated as if never answered. -* The register of solemn concern applied, without exception, in one direction. -* A severe allergic reaction to answering any questions posed. -* A cast that is small, overlapping, and frequently anonymous, with the *plural form* doing the - work of plurality. -* Sockpuppet theater: The same person or footsoldiers thereof appearing as several “supporters” - (and occasionally slipping up mid-performance). -* Articles, ports, and forum threads that cite and reference each other in a closed loop. -* The immediate, reflexive turn to “bullying”, “exclusion” and “toxicity” whenever a claim is refuted. -* A complete absence of engagement with the substance of the answers given, and a complete - absence of stake in the network itself. - -**Response:** A constructive response is one that is proportionate, and does not cost the -community its resources, sanity and dignity: - -* Engage the content once, on its merits (if any), with evidence, in public, and then refer to the - existing records. The answers already exist; they do not need to be re-derived for every new - bottle of piss-sour burgundy served from a man behind the curtains. I will do my best to keep this - chapter up to date with such answers and records, for easy reference, but other great resources - exist. -* Do not become part of the theater performance. The “movement” feeds on engagement in *any* form; - attention is its fuel, and outrage is as good as endorsement for them, if not better. -* Do the headcount. When the audience is reminded that a “movement” is a dozen people - recycling answered falsehoods, its projected power dissipates. The power was never real; - only the audience’s own assumption of scale can make the illusion effective. A polished-looking - “ground up” implementation might look like the work of many people, but is most likely - just the result of an LLM looping on a laptop for a couple of months (this is weird, but it is - the reality we now live in). Four different users suddenly singing in unison may look like the - representatives of a broader sentiment, but is most likely the same person hiding behind the same keyboard. -* Then, spend the saved time on the networks: On building real infrastructure, putting up nodes - and antennas, on real development, real maintenance, real coexistence, and on the people - *who actually use Reticulum*. The things that we can only do *together*, as humans. That is - what the fake “movement” cannot do, no matter how fluent and smooth its productions become. - -## Network Health & Coexistence - -### Substance Of The Networks - -Before examining health, we should consider what is being protected. The networks are not a repository. They are not a website, a social presence, or a set of marketing pages. They are a habitat: A living commons of communication, carried by, from a catious estimate, an intermesh now consisting of over a hundred thousand physical devices, embedded in a plethora of applications and packages, transporting terabytes of traffic every single day - real messages, real coordination, real people - across radios, serial lines, phones, laptops, and backbone servers, in cities, places and ways that no other network reaches. - -The protocol belongs to humanity; no one owns it. The network belongs to everyone on it, and its membership is defined by nothing other than participation: A node on a mast, a phone in a pocket, a server routing for its neighbourhood, a developer building on the commons. The network has no gatekeepers because it needs none; it is held together by the same physics and mathematics everywhere, and by a shared expectation of how members behave. That expectation - unwritten, unenforced by any central authority, and just as important as any code - is the subject of this section. The *Zen of Reticulum* calls the software a habitat. A habitat is healthy or unhealthy as a whole, and the health of this one is decided, every day, by the people who live in it. - -### Health Is A Property Of The Whole - -A network is not made healthy by the excellence of any single part. It is made healthy by the behaviour of every part on the shared medium, and how they interact. And the shared medium is the point. When your node transmits, it transmits into the same spectrum, the same line, the same time, the same mathematical address space, the same bandwidth that its neighbours depend on. - -The network cannot be certified by any single project’s claims. A node that is a bad neighbour, that invents its own channel behaviour on shared spectrum, that announces without discipline, that spawns tens of thousands of links to scrape the network, that floods and retries without mercy, is a hazard to every node that shares its medium, regardless of how well its “creator” imagines its own code to work in isolation. - -On a shared medium, “my software works” is not a complete sentence. The complete sentence is “my software works *and coexists*”. A network’s health deteriorates with its worst-behaved member, and every member has a responsibility to not be *that* member. There is no final, technical solution for this; the solutions are human. - -### A History Of What Works - -Let’s recall, because it is the ground truth this entire thing rests on, that the coexistence model has *worked* - is working, right now, **at scale**. Reticulum’s protocol decisions have been discussed and evaluated in the community, openly, for years. Protocol upgrades have been designed, coordinated, and executed between a loosely organised group of independent developers, node operators, and users - and have succeeded, repeatedly, without any corporate board, without any central command, and without the collapse that the skeptics predict every second week (while screaming for *GOVERNANCE*). The reference implementation has been developed in the open for over a decade, by a maintainer who answers for it, fixes bugs or issues within hours of their discovery, and is set down in code deliberately kept small enough (the entire protocol implementation under 15,000 lines, including its comments) for a human to read, verify, *and challenge*. - -This is the empirical baseline that every narrative examined in [A Movement Of A Dozen](#brandolinis-movement-of-a-dozen) conveniently ignores, in order to justify: - -* The claim that the project is a one-man dictatorship -* The claim that the community is excluded -* The claim that the networks need saving from their own founders - -The **reality**, documented and reproducible, is that a loose community of humans coordinates complex technical evolution *without* the apparatus the narratives insist is missing. The health of the networks is not a promise for the future; it is a demonstrated property of the past and present, under the leadership and community model that brought it into existence and navigated it through difficult decisions and situations over a decade. **My job is to keep it that way**, and you can be damned certain that I take it seriously. - -### The Role Of The Individual - -The networks are built by physical acts and human coordination. You cannot prompt an antenna into existence. No model can string up a dipole, or stand in the rain aligning a radio, or coordinate with a neighbour three valleys away about who covers which repeater frequency, or sit down with a newcomer and explain why their first node didn’t announce. These are the acts that make a network *organic* - present in the physical world, resilient because it is *real*, valuable because it is *human*. The machines and technology we use are tools aimed at a well-defined, openly described goal. But the networks themself are composed of intentional human actions, and every node operator is infrastructure for other humans. - -This has a corollary for the individual: You do not need permission, and you do not need to be an expert, to participate. Run a node. Help a neighbour. Ask questions in the community, and answer the questions you can. Build something small at first. The network grows by these acts, and they are the most meaningful ones - the things that can only be done together, as humans, in the physical world. The parasitic “movement” that haunts the peripheries of this ecosystem cannot do any of this. It cannot build a node because it does not exist, in any real connected and human sense; only people can, and people are precisely what a self-recurrent loop lacks. - -### The Place Of New Implementations - -The future of the network includes more implementations - and they are welcome. This chapter is not an argument against implementing Reticulum. The protocol is public domain, by design, and the network is made stronger by genuine diversity: More *good* implementations, honestly made, mean more resilience, more experimentation, more people who understand the protocol deeply *because they have built it themselves*. That is the goal: **For people to understand and own the technology they rely on**. Not to spew out 600,000 line rust ports that fills no meaningful gap or purpose. - -What the ecosystem needs, from every implementation, is the ordinary floor that [Evaluating A Reticulum Implementation](#brandolinis-evaluating) establishes, and that the reference has met for over a decade: A human who made it and answers for it; provenance disclosed; attribution given; benchmarks honest or absent; interop demonstrated against the real network; code a human can read. And it needs the one thing that costs nothing and changes everything: **Honest labeling**. - -An experimental implementation that says, plainly, as the first thing a user sees: “This is in-progress, experimental, do not depend on it yet, but help if you can”, that is a gift to the ecosystem. It contributes, it invites participation, and it protects users. - -The opposite, the polished “drop-in replacement”, does damage far beyond its own users: It dissuades the honest developers who might have built *the real thing*, because the polished slop persuades them the work is already done. That dissuasion is one of the quietest and most expensive harms in this entire affair, and the antidote is honesty at every project’s door. - -### Expanding The Commons - -Coexistence, reduced to its essentials, is a short formula; the duties of membership in a network no one owns: - -* **Be honest.** Say what you are, what you built, how you built it, and what you used to - build it. Label experimental work as experimental. Credit the work you build on, including - the reference implementation, in the licenses and in the prose. Concealment is not a - defence; it is a very clear diagnosis marker. - -* **Be owned.** Have a human who made the work and can answer for it. Use assistance, don’t - substitute. Never sign what you cannot answer for. The chain of accountability is the - community’s only verification at scale, and every link in it is a *person*, not a facade. - -* **Be a good neighbour.** Respect the shared mediums. Do not invent new behaviour in our - shared space and call it progress. The network belongs to its other members as much as - to you. - -* **Verify against the network.** Compatibility is a property demonstrated with actual nodes, - on actual media, against the actual current protocol, over a period of *years*, not 48 hours. - And not a claim pinned to an old snapshot over loopback. - -* **Participate as a member.** Ask, answer, help, build. *Advertising at* the community is not - the same as being part of it, and it is **not** the first thing you should do. - -None of this is a high bar. It is the bar the reference implementation has met, in public, for over a decade, and the bar the ecosystem has adopted organically itself, successfully, through every protocol upgrade it has ever coordinated, and everything running on Reticulum today, that people actually use. It is not a new requirement; it is the existing, working culture of the networks, now written down so that newcomers can find it and so that those who would exploit us cannot claim they never saw it. - -### Defence Of The Commons - -The networks need defending - not because they are under severe physical attacks (yet), but because attention, talent and resources is being stolen from them, trust is being mined against them, and its newcomers are being harvested by the machinery documented in this chapter. Defence, for a commons, has specific shapes, and it is not what the theater would like it to be. - -* Defence is **documentation**: Records kept public, of what was claimed, what was - answered, and what turned out true. -* Defence is **a headcount**: Calm, unexcited reminders that a “movement” might just be a dozen - people having a tantrum. -* Defence is **honesty in action**: The visible, ordinary practice of the good behaviour - by the people who actually run the networks. -* And defence is **building**: Every node raised, every page of *real* documentation, every - honest implementation, every new user helped into the community with the knowledge to know - what they are doing is more durable than any rebuttal, because it adds to the thing being - defended. - ---- - -**The theatre cannot keep up with a network.** - -**It can only orbit it, while it recycles its own sour piss.** - ---- - -A lot of us rely on these networks every day. They carry our messages, our coordination, our friendships, and in some places, our safety. That is exactly why they are a target: - -They are valuable, free, and very hard to shut down. The response is not to become aggressive. It is to be *more* of what the network already is: Real, open, owned, and shared. - -*Mark* - -## Addendum A: The Prns Case - -### Purpose & Scope - -This section provides a concrete example, which future evaluations can use as a template. Its purpose is not to be the last word on this particular subject, but to be an example of how the ideas in this chapter can be applied, and to make the next such examination easier. - -The subject of this case study is **Prns**, a project published at the repository `github.com/KenAKAFrosty/Prns`, presented as a “ground-up implementation of Reticulum, written in Rust”, marketed as faster, safer, and more strictly tested than the reference, widely promoted with the claim of “up to 89× the throughput” of the reference implementation, and an “author” actively engaged in the tactics described in [A Movement Of A Dozen](#brandolinis-recycled-wine) on online platforms. The record described below is the project’s own, and sourced from its GitHub repository, analyzed locally. - -### The Project In Brief - -* **Time span.** 3,374 commits between 2026-05-26 and 2026-09-12: approximately 110 days, - at a mean rate of ~31 commits per day. Monthly: 132 / 1,137 / 1,097 / 805 / 203. - -* **Authorship.** 3,252 of 3,374 commits (96.4%) are authored by a single account. A - second identity, “Prns Tests [tests@example.test](mailto:tests@example.test)” - a fictitious identity on a reserved - domain - contributed 35 commits, every one of them website and marketing content. - -* **Size.** 3,519 tracked files; 1,983 Rust files; 636,226 lines of Rust; approximately - 22.7 MB of Rust source; 69.5 MB of total repository content. - -* **Scope claimed.** A daemon (“drop-in replacement” for the reference daemon), embedded - firmware for three microcontroller families (including six ESP32-S3 board variants and - nRF52840), SDKs and bindings for nine languages, a browser-based node and web flasher, a - twelve-language website, a benchmark apparatus with its own publication and “proof” - pipeline, and a validation hub with oracles, fuzzing, mutation testing, and formal-proof - claims. - -* **Marketing claims.** “Ground-up implementation of Reticulum”; “up to 89× the - throughput”; “48× smaller peak-memory footprint”; “33× the energy efficiency”; - “Measured, not just claimed”; “Enforced, then audited”; “byte-for-byte wire parity with - the reference implementation”; “works with Sideband, NomadNet, MeshChat, etc.”; - “no_std (no alloc required either)”. - -* **Status at the time of writing.** Version 0.3.7; published version history begins at - 0.3.0. The repository continues to receive commits. - -### Question 1: Is this a clean implementation, or was it directly generated from the reference? - -The first of the two deciding questions from [Evaluating A Reticulum Implementation](#brandolinis-evaluating) is settled by the project’s own record. An actual [clean-room](#brandolinis-cleanroom-delusion) implementation, reasoned out and written by humans, would owe nothing to any license. The data available shows clearly that Prns is not that. - -* **The founding state.** The first commit, “Scaffold Personal Reticulum suite”, contains - the scaffolding of not one but two ports - `personal-rns` and `personal-lxmf`, the latter - being the reference’s companion messaging protocol - together with a daemon scaffold and - a 233-line founding document, `docs/build-ethos.md`. That document declares the project’s - goal as a “performance-focused drop-in-replacement for rnsd” and its governing principle - as: “Port the contract, not the implementation.” It further states directly to the - machine-loop, that **“You owe the reference fidelity at exactly two boundaries: - The wire… The behavior. … Between those two boundaries, the model is yours”**. - At this point, the very first commit, effectively the whole of the implementation design - and any relevant decisions are handed over to the LLM. - -* **Continuous ingestion from the reference.** 506 commits reference RNS or Reticulum - source behaviour; the word “parity” appears in 49 commit messages. Commits cite the reference - implementation’s own source locations (for example `Transport.py:1367`) when correcting - their own divergences and mistakes. - -* **Byte-level fidelity as an explicit goal.** Within 48 hours of the first commit, the - record shows a “crypto adapter - vetted primitives, byte-exact vs RNS 1.3.1”, and a wire - implementation matching the reference’s packet header layout. The project’s own - validation apparatus decodes wire vectors using the reference implementation’s own - packet parser, and the interop suite consists of real, stock-RNS peer nodes - that is, - the reference source itself, used as the ground truth against which this LLM output is - checked, and committed as accepted and verified after 48 hours of the project’s recorded start. - -* **The marketing mismatch.** The same record that documents all of the above describes the - project, publicly, as “a ground-up implementation of Reticulum” - while declining, on its - website, to link to the reference implementation at all: the entire tree mentions the - reference’s author in six files, and the website in none. - -Conclusion: the project’s own history and commit record establishes that it was generated directly from the reference implementation’s source code, not reasoned out from the public-domain protocol. It is a derivative of licensed work within the meaning of [the Reticulum License](#brandolinis-what-the-license-is) and [general copyright law](#brandolinis-copyright-and-layers). The “ground-up” claim, which carries its entire legal and marketing posture, is disproven by the project’s own history. - -### Question 2: Who, or what, is the agent of the work? - -The second deciding question is answered by the application of [Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution) criteria to the project. - -**Scope that no human could have authored** - -636,226 lines of Rust in 110 days is approximately 5,800 lines per day, every day, including weekends - with 149 commits recorded between midnight and 06:00, and Saturday the single busiest weekday. No human review or decision capacity is commensurable with this volume; the review capacity and the output volume are structurally incommensurable, which is the definition of unauthored scope. - -**Uniform generation** - -The record shows no sign of human development, growth, or learning: No contested decisions, no style drift. Instead: endless renaming (in the first 48 hours: `State → EngineState → TickInput → TickOutput`), uniform fluent register in every commit, and the exact same, synthetic voice in code, documentation, and website copy. - -**Discovery by regeneration** - -Corrections arrived by the machine’s own subsequent runs, recorded as planned milestones. In early July 2026, a series of “core review” commits documented, in the project’s own words: - -* announce-ID history “was unbounded per-slot growth” (`54cc8d601`); -* interface announce limiting could “leak a sustained flood” (`9d27af75c`); -* a “phantom-airtime leak” where frames “used to vanish silently” (`4ed75ffdd`); -* Group-packet deduplication “which RNS does not do” - a behaviour the reference never had, - implemented and later removed (`edd8e72be`); -* “both `unwrap_or(&[])` arms were unreachable dead defaults” (`74bea355e`); -* a benchmark crate “hasn’t compiled since” a prior rename, undetected for weeks because it - was outside the CI’s workspace (`86240b027`). - -Earlier still: the record shows a “>1 MiB RNS-to-Prns interop stall” closed three weeks in -(`cc4d524a3`), and a known send-path hang shipped with the note “typed fast-fail a -candidate for later” (`a010e6613`). - -**Laundered provenance** - -The repository contains a committed commit-message hook (`.githooks/commit-msg`) that *blocks* any commit containing attribution for machine generation (“generated with claude/codex/copilot/cursor/gemini”, “co-authored-by”), with the explanation “The committer owns the commit”. The contributing guide is explicit: “AI tools … are welcome … what you submit is *yours*”, and describes the contributing population as “both human and automated contributors”. The fictitious “Prns Tests” identity completes the picture: A manufactured, unknowable contributor, used for marketing content. - -**Authorship by assignment** - -The human whose name is on the work cannot answer for it, because the choices were never made by a human, and only the general shape and constraints of the outward presentation was. Nothing in the project’s history shows an individual explaining a design decision, accepting a mistake in the first person, or going deeper under technical questioning; the record shows process language and marketing instead. Per the earlier mentioned test: any other person could have pressed the same button, and nothing would be different. The project is effectively unowned, and void of value. - -By the criteria of [Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution), this is substitution. The machine was the agent of the work; the “author” was a facade. - -### The performance claims - -The project’s central marketing claim - “up to 89× the throughput” of the reference - is also its most completely documented failure, because the project published the evidence of its own measurement. The apparatus, the results, and the problems are all on the record. - -**The claim** - -The README states “up to 89× the throughput”; the on-network marketing page repeats “up to 89× the throughput, 48× smaller peak-memory footprint, and 33× the energy efficiency of stock RNS 1.4.2”; the website’s front page carries the tagline “Measured, not just claimed”. - -**The apparatus** - -The reference is loaded through `benchmarks/reference/compiled_reference.py`, which installs `pyximport` and then asserts two things: that `RNS.compiled == True`, and that at least *one* RNS module was loaded from a native extension. The project’s own published “proof” records show exactly what those checks captured, on every platform: - -```default -aarch64-apple-darwin: .../RNS.cpython-313-darwin.so, Cython 3.2.8, Python 3.13.13 -x86_64-unknown-linux-gnu: .../RNS.cpython-313-x86_64-linux-gnu.so, gcc 11.4.0 -x86_64-pc-windows-msvc: ~/.prns-oc/lib.win-amd64-cpython-313/RNS.cp313-win_amd64.pyd -``` - -In every case the recorded native module is the **top-level package only** - `RNS.so`, nothing below it. Nothing named `RNS.Transport`, `RNS.Destination`, `RNS.Packet`, `RNS.Link`, or `RNS.Resource` ever appears, because *nothing below the top level was ever compiled*. Nobody bothered to check or verify *anything* here. - -The machine output was simply taken for truth, and a full marketing strategy was spun and produced on it. “RNS 1.4.2 (compiled)” - the label used in every table, chart, and marketing page - *is not a mode of RNS that exists*, but the hallucinations of a machine, which the humans created a marketing strategy on. In reality, it is the old `CRNS` development shim, whose top-level-only, debug-oriented “compilation” is slower than plain interpreted RNS, and which was *defunct for over a year* before these benchmarks were published. - -**Interface choices** - -The harness writes the reference’s configuration itself (`benchmarks/reference/participant_node.py`): `TCPClientInterface` for the initiator, `TCPServerInterface` for the responder and both relay sides, with `UDPInterface` as the abstract wire. The reference was never given its modern `BackboneInterface` on any platform, forcing the slowest possible interrfaces. The interface selection is never mentioned in any methodology text. - -**The policy** - -The published default-policy tables record the asymmetry directly: Prns at 500 Mbps / 128 KiB TCP policy against the reference at 10 Mbps / 8 KiB - slyly footnoted as “preserves each implementation’s normal TCP policy” (in the case of RNS, a conservative default policy that was selected to prioritize coexistence on slower, shared links, requiring the user to dial it up to full speed if they knew what they were doing). Raw transport “throughput” ratios of 20.65× (Linux) and 38.84× (Windows) are therefore mostly policy, not engine. - -**The cross-host evidence** - -The published results show the same scenario producing wildly different ratios by host, and the variance is entirely in the reference artifact: - -| Host | Prns, single-packet | “Reference”, single-packet | Ratio | -|-------------------------|-----------------------|------------------------------|---------| -| macOS (Apple M4) | 37.4k/s | 420/s | 89.09× | -| Linux (i7-1260P) | 25.3k/s | 3.5k/s | 7.19× | -| Windows (Ryzen 5 5600X) | 30.8k/s | 3.9k/s | 8.00× | - -Prns is consistent (25–37k/s). The reference artifact collapses to different depths on different hosts, and the single worst collapse - 420 packets per second on macOS - is the cell selected as the global headline. The “89×” is not a measure of Prns; it is a measure of how badly the broken baseline happened to fail on one host. - -For reality calibration: Stock, interpreted RNS 1.5.4 on an ordinary mid-range laptop, single core, sustains roughly 30,000 single-packet deliveries per second (including ephemeral-key decryption), ~175,000–280,000 packets per second in transit relay, and on the order of 10 Gbps at 16 KiB payloads. The project’s “compiled” artifact measured 420–3,900 packets per second for its single-packet scenario: An order of magnitude or more *below* stock interpreted RNS. The comparison being marketed was not “Rust vs Python”; **it was “an implementation running under optimal conditions versus a mislabeled, broken shim on a slow legacy interface at a severe policy disadvantage”**. - -**The equalized rows** - -The project’s own “1 Gbps policy” rows, in which both sides are explicitly configured identically, publish ratios of roughly 2.4–8.4× across all hosts and scenarios - still against the broken baseline, but nowhere near 89×, and consistent with the ordinary, uncontroversial observation that a native implementation tends to outperform an interpreted one in raw throughput. That point needed no fudging to make, and is banal. - -Interestingly, though, it seems that Prns is really only *marginally* faster than a Python implementation, running in interpreted mode, on a single CPU core. - -**Assessment** - -The front page says “Measured, not just claimed.” The record shows the opposite ordering: claimed, and arranged to look measured. No methodology section anywhere states what “compiled” means, that it is a deprecated development shim, that it builds debug targets, or which interface the reference was given; the one statement of the policy asymmetry appears in a footnote of the results tables themselves. If they did not need to fudge it, they would have published the method clearly. - -### Overall Evaluation - -Evaluated against the conditions of [Evaluating A Reticulum Implementation](#brandolinis-evaluating): - -* **Provenance and attribution.** Fails: The fake “ground-up” origin story; zero reference links - on the website; a single well-hidden link in the README; a license file reading - `Copyright (c) 2026 The Prns Authors` over work directly generated from the licensed reference; - no original copyright or permission notice anywhere. - -* **Agency and answerability.** Fails on every criterion of [Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution): - Unauthored scope, uniform generation, discovery by regeneration, laundered provenance, - authorship by assignment. - -* **Engineering.** Fails: The benchmark apparatus above; interop demonstrated only - against a pinned, outdated reference version over loopback while the marketing claims current - parity, interop failures with stock RNS; vendored prebuilt binaries and a republished fork - of an upstream crate (`nrf-softdevice 0.1.0-prns.1`) with no verifiable provenance; - committed WASM bundles. - -* **Network behaviour.** Fails: Invented spectrum behaviour and compatibility claims - about third-party applications of the reference ecosystem (Sideband, NomadNet, MeshChat) - that are not established by any evidence. - -* **Monetary and influence signals.** The marketing apparatus - website before protocol, - day-three twelve-language marketing, false benchmark headlines, “audited” and “formal proof” - claims - the conformal surface described in [Evaluating A Reticulum Implementation](#brandolinis-evaluating): - is where this project invested. - -### Licensing Analysis - -Applying the two legal layers established in [Copyright & The Two Legal Layers](#brandolinis-copyright-and-layers): - -**Layer A - copyright infringement (license-independent)** - -The record shows a direct copy roundtrip: the licensed reference implementation fed into a machine, transformed, at a scale and cadence no human could review, with a license file asserting a fresh copyright in the operator’s name and no acknowledgment of the work the project was generated from. No substantial, new human creative input is demonstrated anywhere in the record - the founding document explicitly frames the design as an instructed port of the reference’s contract. A primarily machine-generated copy of a copyrighted work, not accompanied by demonstrable substantial human creative input, is a derivative work over which the copier holds no copyright, and a reproduction of which without the original author’s permission infringes the original author’s copyright. This holds under any license, MIT included. The operator’s copyright claim over the result is, on this evidence, not merely unsubstantiated; it is the absence of one, directly asserted in the project’s own writing. - -**Layer B - license conditions (Reticulum License specific)** - -Modern Reticulum - the current wire format, the AES-256-based link encryption, ratchets, the cryptographic machinery as it exists today - has been published under the Reticulum License since April 15, 2025, effective from release 0.9.4 and every release since. As a derivative of that work, the license’s conditions attach: The copyright and permission notice must be included in all copies and substantial portions; the AI-training condition must be honoured; the harm condition must be honoured. The record shows none of the notice requirements met, and a production process that consists of the licensed work being ingested into a machine and regurgitated, at industrial scale and cadence. - -**The cascading consequence** - -Prns is distributed claiming dual MIT/Apache-2.0 licensing. It is, however, a direct derivative of Reticulum-licensed work, and the claimed grant is therefore void. Every downstream project that builds on Prns believing it is MIT/Apache-licensed is building on a grant that does not exist. The users of Prns, and all software developers using it, have in effect, been placed under conditions they were never told about. The “ground-up” claim is not merely false; it is the sole load-bearing element of an entire licensing posture, and it fails spectacularly. - -### Consequences - -The consequences of this pattern are examined in the following sections: The harm to users and to the network in [A Movement Of A Dozen](#brandolinis-movement-of-a-dozen), and the human dimension in [Network Health & Coexistence](#brandolinis-health-and-coexistence). The contribution of this addendum is narrower and intended as an instructive example: This is what a substitution project looks like, in full, on its own record. It is a reference example for [Evaluating A Reticulum Implementation](#brandolinis-evaluating), and it demonstrates, in one case, most of the elements this chapter describes. - -### Verdict - -Applying the three verdicts of [Evaluating A Reticulum Implementation](#brandolinis-evaluating), this project does not meet the standard. It is not “experimental but honest” - it is not labeled as experimental anywhere; the first thing users saw was a polished marketing presence promising a finished, tested, drop-in replacement. It is a project whose central claims are fabricated (the performance apparatus), whose provenance is laundered (the authorship machinery), whose foundation is misrepresented (the “ground-up” story), whose core semantics were learned late and by regeneration, and whose operator cannot possibly answer for any of it. It is also a blatant copyright violation of the reference implementation. - -The proposed conclusion, for users and for the network: Treat the Prns project as hostile regardless of its polished surface, and as a hazard insofar as it invites users onto shared networks. Nothing in this section is a judgment about the people involved; it is a reading of the results produced, their history and how they came to be, which in the regrettable lack of any human authorship is the only thing that can be expected to be held to account. - -### Dating - -This examination reflects the publicly available material as of September 2026; the repository in question continued to receive commits at the time of writing, so readers should treat citations to commits and files as point-in-time evidence. A [forensics snapshot](https://github.com/markqvist/forensics_prns) is publicly available on GitHub. \ No newline at end of file diff --git a/CRNS/__init__.py b/CRNS/__init__.py index 591c69f3..49340625 100644 --- a/CRNS/__init__.py +++ b/CRNS/__init__.py @@ -1,278 +1,12 @@ compiling = False noticed = False notice_delay = 0.3 -import os -import sys import time +import sys import threading from importlib.util import find_spec if find_spec("pyximport") and find_spec("cython"): - import json - import shutil - import subprocess - from importlib.abc import MetaPathFinder - from importlib.machinery import ExtensionFileLoader, SourceFileLoader - from importlib.machinery import EXTENSION_SUFFIXES - from importlib.util import spec_from_file_location - from distutils.extension import Extension - from pyximport import pyxbuild - - COMPILE_DIRECTIVES = { "annotation_typing": False } - - _WORKER_SCRIPT = r""" -import sys, os, json -from distutils.extension import Extension -from pyximport import pyxbuild -def main(): - tasks_path = sys.argv[1] - with open(tasks_path, "r") as fh: tasks = json.load(fh) - results = [] - for task in tasks: - try: - os.makedirs(task["scratch"], exist_ok=True) - extension = Extension(name=task["basename"], sources=[task["source"]]) - extension.cython_directives = task["directives"] - so_path = pyxbuild.pyx_to_dll(task["source"], ext=extension, build_in_temp=True, pyxbuild_dir=task["scratch"]) - os.makedirs(os.path.dirname(task["target"]), exist_ok=True) - os.replace(so_path, task["target"]) - results.append({"fullname": task["fullname"], "ok": True, "error": None}) - except Exception as e: - results.append({"fullname": task["fullname"], "ok": False, - "error": "{}: {}".format(type(e).__name__, e)}) - with open(tasks_path + ".results", "w") as fh: json.dump(results, fh) -if __name__ == "__main__": main() -""" - - def _compile_jobs(): - jobs = os.environ.get("CRNS_JOBS") - if jobs: return max(1, int(jobs)) - return max(1, min(os.cpu_count() or 1, 16)) - - class _ImportFinder(MetaPathFinder): - - def __init__(self): - self.build_dir = os.path.join(os.path.expanduser("~"), ".pyxbld") - self.mirror_root = os.path.join(self.build_dir, "rnslib") - self.source_root = self._find_source_root() - self.abi_suffix = EXTENSION_SUFFIXES[0] - self.unbuildable = {} - self.unbuildable_path = os.path.join(self.build_dir, "unbuildable.json") - self._verify_mirror_directives() - try: - with open(self.unbuildable_path, "r") as fh: self.unbuildable = json.load(fh) - except Exception: self.unbuildable = {} - - def _verify_mirror_directives(self): - # If the compiler directives have changed since the mirror was - # built, the entire mirror is invalidated and rebuilt. - stamp_path = os.path.join(self.build_dir, "rnslib.stamp") - directives = json.dumps(COMPILE_DIRECTIVES, sort_keys=True) - stamp = directives + "\n" + self.abi_suffix - previous = None - try: - with open(stamp_path, "r") as fh: previous = fh.read() - except Exception: pass - if previous != stamp: - shutil.rmtree(self.mirror_root, ignore_errors=True) - os.makedirs(self.mirror_root, exist_ok=True) - try: - os.makedirs(self.build_dir, exist_ok=True) - with open(stamp_path, "w") as fh: fh.write(stamp) - except Exception: pass - - def _save_unbuildable(self): - try: - os.makedirs(os.path.dirname(self.unbuildable_path), exist_ok=True) - with open(self.unbuildable_path, "w") as fh: json.dump(self.unbuildable, fh, indent=1) - except Exception: pass - - def _mark_unbuildable(self, fullname, source): - try: - self.unbuildable[fullname] = int(os.path.getmtime(source)) - self._save_unbuildable() - except Exception: pass - - def _unmark_buildable(self, fullname): - if fullname in self.unbuildable: - del self.unbuildable[fullname] - self._save_unbuildable() - - def _is_unbuildable(self, fullname, source): - if fullname not in self.unbuildable: return False - try: - if int(os.path.getmtime(source)) == self.unbuildable[fullname]: return True - except Exception: pass - self.unbuildable.pop(fullname, None) - return False - - def _find_source_root(self): - here = os.path.dirname(os.path.abspath(__file__)) - candidate = os.path.abspath(os.path.join(here, "..", "RNS", "__init__.py")) - if os.path.exists(candidate): return os.path.dirname(os.path.dirname(candidate)) - for entry in [os.getcwd()] + list(sys.path): - if not entry: continue - candidate = os.path.join(entry, "RNS", "__init__.py") - if os.path.exists(candidate): return os.path.dirname(candidate) - raise RuntimeError("CRNS could not locate the RNS source tree") - - def _all_source_modules(self): - rns_root = os.path.join(self.source_root, "RNS") - for root, dirs, files in os.walk(rns_root): - dirs[:] = [d for d in dirs if d not in ("__pycache__", "i2plib")] - rel = os.path.relpath(root, self.source_root).replace(os.sep, ".") - for fn in sorted(files): - if not fn.endswith(".py"): continue - if fn == "__init__.py": fullname = rel - else: fullname = rel + "." + fn[:-3] - yield fullname - - def _mirror_target(self, fullname, is_package): - rel = fullname.split(".") - basename = rel[-1] - if is_package: return os.path.join(self.mirror_root, *rel, "__init__" + self.abi_suffix) - return os.path.join(self.mirror_root, *rel[:-1], basename + self.abi_suffix) - - def _source(self, fullname): - rel = fullname.split(".") - package_source = os.path.join(self.source_root, *rel, "__init__.py") - if os.path.exists(package_source): return package_source, True - module_source = os.path.join(self.source_root, *rel) + ".py" - if os.path.exists(module_source): return module_source, False - return None, None - - def _compile(self, fullname, source): - basename = fullname.rsplit(".", 1)[-1] - extension = Extension(name=basename, sources=[source]) - extension.cython_directives = COMPILE_DIRECTIVES - so_path = pyxbuild.pyx_to_dll(source, ext=extension, build_in_temp=True, pyxbuild_dir=self.build_dir) - abi_suffix = os.path.basename(so_path)[len(basename):] - return so_path, abi_suffix - - def _is_current(self, fullname, source, is_package): - target = self._mirror_target(fullname, is_package) - if not os.path.exists(target): return False - try: return os.path.getmtime(target) >= os.path.getmtime(source) - except Exception: return False - - def _ensure_compiled(self, fullname, source, is_package): - target = self._mirror_target(fullname, is_package) - if os.path.exists(target) and os.path.getmtime(target) >= os.path.getmtime(source): return target - so_path, abi_suffix = self._compile(fullname, source) - os.makedirs(os.path.dirname(target), exist_ok=True) - os.replace(so_path, target) - return target - - def precompile(self): - if not os.path.isdir(self.mirror_root): - os.makedirs(self.mirror_root, exist_ok=True) - - pending = [] - for fullname in self._all_source_modules(): - source, is_package = self._source(fullname) - if source is None: - continue - if self._is_current(fullname, source, is_package): - self._unmark_buildable(fullname) - continue - if self._is_unbuildable(fullname, source): - continue - - pending.append((fullname, source, is_package)) - - if not pending: return - - jobs = min(_compile_jobs(), len(pending)) - if jobs > 1: - # Round-robin assignment spreads the large modules evenly - # across workers. - chunks = [[] for _ in range(jobs)] - for i, item in enumerate(pending): chunks[i % jobs].append(item) - - workers_dir = os.path.join(self.build_dir, "workers") - os.makedirs(workers_dir, exist_ok=True) - procs = [] - for w in range(jobs): - tasks = [] - for fullname, source, is_package in chunks[w]: - tasks.append({ "fullname": fullname, - "basename": fullname.rsplit(".", 1)[-1], - "source": source, - "target": self._mirror_target(fullname, is_package), - "scratch": os.path.join(workers_dir, "w{}".format(w)), - "directives": COMPILE_DIRECTIVES }) - task_path = os.path.join(workers_dir, "tasks{}.json".format(w)) - with open(task_path, "w") as fh: json.dump(tasks, fh) - procs.append((w, task_path, subprocess.Popen([sys.executable, "-c", _WORKER_SCRIPT, task_path], - stdout=subprocess.PIPE, stderr=subprocess.PIPE))) - - for w, task_path, proc in procs: - stdout, stderr = proc.communicate() - if proc.returncode != 0: - for fullname, _, _ in chunks[w]: - source, _ = self._source(fullname) - self._mark_unbuildable(fullname, source) - print(f"CRNS: compile worker failed for {fullname}, using interpreted module", file=sys.stderr) - if stderr: - tail = stderr.decode(errors="replace").strip().splitlines()[-5:] - print("CRNS: worker output:\n" + "\n".join(tail), file=sys.stderr) - continue - - results = {} - try: - with open(task_path + ".results", "r") as fh: - for entry in json.load(fh): results[entry["fullname"]] = entry - except Exception: results = {} - - for fullname, _, _ in chunks[w]: - entry = results.get(fullname) - if entry is not None and entry.get("ok"): self._unmark_buildable(fullname) - else: - error = "" if entry is None else entry.get("error", "") - source, _ = self._source(fullname) - self._mark_unbuildable(fullname, source) - print(f"Could not compile {fullname}, using interpreted module: {error}", file=sys.stderr) - else: - for fullname, source, is_package in pending: - try: - self._ensure_compiled(fullname, source, is_package) - self._unmark_buildable(fullname) - except Exception as e: - self._mark_unbuildable(fullname, source) - print(f"Could not compile {fullname}, using interpreted module: {e}", file=sys.stderr) - - def _spec(self, fullname, source, is_package, compile_attempted): - if fullname == "RNS.vendor.i2plib" or fullname.startswith("RNS.vendor.i2plib."): - rel = fullname.split(".") - locations = [os.path.join(self.source_root, *rel)] if is_package else None - return spec_from_file_location(fullname, source, loader=SourceFileLoader(fullname, source), submodule_search_locations=locations) - - if compile_attempted: - try: - target = self._ensure_compiled(fullname, source, is_package) - self._unmark_buildable(fullname) - if is_package: locations = [os.path.dirname(target)] - else: locations = None - return spec_from_file_location(fullname, target, loader=ExtensionFileLoader(fullname, target), - submodule_search_locations=locations) - except Exception as e: - self._mark_unbuildable(fullname, source) - print(f"Could not compile {fullname}, using interpreted module: {e}", file=sys.stderr) - - rel = fullname.split(".") - locations = [os.path.join(self.source_root, *rel)] if is_package else None - return spec_from_file_location(fullname, source,loader=SourceFileLoader(fullname, source),submodule_search_locations=locations) - - def find_spec(self, fullname, path, target=None): - if not isinstance(fullname, str): return None - if not (fullname == "RNS" or fullname.startswith("RNS.")): return None - - source, is_package = self._source(fullname) - if source is None: return None - - return self._spec(fullname, source, is_package, compile_attempted=not self._is_unbuildable(fullname, source)) - - _importfinder = _ImportFinder() - sys.meta_path.insert(0, _importfinder) + import pyximport; pyxloader = pyximport.install(pyimport=True, language_level=3)[1] def notice_job(): global noticed @@ -280,13 +14,13 @@ def notice_job(): while compiling: if time.time() > started+notice_delay and compiling: noticed = True - print("Compiling RNS object code...") + print("Compiling RNS object code... ", end="") sys.stdout.flush() break time.sleep(0.1) + compiling = True threading.Thread(target=notice_job, daemon=True).start() -_importfinder.precompile() import RNS; compiling = False -if noticed: print("Compilation done"); sys.stdout.flush() +if noticed: print("Done."); sys.stdout.flush() \ No newline at end of file diff --git a/Changelog.md b/Changelog.md index b0e0841c..8f55bc36 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,27 +1,10 @@ -### 2026-09-29: RNS 1.5.5 +### 2026-07-26: RNS 1.4.2 -This release adds live interface attach, detach and reload, as well as significant improvements to interface discovery. Auto-connecting interfaces is now also supported on Windows and macOS. - -It also brings improvements to `rngit` and fixes discovered `I2PInterface` configuration snippets, along with a number of other bugs. +This maintenance release fixes bugs in blackholed identity filtering and recursive path request handling for RNode interfaces. **Changes** -- Added ability to `--attach`, `--detach` and `--reload` interfaces to the `rnstatus` utility -- Added interface discovery auto-connect support on Windows and macOS -- Added ability to download micron-converted versions of markdown files to the `rngit` page node -- Added workdoc counts to filter scope links to the `rngit` page node -- Added version info to `rnstatus` discovered interfaces output -- Added `--show-stale` and `--show-unknown` options to `rnstatus` -- Added sequential naming of auto-connected interfaces on announced name collisions -- Added auto-connect implementation and version criteria filtering -- Added configuration option for allowing auto-connects to discovered interfaces with missing version info -- Improved sanitization of nonsensical interface discovery IFAC announce data -- Fixed `I2PInterface` discovery config snippets not including `.b32.i2p` -- Fixed timeout and response rejects not being processed for single-packet requests, by **JRG** -- Fixed missing configurable property propagations on spawned interfaces, by **JRG** -- Fixed ingress control producer comprehension at mid-watermark using incorrect variable name for interface type check, by **JRG** -- Fixed instances of `announce_cap` property ensurance logic not storing announce cap value as a fraction, by **JRG** -- Fixed a potential race condition in `LocalInterface` initialization. -- Updated documentation and manual +- Fixed a bug where recursive path requests would cause a division by zero error on uninitialized RNode interfaces. +- Fixed slow blackholed filtering for discovered interfaces on Android in some cases. **Verified Retrieval** You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To retrieve only the installation `.whl` package, and the release manifest for future updates, you can use: @@ -51,145 +34,10 @@ rnid -i bc7291552be7a58f361522990465165c -V rns_*.rsm *.rsg The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded. -### 2026-09-11: RNS 1.5.4 - -This release improves RNode BLE connectivity reliability on desktop operating systems. - -**Changes** -- Fixed RNode BLE device address acquisition on windows, by **Nickie Deuxyeux** -- Fixed RNode BLE re-connection deadlock on desktop -- Improved RNode BLE reconnect reliability - -### 2026-09-10: RNS 1.5.3 - -This maintenance release adds functionality to, and fixes bugs in `rngit`, and includes a few other optimizations and improvements. - -**Changes** -- Added media request handler to `rngit`, for serving images to `nomadnet` clients -- Added media conversion handler to `rngit`, auto-converting images to webp -- Added importable helper method for using the `rngit` media converter in other programs -- Added a `no_ident` template to the `rngit` page node -- Added immediate activation of changed permissions with `rngit perms` -- Added optimized HDLC framer -- Fixed `rngit` repo admins not being able activate/complete workdocs from other users, by **Bergie** -- Fixed a typo, I'm positively certain this is the last one - -### 2026-08-28: RNS 1.5.2 - -This maintenance release fixes a regression in resource transfers that was introduced in 1.5.1, an `I2PInterface` bug, and sets dataplane control parameters to default values that are actually sensible. If you installed `1.5.1`, it's update time again, baby. - -**Changes** -- Tuned dataplane control parameters -- Added example for blocking unidentified page node peers to `rngit` default config -- Fixed regression in resource transfers causing some `rngit` file downloads to fail -- Fixed keepalive frames being passed to transport core on `I2PInterface` - -### 2026-08-28: RNS 1.5.1 - -This release focuses on dataplane control, memory bounding and transport throughput, with adaptive ingress and egress control for interfaces, a new (and very efficient) zero-copy coalescing transmit buffer, an optimized HDLC deframer, and a range of other improvements to the inbound packet processing paths. It also introduces support for live profiling, and various new diagnostics output in `rnstatus`, along with a number of bug fixes and general improvements. - -**Changes** -- Added adaptive dataplane egress control -- Added adaptive dataplane ingress control -- Added zero-copy coalescing transmit buffers to interfaces using the `BackboneInterface` backend -- Added early protocol violation checks for invalid frames -- Added transport implementation name and version to discovery information requirements -- Added full live profiling to the built-in `Profiler` primitive, by **K8** -- Added the `@RNS.Profiler.profile` decorator, by **K8** -- Added support for indefinitely running, reentrant profilers with bounded capture, by **K8** -- Added live profiling results output to `rnstatus`, by **K8** -- Added PPS statistics to `rnstatus` -- Added interface MTU display to `rnstatus` -- Added interface TX drop statistics to `rnstatus` -- Added interface TX buffer size output to `rnstatus` -- Added throughput benchmarker to the test suite -- Added support for natively compiled module builds, and the ability to load compiled modules when available -- Added module compilation status reporting from in-wheel build information -- Added parity tests for HDLC, IFAC and HKDF against the legacy implementations -- Added shared medium hints to interfaces -- Improved memory and CPU consumption under high traffic loads -- Improved traffic class handling -- Optimized HKDF to ~5.7x performance, if this worries you, **good**; then see the full parity and standard vector test suite and read the code -- Optimized inbound and outbound IFAC handling to ~90x performance on large frames -- Optimized inbound packet processing by reducing lock acquisitions and path table contention -- Optimized lookups for pending and active links using hash maps for lookup operations -- Optimized announce validation by caching signature validation results, significantly lowering announce storm CPU load -- Optimized the HDLC deframer -- Reduced redundant packet hashing in inbound processing -- Tuned default inbound queue lengths and announce queuing -- Tuned automatic interface MTU configuration -- Fixed `Resource` transfers failing when initialized from stream-based data sources that fell outside `MAX_EFFICIENT_SIZE` -- Fixed an invalid prefix stripping bug in the `rngit` page server (`.something` files not viewable) -- Fixed RSSI/SNR reporting regression -- Fixed keepalive handling on non-epoll backends -- Fixed a bug in `rngit` page navigation content initialization order -- Fixed the `rngit` page node not being able to serve in-tree downloads for large files -- Fixed `rnstatus` blocked IP listing including IPs that were actually not yet blocked -- Fixed `rnstatus` traffic totals counting the local shared instance inter-app transit in totals -- Fixed various minor bugs in `rnsh`, `rnir`, identity handling -- Fixed a latent bug in the AES module, where an exception would not resolve it's exception description correctly, and instead raise another exception. -- Removed dead Python 2 code from `umsgpack` - -### 2026-08-22: RNS 1.5.0 - -This release significantly improves the core Transport handling of RNS, with a priority-based ingress queue backend, substantially improved efficiency of both data and management traffic handling, more efficient ingress/egress limiting, and a long list of other improvements and bugfixes. - -**Changes** -- Added ability to include operator LXMF address in interface discovery information -- Added prioritized inbound traffic processing to the transport core -- Added configurable inbound queue lengths for data, announce, path request and ingress limited traffic -- Added early filtering for inbound traffic and improved filtering performance -- Added per-interface protocol violation tracking -- Added in-flight path requests tracking -- Added request and response batching for in-flight path requests -- Added ability to signal blackholed status in announce validation return to the API -- Added full link MDU utilization for `Channel` and `Buffer` -- Added inbound queue pressure and queue drop statistics to `rnstatus` -- Added detailed announce and path request traffic flow statistics per interface to `rnstatus` -- Added total announce and PR count/frequency stats per interface to `rnstatus` -- Added data flow speed and composition stats to `rnstatus` -- Added protocol violation stats to `rnstatus` -- Added active link statistics to `rnstatus` -- Added blocked IP listings to `rnstatus` -- Added medium bitrate based timeout calculation helpers and RPC functions, by **Zenith** -- Added extra timeout for discovery path requests when slow interfaces are online, by **Zenith** -- Added adaptive timeouts to `rncp`, `rnpath`, `rnprobe` and `rnx`, by **Zenith** -- Added adaptive timeout calculation to `rngit`, by **Zenith** -- Improved overall Transport inbound processing speed -- Improved path request handling significantly -- Improved path request ingress limiting and accounting -- Improved egress limiting responsiveness under high incoming path request load -- Improved transport background job processing -- Improved early rejection of packets with excessive hop counts -- Fixed `BackboneInterface` EPOLL receive starvation and ingress control timestamp handling, by **JRG** -- Fixed potential transport deadlock on receipts lock when callbacks sends packets, by **JRG** -- Fixed transport state handling edge cases in path request, announce queue and pending link processing, by **JRG** -- Fixed link watchdog not resetting on receive exceptions, by **JRG** -- Fixed `Resource` cancellation on multi-segment resources, by **JRG** -- Fixed `Resource` transfer part index alignment and rebinding, by **JRG** -- Fixed stale BLE device reference in `RNodeInterface`, by **JRG** -- Fixed ratchet cleaning retained preservation, by **JRG** -- Fixed invalid `rnstatus` statistics handling, by **JRG** -- Fixed various bugs in packet, link and interface handling, by **JRG** -- Fixed per-interface burst count inconsistencies in `rnstatus` output -- Fixed `rngit` file resource operations failing on Windows -- Fixed `rnodeconf` config summary incorrectly displaying WiFi mode -- Fixed speedtest example aborting transfers on stale link status -- Updated documentation, including manual sections on queue tuning and interface discovery options - -### 2026-07-26: RNS 1.4.2 - -This maintenance release fixes bugs in blackholed identity filtering and recursive path request handling for RNode interfaces. - -**Changes** -- Fixed a bug where recursive path requests would cause a division by zero error on uninitialized RNode interfaces. -- Fixed slow blackholed filtering for discovered interfaces on Android in some cases. - ### 2026-07-24: RNS 1.4.1 This release significantly improves path convergence and stability with the new path re-balancing functionality. It also introduces interface gravity configuration, new API functionality and fixes a range of bugs and inefficiencies. -**Changes** - Added dynamic path re-balancing - Added `set_max_request_size` to `Destination` API - Added `max_response_size` to request API @@ -208,7 +56,6 @@ This release significantly improves path convergence and stability with the new - Fixed various memory inefficiencies - Fixed loglevel `LOG_EXTREME` not being usable in some cases - Fixed historical interface discoveries not being cleaned according to blackholed identities - ### 2026-07-20: RNS 1.4.0 This release focuses on performance, stability and reliability improvements, particularly with resource-constrained systems in mind. It also fixes several bugs, reduces logging noise, and makes information about blocked `BackboneInterface` clients available in `ifstats`. diff --git a/Contributing.md b/Contributing.md index 38372f79..3965f844 100644 --- a/Contributing.md +++ b/Contributing.md @@ -2,66 +2,57 @@ Welcome, and thank you for your interest in contributing to Reticulum! -Before interacting with this community, read these short and simple guidelines. - -If you didn't shit your pants after reading this document, you may well find that we are all really quite cuddly and nice to be around. Mostly. +Apart from writing code, there are many ways in which you can contribute. Before interacting with this community, read these short and simple guidelines. ## Expected Conduct -First and foremost, there is one simple requirement for taking part in the development of this technology: While we primarily interact virtually, your actions matter and have real consequences. Therefore: **Act like a responsible, civilized person** - especially in the face of disputes and heated disagreements. Speak your mind here; discussions are welcome. Just do so in the spirit of being face-to-face with everyone else. Thank you. +First and foremost, there is one simple requirement for taking part in this community: While we primarily interact virtually, your actions matter and have real consequences. Therefore: **Act like a responsible, civilized person** - especially in the face of disputes and heated disagreements. Speak your mind here; discussions are welcome. Just do so in the spirit of being face-to-face with everyone else. Thank you. -In order to keep the discourse navigable and useful, the following types of posts and inquiries will be deleted and ignored: +In order to keep the discussion forums and issue trackers navigable and useful, the following types of posts will be deleted without notice: - Spam. -- Questions that have already been adequately answered elsewhere. Utilize search functions. -- Low-effort posts or comments that contain no actual information or useful substance. [This is not a tea-house](./This%20Is%20Not%20a%20Teahouse.md). +- Questions that have already been adequately answered elsewhere. Use the search function. +- Low-effort posts or comments that contain no actual information or useful content. This is not a tea-house. - Post or comments solely containing personal opinions or beliefs without adding anything to the discussion. Facebook and X exists. - Content that simply waste the developer's / maintainer's time with completely obvious "ideas", "insights" or "recommendations". Yes, we have *at least* 8 neurons ourselves. - Posts that fail to understand that developing a highly complex software project with a very small amount of resources and people takes time. Imagining perfection on our behalf is useless. -If you're new to the community and start out your engagement with any of the above transgressions, you will simply be banned permanently without notice or explanation, and any further communication from you will be ignored. - -Except for the case of real, well-grounded, clearly documented, human-written security advisories, technical investigations or bug reports, if your inquiry or communication is not attached to a Reticulum Identity, expect it to be ignored. +If you're new to the community and start out your engagement with any of the above transgressions, you will simply be banned without notice or explanation, and your post will be deleted. If you find this "harsh", "unfair" or "unwelcoming", go somewhere else. This is not social club, but a work environment for the people contributing to the project. -## Reporting Bugs +## Asking Questions -If you have found a bug or issue in this project, please report it to the maintainer over LXMF, or if strictly not possible, over email. Detailed and serious bug reports can also be posted in the various on-network forums and RRC channels that the developers frequent. Due to the realities described in [This Is Not a Teahouse](./This%20Is%20Not%20a%20Teahouse.md), there is no way to report issues or request features over the internet. +If you want to ask a question, **do not open an issue**. The issue tracker is used by people *working on Reticulum* to track bugs, issues and improvements. Instead, ask away on the [discussions](https://github.com/markqvist/Reticulum/discussions). + +Do not post feature requests or general ideas on the issue tracker, or in direct messages to the primary developers. You are much more likely to get a response and start a constructive discussion by posting your ideas in the public channels created for these purposes. + +## Reporting Issues + +If you have found a bug or issue in this project, please report it using the [issue tracker](https://github.com/markqvist/Reticulum/issues). Be sure to include details on how to reproduce the bug. + +Anything submitted to the issue tracker that does not follow these guidelines will be closed and removed without comments or explanation. ## Writing Code -Patches and code contributions are *generally* only welcomed from developers already known to the community, with a proven track-record of competence, skill, careful and considerate design and responsible conduct. If we do not know you yet, but you have created something that you believe lives up to such standards, we are eager to hear from you. +If you are interested in contributing code, fixing open issues or adding features, please coordinate the effort with the maintainer or one of the main developers **before** submitting a pull request. Before deciding to contribute, it is also a good idea to ensure your efforts are in alignment with the [Roadmap](./Roadmap.md) and current development focus. -Current development priorities and work documents are not publicly available on the internet, but shared and coordinated directly between the developers over Reticulum itself, following the principles outlined in [Distributed Development](https://reticulum.network/manual/distributed.html). +Pull requests have a high chance of being accepted if they are: -## Commit Signing +- In alignment with the [Roadmap](./Roadmap.md) or solve an open issue or feature request +- Sufficiently tested to work with all API functions, and pass the standard test suite +- Functionally and conceptually complete and well-designed +- Not simply formatting or code style changes +- Well-documented -All commits in Reticulum and related projects **must** be signed with a Reticulum identity. If you want your author credentials to be included directly in the commit log, ensure that you have [configured commit signing](https://reticulum.network/manual/git.html#commit-signing). Bare patches submitted manually or fetched remote commits without signatures are still very welcome, but we will review, squash merge and sign with the key of the maintainer that approved the changes. Attribution will be provided in the commit message instead (unless you wish to remain anonymous). +Even new ideas and proposals that have not been approved by a maintainer, or fall outside the established roadmap, are *occasionally* accepted - if they possess the remaining of the above qualities. If not, they will be closed and removed without comments or explanation. -## Machine Assistance & LLMs +## Generative AI Policy -For a highly complex, critical piece of cryptographically based networking infrastructure, we should not need to state the following, but alas, experience has proven otherwise. Therefore: +Contributions written using large language models (LLMs) or other generative 'AI' programs are prohibited. LLMs produce errors so frequently and in a way that is so unlike human error that such issues are incredibly time-consuming to spot and fix. This is not a worthwhile tradeoff for Reticulum. -**Including material generated using LLMs or other "AI" programs in Reticulum is, almost without exception, prohibited.** - -Before even *thinking* about using a generative software system to "create" something for Reticulum, read [Brandolini's Reference](https://reticulum.network/manual/brandolinis.html), in full. If you do not have the capacity for that, you have failed the first requirement for contributing: Basic literacy. - -*If* you are not already a known contributor with a proven record of being able to responsibly handle these tools, a **full** audit trail of all ML-utilizing workflows, including prompts, LLM reasoning traces, tool calls and outputs (that is, the *entire*, unaltered session log) will need to be provided with any proposed contribution. Without that, we will not even *consider* looking at it. - -This applies to all Reticulum-related projects and documentation, as well as all submitted issues, reports and discussion, **except** of course in cases where language translation, speech recogntion, or other assistive technologies are required. - -In **all** circumstances, clear disclosure is **mandatory**. Any piece of material, file, code snippet, method, function or similar that was generated with machine assistance must be prepended with a disclosure header or comment that states the framework, exact model used and reviewer, such as: - -```python -# Generated with lc/deepseek_v4_flash_q8_k_xl -# Reviewed and corrected by Mark Qvist -``` - -Additionally, any explanatory comments or doc-strings generated by software must be manually re-written in clear, concise language by the reviewer stated in the disclosure header. +This applies to all Reticulum-related projects and documentation, as well as all submitted issues and discussion in official channels, except in cases where language translation and/or speech recogntion technologies are required for communication. ## Contributor License Agreement By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [Reticulum License](./LICENSE). - -For a more in-depth elaboration on the current CLA position, see [this post](https://rns.recipes/forum/general/brandolinis-reference-or-alternatively-agentsmd?page=1#post-3002). diff --git a/Examples/Speedtest.py b/Examples/Speedtest.py index d26f85ac..755d5f97 100644 --- a/Examples/Speedtest.py +++ b/Examples/Speedtest.py @@ -239,7 +239,7 @@ def link_established(link): RNS.log("Link established with server, sending...") rd = os.urandom(link.mdu) started = time.time() - while link.status != RNS.Link.CLOSED and data_sent < data_cap*1.25: + while link.status == RNS.Link.ACTIVE and data_sent < data_cap*1.25: RNS.Packet(server_link, rd, create_receipt=False).send() data_sent += len(rd) diff --git a/Makefile b/Makefile index 2f0c9caf..4de79958 100644 --- a/Makefile +++ b/Makefile @@ -21,19 +21,6 @@ clean: @-rm -rf ./tests/__pycache__ @-rm -rf ./tests/rnsconfig/storage @-rm -rf ./*.egg-info - @-rm -rf ./RNS/*.c - @-rm -rf ./RNS/Cryptography/*.c - @-rm -rf ./RNS/Cryptography/aes/*.c - @-rm -rf ./RNS/Cryptography/pure25519/*.c - @-rm -rf ./RNS/Interfaces/*.c - @-rm -rf ./RNS/Interfaces/util/*.c - @-rm -rf ./RNS/Interfaces/Android/*.c - @-rm -rf ./RNS/Utilities/*.c - @-rm -rf ./RNS/Utilities/rngit/*.c - @-rm -rf ./RNS/Utilities/rnsh/*.c - @-rm -rf ./RNS/vendor/*.c - @-rm -rf ./RNS/vendor/i2plib/*.c - @-rm -rf @make -C docs clean @echo Done @@ -63,9 +50,6 @@ build_wheel: build_pure_wheel: python3 setup.py bdist_wheel --pure -build_native: - python3 setup.py bdist_wheel --native - documentation: make -C docs html markdown @@ -81,8 +65,6 @@ release: test remove_symlinks build_sdist build_wheel build_pure_wheel documenta debug: remove_symlinks build_wheel build_pure_wheel create_symlinks -native: remove_symlinks build_native create_symlinks - local: release sign sign: diff --git a/README.md b/README.md index f04c6b0e..2e2f8b2e 100755 --- a/README.md +++ b/README.md @@ -1,32 +1,40 @@ Reticulum Network Stack ========== -> [!WARNING] -> A significant number of hastily launched, incorrect, dubious, directly harmful and, in most cases, entirely LLM-generated fakes of Reticulum implementations, "documentation", "guides" and user-facing applications are currently being marketed under polished, but ultimately misleading or outright deceptive claims. -> -> For more detailed information, see the relevant sections of the [Brandolini's Reference](https://reticulum.network/manual/brandolinis.html) chapter of the manual. Known good community implementations are listed in the [Community Implementations](#community-implementations) section of this readme. For software and projects recognized by the Reticulum community, see the [Programs Using Reticulum](https://reticulum.network/manual/software.html) chapter of the manual. -> -> Machine-generated content derived from the RNS source code or documentation (such as "implementations", "guides" for sale, et cetera), are in several, well-documented cases violating the open source licenses that granted rights of copying and distribution. -> -> Any claimed assertion of copyright or grant of license (Apache, GPL, et cetera) that such projects are making are [void grants](https://reticulum.network/manual/brandolinis.html#void-grants-legal-foundations-of-machine-generated-code). Both the violators themselves, and all downstream projects using the infringing derivatives, are **directly** and personally liable for the legal consequences. -> -> Do **not** use these "projects", they are a hazard to the entire ecosystem we have been carefully building over the last ten years. -

*This repository is [a public mirror](./MIRROR.md). All development is happening elsewhere.* To understand the foundational philosophy and goals of this system, read the [Zen of Reticulum](Zen%20of%20Reticulum.md). -Reticulum is the cryptography-based networking stack for building local and wide-area networks with readily available hardware. It can operate even with very high latency and extremely low bandwidth. Reticulum allows you to build wide-area networks with off-the-shelf tools, and offers end-to-end encryption and connectivity, initiator anonymity, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable delivery acknowledgements and more. +Reticulum is the cryptography-based networking stack for building local and wide-area +networks with readily available hardware. It can operate even with very high latency +and extremely low bandwidth. Reticulum allows you to build wide-area networks +with off-the-shelf tools, and offers end-to-end encryption and connectivity, +initiator anonymity, autoconfiguring cryptographically backed multi-hop +transport, efficient addressing, unforgeable delivery acknowledgements and +more. -The vision of Reticulum is to allow anyone to be their own network operator, and to make it cheap and easy to cover vast areas with a myriad of independent, inter-connectable and autonomous networks. Reticulum **is not** *one* network. It is **a tool** for building *thousands of networks*. Networks without kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate with each other, and require no central oversight. Networks for human beings. *Networks for the people*. +The vision of Reticulum is to allow anyone to be their own network operator, +and to make it cheap and easy to cover vast areas with a myriad of independent, +inter-connectable and autonomous networks. Reticulum **is not** *one* network. +It is **a tool** for building *thousands of networks*. Networks without +kill-switches, surveillance, censorship and control. Networks that can freely +interoperate, associate and disassociate with each other, and require no +central oversight. Networks for human beings. *Networks for the people*. -Reticulum is a complete networking stack, and does not rely on IP or higher layers, but it is possible to use IP as the underlying carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the Internet or private IP networks. +Reticulum is a complete networking stack, and does not rely on IP or higher +layers, but it is possible to use IP as the underlying carrier for Reticulum. +It is therefore trivial to tunnel Reticulum over the Internet or private IP +networks. -Having no dependencies on traditional networking stacks frees up overhead that has been used to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality, even in open and trustless networks. +Having no dependencies on traditional networking stacks frees up overhead that +has been used to implement a networking stack built directly on cryptographic +principles, allowing resilience and stable functionality, even in open and +trustless networks. -No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3. +No kernel modules or drivers are required. Reticulum runs completely in +userland, and can run on practically any system that runs Python 3. ## Read The Manual The full documentation for Reticulum is available at [markqvist.github.io/Reticulum/manual/](https://markqvist.github.io/Reticulum/manual/). @@ -70,20 +78,12 @@ For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ - Low cost of keeping links open at only 0.44 bits per second - Reliable sequential delivery with Channel and Buffer mechanisms -## Community Resources - -The references listed here are not run, managed or moderated by the Reticulum project or its maintainer. They are provided by dedicated, independent volunteers in the community, for the benefit of anyone interested in learning more about Reticulum. - -- [Reticulum Community Wiki](https://reticulum.miraheze.org/wiki) -- [RNS Recipes](https://rns.recipes/) -- [RNS Moscow](https://rns.moscow/) -- [RMAP.world](https://rmap.world/) - -If you find anything that is no longer accurate, incorrect or simply not quite up to date, do not demand or complain towards the maintainers of these resources. Instead, take the task upon yourself to help out and improve things by working on, and offering the contributions you would like to see, like the creators of the above did themselves. - ## Reference Implementation -The Python code in this repository is the Reference Implementation of Reticulum. The Reticulum Protocol is defined entirely and authoritatively by this reference implementation, and its associated manual. It is maintained by Mark Qvist, identified by the Reticulum Identity ``. +The Python code in this repository is the Reference Implementation of Reticulum. +The Reticulum Protocol is defined entirely and authoritatively by this reference +implementation, and its associated manual. It is maintained by Mark Qvist, +identified by the Reticulum Identity ``. No second-order protocol specification derivatives currently do, or will ever exist. You can't get a better specification than one that is executable, functional, testable, deployed and operating 24/7 across the planet. @@ -91,39 +91,18 @@ There is not, and never will be a separate, derived "formal spec", "RFC" or anyt **The reference implementation *is* the final, authoritative specification.** -Compatibility with the Reticulum Protocol is defined as having full interoperability, and sufficient functional parity with this reference implementation. Any specific protocol implementation that achieves this is Reticulum. Any that does not is not Reticulum. +Compatibility with the Reticulum Protocol is defined as having full interoperability, +and sufficient functional parity with this reference implementation. Any specific protocol +implementation that achieves this is Reticulum. Any that does not is not Reticulum. The reference implementation is licensed under the Reticulum License. The Reticulum Protocol was dedicated to the Public Domain in 2016. -## Community Implementations - -The implementations listed here have demonstrated wire-compatibility, sufficient feature parity, sensible security and development practices, long-term commitment and care for implementation quality. Additionally, they have a track record of at least 18 months of active development. - -Most importantly, their developers and maintainers are recognized by the community to actually know what they are doing, and as having a commitment to improving the overall Reticulum ecosystem. - -- [Chad Atterman's](https://github.com/attermann) [microReticulum](https://github.com/attermann/microReticulum/) is a C++ implementation targeting 32-bit micro-controllers. -- [Quad4's](https://quad4.io/) [Reticulum-Go](https://reticulum-go.quad4.io/) is a Go implementation targeting both desktop, mobile and embedded systems. - -*If you are the maintainer of an implementation not listed above, and believe that it deserves inclusion by following the same standards of rigor and commitment, your proposal will be welcomed.* - ---- - -**Warning!** A significant number of hastily launched, incorrect, dubious, directly harmful and, in most cases, entirely LLM-generated fakes of Reticulum implementations (and user facing applications) are currently being marketed and pushed under polished, but ultimately misleading or outright deceptive claims. Most of these are either quick attention or money grabs, completely unreviewed, incompatible, without maintenance or human oversight, actively trying to fragment the community, or a combination thereof. - ---- - -For more extensive information on this topic, read the [Brandolini's Reference](https://reticulum.network/manual/brandolinis.html) chapter of the manual (for the legal foundations, see [Void Grants](https://reticulum.network/manual/brandolinis.html#void-grants-legal-foundations-of-machine-generated-code) specifically). The chapter is very long, very detailed, and is the polite, measured analysis, intended as an evolving, sectioned reference on these issues. - -If you would rather just have the short, unfiltered and quite angry version, which consists of an enacted response to one particular manifestation of the phenomenon as a whole, used as an example, you can read [Yes, I am Angry](docs/history/2026_09_18_Yes_I_am_Angry.md). It gets the same point across, in a very different language, if you can handle that sort of communication. - -The core Reticulum maintainers and developers **strongly advise against using any such "implementation"**. The [deceitful and self-serving behavior](https://reticulum.network/manual/brandolinis.html#a-persistent-but-fragile-triad) of a small number of either malintented, attention-seeking or incompetent individuals has caused significant [damage and waste of time and resources](https://reticulum.network/manual/brandolinis.html#intent-consequence) for both developers, node operators, users and the community and Reticulum project at large. - -We kindly, but urgently ask all users to excercise caution, common sense and healthy skepticism when [evaluating](https://reticulum.network/manual/brandolinis.html#evaluating-a-reticulum-implementation) the potential use of any implementation not listed above; and to accept that **choosing to do so regardless translates the responsibility for potential damage from an irresponsible or malicious individual onto yourself**. - ## Examples of Reticulum Applications -If you want to quickly get an idea of what Reticulum can do, take a look at the [Programs Using Reticulum](https://reticulum.network/manual/software.html) section of the manual, or the following resources: +If you want to quickly get an idea of what Reticulum can do, take a look at the +[Programs Using Reticulum](https://reticulum.network/manual/software.html) +section of the manual, or the following resources: - [LXMF](https://github.com/markqvist/lxmf) is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum - The [LXST](https://github.com/markqvist/lxst) protocol and framework provides real-time audio and signals transport over Reticulum. It includes primitives and utilities for building voice-based applications and hardware devices, such as the `rnphone` program, that can be used to build hardware telephones. @@ -131,27 +110,50 @@ If you want to quickly get an idea of what Reticulum can do, take a look at the - The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and many advanced features, such as file transfers, image and voice messages, real-time voice calls, a distributed telemetry system, mapping capabilities and full plugin extensibility. - [MeshChatX](https://git.quad4.io/RNS-Things/MeshChatX) is a full-featured LXMF client with many built-in tools and functionalities, that also supports image and voice messages, file transfers and voice calls. It also includes a built-in page browser for browsing Nomad Network nodes. - You can use the included [rnsh](https://reticulum.network/manual/using.html#the-rnsh-utility) program to establish remote shell sessions over Reticulum. -- The included [rngit](https://reticulum.network/manual/git.html) system provides a full solution for managing, hosting, collaborating on and interacting with Git repositories over Reticulum. ## Where can Reticulum be used? -Over practically any medium that can support at least a half-duplex channel with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, WiFi and Ethernet devices, free-space optical links, and similar systems are all examples of the types of physical devices Reticulum can use. +Over practically any medium that can support at least a half-duplex channel +with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios, +modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, +WiFi and Ethernet devices, free-space optical links, and similar systems are +all examples of the types of physical devices Reticulum can use. -An open-source LoRa-based interface called [RNode](https://markqvist.github.io/Reticulum/manual/hardware.html#rnode) has been designed specifically for use with Reticulum. It is possible to build yourself, or it can be purchased as a complete transceiver that just needs a USB, WiFi or Bluetooth connection to the host. +An open-source LoRa-based interface called +[RNode](https://markqvist.github.io/Reticulum/manual/hardware.html#rnode) has +been designed specifically for use with Reticulum. It is possible to build +yourself, or it can be purchased as a complete transceiver that just needs a +USB connection to the host. -Reticulum can also be encapsulated over existing IP networks, so there's nothing stopping you from using it over wired Ethernet, your local WiFi network or the Internet, where it'll work just as well. In fact, one of the strengths of Reticulum is how easily it allows you to connect different mediums into a self-configuring, resilient and encrypted mesh, using any available mixture of available infrastructure. +Reticulum can also be encapsulated over existing IP networks, so there's +nothing stopping you from using it over wired Ethernet, your local WiFi network +or the Internet, where it'll work just as well. In fact, one of the strengths +of Reticulum is how easily it allows you to connect different mediums into a +self-configuring, resilient and encrypted mesh, using any available mixture of +available infrastructure. -As an example, it's possible to set up a Raspberry Pi connected to both a LoRa radio, a packet radio TNC and a WiFi network. Once the interfaces are configured, Reticulum will take care of the rest, and any device on the WiFi network can communicate with nodes on the LoRa and packet radio sides of the network, and vice versa. +As an example, it's possible to set up a Raspberry Pi connected to both a LoRa +radio, a packet radio TNC and a WiFi network. Once the interfaces are +configured, Reticulum will take care of the rest, and any device on the WiFi +network can communicate with nodes on the LoRa and packet radio sides of the +network, and vice versa. ## How do I get started? -The best way to get started with the Reticulum Network Stack depends on what you want to do. For full details and examples, have a look at the [Getting Started Fast](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/). +The best way to get started with the Reticulum Network Stack depends on what +you want to do. For full details and examples, have a look at the +[Getting Started Fast](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html) +section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/). -To simply install Reticulum and related utilities on your system, the easiest way is via `pip`. You can then start any program that uses Reticulum, or start Reticulum as a system service with [the rnsd utility](https://markqvist.github.io/Reticulum/manual/using.html#the-rnsd-utility). +To simply install Reticulum and related utilities on your system, the easiest way is via `pip`. +You can then start any program that uses Reticulum, or start Reticulum as a system service with +[the rnsd utility](https://markqvist.github.io/Reticulum/manual/using.html#the-rnsd-utility). ```bash pip install rns ``` -If you are using an operating system that blocks normal user package installation via `pip`, you can return `pip` to normal behaviour by editing the `~/.config/pip/pip.conf` file, and adding the following directive in the `[global]` section: +If you are using an operating system that blocks normal user package installation via `pip`, +you can return `pip` to normal behaviour by editing the `~/.config/pip/pip.conf` file, +and adding the following directive in the `[global]` section: ```text [global] @@ -164,14 +166,23 @@ Alternatively, you can use the `pipx` tool to install Reticulum in an isolated e pipx install rns ``` -When first started, Reticulum will create a default configuration file, providing basic connectivity to other Reticulum peers that might be locally reachable. The default config file contains a few examples, and references for creating a more complex configuration. +When first started, Reticulum will create a default configuration file, +providing basic connectivity to other Reticulum peers that might be locally +reachable. The default config file contains a few examples, and references for +creating a more complex configuration. If you have an old version of `pip` on your system, you may need to upgrade it first with `pip install pip --upgrade`. If you no not already have `pip` installed, you can install it using the package manager of your system with `sudo apt install python3-pip` or similar. -For more detailed examples on how to expand communication over many mediums such as packet radio or LoRa, serial ports, or over fast IP links and the Internet using the UDP and TCP interfaces, take a look at the [Supported Interfaces](https://markqvist.github.io/Reticulum/manual/interfaces.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/). +For more detailed examples on how to expand communication over many mediums such +as packet radio or LoRa, serial ports, or over fast IP links and the Internet using +the UDP and TCP interfaces, take a look at the [Supported Interfaces](https://markqvist.github.io/Reticulum/manual/interfaces.html) +section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/). ## Included Utilities -Reticulum includes a range of useful utilities for managing your networks, viewing status and information, and other tasks. You can read more about these programs in the [Included Utility Programs](https://markqvist.github.io/Reticulum/manual/using.html#included-utility-programs) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/). +Reticulum includes a range of useful utilities for managing your networks, +viewing status and information, and other tasks. You can read more about these +programs in the [Included Utility Programs](https://markqvist.github.io/Reticulum/manual/using.html#included-utility-programs) +section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/). - The system daemon `rnsd` for running Reticulum as an always-available service - An interface status utility called `rnstatus`, that displays information about interfaces @@ -184,13 +195,19 @@ Reticulum includes a range of useful utilities for managing your networks, viewi - The `rngit` program provides a full multi-repository Git node for serving repositories over Reticulum - The included `git-remote-rns` helper allows you to interact with Git repositories over Reticulum -All tools, including `rnx` and `rncp`, work reliably and well even over very low-bandwidth links like LoRa or Packet Radio. For full-featured remote shells over Reticulum, also have a look at the [rnsh](https://github.com/acehoss/rnsh) program. +All tools, including `rnx` and `rncp`, work reliably and well even over very +low-bandwidth links like LoRa or Packet Radio. For full-featured remote shells +over Reticulum, also have a look at the [rnsh](https://github.com/acehoss/rnsh) +program. ## Supported interface types and devices -Reticulum implements a range of generalised interface types that covers most of the communications hardware that Reticulum can run over. If your hardware is not supported, it's [simple to implement a custom interface module](https://markqvist.github.io/Reticulum/manual/interfaces.html#custom-interfaces). +Reticulum implements a range of generalised interface types that covers most of +the communications hardware that Reticulum can run over. If your hardware is +not supported, it's [simple to implement a custom interface module](https://markqvist.github.io/Reticulum/manual/interfaces.html#custom-interfaces). -Contributions of custom interfaces are gratefully accepted, provided they are generally useful and well-tested in real-world usage. +Pull requests for custom interfaces are gratefully accepted, provided they are +generally useful and well-tested in real-world usage. Currently, the following built-in interfaces are supported: @@ -205,42 +222,62 @@ Currently, the following built-in interfaces are supported: - Custom hardware via stdio or pipes ## Performance -*All performance numbers here assume `rnsd` running in fully interpreted Python mode, limited to a single CPU core.* +Reticulum targets a *very* wide usable performance envelope, but prioritises +functionality and performance on low-bandwidth mediums. The goal is to +provide a dynamic performance envelope from 250 bits per second, to 1 gigabit +per second on normal hardware. -Reticulum targets a *very* wide usable performance envelope, but prioritises functionality and performance on low-bandwidth mediums. The goal is to provide a dynamic performance envelope from 100 bits per second, to 10 gigabits per second on normal hardware. - -Currently, the on-network, usable performance envelope is approximately 150 bits per second to 1 gigabit per second on modest hardware. Real-world performance will vary widely with the speed of your system, availability of cryptographic hardware acceleration, and other factors. - -Raw routing throughput for a transport node hosted on a Raspberry Pi 5 is approximately 5 gigabits per second. Systems with modern crypto hardware acceleration can achieve transport throughput of 15 gigabits per second and higher. - -While the reference implementation is perfectly capable of reaching 100+ Gbps, by utilizing multiple CPU cores and vectorized transport, performance beyond the current level is not higly prioritised, since RNS is already able to saturate available physical medium bandwidth in almost all real-world situations. - -The primary objectives for RNS are *reliability*, *correctness*, *security*, *proper traffic management* and *actually practical performance*, not hollow on-paper "benchmarks" with little to no bearing on real-world usability. - -All alternative-implementation maintainers are *highly* encouraged to adopt a similar approach, and especially to focus on getting their implementations working properly and reliably before publishing empirically dubious or factually wrong performance claims. +Currently, the usable performance envelope is approximately 150 bits per second +to 500 megabits per second, with physical mediums faster than that not being +saturated. Performance beyond the current level is intended for future +upgrades, but not highly prioritised at this point in time. ## Current Status -All core protocol features are implemented and functioning, but additions will probably occur as real-world use is explored and understood. The API and wire-format can be considered stable. +All core protocol features are implemented and functioning, but additions will +probably occur as real-world use is explored and understood. The API and wire-format +can be considered stable. ## Dependencies -The installation of the default `rns` package requires only two external dependencies, listed below. Almost all systems and distributions have readily available packages for these dependencies, and when the `rns` package is installed with `pip`, they will be downloaded and installed as well. +The installation of the default `rns` package requires only two external dependencies, listed +below. Almost all systems and distributions have readily available packages for +these dependencies, and when the `rns` package is installed with `pip`, they +will be downloaded and installed as well. - [PyCA/cryptography](https://github.com/pyca/cryptography) - [pyserial](https://github.com/pyserial/pyserial) -On more unusual systems, and in some rare cases, it might not be possible to install or even compile one or more of the above modules. In such situations, you can use the `rnspure` package instead, which require no external dependencies for installation. Please note that the contents of the `rns` and `rnspure` packages are *identical*. The only difference is that the `rnspure` package lists no dependencies required for installation. +On more unusual systems, and in some rare cases, it might not be possible to +install or even compile one or more of the above modules. In such situations, +you can use the `rnspure` package instead, which require no external +dependencies for installation. Please note that the contents of the `rns` and +`rnspure` packages are *identical*. The only difference is that the `rnspure` +package lists no dependencies required for installation. -No matter how Reticulum is installed and started, it will load external dependencies only if they are *needed* and *available*. If for example you want to use Reticulum on a system that cannot support [pyserial](https://github.com/pyserial/pyserial), it is perfectly possible to do so using the `rnspure` package, but Reticulum will not be able to use serial-based interfaces. All other available modules will still be loaded when needed. +No matter how Reticulum is installed and started, it will load external +dependencies only if they are *needed* and *available*. If for example you want +to use Reticulum on a system that cannot support +[pyserial](https://github.com/pyserial/pyserial), it is perfectly possible to +do so using the `rnspure` package, but Reticulum will not be able to use +serial-based interfaces. All other available modules will still be loaded when +needed. -**Please Note!** If you use the `rnspure` package to run Reticulum on systems that do not support [PyCA/cryptography](https://github.com/pyca/cryptography), it is important that you read and understand the [Cryptographic Primitives](#cryptographic-primitives) section of this document. +**Please Note!** If you use the `rnspure` package to run Reticulum on systems +that do not support [PyCA/cryptography](https://github.com/pyca/cryptography), +it is important that you read and understand the [Cryptographic +Primitives](#cryptographic-primitives) section of this document. ## Bootstrapping Connectivity -Reticulum is not a service you subscribe to, nor is it a single global network you "join". Reticulum provides functionality for discovering available public interfaces over the network itself, and the broader community has provided various directories of publicly available entrypoints to bootstrap connectivity. +Reticulum is not a service you subscribe to, nor is it a single global network you "join". +Reticulum provides functionality for discovering available public interfaces +over the network itself, and the broader community has provided various directories +of publicly available entrypoints to bootstrap connectivity. To learn how to establish initial connectivity over Reticulum, read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual. -If you already have a general idea of how this works, you can use community-run sites such as [directory.rns.recipes](https://directory.rns.recipes/) and [rmap.world](https://rmap.world) to find interface definitions for initial connectivity to the global distributed Reticulum backbone. +If you already have a general idea of how this works, you can use community-run +sites such as [directory.rns.recipes](https://directory.rns.recipes/) and [rmap.world](https://rmap.world) +to find interface definitions for initial connectivity to the global distributed Reticulum backbone. ## Public Testnet ***Important!** Historically, a developer-targeted testnet was made available by the Reticulum project itself. As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, this public testnet, including the Amsterdam Testnet entrypoint, has now been decommissioned. If your still have instances that relied on this entrypoint for connectivity, transition to using the distributed backbone instead. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual for pointers.* @@ -265,11 +302,17 @@ For this to be possible, I need your help. Please support the continued developm - Ko-Fi: https://ko-fi.com/markqvist ## Cryptographic Primitives -Reticulum uses a simple suite of efficient, strong and well-tested cryptographic primitives, with widely available implementations that can be used both on general-purpose CPUs and on microcontrollers. +Reticulum uses a simple suite of efficient, strong and well-tested cryptographic +primitives, with widely available implementations that can be used both on +general-purpose CPUs and on microcontrollers. -One of the primary considerations for choosing this particular set of primitives is that they can be implemented *safely* with relatively few pitfalls, on practically all current computing platforms. +One of the primary considerations for choosing this particular set of primitives is +that they can be implemented *safely* with relatively few pitfalls, on practically +all current computing platforms. -The primitives listed here **are authoritative**. Anything claiming to be Reticulum, but not using these exact primitives **is not** Reticulum, and possibly an intentionally compromised or weakened clone. The utilised primitives are: +The primitives listed here **are authoritative**. Anything claiming to be Reticulum, +but not using these exact primitives **is not** Reticulum, and possibly an +intentionally compromised or weakened clone. The utilised primitives are: - Reticulum Identity Keys are 512-bit Curve25519 keysets - A 256-bit Ed25519 key for signatures @@ -284,7 +327,13 @@ The primitives listed here **are authoritative**. Anything claiming to be Reticu - SHA-256 - SHA-512 -In the default installation configuration, the `X25519`, `Ed25519`, and `AES-256-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/) (via the [PyCA/cryptography](https://github.com/pyca/cryptography) package). The hashing functions `SHA-256` and `SHA-512` are provided by the standard Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `HKDF`, `HMAC`, `Token` primitives, and the `PKCS7` padding function are always provided by the following internal implementations: +In the default installation configuration, the `X25519`, `Ed25519`, +and `AES-256-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/) +(via the [PyCA/cryptography](https://github.com/pyca/cryptography) package). +The hashing functions `SHA-256` and `SHA-512` are provided by the standard +Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `HKDF`, +`HMAC`, `Token` primitives, and the `PKCS7` padding function are always +provided by the following internal implementations: - [HKDF.py](RNS/Cryptography/HKDF.py) - [HMAC.py](RNS/Cryptography/HMAC.py) @@ -292,18 +341,39 @@ In the default installation configuration, the `X25519`, `Ed25519`, and `AES-256 - [PKCS7.py](RNS/Cryptography/PKCS7.py) -Reticulum also includes a complete implementation of all necessary primitives in pure Python. If OpenSSL and PyCA are not available on the system when Reticulum is started, Reticulum will instead use the internal pure-python primitives. A trivial consequence of this is performance, with the OpenSSL backend being *much* faster. The most important consequence however, is the potential loss of security by using primitives that has not seen the same amount of scrutiny, testing and review as those from OpenSSL. +Reticulum also includes a complete implementation of all necessary primitives +in pure Python. If OpenSSL and PyCA are not available on the system when +Reticulum is started, Reticulum will instead use the internal pure-python +primitives. A trivial consequence of this is performance, with the OpenSSL +backend being *much* faster. The most important consequence however, is the +potential loss of security by using primitives that has not seen the same +amount of scrutiny, testing and review as those from OpenSSL. -Please note that by default, installing Reticulum will **require** OpenSSL and PyCA to also be automatically installed if not already available. It is only possible to use the pure-python primitives if this requirement is specifically overridden by the user, for example by installing the `rnspure` package instead of the normal `rns` package, or by running directly from local source-code. +Please note that by default, installing Reticulum will **require** OpenSSL and +PyCA to also be automatically installed if not already available. It is only +possible to use the pure-python primitives if this requirement is specifically +overridden by the user, for example by installing the `rnspure` package instead +of the normal `rns` package, or by running directly from local source-code. -If you want to use the internal pure-python primitives, it is **highly advisable** that you have a good understanding of the risks that this pose, and make an informed decision on whether those risks are acceptable to you. +If you want to use the internal pure-python primitives, it is **highly +advisable** that you have a good understanding of the risks that this pose, and +make an informed decision on whether those risks are acceptable to you. -Having been in development for ten, and wider deployment for around five years, Reticulum is relatively young software, and should be considered as such. While it has been carefully built with cryptography best-practices very foremost in mind, there could very well still be privacy or security breaking bugs. +Reticulum is relatively young software, and should be considered as such. While +it has been built with cryptography best-practices very foremost in mind, it +_has not_ been externally security audited, and there could very well be +privacy or security breaking bugs. If you want to help out, or help sponsor an +audit, please do get in touch. ## Acknowledgements & Credits -Reticulum can only exist because of the mountain of Open Source work it was built on top of, the contributions of everyone involved, and everyone that has supported the project through the years. To everyone who has helped, thank you so much. +Reticulum can only exist because of the mountain of Open Source work it was +built on top of, the contributions of everyone involved, and everyone that has +supported the project through the years. To everyone who has helped, thank you +so much. -A number of other modules and projects are either part of, or used by Reticulum. Sincere thanks to the authors and contributors of the following projects: +A number of other modules and projects are either part of, or used by +Reticulum. Sincere thanks to the authors and contributors of the following +projects: - [PyCA/cryptography](https://github.com/pyca/cryptography), *BSD License* - [Pure-25519](https://github.com/warner/python-pure25519) by [Brian Warner](https://github.com/warner), *MIT License* diff --git a/README.mu b/README.mu index c64a7a50..2d361909 100644 --- a/README.mu +++ b/README.mu @@ -4,8 +4,6 @@ To understand the foundational philosophy and goals of this system, read the `_` Reticulum is the cryptography-based networking stack for building local and wide-area networks with readily available hardware. It can operate even with very high latency and extremely low bandwidth. Reticulum allows you to build wide-area networks with off-the-shelf tools, and offers end-to-end encryption and connectivity, initiator anonymity, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable delivery acknowledgements and more. -`(RNS Logo`w=18`a=c`:/media/reticulum/reticulum/HEAD/docs/source/graphics/rns_logo_256.webp) - The vision of Reticulum is to allow anyone to be their own network operator, and to make it cheap and easy to cover vast areas with a myriad of independent, inter-connectable and autonomous networks. Reticulum `!is not`! `*one`* network. It is `!a tool`! for building `*thousands of networks`*. Networks without kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate with each other, and require no central oversight. Networks for human beings. `*Networks for the people`*. Reticulum is a complete networking stack, and does not rely on IP or higher layers, but it is possible to use IP as the underlying carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the Internet or private IP networks. @@ -172,19 +170,9 @@ Currently, the following built-in interfaces are supported: >> Performance -`*All performance numbers here assume `B333rnsd`b running in fully interpreted Python mode, limited to a single CPU core.`* +Reticulum targets a `*very`* wide usable performance envelope, but prioritises functionality and performance on low-bandwidth mediums. The goal is to provide a dynamic performance envelope from 250 bits per second, to 1 gigabit per second on normal hardware. -Reticulum targets a `*very`* wide usable performance envelope, but prioritises functionality and performance on low-bandwidth mediums. The goal is to provide a dynamic performance envelope from 100 bits per second, to 10 gigabits per second on normal hardware. - -Currently, the on-network, usable performance envelope is approximately 150 bits per second to 1 gigabit per second on modest hardware. Real-world performance will vary widely with the speed of your system, availability of cryptographic hardware acceleration, and other factors. - -Raw routing throughput for a transport node hosted on a Raspberry Pi 5 is approximately 4 gigabits per second. Systems with modern crypto hardware acceleration can achieve transport throughput of 15 gigabits per second and higher. - -While the reference implementation is perfectly capable of reaching 100+ Gbps, by utilizing multiple CPU cores and vectorized transport, performance beyond the current level is not higly prioritised, since RNS is already able to saturate available physical medium bandwidth in almost all real-world situations. - -The primary objectives for RNS are `*reliability`*, `*correctness`*, `*security`*, `*proper traffic management`* and `*actually practical performance`*, not hollow on-paper "benchmarks" with little to no bearing on real-world usability. - -All alternative-implementation maintainers are `*highly`* encouraged to adopt a similar approach, and especially to focus on getting their implementations working properly before publishing empirically dubious or factually wrong performance claims. +Currently, the usable performance envelope is approximately 150 bits per second to 500 megabits per second, with physical mediums faster than that not being saturated. Performance beyond the current level is intended for future upgrades, but not highly prioritised at this point in time. >> Current Status @@ -268,7 +256,7 @@ Please note that by default, installing Reticulum will `!require`! OpenSSL and P If you want to use the internal pure-python primitives, it is `!highly advisable`! that you have a good understanding of the risks that this pose, and make an informed decision on whether those risks are acceptable to you. -Having been in development for ten, and wider deployment for around five years, Reticulum is relatively young software, and should be considered as such. While it has been carefully built with cryptography best-practices very foremost in mind, there could very well still be privacy or security breaking bugs. +Reticulum is relatively young software, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy or security breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch. >> Acknowledgements & Credits diff --git a/RNS/Buffer.py b/RNS/Buffer.py index 807b081a..b128a6cc 100644 --- a/RNS/Buffer.py +++ b/RNS/Buffer.py @@ -53,8 +53,7 @@ class StreamDataMessage(MessageBase): The stream id is limited to 2 bytes - 2 bit """ - HEADER_LEN = 2 # 2 for stream data message header - OVERHEAD = HEADER_LEN + 6 # 6 for channel envelope + OVERHEAD = 2 + 6 # 2 for stream data message header, 6 for channel envelope MAX_DATA_LEN = RNS.Link.MDU - OVERHEAD """ When the Buffer package is imported, this value is @@ -227,7 +226,7 @@ class RawChannelWriter(RawIOBase, AbstractContextManager): self._stream_id = stream_id self._channel = channel self._eof = False - self._mdu = channel.mdu - StreamDataMessage.HEADER_LEN + self._mdu = channel.mdu - StreamDataMessage.OVERHEAD def write(self, __b: bytes) -> int | None: try: @@ -243,7 +242,7 @@ class RawChannelWriter(RawIOBase, AbstractContextManager): chunk_segment_length = int(chunk_len/comp_try) compressed_chunk = bz2.compress(__b[:chunk_segment_length]) compressed_length = len(compressed_chunk) - if compressed_length < self._mdu and compressed_length < chunk_segment_length: + if compressed_length < StreamDataMessage.MAX_DATA_LEN and compressed_length < chunk_segment_length: comp_success = True break else: @@ -253,7 +252,7 @@ class RawChannelWriter(RawIOBase, AbstractContextManager): chunk = compressed_chunk processed_length = chunk_segment_length else: - chunk = bytes(__b[:self._mdu]) + chunk = bytes(__b[:StreamDataMessage.MAX_DATA_LEN]) processed_length = len(chunk) message = StreamDataMessage(self._stream_id, chunk, self._eof, comp_success) diff --git a/RNS/Cryptography/AES.py b/RNS/Cryptography/AES.py index c2b93665..9025ab77 100644 --- a/RNS/Cryptography/AES.py +++ b/RNS/Cryptography/AES.py @@ -43,7 +43,7 @@ elif cp.PROVIDER == cp.PROVIDER_PYCA: class AES_128_CBC: @staticmethod def encrypt(plaintext, key, iv): - if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for AES_128_CBC") + if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}") if cp.PROVIDER == cp.PROVIDER_INTERNAL: cipher = AES128(key) return cipher.encrypt(plaintext, iv) @@ -60,7 +60,7 @@ class AES_128_CBC: @staticmethod def decrypt(ciphertext, key, iv): - if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for AES_128_CBC") + if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}") if cp.PROVIDER == cp.PROVIDER_INTERNAL: cipher = AES128(key) return cipher.decrypt(ciphertext, iv) @@ -78,7 +78,7 @@ class AES_128_CBC: class AES_256_CBC: @staticmethod def encrypt(plaintext, key, iv): - if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for AES_256_CBC") + if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}") if cp.PROVIDER == cp.PROVIDER_INTERNAL: cipher = AES256(key) return cipher.encrypt_cbc(plaintext, iv) @@ -95,7 +95,7 @@ class AES_256_CBC: @staticmethod def decrypt(ciphertext, key, iv): - if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for AES_256_CBC") + if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}") if cp.PROVIDER == cp.PROVIDER_INTERNAL: cipher = AES256(key) return cipher.decrypt_cbc(ciphertext, iv) diff --git a/RNS/Cryptography/HKDF.py b/RNS/Cryptography/HKDF.py index e88e5616..33d95d19 100644 --- a/RNS/Cryptography/HKDF.py +++ b/RNS/Cryptography/HKDF.py @@ -32,59 +32,8 @@ import hashlib from math import ceil from RNS.Cryptography import HMAC -# Precomputed byte-permutation tables used to key the inner -# and outer hashing states of the HMAC construction -trans_5C = bytes((x ^ 0x5C) for x in range(256)) -trans_36 = bytes((x ^ 0x36) for x in range(256)) - -# Precomputed HKDF counter bytes -_counters = [bytes([(i + 1) % 0x100]) for i in range(0x100)] - def hkdf(length=None, derive_from=None, salt=None, context=None): hash_len = 32 - hash_block_size = 64 - - if length == None or length < 1: raise ValueError("Invalid output key length") - if derive_from == None or derive_from == "": raise ValueError("Cannot derive key from empty input material") - - if salt == None or len(salt) == 0: salt = bytes([0] * hash_len) - if context == None: context = b"" - - pseudorandom_key = HMAC.new(salt, derive_from).digest() - - # The pseudorandom key is always a 32-byte SHA-256 digest, so the - # HMAC key schedule below never needs the long-key hashing branch. - key = pseudorandom_key.ljust(hash_block_size, b"\x00") - - # Precompute the keyed inner and outer hashing states of the - # HMAC construction once. Each expansion block then only requires - # two cheap hash-object copies instead of constructing a complete - # HMAC object per 32 bytes of output. This is possible since the - # PRK is **always** 32-byte SHA-256 HMAC output. - inner_state = hashlib.sha256(key.translate(trans_36)) - outer_state = hashlib.sha256(key.translate(trans_5C)) - - # Expand - has_context = len(context) > 0 - block = b"" - derived = bytearray() - - for i in range(ceil(length / hash_len)): - inner = inner_state.copy() - inner.update(block) - if has_context: inner.update(context) - inner.update(_counters[i & 0xFF]) - - outer = outer_state.copy() - outer.update(inner.digest()) - - block = outer.digest() - derived += block - - return bytes(derived[:length]) - -def hkdf_legacy(length=None, derive_from=None, salt=None, context=None): - hash_len = 32 def hmac_sha256(key, data): return HMAC.new(key, data).digest() diff --git a/RNS/Cryptography/HMAC.py b/RNS/Cryptography/HMAC.py index 9996083c..848a2313 100644 --- a/RNS/Cryptography/HMAC.py +++ b/RNS/Cryptography/HMAC.py @@ -180,9 +180,4 @@ def digest(key, msg, digest): outer.update(key.translate(trans_5C)) inner.update(msg) outer.update(inner.digest()) - return outer.digest() - -def _cd_internal(a, b): return a == b -try: from hmac import compare_digest as _cd -except ImportError: _cd = _cd_internal -compare_digest = _cd \ No newline at end of file + return outer.digest() \ No newline at end of file diff --git a/RNS/Cryptography/Token.py b/RNS/Cryptography/Token.py index fb9577aa..c0e457d6 100644 --- a/RNS/Cryptography/Token.py +++ b/RNS/Cryptography/Token.py @@ -80,7 +80,7 @@ class Token(): received_hmac = token[-32:] expected_hmac = HMAC.new(self._signing_key, token[:-32]).digest() - if HMAC.compare_digest(received_hmac, expected_hmac): return True + if received_hmac == expected_hmac: return True else: return False diff --git a/RNS/Cryptography/__init__.py b/RNS/Cryptography/__init__.py index 0ccc23d7..925221de 100644 --- a/RNS/Cryptography/__init__.py +++ b/RNS/Cryptography/__init__.py @@ -52,7 +52,5 @@ elif cp.PROVIDER == cp.PROVIDER_PYCA: py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") -so_modules = glob.glob(os.path.dirname(__file__)+"/*.so") -pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd") -modules = py_modules+pyc_modules+so_modules+pyd_modules -__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"])) \ No newline at end of file +modules = py_modules+pyc_modules +__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) \ No newline at end of file diff --git a/RNS/Destination.py b/RNS/Destination.py index c5a65a2d..9fc6ab03 100755 --- a/RNS/Destination.py +++ b/RNS/Destination.py @@ -206,7 +206,7 @@ class Destination: def _clean_ratchets(self): if self.ratchets != None: if len (self.ratchets) > self.retained_ratchets: - self.ratchets = self.ratchets[:self.retained_ratchets] + self.ratchets = self.ratchets[:Destination.RATCHET_COUNT] def _persist_ratchets(self): try: diff --git a/RNS/Discovery.py b/RNS/Discovery.py index 7dbf09c6..cdcb5345 100644 --- a/RNS/Discovery.py +++ b/RNS/Discovery.py @@ -1,33 +1,3 @@ -# Reticulum License -# -# Copyright (c) 2016-2026 Mark Qvist -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - import os import re import RNS @@ -40,16 +10,8 @@ from threading import Lock from .vendor import umsgpack as msgpack from collections import deque -# Discoverable interfaces must include a short, -# unique implementation-specific identifier and -# version tag in announced discovery information. -IMPLEMENTATION_NAME = "RNS" -from RNS._version import __version__ as RNS_VERSION - NAME = 0xFF TRANSPORT_ID = 0xFE -TRANSPORT_IMPL = 0xFD -TRANSPORT_VERS = 0xFC INTERFACE_TYPE = 0x00 TRANSPORT = 0x01 REACHABLE_ON = 0x02 @@ -65,7 +27,6 @@ SPREADINGFACTOR = 0x0B CODINGRATE = 0x0C MODULATION = 0x0D CHANNEL = 0x0E -OP_ADDR = 0xF0 APP_NAME = "rnstransport" @@ -162,7 +123,7 @@ class InterfaceAnnouncer(): interface.discovery_height = dhgt except Exception as e: - RNS.log(f"Error while getting discovery location from executable at {interface.reachable_on}: {e}", RNS.LOG_ERROR) + RNS.log(f"Error while getting reachable_on from executable at {interface.reachable_on}: {e}", RNS.LOG_ERROR) RNS.log(f"Aborting discovery announce", RNS.LOG_ERROR) return None @@ -170,21 +131,16 @@ class InterfaceAnnouncer(): info = {INTERFACE_TYPE: interface_type, TRANSPORT: RNS.Reticulum.transport_enabled(), TRANSPORT_ID: RNS.Transport.identity.hash, - TRANSPORT_IMPL: IMPLEMENTATION_NAME, - TRANSPORT_VERS: RNS_VERSION, NAME: self.sanitize(interface.discovery_name), LATITUDE: interface.discovery_latitude, LONGITUDE: interface.discovery_longitude, HEIGHT: interface.discovery_height} - if interface.discovery_lxmf_address: info[OP_ADDR] = interface.discovery_lxmf_address - if interface_type == "TCPClientInterface" and not interface.kiss_framing: RNS.log(f"Invalid interface discovery configuration for {interface}, aborting discovery announce", RNS.LOG_ERROR) return None if interface_type in ["BackboneInterface", "TCPServerInterface"]: - if not interface.reachable_on: return None reachable_on = self.sanitize(interface.reachable_on) if not RNS.vendor.platformutils.is_windows(): @@ -234,8 +190,8 @@ class InterfaceAnnouncer(): info[MODULATION] = self.sanitize(interface.discovery_modulation) if interface.discovery_publish_ifac == True: - if interface.ifac_netname: info[IFAC_NETNAME] = self.sanitize(interface.ifac_netname) - if interface.ifac_netkey: info[IFAC_NETKEY] = self.sanitize(interface.ifac_netkey) + info[IFAC_NETNAME] = self.sanitize(interface.ifac_netname) + info[IFAC_NETKEY] = self.sanitize(interface.ifac_netkey) packed = msgpack.packb(info) infohash = RNS.Identity.full_hash(packed) @@ -345,8 +301,6 @@ class InterfaceAnnounceHandler: if INTERFACE_TYPE in unpacked: interface_type = unpacked[INTERFACE_TYPE] name = self.sanitize_name(unpacked[NAME]) - impl_name = unpacked[TRANSPORT_IMPL] if TRANSPORT_IMPL in unpacked else None - impl_version = unpacked[TRANSPORT_VERS] if TRANSPORT_VERS in unpacked else None if type(unpacked[TRANSPORT]) != bool: raise ValueError("Invalid data in transport field of announce") if type(unpacked[LATITUDE]) not in [type(None), float]: raise ValueError("Invalid data in latitude field of announce") @@ -361,8 +315,6 @@ class InterfaceAnnounceHandler: raise ValueError("Invalid data in reachable_on field of announce") info = {"type": interface_type, - "impl_name": impl_name, - "version": impl_version, "transport": unpacked[TRANSPORT], "name": name or f"Discovered {interface_type}", "received": time.time(), @@ -375,8 +327,8 @@ class InterfaceAnnounceHandler: "longitude": unpacked[LONGITUDE], "height": unpacked[HEIGHT]} - if IFAC_NETNAME in unpacked and unpacked[IFAC_NETNAME] and type(unpacked[IFAC_NETNAME]) == str: info["ifac_netname"] = str(unpacked[IFAC_NETNAME]) - if IFAC_NETKEY in unpacked and unpacked[IFAC_NETKEY] and type(unpacked[IFAC_NETKEY]) == str: info["ifac_netkey"] = str(unpacked[IFAC_NETKEY]) + if IFAC_NETNAME in unpacked: info["ifac_netname"] = str(unpacked[IFAC_NETNAME]) + if IFAC_NETKEY in unpacked: info["ifac_netkey"] = str(unpacked[IFAC_NETKEY]) if interface_type in ["BackboneInterface", "TCPServerInterface"]: backbone_support = not RNS.vendor.platformutils.is_windows() and not RNS.vendor.platformutils.is_darwin() @@ -405,7 +357,7 @@ class InterfaceAnnounceHandler: cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else "" cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else "" cfg_identity_str = f"\n transport_identity = {cfg_identity}" - info["config_entry"] = f"[[{cfg_name}]]\n type = I2PInterface\n enabled = yes\n peers = {cfg_remote}.b32.i2p{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}" + info["config_entry"] = f"[[{cfg_name}]]\n type = I2PInterface\n enabled = yes\n peers = {cfg_remote}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}" if interface_type == "RNodeInterface": info["frequency"] = unpacked[FREQUENCY] @@ -458,11 +410,6 @@ class InterfaceAnnounceHandler: discovery_hash_material = info["transport_id"]+info["name"] info["discovery_hash"] = RNS.Identity.full_hash(discovery_hash_material.encode("utf-8")) - if info and OP_ADDR in unpacked: - if type(unpacked[OP_ADDR]) not in [type(None), bytes]: raise ValueError("Invalid data in operator LXMF address field of announce") - if unpacked[OP_ADDR] and len(unpacked[OP_ADDR]) == RNS.Identity.TRUNCATED_HASHLENGTH//8: - info["operator_lxmf_address"] = RNS.hexrep(unpacked[OP_ADDR], delimit=False) - if self.callback and callable(self.callback): self.callback(info) except Exception as e: @@ -480,17 +427,13 @@ class InterfaceDiscovery(): STATUS_UNKNOWN = 100 STATUS_AVAILABLE = 1000 STATUS_CODE_MAP = {"available": STATUS_AVAILABLE, "unknown": STATUS_UNKNOWN, "stale": STATUS_STALE} - AUTOCONNECT_TYPES = ["BackboneInterface"] + AUTOCONNECT_TYPES = ["BackboneInterface", "TCPServerInterface"] DISCOVERABLE_TYPES = ["BackboneInterface", "TCPServerInterface", "I2PInterface", "RNodeInterface", "WeaveInterface", "KISSInterface"] - AUTOCONNECT_IMPLS = ["RNS"] - AUTOCONNECT_MIN_V = "1.5.2" - AC_TRANSPORT_MODE = RNS.Interfaces.Interface.Interface.MODE_GATEWAY AC_GRAVITY = 0 discovery_lock = Lock() - autoconnect_lock = Lock() def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None, discover_interfaces=True): if not required_value: required_value = InterfaceAnnouncer.DEFAULT_STAMP_VALUE @@ -536,16 +479,7 @@ class InterfaceDiscovery(): should_remove = False heard_delta = now-info["last_heard"] info["name"] = InterfaceAnnounceHandler.sanitize_name(info["name"]) - - # TODO: Temporary measure to sanitize invalidly persisted "None" values - # for incorrect IFAC parameters published due to unguarded nonsensical - # node-side configuration. Potentially remove once nodes upgrade to RNS - # versions that guard against the configuration mismatch. - try: - if "ifac_netname" in info and info["ifac_netname"] == "None": info.pop("ifac_netname") - if "ifac_netkey" in info and info["ifac_netkey"] == "None": info.pop("ifac_netkey") - except Exception as e: RNS.log(f"Error while sanitizing discovery info: {e}", RNS.LOG_ERROR) - + if heard_delta > self.THRESHOLD_REMOVE: should_remove = True elif not "transport_id" in info or not info["transport_id"]: should_remove = True elif not "network_id" in info or not info["network_id"]: should_remove = True @@ -591,13 +525,11 @@ class InterfaceDiscovery(): interface_type = info["type"] discovery_hash = info["discovery_hash"] discovered_type = info["type"] - has_impl_info = "impl_name" in info and "version" in info and info["impl_name"] and info["version"] - version_str = f" ({info['impl_name']} {info['version']})" if has_impl_info else " (unknown implementation)" if not discovered_type in self.DISCOVERABLE_TYPES: return hops = info["hops"]; ms = "" if hops == 1 else "s" filename = RNS.hexrep(discovery_hash, delimit=False) filepath = os.path.join(self.storagepath, filename) - RNS.log(f"Discovered {interface_type}{version_str} {hops} hop{ms} away with stamp value {value}: {name}", RNS.LOG_DEBUG) + RNS.log(f"Discovered {interface_type} {hops} hop{ms} away with stamp value {value}: {name}", RNS.LOG_DEBUG) with self.discovery_lock: if not os.path.isfile(filepath): try: @@ -665,27 +597,22 @@ class InterfaceDiscovery(): autoconnected_interfaces = self.autoconnect_count() for interface in self.monitored_interfaces: try: - if not interface in RNS.Transport.interfaces: - RNS.log(f"A monitored auto-connected interface was manually detached, removing from monitoring", RNS.LOG_DEBUG) - detached_interfaces.append(interface) + if interface.online: + online_interfaces += 1 + if hasattr(interface, "autoconnect_down") and interface.autoconnect_down != None: + RNS.log(f"Auto-discovered interface {interface} reconnected") + interface.autoconnect_down = None else: - if interface.online: - online_interfaces += 1 - if hasattr(interface, "autoconnect_down") and interface.autoconnect_down != None: - RNS.log(f"Auto-discovered interface {interface} reconnected") - interface.autoconnect_down = None + if not hasattr(interface, "autoconnect_down") or interface.autoconnect_down == None: + RNS.log(f"Auto-discovered interface {interface} disconnected", RNS.LOG_DEBUG) + interface.autoconnect_down = time.time() else: - if not hasattr(interface, "autoconnect_down") or interface.autoconnect_down == None: - RNS.log(f"Auto-discovered interface {interface} disconnected", RNS.LOG_DEBUG) - interface.autoconnect_down = time.time() - - else: - down_for = time.time()-interface.autoconnect_down - if down_for >= self.detach_threshold: - RNS.log(f"Auto-discovered interface {interface} has been down for {RNS.prettytime(down_for)}, detaching", RNS.LOG_DEBUG) - detached_interfaces.append(interface) + down_for = time.time()-interface.autoconnect_down + if down_for >= self.detach_threshold: + RNS.log(f"Auto-discovered interface {interface} has been down for {RNS.prettytime(down_for)}, detaching", RNS.LOG_DEBUG) + detached_interfaces.append(interface) except Exception as e: RNS.log(f"Error while checking auto-connected interface state for {interface}: {e}", RNS.LOG_ERROR) @@ -719,10 +646,8 @@ class InterfaceDiscovery(): RNS.log(f"Error while de-registering auto-connected interface from transport: {e}", RNS.LOG_ERROR) def teardown_interface(self, interface): - if interface in RNS.Transport.interfaces: - was_detached = RNS.Reticulum.get_instance()._detach_interface(interface.name, internal_forced=True) - if not was_detached: RNS.log(f"Detach of auto-connected interface {interface} did not succeed", RNS.LOG_ERROR) - + interface.detach() + RNS.Transport.remove_interface(interface) if interface in self.monitored_interfaces: self.monitored_interfaces.remove(interface) def autoconnect_count(self): @@ -769,26 +694,6 @@ class InterfaceDiscovery(): return exists - def autoconnect_interface_name(self, name): - names = {interface.name for interface in RNS.Transport.interfaces} - if not name in names: return name - else: - n = 2; seq_name = f"{name} ({n})" - while seq_name in names: - n += 1; seq_name = f"{name} ({n})" - - return seq_name - - def autoconnect_qualified(self, info): - if RNS.Reticulum.should_autoconnect_unverified_implementations(): return True - if not "impl_name" in info: return False - if not info["impl_name"] in self.AUTOCONNECT_IMPLS: return False - if not "version" in info or not info["version"]: return False - - version = version_tuple(info["version"]) - if version == None: return False - return version >= version_tuple(self.AUTOCONNECT_MIN_V) - def autoconnect(self, info): try: if RNS.Reticulum.should_autoconnect_discovered_interfaces(): @@ -796,84 +701,55 @@ class InterfaceDiscovery(): if autoconnected_count < RNS.Reticulum.max_autoconnected_interfaces(): interface_type = info["type"] if interface_type in self.AUTOCONNECT_TYPES: - if not self.autoconnect_qualified(info): - impl = f"{info['impl_name']} {info['version']}" if ("impl_name" in info and "version" in info and info["impl_name"] and info["version"]) else "unknown implementation" - RNS.log(f"Not auto-connecting discovered {interface_type} {info['name']} ({impl}), auto-connect criteria not satisfied", RNS.LOG_DEBUG) - return - endpoint_hash = self.endpoint_hash(info) exists = self.interface_exists(info) if exists: RNS.log(f"Discovered {interface_type} already exists, not auto-connecting", RNS.LOG_DEBUG) else: + if interface_type == "TCPClientInterface": + RNS.log(f"Your operating system does not support the Backbone interface type, and must degrade to using TCPClientInterface instead", RNS.LOG_WARNING) + RNS.log(f"Auto-connecting discovered TCPClient interfaces is not yet implemented, aborting auto-connect", RNS.LOG_WARNING) + RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_WARNING) + return + if interface_type == "I2PInterface": - RNS.log(f"Auto-connecting discovered I2P interfaces is not yet implemented, aborting auto-connect", RNS.LOG_DEBUG) - RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_DEBUG) + RNS.log(f"Auto-connecting discovered I2P interfaces is not yet implemented, aborting auto-connect", RNS.LOG_WARNING) + RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_WARNING) return if is_ygg_ipv6(info["reachable_on"]): # TODO: Somehow detect if yggdrasil is enabled on the system return - if is_onion_address(info["reachable_on"]): - # TODO: Somehow detect if TOR is enabled on the system - return + interface_name = info["name"] + config_entry = info["config_entry"] + interface_config = {} + interface_config["name"] = f"{interface_name}" + ifac_netname = info["ifac_netname"] if "ifac_netname" in info else None + ifac_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None + interface = None - if is_ip_address(info["reachable_on"]) and is_invalid_ip_address(info["reachable_on"]): - RNS.log(f"Not auto-connecting discovered interface with invalid IP address: {info['reachable_on']}", RNS.LOG_DEBUG) - return + if interface_type == "BackboneInterface": + from RNS.Interfaces import BackboneInterface + interface_config["target_host"] = info["reachable_on"] + interface_config["target_port"] = info["port"] + interface = BackboneInterface.BackboneClientInterface(RNS.Transport, interface_config) - with self.autoconnect_lock: - if self.interface_exists(info): return - backbone_support = (not RNS.vendor.platformutils.is_windows() and not RNS.vendor.platformutils.is_darwin()) - - interface_name = self.autoconnect_interface_name(info["name"]) - if interface_name != info["name"]: - RNS.log(f"Auto-connect name collision for \"{info['name']}\", connecting as \"{interface_name}\"", RNS.LOG_NOTICE) - - config_entry = info["config_entry"] - interface_config = {} - interface_config["name"] = f"{interface_name}" - - # TODO: Temporary measure to sanitize invalidly persisted "None" values - # for incorrect IFAC parameters published due to unguarded nonsensical - # node-side configuration. Potentially remove once nodes upgrade to RNS - # versions that guard against the configuration mismatch. - if "ifac_netname" in info and info["ifac_netname"] == "None": info["ifac_netname"] = None - if "ifac_netkey" in info and info["ifac_netkey"] == "None": info["ifac_netkey"] = None - - ifac_netname = info["ifac_netname"] if "ifac_netname" in info and info["ifac_netname"] else None - ifac_netkey = info["ifac_netkey"] if "ifac_netkey" in info and info["ifac_netkey"] else None - interface = None - - if interface_type == "BackboneInterface": - if backbone_support: - from RNS.Interfaces import BackboneInterface - interface_class = BackboneInterface.BackboneClientInterface - else: - from RNS.Interfaces import TCPInterface - interface_class = TCPInterface.TCPClientInterface - RNS.log(f"BackboneInterface is not yet supported on this operating system, auto-connecting discovered {interface_type} {interface_name} using TCPClientInterface", RNS.LOG_NOTICE) - - interface_config["target_host"] = info["reachable_on"] - interface_config["target_port"] = info["port"] - interface = interface_class(RNS.Transport, interface_config) - - if interface: - RNS.log(f"Auto-connecting discovered {interface_type} {interface_name}") - interface.autoconnect_hash = endpoint_hash - interface.autoconnect_source = info["network_id"] - if RNS.Reticulum.autoconnect_interface_mode(): mode = RNS.Reticulum.autoconnect_interface_mode() - else: mode = self.AC_TRANSPORT_MODE if RNS.Reticulum.transport_enabled() else None - internal_a = True if RNS.Reticulum.autoconnect_announces_to_internal() else None - gravity = RNS.Reticulum.autoconnect_interface_gravity() or self.AC_GRAVITY - ar_target = RNS.Reticulum.get_instance()._default_ar_target() if RNS.Reticulum.transport_enabled() else None - ar_penalty = RNS.Reticulum.get_instance()._default_ar_penalty() if RNS.Reticulum.transport_enabled() else None - ar_grace = RNS.Reticulum.get_instance()._default_ar_grace() if RNS.Reticulum.transport_enabled() else None - RNS.Reticulum.get_instance()._add_interface(interface, mode=mode, ifac_netname=ifac_netname, ifac_netkey=ifac_netkey, configured_bitrate=5E6, - announce_rate_target=ar_target, announce_rate_grace=ar_grace, announce_rate_penalty=ar_penalty, - announces_to_internal=internal_a, gravity=gravity) - self.monitor_interface(interface) + if interface: + RNS.log(f"Auto-connecting discovered {interface_type} {interface_name}") + interface.autoconnect_hash = endpoint_hash + interface.autoconnect_source = info["network_id"] + if RNS.Reticulum.autoconnect_interface_mode(): mode = RNS.Reticulum.autoconnect_interface_mode() + else: mode = self.AC_TRANSPORT_MODE if RNS.Reticulum.transport_enabled() else None + internal_a = True if RNS.Reticulum.autoconnect_announces_to_internal() else None + gravity = RNS.Reticulum.autoconnect_interface_gravity() or self.AC_GRAVITY + ar_target = RNS.Reticulum.get_instance()._default_ar_target() if RNS.Reticulum.transport_enabled() else None + ar_penalty = RNS.Reticulum.get_instance()._default_ar_penalty() if RNS.Reticulum.transport_enabled() else None + ar_grace = RNS.Reticulum.get_instance()._default_ar_grace() if RNS.Reticulum.transport_enabled() else None + RNS.Reticulum.get_instance()._add_interface(interface, mode=mode, ifac_netname=ifac_netname, ifac_netkey=ifac_netkey, configured_bitrate=5E6, + announce_rate_target=ar_target, announce_rate_grace=ar_grace, announce_rate_penalty=ar_penalty, + announces_to_internal=internal_a, gravity=gravity) + self.monitor_interface(interface) except Exception as e: RNS.log(f"Error while auto-connecting discovered interface: {e}", RNS.LOG_ERROR) @@ -975,15 +851,6 @@ def is_ygg_ipv6(address_string): try: return ipaddress.ip_address(address_string) in ipaddress.IPv6Network("200::/7") except: return False -def is_onion_address(address_string): - try: return address_string.lower().endswith(".onion") - except: return False - -INVALID_IP_ADDRESSES = ["127.0.0.1", "0.0.0.0"] -def is_invalid_ip_address(address_string): - if not address_string in INVALID_IP_ADDRESSES: return False - else: return True - def is_hostname(hostname): if hostname[-1] == ".": hostname = hostname[:-1] if len(hostname) > 253: return False @@ -992,18 +859,6 @@ def is_hostname(hostname): allowed = re.compile(r"(?!-)[a-z0-9-]{1,63}(? 0: if announced_identity.hash in RNS.Transport.blackholed_identities: RNS.log(f"Invalidated and dropped announce from blackholed identity {RNS.prettyhexrep(announced_identity.hash)}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - if signal_blackholed: return "blackholed" - else: return False + return False - if announced_identity.pub != None and (packet.announce_signature_validated or announced_identity.validate(signature, signed_data)): - packet.announce_signature_validated = True + if announced_identity.pub != None and announced_identity.validate(signature, signed_data): if only_validate_signature: del announced_identity return True @@ -759,7 +757,9 @@ class Identity: return True except Exception as e: - RNS.log(f"Failed to load identity key, the contained exception was: {e}", RNS.LOG_ERROR) + raise e + RNS.log("Failed to load identity key", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) return False def load_public_key(self, pub_bytes): @@ -777,12 +777,8 @@ class Identity: self.sig_pub = Ed25519PublicKey.from_public_bytes(self.sig_pub_bytes) self.update_hashes() - - return True - except Exception as e: - RNS.log(f"Error while loading public key, the contained exception was: {e}", RNS.LOG_ERROR) - return False + RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR) def update_hashes(self): self.hash = Identity.truncated_hash(self.get_public_key()) diff --git a/RNS/Interfaces/AX25KISSInterface.py b/RNS/Interfaces/AX25KISSInterface.py index 562de5d4..abda34fe 100644 --- a/RNS/Interfaces/AX25KISSInterface.py +++ b/RNS/Interfaces/AX25KISSInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -128,9 +128,7 @@ class AX25KISSInterface(Interface): self.packet_queue = [] self.flow_control = flow_control - self.shared_medium = True self.interface_ready = False - self.reconnecting = False self.flow_control_timeout = 5 self.flow_control_locked = time.time() @@ -153,18 +151,14 @@ class AX25KISSInterface(Interface): try: self.open_port() - if self.serial.is_open: self.configure_device() - else: raise IOError("Could not open serial port") - except Exception as e: - RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) - RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) - RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: - thread = threading.Thread(target=self.reconnect_port) - thread.daemon = True - thread.start() + RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) + raise e + if self.serial.is_open: + self.configure_device() + else: + raise IOError("Could not open serial port") def open_port(self): RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE) @@ -182,12 +176,6 @@ class AX25KISSInterface(Interface): dsrdtr = False, ) - def detach(self): - self.detached = True - self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) - def configure_device(self): # Allow time for interface to initialise before config sleep(2.0) @@ -384,28 +372,30 @@ class AX25KISSInterface(Interface): RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) - if RNS.Reticulum.panic_on_interface_error: RNS.panic() + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: self.reconnect_port() + self.serial.close() + self.reconnect_port() def reconnect_port(self): - self.reconnecting = True - while not self.online and not self.detached: + while not self.online: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: self.configure_device() - except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: + self.configure_device() + except Exception as e: + RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - self.reconnecting = False - if self.online: RNS.log("Reconnected serial port for "+str(self)) + RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): return False + def should_ingress_limit(self): + return False - def __str__(self): return "AX25KISSInterface["+self.name+"]" \ No newline at end of file + def __str__(self): + return "AX25KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/Android/KISSInterface.py b/RNS/Interfaces/Android/KISSInterface.py index 0bdbf00f..b14f45c6 100644 --- a/RNS/Interfaces/Android/KISSInterface.py +++ b/RNS/Interfaces/Android/KISSInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -128,11 +128,9 @@ class KISSInterface(Interface): self.beacon_d = beacon_data.encode("utf-8") self.first_tx = None self.bitrate = KISSInterface.BITRATE_GUESS - self.reconnecting = False self.packet_queue = [] self.flow_control = flow_control - self.shared_medium = True self.interface_ready = False self.flow_control_timeout = 5 self.flow_control_locked = time.time() @@ -150,23 +148,15 @@ class KISSInterface(Interface): try: self.open_port() - if self.serial.is_open: self.configure_device() - else: raise IOError("Could not open serial port") - except Exception as e: - RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) - RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) - RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: - thread = threading.Thread(target=self.reconnect_port) - thread.daemon = True - thread.start() + RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR) + raise e + + if self.serial.is_open: + self.configure_device() + else: + raise IOError("Could not open serial port") - def detach(self): - self.detached = True - self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) def open_port(self): RNS.log("Opening serial port "+self.port+"...") @@ -426,23 +416,24 @@ class KISSInterface(Interface): RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: self.reconnect_port() + self.serial.close() + self.reconnect_port() def reconnect_port(self): - self.reconnecting = True while not self.online: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: self.configure_device() - except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: + self.configure_device() + except Exception as e: + RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - self.reconnecting = False - if self.online: RNS.log("Reconnected serial port for "+str(self)) + RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): return False + def should_ingress_limit(self): + return False - def __str__(self): return "KISSInterface["+self.name+"]" \ No newline at end of file + def __str__(self): + return "KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/Android/RNodeInterface.py b/RNS/Interfaces/Android/RNodeInterface.py index aa999b9c..6dfece6b 100644 --- a/RNS/Interfaces/Android/RNodeInterface.py +++ b/RNS/Interfaces/Android/RNodeInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -282,9 +282,12 @@ class RNodeInterface(Interface): if (port != None or device_serial != None) and (enable_bluetooth or disable_bluetooth or pairing_mode): serial = None bluetooth_state = None - if pairing_mode: bluetooth_state = 0x01 - elif enable_bluetooth: bluetooth_state = 0x01 - elif disable_bluetooth: bluetooth_state = 0x00 + if pairing_mode: + bluetooth_state = 0x01 + elif enable_bluetooth: + bluetooth_state = 0x01 + elif disable_bluetooth: + bluetooth_state = 0x00 if port != None: RNS.log("Opening serial port "+port+"...") @@ -515,7 +518,6 @@ class RNodeInterface(Interface): self.packet_queue = [] self.flow_control = flow_control - self.shared_medium = True self.interface_ready = False self.announce_rate_target = None self.last_port_io = 0 @@ -1504,13 +1506,13 @@ class RNodeInterface(Interface): except Exception as e: self.online = False - if not self.detached: - RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) + RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) - if RNS.Reticulum.panic_on_interface_error: RNS.panic() + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() - RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) + RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False @@ -1570,14 +1572,10 @@ class RNodeInterface(Interface): except Exception as e: RNS.log(f"An error occurred while detaching {self}: {e}", RNS.LOG_ERROR) - if self.use_ble: - self.ble.close() - self.ble.cleanup() - elif self.use_tcp: + if self.use_ble: self.ble.close() + if self.use_tcp: time.sleep(0.5) self.tcp.close() - else: - self.serial.close() def should_ingress_limit(self): return False @@ -1656,9 +1654,6 @@ class BLEConnection(BluetoothDispatcher): self.owner.ble_rx_queue = b"" return data - def cleanup(self): - self.should_run = False - def close(self): try: if self.connected: @@ -1747,7 +1742,7 @@ class BLEConnection(BluetoothDispatcher): ble_devices = self.find_target_devices() if len(ble_devices) > 0: self.ble_device = ble_devices.pop() - else: self.ble_device = None + else: self.ble_device == None if self.ble_device != None: if self.was_connected: diff --git a/RNS/Interfaces/Android/SerialInterface.py b/RNS/Interfaces/Android/SerialInterface.py index 22c28765..2e0cb177 100644 --- a/RNS/Interfaces/Android/SerialInterface.py +++ b/RNS/Interfaces/Android/SerialInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -95,7 +95,6 @@ class SerialInterface(Interface): raise ValueError("No port specified for serial interface") self.HW_MTU = 564 - self.shared_medium = True self.pyserial = serial self.serial = None @@ -109,7 +108,6 @@ class SerialInterface(Interface): self.timeout = 100 self.online = False self.bitrate = self.speed - self.reconnecting = False if parity.lower() == "e" or parity.lower() == "even": self.parity = "E" @@ -119,23 +117,15 @@ class SerialInterface(Interface): try: self.open_port() - if self.serial.is_open: self.configure_device() - else: raise IOError("Could not open serial port") - except Exception as e: - RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) - RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) - RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: - thread = threading.Thread(target=self.reconnect_port) - thread.daemon = True - thread.start() + RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) + raise e + + if self.serial.is_open: + self.configure_device() + else: + raise IOError("Could not open serial port") - def detach(self): - self.detached = True - self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) def open_port(self): RNS.log("Opening serial port "+self.port+"...") @@ -267,23 +257,24 @@ class SerialInterface(Interface): RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: self.reconnect_port() + self.serial.close() + self.reconnect_port() def reconnect_port(self): - self.reconnecting = True - while not self.online and not self.detached: + while not self.online: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: self.configure_device() - except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: + self.configure_device() + except Exception as e: + RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - self.reconnecting = False - if self.online: RNS.log("Reconnected serial port for "+str(self)) + RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): return False + def should_ingress_limit(self): + return False - def __str__(self): return "SerialInterface["+self.name+"]" + def __str__(self): + return "SerialInterface["+self.name+"]" diff --git a/RNS/Interfaces/Android/__init__.py b/RNS/Interfaces/Android/__init__.py index 454b31ff..a9a74ff3 100644 --- a/RNS/Interfaces/Android/__init__.py +++ b/RNS/Interfaces/Android/__init__.py @@ -33,7 +33,5 @@ import glob py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") -so_modules = glob.glob(os.path.dirname(__file__)+"/*.so") -pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd") -modules = py_modules+pyc_modules+so_modules+pyd_modules -__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"])) \ No newline at end of file +modules = py_modules+pyc_modules +__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) \ No newline at end of file diff --git a/RNS/Interfaces/AutoInterface.py b/RNS/Interfaces/AutoInterface.py index 610b40d8..c136efdf 100644 --- a/RNS/Interfaces/AutoInterface.py +++ b/RNS/Interfaces/AutoInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -127,7 +127,6 @@ class AutoInterface(Interface): self.link_local_addresses = [] self.adopted_interfaces = {} self.interface_servers = {} - self.discovery_sockets = [] self.multicast_echoes = {} self.initial_echoes = {} self.timed_out_interfaces = {} @@ -269,8 +268,6 @@ class AutoInterface(Interface): addr_info = socket.getaddrinfo(link_local_addr+"%"+ifname, self.unicast_discovery_port, socket.AF_INET6, socket.SOCK_DGRAM) unicast_discovery_socket.bind(addr_info[0][4]) - self.discovery_sockets.append(unicast_discovery_socket) - mcast_addr = self.mcast_discovery_address RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME) @@ -299,8 +296,6 @@ class AutoInterface(Interface): discovery_socket.bind(addr_info[0][4]) - self.discovery_sockets.append(discovery_socket) - # Set up thread for multicast discovery packets def discovery_loop(): self.discovery_handler(discovery_socket, ifname) thread = threading.Thread(target=discovery_loop, daemon=True).start() @@ -363,9 +358,9 @@ class AutoInterface(Interface): thread.daemon = True thread.start() - while not self.detached: + while True: data, ipv6_src = socket.recvfrom(1024) - if self.final_init_done and data and ipv6_src: + if self.final_init_done: peering_hash = data[:RNS.Identity.HASHLENGTH//8] expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8")) if peering_hash == expected_hash: @@ -374,7 +369,7 @@ class AutoInterface(Interface): RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG) def peer_jobs(self): - while not self.detached: + while True: time.sleep(self.peer_job_interval) now = time.time() timed_out_peers = [] @@ -483,7 +478,7 @@ class AutoInterface(Interface): def announce_handler(self, ifname): - while not self.detached: + while True: self.peer_announce(ifname) time.sleep(self.announce_interval) @@ -586,10 +581,6 @@ class AutoInterface(Interface): spawned_interface.announce_rate_penalty = self.announce_rate_penalty spawned_interface.mode = self.mode spawned_interface.gravity = self.gravity - spawned_interface.recursive_prs = self.recursive_prs - spawned_interface.announces_from_internal = self.announces_from_internal - spawned_interface.announces_to_internal = self.announces_to_internal - spawned_interface.announce_cap = self.announce_cap spawned_interface.HW_MTU = self.HW_MTU spawned_interface.online = True RNS.Transport.add_interface(spawned_interface) @@ -613,20 +604,7 @@ class AutoInterface(Interface): def process_outgoing(self, data): pass - def detach(self): - self.online = False - self.detached = True - - RNS.log(f"Shutting down all UDP listeners for {self}", RNS.LOG_NOTICE) - for ifname in self.interface_servers: - try: - self.interface_servers[ifname].server_close() - self.interface_servers[ifname].shutdown() - except Exception as e: RNS.log(f"Could not shut down UDP listener {ifname} for {self}: {e}", RNS.LOG_ERROR) - - for s in self.discovery_sockets: - try: s.close() - except Exception as e: RNS.log(f"Error while shutting down discovery socket {s} for {self}: {e}", RNS.LOG_ERROR) + def detach(self): self.online = False def __str__(self): return f"AutoInterface[{self.name}]" diff --git a/RNS/Interfaces/BackboneInterface.py b/RNS/Interfaces/BackboneInterface.py index d6b049a7..048904f3 100644 --- a/RNS/Interfaces/BackboneInterface.py +++ b/RNS/Interfaces/BackboneInterface.py @@ -29,8 +29,6 @@ # SOFTWARE. from RNS.Interfaces.Interface import Interface -from RNS.Interfaces.util.TransmitBuffer import TransmitBuffer -from RNS.Interfaces.util.HDLC import HDLC, ReceiveBuffer import threading import socket import select @@ -39,11 +37,22 @@ import sys import os import RNS +class HDLC(): + FLAG = 0x7E + ESC = 0x7D + ESC_MASK = 0x20 + + @staticmethod + def escape(data): + data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK])) + data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK])) + return data + class BackboneInterface(Interface): - HW_MTU = 1048576 - BITRATE_GUESS = 100_000_000 - DEFAULT_IFAC_SIZE = 16 - AUTOCONFIGURE_MTU = True + HW_MTU = 1048576 + BITRATE_GUESS = 1_000_000_000 + DEFAULT_IFAC_SIZE = 16 + AUTOCONFIGURE_MTU = True BLOCK_FAST_FLAPPING = True FAST_FLAP_THRESHOLD = 20 @@ -52,34 +61,12 @@ class BackboneInterface(Interface): fast_flapping_lock = threading.Lock() fast_flapping = {} - DP_IC_HIGH_WM_PCT = 90 - DP_IC_MID_WM_PCT = 68 - DP_IC_LOW_WM_PCT = 10 - DP_IC_INTERVAL = 0.250 - DP_IC_TRIGGER = 1.5 - DP_IC_RCVBUF = 32768 - DP_IC_IF_HEADROOM = 32 - DP_IC_PENALTY = 1.5 - DP_EC_INTERVAL = 1.0 - DP_EC_MID_WM = 128*1024 - DP_EC_HIGH_WM = 4*1024*1024 - DP_EC_STALL_TICKS = 3 - DP_EC_MAX_ETA = 10.0 - DP_EC_RELEASE_ETA = 5.0 # Gate release hysterisis - DP_EC_DEAD_TIME = 12.0 - epoll = None listener_filenos = {} spawned_interface_filenos = {} - - _job_active = False - _ic_job_active = False - _ec_job_active = False - _job_lock = threading.Lock() - _ec_job_lock = threading.Lock() - _ic_job_lock = threading.Lock() - _dp_ic_lock = threading.Lock() - _dp_ic_snapshot = 0 + epoll = None + _job_active = False + _job_lock = threading.Lock() @staticmethod def get_address_for_if(name, bind_port, prefer_ipv6=False): @@ -182,34 +169,13 @@ class BackboneInterface(Interface): else: raise SystemError("Insufficient parameters to create listener") - __ic_burst_stats_throttle = 0.95 - __last_ic_burst_count_check = 0 - __last_ic_burst_count_state = 0 - @property - def ic_burst_count(self): - if time.time() > self.__last_ic_burst_count_check + self.__ic_burst_stats_throttle: - self.__last_ic_burst_count_state = len([i.ic_burst_active for i in self.spawned_interfaces if i.ic_burst_active]) - self.__last_ic_burst_count_check = time.time() - - return self.__last_ic_burst_count_state - - __last_ic_pr_burst_count_check = 0 - __last_ic_pr_burst_count_state = 0 - @property - def ic_pr_burst_count(self): - if time.time() > self.__last_ic_pr_burst_count_check + self.__ic_burst_stats_throttle: - self.__last_ic_pr_burst_count_state = len([i.ic_pr_burst_active for i in self.spawned_interfaces if i.ic_pr_burst_active]) - self.__last_ic_pr_burst_count_check = time.time() - - return self.__last_ic_pr_burst_count_state __last_ic_burst_check = 0 __last_ic_burst_state = False @property def ic_burst_active(self): - if time.time() > self.__last_ic_burst_check + self.__ic_burst_stats_throttle: + if time.time() > self.__last_ic_burst_check + 2: self.__last_ic_burst_state = any(i.ic_burst_active for i in self.spawned_interfaces) - self.__last_ic_burst_check = time.time() return self.__last_ic_burst_state @@ -220,10 +186,9 @@ class BackboneInterface(Interface): __ic_burst_activated = 0 @property def ic_burst_activated(self): - if time.time() > self.__ic_burst_activated_check + self.__ic_burst_stats_throttle: + if time.time() > self.__ic_burst_activated_check + 2: activated = [i.ic_burst_activated for i in self.spawned_interfaces if i.ic_burst_active] if activated: self.__ic_burst_activated = min(activated) - self.__ic_burst_activated_check = time.time() return self.__ic_burst_activated @@ -235,9 +200,8 @@ class BackboneInterface(Interface): __last_ic_pr_burst_state = False @property def ic_pr_burst_active(self): - if time.time() > self.__last_ic_pr_burst_check + self.__ic_burst_stats_throttle: + if time.time() > self.__last_ic_pr_burst_check + 2: self.__last_ic_pr_burst_state = any(i.ic_pr_burst_active for i in self.spawned_interfaces) - self.__last_ic_pr_burst_check = time.time() return self.__last_ic_pr_burst_state @@ -248,10 +212,9 @@ class BackboneInterface(Interface): __ic_pr_burst_activated = 0 @property def ic_pr_burst_activated(self): - if time.time() > self.__ic_pr_burst_activated_check + self.__ic_burst_stats_throttle: + if time.time() > self.__ic_pr_burst_activated_check + 2: activated = [i.ic_pr_burst_activated for i in self.spawned_interfaces if i.ic_pr_burst_active] if activated: self.__ic_pr_burst_activated = min(activated) - self.__ic_pr_burst_activated_check = time.time() return self.__ic_pr_burst_activated @@ -260,9 +223,7 @@ class BackboneInterface(Interface): @staticmethod def start(): - if not BackboneInterface._job_active: threading.Thread(target=BackboneInterface.__job, daemon=True).start() - if not BackboneInterface._ic_job_active: threading.Thread(target=BackboneInterface.__dp_ic_job, daemon=True).start() - if not BackboneInterface._ec_job_active: threading.Thread(target=BackboneInterface.__dp_ec_job, daemon=True).start() + if not BackboneInterface._job_active: threading.Thread(target=BackboneInterface.__job, daemon=True).start() @staticmethod def ensure_epoll(): @@ -303,9 +264,9 @@ class BackboneInterface(Interface): RNS.log(f"Attempt to register invalid file descriptor {fileno}", RNS.LOG_WARNING) return - try: BackboneInterface.epoll.register(fileno, select.EPOLLIN | select.EPOLLHUP) + try: BackboneInterface.epoll.register(fileno, select.EPOLLIN) except Exception as e: - RNS.log(f"An error occurred while registering EPOLLIN for file descriptor {fileno}: {e}", RNS.LOG_WARNING) + RNS.log(f"An error occurred while registering EPOLL_IN for file descriptor {fileno}: {e}", RNS.LOG_WARNING) @staticmethod def deregister_fileno(fileno): @@ -334,223 +295,13 @@ class BackboneInterface(Interface): if interface.socket: fileno = interface.socket.fileno() if fileno in BackboneInterface.spawned_interface_filenos: - try: - events = select.EPOLLOUT | select.EPOLLHUP - if not interface.dp_ingress_gated: events |= select.EPOLLIN - BackboneInterface.epoll.modify(fileno, events) + try: BackboneInterface.epoll.modify(fileno, select.EPOLLOUT) except Exception as e: if str(e).endswith("No such file or directory"): pass elif str(e).endswith("Bad file descriptor"): pass else: RNS.log(f"Error occurred on {interface} while modifying socket EPOLL state: {e}", RNS.LOG_WARNING) raise e - @staticmethod - def __throttle_ingress(interface, hold=None): - if interface.dp_ingress_gated: return False - try: - fileno = interface.socket.fileno() - if fileno in BackboneInterface.spawned_interface_filenos: - interface.dp_ingress_gated = True - interface.dp_ingress_tcount += 1 - events = select.EPOLLHUP - if interface.transmit_buffer.sendable > 0: events |= select.EPOLLOUT - BackboneInterface.epoll.modify(fileno, events) - if hold: interface.dp_ingress_hold = time.time() + hold - RNS.log(f"Ingress throttled on {interface}", RNS.LOG_NOTICE) if RNS.sl(RNS.LOG_NOTICE) else None - return True - - except Exception as e: RNS.log(f"Error throttling {interface}: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - return False - - @staticmethod - def __release_ingress(interface): - if not interface.dp_ingress_gated: return False - try: - fileno = interface.socket.fileno() - if fileno in BackboneInterface.spawned_interface_filenos: - events = select.EPOLLIN | select.EPOLLHUP - if interface.transmit_buffer.sendable > 0: events |= select.EPOLLOUT - BackboneInterface.epoll.modify(fileno, events) - interface.dp_ingress_gated = False - RNS.log(f"Released ingress throttle on {interface}", RNS.LOG_NOTICE) if RNS.sl(RNS.LOG_NOTICE) else None - return True - - except Exception as e: RNS.log(f"Error releasing throttle on {interface}: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - return False - - @staticmethod - def _throttle_immediate(q_depth): - with BackboneInterface._dp_ic_lock: - st = time.time() - interfaces = list(BackboneInterface.spawned_interface_filenos.values()) - producers = [iface for iface in interfaces if iface.dp_ingress_packets > 0] - - if producers: - producers.sort(key=lambda p: p.dp_ingress_packets, reverse=True) - selected = producers[0] - - now = time.time() - span = now - BackboneInterface._dp_ic_snapshot - total = sum(iface.dp_ingress_packets for iface in producers) - avail = sum(iface.dp_ingress_bytes for iface in interfaces) / span - share = selected.dp_ingress_packets / total - rate = selected.dp_ingress_packets / span - speed = selected.dp_ingress_bytes / span - data = selected.dp_ingress_bytes - pkts = selected.dp_ingress_packets - alloc = 1/((max(len(interfaces), BackboneInterface.DP_IC_IF_HEADROOM))*BackboneInterface.DP_IC_PENALTY) - hold = (avail/(avail*alloc))*span - - if BackboneInterface.__throttle_ingress(selected, hold=hold): - taken = time.time()-st - if RNS.sl(RNS.LOG_DEBUG): - RNS.log(f"Throttled producer with {pkts} packets at {RNS.prettysize(rate, suffix='pps')} / {RNS.prettyspeed(speed*8)}, {round(share*100.0, 2)}% ingress share", RNS.LOG_DEBUG) - RNS.log(f"Available ingress budget {RNS.prettyspeed(avail*8)}, hard-allocated {round(alloc*100.0, 2)}% ({RNS.prettyspeed(avail*alloc*8)}), handled in {RNS.prettyshorttime(taken, compact=True, tight=True)}", RNS.LOG_DEBUG) - RNS.log(f"Holding for {RNS.prettyshorttime(hold, compact=True, tight=True)}, throttling handled in {RNS.prettyshorttime(taken, compact=True, tight=True)} at depth {q_depth}", RNS.LOG_DEBUG) - - @staticmethod - def _dp_ec_evaluate(interface, now): - tb = interface.transmit_buffer - drained = tb._tx_sent - interface._dp_ec_prev_sent - interface._dp_ec_prev_sent = tb._tx_sent - - sendable = tb.sendable - buffered = len(tb) - - if buffered == 0 or sendable == 0: - interface._dp_ec_zero_ticks = 0 - interface._dp_ec_last_drain = now - interface.tx_stalled = False - return False - - if now - interface._dp_ec_last_drain >= BackboneInterface.DP_EC_DEAD_TIME: - RNS.log(f"No egress control drain progress for {RNS.prettyshorttime(BackboneInterface.DP_EC_DEAD_TIME, compact=True)} on {interface}, tearing down", RNS.LOG_NOTICE) - try: - if hasattr(interface, "socket") and interface.socket: - fileno = interface.socket.fileno() - BackboneInterface.deregister_fileno(fileno) - if fileno in BackboneInterface.spawned_interface_filenos: BackboneInterface.spawned_interface_filenos.pop(fileno) - try: interface.socket.close() - except Exception as e: RNS.log(f"Egress control could not close socket for {interface}: {e}", RNS.LOG_ERROR) - except Exception as e: RNS.log(f"Egress control cleanup error for {interface}: {e}", RNS.LOG_ERROR) - - interface.receive(b"") - return True - - if drained > 0: - interface._dp_ec_last_drain = now - interface._dp_ec_zero_ticks = 0 - drain_rate = drained / BackboneInterface.DP_EC_INTERVAL - clear_eta = buffered / drain_rate if drain_rate > 0 else float("inf") - - if buffered > BackboneInterface.DP_EC_MID_WM and clear_eta > BackboneInterface.DP_EC_MAX_ETA: - if not interface.tx_stalled: RNS.log(f"Egress control drain ETA of {RNS.prettyshorttime(clear_eta, compact=True)} exceeds maximum on {interface}, gating outbound", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - interface.tx_stalled = True - - elif clear_eta < BackboneInterface.DP_EC_RELEASE_ETA or buffered <= BackboneInterface.DP_EC_MID_WM: - if interface.tx_stalled: RNS.log(f"Egress control drain recovered on {interface}, resuming outbound", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - interface.tx_stalled = False - - else: - # No drain progress on this tick - if buffered > BackboneInterface.DP_EC_MID_WM: - interface._dp_ec_zero_ticks += 1 - if interface._dp_ec_zero_ticks >= BackboneInterface.DP_EC_STALL_TICKS: - if not interface.tx_stalled: RNS.log(f"No egress control drain progress on {interface}, gating outbound", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - interface.tx_stalled = True - else: - interface._dp_ec_zero_ticks = 0 - interface.tx_stalled = False - - return False - - @staticmethod - def __dp_ec_job(): - with BackboneInterface._ec_job_lock: - if BackboneInterface._ec_job_active: return - else: - BackboneInterface._ec_job_active = True - RNS.log(f"Started dataplane egress control", RNS.LOG_DEBUG) - try: - while True: - time.sleep(BackboneInterface.DP_EC_INTERVAL) - now = time.time() - try: interfaces = list(BackboneInterface.spawned_interface_filenos.values()) - except RuntimeError as e: - RNS.log(f"Deferring egress control evaluation due to error: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - continue - - for interface in interfaces: - if interface.detached: continue - if isinstance(interface, RNS.Interfaces.LocalInterface.LocalClientInterface): continue - BackboneInterface._dp_ec_evaluate(interface, now) - - except Exception as e: - RNS.log(f"BackboneInterface egress control error: {e}", RNS.LOG_ERROR) - RNS.trace_exception(e) - - @staticmethod - def __dp_ic_job(): - with BackboneInterface._ic_job_lock: - if BackboneInterface._ic_job_active: return - else: - BackboneInterface._ic_job_active = True - RNS.log(f"Started dataplane ingress control: High/mid/low water marks are {BackboneInterface.DP_IC_HIGH_WM}/{BackboneInterface.DP_IC_MID_WM}/{BackboneInterface.DP_IC_LOW_WM} packets", RNS.LOG_DEBUG) - try: - while True: - time.sleep(BackboneInterface.DP_IC_INTERVAL) - try: - if RNS.Transport.inbound_queues != None: q_depth = RNS.Transport.inbound_queues.qsize(RNS.Transport.TC_DATA) - else: continue - except Exception: continue - - with BackboneInterface._dp_ic_lock: - st = time.time() - interfaces = list(BackboneInterface.spawned_interface_filenos.values()) - - if q_depth > BackboneInterface.DP_IC_MID_WM: - producers = [iface for iface in interfaces if iface.dp_ingress_packets > 0 and not isinstance(iface, RNS.Interfaces.LocalInterface.LocalClientInterface)] - if producers: - producers.sort(key=lambda p: p.dp_ingress_packets, reverse=True) - selected = producers[0] - - now = time.time() - span = max(now - BackboneInterface._dp_ic_snapshot, 0.001) - total = sum(iface.dp_ingress_packets for iface in producers) - avail = sum(iface.dp_ingress_bytes for iface in interfaces) / span - mean = total / max(len(producers), 2) - share = selected.dp_ingress_packets / total - rate = selected.dp_ingress_packets / span - speed = selected.dp_ingress_bytes / span - data = selected.dp_ingress_bytes - pkts = selected.dp_ingress_packets - alloc = 1/((max(len(interfaces), BackboneInterface.DP_IC_IF_HEADROOM))*BackboneInterface.DP_IC_PENALTY) - hold = (avail/max(avail*alloc, 1))*span - - if BackboneInterface.__throttle_ingress(selected, hold=hold): - taken = time.time()-st - if RNS.sl(RNS.LOG_DEBUG): - RNS.log(f"Throttled producer with {pkts} packets at {RNS.prettysize(rate, suffix='pps')} / {RNS.prettyspeed(speed*8)}, {round(share*100.0, 2)}% ingress share", RNS.LOG_DEBUG) - RNS.log(f"Available ingress budget {RNS.prettyspeed(avail*8)}, hard-allocated {round(alloc*100.0, 2)}% ({RNS.prettyspeed(avail*alloc*8)}), handled in {RNS.prettyshorttime(taken, compact=True, tight=True)}", RNS.LOG_DEBUG) - RNS.log(f"Holding for {RNS.prettyshorttime(hold, compact=True, tight=True)}, throttling handled in {RNS.prettyshorttime(taken, compact=True, tight=True)}", RNS.LOG_DEBUG) - - elif q_depth < BackboneInterface.DP_IC_LOW_WM: - now = time.time() - for iface in interfaces: - if iface.dp_ingress_gated: - if iface.dp_ingress_hold and now < iface.dp_ingress_hold: continue - BackboneInterface.__release_ingress(iface) - break - - BackboneInterface._dp_ic_snapshot = time.time() - for iface in interfaces: - iface.dp_ingress_bytes = 0 - iface.dp_ingress_packets = 0 - - except Exception as e: - RNS.log(f"BackboneInterface ingress control error: {e}", RNS.LOG_ERROR) - RNS.trace_exception(e) - @staticmethod def __job(): with BackboneInterface._job_lock: @@ -560,21 +311,18 @@ class BackboneInterface(Interface): BackboneInterface.ensure_epoll() try: while True: + events = BackboneInterface.epoll.poll(1) for fileno, event in BackboneInterface.epoll.poll(1): if fileno in BackboneInterface.spawned_interface_filenos: spawned_interface = BackboneInterface.spawned_interface_filenos[fileno] client_socket = spawned_interface.socket - socket_valid = client_socket and fileno == client_socket.fileno() - if socket_valid and (event & select.EPOLLIN) and not spawned_interface.dp_ingress_gated: - if spawned_interface.dp_ingress_gated: continue + if client_socket and fileno == client_socket.fileno() and (event & select.EPOLLIN): try: received_bytes = client_socket.recv(spawned_interface.HW_MTU) except Exception as e: RNS.log(f"Error while reading from {spawned_interface}: {e}", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None received_bytes = b"" - if len(received_bytes): - spawned_interface.dp_ingress_bytes += len(received_bytes) - spawned_interface.receive(received_bytes) + if len(received_bytes): spawned_interface.receive(received_bytes) else: BackboneInterface.deregister_fileno(fileno); client_socket.close() try: @@ -589,10 +337,9 @@ class BackboneInterface(Interface): except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR) spawned_interface.receive(received_bytes) - - socket_valid_after_read = socket_valid and fileno in BackboneInterface.spawned_interface_filenos - if socket_valid_after_read and (event & select.EPOLLOUT): - try: written = spawned_interface.transmit_buffer.drain_to(client_socket) + + elif client_socket and fileno == client_socket.fileno() and (event & select.EPOLLOUT): + try: written = client_socket.send(spawned_interface.transmit_buffer) except Exception as e: written = 0 if not spawned_interface.detached: @@ -619,17 +366,16 @@ class BackboneInterface(Interface): except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_WARNING) spawned_interface.receive(b"") + spawned_interface.transmit_buffer = spawned_interface.transmit_buffer[written:] try: - if spawned_interface.transmit_buffer.sendable == 0: - events = select.EPOLLHUP - if not spawned_interface.dp_ingress_gated: events |= select.EPOLLIN - BackboneInterface.epoll.modify(fileno, events) - except Exception as e: RNS.log(f"Error while setting EPOLLIN on {spawned_interface}: {e}", RNS.LOG_ERROR) + if len(spawned_interface.transmit_buffer) == 0: BackboneInterface.epoll.modify(fileno, select.EPOLLIN) + except Exception as e: + RNS.log(f"Error while setting EPOLLIN on {spawned_interface}: {e}", RNS.LOG_ERROR) spawned_interface.txb += written if spawned_interface.parent_interface: spawned_interface.parent_interface.txb += written - elif socket_valid_after_read and (event & select.EPOLLHUP): + elif client_socket and fileno == client_socket.fileno() and event & (select.EPOLLHUP): BackboneInterface.deregister_fileno(fileno) try: if fileno in BackboneInterface.spawned_interface_filenos: BackboneInterface.spawned_interface_filenos.pop(fileno) @@ -652,7 +398,6 @@ class BackboneInterface(Interface): try: client_socket, address = server_socket.accept() client_socket.setblocking(0) - client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, BackboneInterface.DP_IC_RCVBUF) if not owner_interface.incoming_connection(client_socket): try: client_socket.close() except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_WARNING) @@ -738,10 +483,6 @@ class BackboneInterface(Interface): spawned_interface.announce_rate_penalty = self.announce_rate_penalty spawned_interface.mode = self.mode spawned_interface.gravity = self.gravity - spawned_interface.recursive_prs = self.recursive_prs - spawned_interface.announces_from_internal = self.announces_from_internal - spawned_interface.announces_to_internal = self.announces_to_internal - spawned_interface.announce_cap = self.announce_cap spawned_interface.HW_MTU = self.HW_MTU RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None RNS.Transport.add_interface(spawned_interface) @@ -757,25 +498,17 @@ class BackboneInterface(Interface): return True - def received_announce(self, size=0, from_spawned=False): - if from_spawned: - self.ia_freq_deque.append(time.time()) - self.arxb += size + def received_announce(self, from_spawned=False): + if from_spawned: self.ia_freq_deque.append(time.time()) - def sent_announce(self, size=0, from_spawned=False): - if from_spawned: - self.oa_freq_deque.append(time.time()) - self.atxb += size + def sent_announce(self, from_spawned=False): + if from_spawned: self.oa_freq_deque.append(time.time()) - def received_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.ip_freq_deque.append(time.time()) - self.prxb += size + def received_path_request(self, from_spawned=False): + if from_spawned: self.ip_freq_deque.append(time.time()) - def sent_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.op_freq_deque.append(time.time()) - self.ptxb += size + def sent_path_request(self, from_spawned=False): + if from_spawned: self.op_freq_deque.append(time.time()) def process_outgoing(self, data): pass @@ -798,7 +531,7 @@ class BackboneInterface(Interface): @property def blocked_ip_list(self): if not self.block_fast_flapping: return [] - else: return [ip for ip in self.fast_flapping if self.fast_flapping[ip][2] > self.fast_flap_grace] + else: return list(self.fast_flapping.keys()) @property def blocked_ip_count(self): @@ -876,10 +609,8 @@ class BackboneClientInterface(Interface): self.i2p_tunneled = i2p_tunneled self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL self.bitrate = BackboneClientInterface.BITRATE_GUESS - self.transmit_buffer = TransmitBuffer() - self.receive_buffer = ReceiveBuffer(mtu=lambda: self.HW_MTU, min_frame_len=RNS.Reticulum.HEADER_MINSIZE, - max_frame_len=lambda: self.HW_MTU+(getattr(self, "ifac_size", None) or 0), - on_frame=self.process_incoming, on_invalid=self.invalid_frame) + self.frame_buffer = b"" + self.transmit_buffer = b"" if max_reconnect_tries == None: self.max_reconnect_tries = BackboneClientInterface.RECONNECT_MAX_TRIES @@ -930,10 +661,11 @@ class BackboneClientInterface(Interface): def detach(self): self.online = False - self.detached = True if self.socket != None: if hasattr(self.socket, "close"): if callable(self.socket.close): + self.detached = True + try: if self.socket != None: self.socket.shutdown(socket.SHUT_RDWR) except Exception as e: @@ -967,7 +699,6 @@ class BackboneClientInterface(Interface): self.socket.settimeout(BackboneClientInterface.INITIAL_CONNECT_TIMEOUT) self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) self.socket.connect(target_address) - # TODO: Check missing setblocking(0) self.socket.settimeout(None) BackboneInterface.add_client_socket(self.socket, self) @@ -1027,31 +758,55 @@ class BackboneClientInterface(Interface): self.rxb += len(data) if hasattr(self, "parent_interface") and self.parent_interface != None: self.parent_interface.rxb += len(data) - - self.dp_ingress_packets += 1 + self.owner.inbound(data, self) def process_outgoing(self, data): if self.online and not self.detached: try: - frame = HDLC.frame(data) - if self.tx_stalled or not self.transmit_buffer.append(frame, self.tx_hwm): - self.tx_drops += 1 - self.tx_dropped_bytes += len(frame) - RNS.log(f"Egress control dropping outbound frame of {RNS.prettysize(len(frame))} on {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - else: BackboneInterface.tx_ready(self) + self.transmit_buffer += bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) + BackboneInterface.tx_ready(self) except Exception as e: RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) self.teardown() + def check_frame_len(self, frame_len): + if frame_len <= RNS.Reticulum.HEADER_MINSIZE: return False + elif frame_len > self.HW_MTU + (self.ifac_size or 0): return False + else: return True + def invalid_frame(self, frame_len): RNS.log(f"Invalid HDLC frame of {RNS.prettysize(frame_len)} received on {self}, dropping frame", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None def receive(self, data_in): try: - if len(data_in) > 0: self.receive_buffer.feed(data_in) + if len(data_in) > 0: + self.frame_buffer += data_in + flags_remaining = True + while flags_remaining: + frame_start = self.frame_buffer.find(HDLC.FLAG) + if frame_start != -1: + frame_end = self.frame_buffer.find(HDLC.FLAG, frame_start+1) + if frame_end != -1: + frame = self.frame_buffer[frame_start+1:frame_end] + frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG])) + frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC])) + frame_len = len(frame) + if frame_len != 0: + if self.check_frame_len(frame_len): self.process_incoming(frame) + else: self.invalid_frame(len(frame)) + + self.frame_buffer = self.frame_buffer[frame_end:] + + else: + if len(self.frame_buffer) > self.HW_MTU*2: self.frame_buffer = b"" + flags_remaining = False + else: + self.frame_buffer = b"" + flags_remaining = False + else: self.online = False if self.initiator and not self.detached: @@ -1070,7 +825,8 @@ class BackboneClientInterface(Interface): RNS.log("Attempting to reconnect...", RNS.LOG_WARNING) def job(): self.reconnect() threading.Thread(target=job, daemon=True).start() - else: self.teardown() + else: + self.teardown() def teardown(self): if self.initiator and not self.detached: diff --git a/RNS/Interfaces/I2PInterface.py b/RNS/Interfaces/I2PInterface.py index 0561829d..381e50c5 100644 --- a/RNS/Interfaces/I2PInterface.py +++ b/RNS/Interfaces/I2PInterface.py @@ -537,7 +537,6 @@ class I2PInterfacePeer(Interface): raise IOError("Attempt to reconnect on a non-initiator I2P interface") def process_incoming(self, data): - if not data: return self.rxb += len(data) if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count: self.parent_interface.rxb += len(data) @@ -872,25 +871,17 @@ class I2PInterface(Interface): def process_outgoing(self, data): pass - def received_announce(self, size=0, from_spawned=False): - if from_spawned: - self.ia_freq_deque.append(time.time()) - self.arxb += size + def received_announce(self, from_spawned=False): + if from_spawned: self.ia_freq_deque.append(time.time()) - def sent_announce(self, size=0, from_spawned=False): - if from_spawned: - self.oa_freq_deque.append(time.time()) - self.atxb += size + def sent_announce(self, from_spawned=False): + if from_spawned: self.oa_freq_deque.append(time.time()) - def received_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.ip_freq_deque.append(time.time()) - self.prxb += size + def received_path_request(self, from_spawned=False): + if from_spawned: self.ip_freq_deque.append(time.time()) - def sent_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.op_freq_deque.append(time.time()) - self.ptxb += size + def sent_path_request(self, from_spawned=False): + if from_spawned: self.op_freq_deque.append(time.time()) def detach(self): RNS.log("Detaching "+str(self), RNS.LOG_DEBUG) diff --git a/RNS/Interfaces/Interface.py b/RNS/Interfaces/Interface.py index 619f32da..996ed8bc 100755 --- a/RNS/Interfaces/Interface.py +++ b/RNS/Interfaces/Interface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -82,7 +82,7 @@ class Interface: IC_BURST_PENALTY = 15 IC_HELD_RELEASE_INTERVAL = 5 IC_DEQUE_MIN_SAMPLE = 2 - EC_BURST_MIN_SAMPLES = 2 + IC_BURST_MIN_SAMPLES = 6 EC_PR_FREQ = 5 EGRESS_CONTROL = False @@ -93,19 +93,10 @@ class Interface: AUTOCONFIGURE_MTU = False FIXED_MTU = False - DEFAULT_IFAC_SIZE = 16 def __init__(self): self.rxb = 0 self.txb = 0 - self.arxb = 0 - self.atxb = 0 - self.arxc = 0 - self.atxc = 0 - self.prxb = 0 - self.ptxb = 0 - self.prxc = 0 - self.ptxc = 0 self.gravity = 0 self.created = time.time() self.detached = False @@ -114,7 +105,6 @@ class Interface: self.HW_MTU = None self.__hash = None - self.announce_cap = RNS.Reticulum.ANNOUNCE_CAP/100.0 self.supports_discovery = False self.discoverable = False self.last_discovery_announce = 0 @@ -127,15 +117,11 @@ class Interface: self.tunnel_id = None self.ingress_control = True self.phy_keepalive = False - self.shared_medium = False self.ic_burst_active = False self.ic_burst_activated = 0 - self.ic_burst_sustained = 0 self.ic_pr_burst_active = False self.ic_pr_burst_activated = 0 - self.ic_pr_burst_sustained = 0 - self.ic_pr_burst_cooldown = 0 self.ic_held_release = 0 self.ic_max_held_announces = RNS.Reticulum.get_instance()._default_ic_max_held_announces() self.ic_burst_hold = RNS.Reticulum.get_instance()._default_ic_burst_hold() @@ -149,35 +135,12 @@ class Interface: self.ec_pr_freq = RNS.Reticulum.get_instance()._default_ec_pr_freq() self.egress_control = RNS.Reticulum.get_instance()._default_egress_control() self.held_announces = {} - self.transmit_buffer = None self.ia_freq_deque = deque(maxlen=Interface.IA_FREQ_SAMPLES) self.oa_freq_deque = deque(maxlen=Interface.OA_FREQ_SAMPLES) self.ip_freq_deque = deque(maxlen=Interface.IA_FREQ_SAMPLES) self.op_freq_deque = deque(maxlen=Interface.OA_FREQ_SAMPLES) - self.protocol_violations = 0 - self.ifac_violations = 0 - self.packet_filter_hits = 0 - self.dp_ingress_tcount = 0 - self.dp_ingress_bytes = 0 - self.dp_ingress_packets = 0 - self.dp_ingress_hold = None - self.dp_ingress_gated = False - - self.tx_hwm = 4*1024*1024 - self.tx_stalled = False - self.tx_drops = 0 - self.tx_dropped_bytes = 0 - self._dp_ec_prev_sent = 0 - self._dp_ec_zero_ticks = 0 - self._dp_ec_last_drain = time.time() - - self.reports_phy_stats = False - self.r_stat_rssi = None - self.r_stat_snr = None - self.r_stat_q = None - def get_hash(self): if not self.__hash: self.__hash = RNS.Identity.full_hash(str(self).encode("utf-8")) return self.__hash @@ -192,10 +155,8 @@ class Interface: ia_freq = self.incoming_announce_frequency() if self.ic_burst_active: - if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold and time.time() > self.ic_burst_sustained+self.ic_burst_hold: + if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold: if len(self.ia_freq_deque) >= self.IC_DEQUE_MIN_SAMPLE: self.ic_burst_active = False - else: - if ia_freq >= freq_threshold: self.ic_burst_sustained = time.time() return True @@ -203,7 +164,6 @@ class Interface: if ia_freq > freq_threshold: self.ic_burst_active = True self.ic_burst_activated = time.time() - self.ic_burst_sustained = time.time() self.ic_held_release = time.time() + self.ic_burst_penalty return True @@ -217,12 +177,8 @@ class Interface: ip_freq = self.incoming_pr_frequency() if self.ic_pr_burst_active: - if ip_freq < freq_threshold and time.time() > self.ic_pr_burst_activated+self.ic_burst_hold and time.time() > self.ic_pr_burst_sustained+self.ic_burst_hold: - if self.ic_pr_burst_cooldown <= 0: self.ic_pr_burst_active = False - else: self.ic_pr_burst_cooldown -= 1 - else: - self.ic_pr_burst_cooldown = 3 - if ip_freq >= freq_threshold: self.ic_pr_burst_sustained = time.time() + if ip_freq < freq_threshold and time.time() > self.ic_pr_burst_activated+self.ic_burst_hold: + self.ic_pr_burst_active = False return True @@ -230,8 +186,6 @@ class Interface: if ip_freq > freq_threshold: self.ic_pr_burst_active = True self.ic_pr_burst_activated = time.time() - self.ic_pr_burst_sustained = time.time() - self.ic_pr_burst_cooldown = 3 return True else: return False @@ -241,28 +195,28 @@ class Interface: def should_egress_limit_pr(self): if self.egress_control: freq_threshold = self.ec_pr_freq - op_freq = self.outgoing_pr_frequency(preemptive=True) + op_freq = self.outgoing_pr_frequency() if op_freq > freq_threshold: - if len(self.op_freq_deque) >= self.EC_BURST_MIN_SAMPLES: return True + if len(self.op_freq_deque) >= self.IC_BURST_MIN_SAMPLES: return True return False def optimise_mtu(self): if self.AUTOCONFIGURE_MTU: if self.bitrate >= 1_000_000_000: self.HW_MTU = 524288 - elif self.bitrate >= 750_000_000: self.HW_MTU = 262144 - elif self.bitrate >= 400_000_000: self.HW_MTU = 131072 - elif self.bitrate >= 200_000_000: self.HW_MTU = 65536 - elif self.bitrate >= 100_000_000: self.HW_MTU = 32768 - elif self.bitrate >= 10_000_000: self.HW_MTU = 16384 - elif self.bitrate >= 5_000_000: self.HW_MTU = 8192 - elif self.bitrate >= 2_000_000: self.HW_MTU = 4096 - elif self.bitrate >= 1_000_000: self.HW_MTU = 2048 - elif self.bitrate >= 62_500: self.HW_MTU = 1024 + elif self.bitrate > 750_000_000: self.HW_MTU = 262144 + elif self.bitrate > 400_000_000: self.HW_MTU = 131072 + elif self.bitrate > 200_000_000: self.HW_MTU = 65536 + elif self.bitrate > 100_000_000: self.HW_MTU = 32768 + elif self.bitrate > 10_000_000: self.HW_MTU = 16384 + elif self.bitrate > 5_000_000: self.HW_MTU = 8192 + elif self.bitrate > 2_000_000: self.HW_MTU = 4096 + elif self.bitrate > 1_000_000: self.HW_MTU = 2048 + elif self.bitrate > 62_500: self.HW_MTU = 1024 else: self.HW_MTU = None - RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_EXTREME) + RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_PATHING) def age(self): return time.time()-self.created @@ -293,56 +247,32 @@ class Interface: RNS.log("Releasing held announce packet "+str(selected_announce_packet)+" from "+str(self), RNS.LOG_EXTREME) self.ic_held_release = time.time() + self.ic_held_release_interval self.held_announces.pop(selected_announce_packet.destination_hash) - def release(): RNS.Transport.inbound(selected_announce_packet.raw, selected_announce_packet.receiving_interface, tc=RNS.Transport.TC_INGRESS_LIMITED, ifac_handled=True) + def release(): RNS.Transport.inbound(selected_announce_packet.raw, selected_announce_packet.receiving_interface) threading.Thread(target=release, daemon=True).start() except Exception as e: RNS.log("An error occurred while processing held announces for "+str(self), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) - def received_announce(self, size=0, from_spawned=False): - self.arxc += 1; self.arxb += size + def received_announce(self, from_spawned=False): self.ia_freq_deque.append(time.time()) if hasattr(self, "parent_interface") and self.parent_interface != None: - self.parent_interface.received_announce(size=size, from_spawned=True) + self.parent_interface.received_announce(from_spawned=True) - def sent_announce(self, size=0, from_spawned=False): - self.atxc += 1; self.atxb += size + def sent_announce(self, from_spawned=False): self.oa_freq_deque.append(time.time()) if hasattr(self, "parent_interface") and self.parent_interface != None: - self.parent_interface.sent_announce(size=size, from_spawned=True) + self.parent_interface.sent_announce(from_spawned=True) - def received_path_request(self, size=0, from_spawned=False): - self.prxc += 1; self.prxb += size + def received_path_request(self, from_spawned=False): self.ip_freq_deque.append(time.time()) if hasattr(self, "parent_interface") and self.parent_interface != None: - self.parent_interface.received_path_request(size=size, from_spawned=True) + self.parent_interface.received_path_request(from_spawned=True) - def sent_path_request(self, size=0, from_spawned=False): - self.ptxc += 1; self.ptxb += size + def sent_path_request(self, from_spawned=False): self.op_freq_deque.append(time.time()) if hasattr(self, "parent_interface") and self.parent_interface != None: - self.parent_interface.sent_path_request(size=size, from_spawned=True) - - def protocol_violation(self, description=None): - self.protocol_violations += 1 - RNS.log(f"Protocol violation on {self}: {description}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - return None - - def ifac_violation(self, description=None): - self.ifac_violations += 1 - RNS.log(f"IFAC violation on {self}: {description}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - return None - - def packet_filter_hit(self): - self.packet_filter_hits += 1 - return None - - @property - def ic_burst_count(self): return None - - @property - def ic_pr_burst_count(self): return None + self.parent_interface.sent_path_request(from_spawned=True) def incoming_announce_frequency(self): n = len(self.ia_freq_deque) @@ -377,8 +307,8 @@ class Interface: hz = n / span return hz - def outgoing_pr_frequency(self, preemptive=False): - n = len(self.op_freq_deque)+(1 if preemptive else 0) + def outgoing_pr_frequency(self): + n = len(self.op_freq_deque) if not len(self.op_freq_deque) > 1: return 0 else: oldest = self.op_freq_deque[0] @@ -390,7 +320,7 @@ class Interface: def process_announce_queue(self): if not hasattr(self, "announce_cap"): - self.announce_cap = RNS.Reticulum.ANNOUNCE_CAP/100.0 + self.announce_cap = RNS.Reticulum.ANNOUNCE_CAP if hasattr(self, "announce_queue"): try: @@ -411,13 +341,12 @@ class Interface: selected = entries[0] now = time.time() - tx_size = len(selected["raw"]) - tx_time = (tx_size*8) / self.bitrate + tx_time = (len(selected["raw"])*8) / self.bitrate wait_time = (tx_time / self.announce_cap) self.announce_allowed_at = now + wait_time - RNS.Transport.transmit(self, selected["raw"]) - self.sent_announce(tx_size) + self.process_outgoing(selected["raw"]) + self.sent_announce() if selected in self.announce_queue: self.announce_queue.remove(selected) @@ -437,9 +366,6 @@ class Interface: def detach(self): pass - def teardown(self): - pass - @staticmethod def get_config_obj(config_in): if type(config_in) == ConfigObj: return config_in diff --git a/RNS/Interfaces/KISSInterface.py b/RNS/Interfaces/KISSInterface.py index 9323ea6f..d343f0a3 100644 --- a/RNS/Interfaces/KISSInterface.py +++ b/RNS/Interfaces/KISSInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -122,9 +122,7 @@ class KISSInterface(Interface): self.packet_queue = [] self.flow_control = flow_control - self.shared_medium = True self.interface_ready = False - self.reconnecting = False self.flow_control_timeout = 5 self.flow_control_locked = time.time() @@ -141,17 +139,15 @@ class KISSInterface(Interface): try: self.open_port() - if self.serial.is_open: self.configure_device() - else: raise IOError("Could not open serial port") - except Exception as e: - RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) - RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) - RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: - thread = threading.Thread(target=self.reconnect_port) - thread.daemon = True - thread.start() + RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR) + raise e + + if self.serial.is_open: + self.configure_device() + else: + raise IOError("Could not open serial port") + def open_port(self): RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE) @@ -169,11 +165,6 @@ class KISSInterface(Interface): dsrdtr = False, ) - def detach(self): - self.detached = True - self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) def configure_device(self): # Allow time for interface to initialise before config @@ -254,7 +245,6 @@ class KISSInterface(Interface): def process_incoming(self, data): - if not data: return self.rxb += len(data) self.owner.inbound(data, self) @@ -369,28 +359,30 @@ class KISSInterface(Interface): RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) - if RNS.Reticulum.panic_on_interface_error: RNS.panic() + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: self.reconnect_port() + self.serial.close() + self.reconnect_port() def reconnect_port(self): - self.reconnecting = True - while not self.online and not self.detached: + while not self.online: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: self.configure_device() - except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: + self.configure_device() + except Exception as e: + RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - self.reconnecting = False - if self.online: RNS.log("Reconnected serial port for "+str(self)) + RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): return False + def should_ingress_limit(self): + return False - def __str__(self): return "KISSInterface["+self.name+"]" \ No newline at end of file + def __str__(self): + return "KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/LocalInterface.py b/RNS/Interfaces/LocalInterface.py index bef1c3a2..a214c124 100644 --- a/RNS/Interfaces/LocalInterface.py +++ b/RNS/Interfaces/LocalInterface.py @@ -30,8 +30,6 @@ from RNS.Interfaces.Interface import Interface from RNS.Interfaces.BackboneInterface import BackboneInterface -from RNS.Interfaces.util.TransmitBuffer import TransmitBuffer -from RNS.Interfaces.util.HDLC import HDLC, ReceiveBuffer import socketserver import threading import socket @@ -41,6 +39,17 @@ import os import RNS from threading import Lock +class HDLC(): + FLAG = 0x7E + ESC = 0x7D + ESC_MASK = 0x20 + + @staticmethod + def escape(data): + data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK])) + data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK])) + return data + class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): def server_bind(self): if RNS.vendor.platformutils.is_windows(): @@ -58,12 +67,9 @@ class LocalClientInterface(Interface): def __init__(self, owner, name, target_port = None, connected_socket=None, socket_path=None): super().__init__() - self.owner = owner - self.bitrate = 1_000_000_000 - self.epoll_backend = False - self.HW_MTU = 262144 - self.online = False - self.ifac_size = self.DEFAULT_IFAC_SIZE + self.epoll_backend = False + self.HW_MTU = 262144 + self.online = False if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}" else: self.socket_path = None @@ -77,9 +83,8 @@ class LocalClientInterface(Interface): self.detached = False self.name = name self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL - self.transmit_buffer = TransmitBuffer() - self.receive_buffer = ReceiveBuffer(mtu=lambda: self.HW_MTU, min_frame_len=RNS.Reticulum.HEADER_MINSIZE, - max_frame_len=lambda: self.HW_MTU, on_frame=self.process_incoming) + self.frame_buffer = b"" + self.transmit_buffer = b"" if RNS.vendor.platformutils.use_epoll(): self.epoll_backend = True @@ -112,6 +117,8 @@ class LocalClientInterface(Interface): self.target_port = target_port self.connect() + self.owner = owner + self.bitrate = 1_000_000_000 self.online = True self.writing = False @@ -187,15 +194,15 @@ class LocalClientInterface(Interface): def send_keepalive(self): if self.online: - RNS.log(f"Sending keepalive on {self}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None + RNS.log(f"Sending keepalive on {self}", RNS.LOG_DEBUG) # TODO: Remove try: if self.epoll_backend: - self.transmit_buffer.append(bytes([HDLC.FLAG])+bytes([HDLC.FLAG])) + self.transmit_buffer += bytes([HDLC.FLAG])+bytes([HDLC.FLAG]) BackboneInterface.tx_ready(self) else: self.writing = True - data = bytes([HDLC.FLAG])+bytes([HDLC.FLAG]) + data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) self.socket.sendall(data) self.writing = False @@ -212,30 +219,28 @@ class LocalClientInterface(Interface): def process_outgoing(self, data): if self.pause_on_client_sleep and time.time() > self.pause_timeout: - RNS.log(f"TX paused for LocalInterface client, dropping outbound packet", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None # TODO: Remove + RNS.log(f"TX paused for LocalInterface client, dropping outbound packet", RNS.LOG_DEBUG) # TODO: Remove return if self.online: try: if self.epoll_backend: - frame = HDLC.frame(data) - if self.tx_stalled or not self.transmit_buffer.append(frame, self.tx_hwm): - self.tx_drops += 1 - self.tx_dropped_bytes += len(frame) - RNS.log(f"Egress control dropping outbound frame of {RNS.prettysize(len(frame))} on {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - else: BackboneInterface.tx_ready(self) + self.transmit_buffer += bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) + BackboneInterface.tx_ready(self) else: self.writing = True if self._force_bitrate: - if not hasattr(self, "send_lock"): self.send_lock = Lock() + if not hasattr(self, "send_lock"): + self.send_lock = Lock() + with self.send_lock: # RNS.log(f"Simulating latency of {RNS.prettytime(s)} for {len(data)} bytes", RNS.LOG_EXTREME) s = len(data) / self.bitrate * 8 time.sleep(s) - data = HDLC.frame(data) + data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) self.socket.sendall(data) self.writing = False self.txb += len(data) @@ -249,7 +254,22 @@ class LocalClientInterface(Interface): self.teardown() def handle_hdlc(self, data_in): - self.receive_buffer.feed(data_in) + self.frame_buffer += data_in + flags_remaining = True + while flags_remaining: + frame_start = self.frame_buffer.find(HDLC.FLAG) + if frame_start != -1: + frame_end = self.frame_buffer.find(HDLC.FLAG, frame_start+1) + if frame_end != -1: + frame = self.frame_buffer[frame_start+1:frame_end] + frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG])) + frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC])) + if len(frame) > RNS.Reticulum.HEADER_MINSIZE: self.process_incoming(frame) + self.frame_buffer = self.frame_buffer[frame_end:] + + else: flags_remaining = False + + else: flags_remaining = False def receive(self, data_in): try: @@ -263,7 +283,8 @@ class LocalClientInterface(Interface): # there's no other connectivity left to block anyway, it might be # unnecessary. self.reconnect() - else: self.teardown(nowarning=True) + else: + self.teardown(nowarning=True) except Exception as e: self.online = False @@ -275,7 +296,7 @@ class LocalClientInterface(Interface): def read_loop(self): try: - self.receive_buffer.reset() + self.frame_buffer = b"" data_in = b"" while True: data_in = self.socket.recv(4096) @@ -361,7 +382,6 @@ class LocalServerInterface(Interface): self.epoll_backend = False self.online = False self.clients = 0 - self.ifac_size = self.DEFAULT_IFAC_SIZE if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}" else: self.socket_path = None @@ -409,7 +429,6 @@ class LocalServerInterface(Interface): self.announce_rate_grace = None self.announce_rate_penalty = None - self.HW_MTU = 262144 self.bitrate = 1000*1000*1000 self.online = True @@ -462,25 +481,17 @@ class LocalServerInterface(Interface): def process_outgoing(self, data): pass - def received_announce(self, size=0, from_spawned=False): - if from_spawned: - self.ia_freq_deque.append(time.time()) - self.arxb += size + def received_announce(self, from_spawned=False): + if from_spawned: self.ia_freq_deque.append(time.time()) - def sent_announce(self, size=0, from_spawned=False): - if from_spawned: - self.oa_freq_deque.append(time.time()) - self.atxb += size + def sent_announce(self, from_spawned=False): + if from_spawned: self.oa_freq_deque.append(time.time()) - def received_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.ip_freq_deque.append(time.time()) - self.prxb += size + def received_path_request(self, from_spawned=False): + if from_spawned: self.ip_freq_deque.append(time.time()) - def sent_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.op_freq_deque.append(time.time()) - self.ptxb += size + def sent_path_request(self, from_spawned=False): + if from_spawned: self.op_freq_deque.append(time.time()) def __str__(self): if self.socket_path: return "Shared Instance["+str(self.socket_path.replace("\0", ""))+"]" @@ -491,4 +502,5 @@ class LocalInterfaceHandler(socketserver.BaseRequestHandler): self.callback = callback socketserver.BaseRequestHandler.__init__(self, *args, **keys) - def handle(self): self.callback(handler=self) \ No newline at end of file + def handle(self): + self.callback(handler=self) \ No newline at end of file diff --git a/RNS/Interfaces/PipeInterface.py b/RNS/Interfaces/PipeInterface.py index eca88b5c..1574753c 100644 --- a/RNS/Interfaces/PipeInterface.py +++ b/RNS/Interfaces/PipeInterface.py @@ -84,7 +84,6 @@ class PipeInterface(Interface): self.pipe_is_open = False self.bitrate = PipeInterface.BITRATE_GUESS self.respawn_delay = respawn_delay - self.shared_medium = True try: self.open_pipe() @@ -120,7 +119,6 @@ class PipeInterface(Interface): def process_incoming(self, data): - if not data: return self.rxb += len(data) self.owner.inbound(data, self) diff --git a/RNS/Interfaces/RNodeInterface.py b/RNS/Interfaces/RNodeInterface.py index bf623672..12cdbd46 100644 --- a/RNS/Interfaces/RNodeInterface.py +++ b/RNS/Interfaces/RNodeInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -258,7 +258,6 @@ class RNodeInterface(Interface): self.r_stat_tx = None self.r_stat_rssi = None self.r_stat_snr = None - self.r_stat_q = None self.r_st_alock = None self.r_lt_alock = None self.r_random = None @@ -279,7 +278,6 @@ class RNodeInterface(Interface): self.r_noise_floor = None self.r_interference = None self.r_interference_l = None - self.reports_phy_stats = True self.r_battery_state = RNodeInterface.BATTERY_STATE_UNKNOWN self.r_battery_percent = 0 @@ -296,7 +294,6 @@ class RNodeInterface(Interface): self.packet_queue = [] self.flow_control = flow_control - self.shared_medium = True self.interface_ready = False self.announce_rate_target = None self.supports_discovery = True @@ -386,7 +383,6 @@ class RNodeInterface(Interface): RNS.log(f"Opening BLE connection for {self}...") self.timeout = 1250 if self.ble != None and self.ble.running == False: - RNS.log(f"Cleaning up previous BLE connection for {self}...") self.ble.close() self.ble.cleanup() self.ble = None @@ -443,11 +439,7 @@ class RNodeInterface(Interface): ble_detect_timeout = 5.0 detect_time = time.time() while not self.detected and time.time() < detect_time + ble_detect_timeout: time.sleep(0.1) - if not self.detected: - RNS.log(f"RNode detect timed out over BLE", RNS.LOG_ERROR) - if self.ble: - self.ble.should_run = False - self.ble.must_disconnect = True + if not self.detected: RNS.log(f"RNode detect timed out over BLE", RNS.LOG_ERROR) else: sleep(0.2) @@ -485,7 +477,8 @@ class RNodeInterface(Interface): self.setLTALock() self.setRadioState(KISS.RADIO_STATE_ON) - if self.use_ble: time.sleep(2) + if self.use_ble: + time.sleep(2) def detect(self): kiss_command = bytes([KISS.FEND, KISS.CMD_DETECT, KISS.DETECT_REQ, KISS.FEND, KISS.CMD_FW_VERSION, 0x00, KISS.FEND, KISS.CMD_PLATFORM, 0x00, KISS.FEND, KISS.CMD_MCU, 0x00, KISS.FEND]) @@ -691,8 +684,10 @@ class RNodeInterface(Interface): RNS.log("Radio state mismatch", RNS.LOG_ERROR) self.validcfg = False - if (self.validcfg): return True - else: return False + if (self.validcfg): + return True + else: + return False def updateBitrate(self): @@ -704,9 +699,11 @@ class RNodeInterface(Interface): self.bitrate = 0 def process_incoming(self, data): - if not data: return self.rxb += len(data) self.owner.inbound(data, self) + self.r_stat_rssi = None + self.r_stat_snr = None + def process_outgoing(self,data): datalen = len(data) @@ -1157,17 +1154,19 @@ class RNodeInterface(Interface): except Exception as e: self.online = False - if not self.detached: - RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) + RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) - if RNS.Reticulum.panic_on_interface_error: RNS.panic() + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() - RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) + RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - try: self.serial.close() - except Exception as e: pass + try: + self.serial.close() + except Exception as e: + pass if not self.detached and not self.reconnecting: self.reconnect_port() @@ -1193,20 +1192,14 @@ class RNodeInterface(Interface): self.disable_external_framebuffer() self.setRadioState(KISS.RADIO_STATE_OFF) self.leave() - self.serial.close() except Exception as e: RNS.log(f"An error occurred while detaching {self}: {e}", RNS.LOG_ERROR) - if self.use_ble: - self.ble.close() - self.ble.cleanup() - self.ble = None - elif self.use_tcp: + if self.use_ble: self.ble.close() + if self.use_tcp: time.sleep(0.5) self.tcp.close() - else: - self.serial.close() def should_ingress_limit(self): return False @@ -1304,7 +1297,6 @@ class BLEConnection(): self.must_disconnect = False self.connect_job_running = False self.device_disappeared = False - self._windows_paired_addrs = None import importlib.util if BLEConnection.bleak == None: @@ -1315,8 +1307,8 @@ class BLEConnection(): import asyncio BLEConnection.asyncio = asyncio else: - RNS.log("Using the RNode interface over BLE requires the \"bleak\" module to be installed.", RNS.LOG_CRITICAL) - RNS.log("You can install it with the command: python3 -m pip install bleak", RNS.LOG_CRITICAL) + RNS.log("Using the RNode interface over BLE requires a the \"bleak\" module to be installed.", RNS.LOG_CRITICAL) + RNS.log("You can install one with the command: python3 -m pip install bleak", RNS.LOG_CRITICAL) RNS.panic() self.should_run = True @@ -1332,7 +1324,6 @@ class BLEConnection(): self.should_run = False def connection_job(self): - self.running = True while self.should_run: if self.ble_device == None: self.ble_device = self.find_target_device() @@ -1359,7 +1350,6 @@ class BLEConnection(): self.owner.ble_receive(data) self.connected = True - self.device_disappeared = False self.ble_device = ble_client self.last_client = ble_client self.owner.port = str(f"ble://{ble_client.address}") @@ -1394,8 +1384,6 @@ class BLEConnection(): def find_target_device(self): RNS.log(f"Searching for attachable BLE device for {self.owner}...", RNS.LOG_EXTREME) - if RNS.vendor.platformutils.is_windows(): - self._windows_paired_addrs = self._get_windows_paired_ble_addresses() def device_filter(device: self.bleak.backends.device.BLEDevice, adv: self.bleak.backends.scanner.AdvertisementData): if BLEConnection.UART_SERVICE_UUID.lower() in adv.service_uuids: if self.device_bonded(device): @@ -1427,9 +1415,6 @@ class BLEConnection(): def device_bonded(self, device): try: - if self._windows_paired_addrs is not None: - return device.address is not None and device.address.lower() in self._windows_paired_addrs - if hasattr(device, "details"): if "props" in device.details and "Bonded" in device.details["props"]: if device.details["props"]["Bonded"] == True: @@ -1440,15 +1425,6 @@ class BLEConnection(): return False - def _get_windows_paired_ble_addresses(self): - from winrt.windows.devices.bluetooth import BluetoothLEDevice - from winrt.windows.devices.enumeration import DeviceInformation - async def _query(): - selector = BluetoothLEDevice.get_device_selector_from_pairing_state(True) - infos = await DeviceInformation.find_all_async_aqs_filter(selector) - return set(info.id.split("-")[-1].lower() for info in infos) - return self.asyncio.run(_query()) - class TCPConnection(): TARGET_PORT = 7633 CONNECT_TIMEOUT = 5.0 diff --git a/RNS/Interfaces/RNodeMultiInterface.py b/RNS/Interfaces/RNodeMultiInterface.py index 6e0f5fc9..d311e736 100644 --- a/RNS/Interfaces/RNodeMultiInterface.py +++ b/RNS/Interfaces/RNodeMultiInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -269,7 +269,6 @@ class RNodeMultiInterface(Interface): self.r_stat_tx = None self.r_stat_rssi = None self.r_stat_snr = None - self.r_stat_q = None self.r_st_alock = None self.r_lt_alock = None self.r_random = None @@ -277,8 +276,6 @@ class RNodeMultiInterface(Interface): self.packet_queue = [] self.interface_ready = False self.announce_rate_target = None - self.reports_phy_stats = True - self.validcfg = True if id_interval != None and id_callsign != None: @@ -546,25 +543,17 @@ class RNodeMultiInterface(Interface): if written != len(frame): raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) - def received_announce(self, size=0, from_spawned=False): - if from_spawned: - self.ia_freq_deque.append(time.time()) - self.arxb += size + def received_announce(self, from_spawned=False): + if from_spawned: self.ia_freq_deque.append(time.time()) - def sent_announce(self, size=0, from_spawned=False): - if from_spawned: - self.oa_freq_deque.append(time.time()) - self.atxb += size + def sent_announce(self, from_spawned=False): + if from_spawned: self.oa_freq_deque.append(time.time()) - def received_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.ip_freq_deque.append(time.time()) - self.prxb += size + def received_path_request(self, from_spawned=False): + if from_spawned: self.ip_freq_deque.append(time.time()) - def sent_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.op_freq_deque.append(time.time()) - self.ptxb += size + def sent_path_request(self, from_spawned=False): + if from_spawned: self.op_freq_deque.append(time.time()) def readLoop(self): try: @@ -916,7 +905,6 @@ class RNodeMultiInterface(Interface): if interface != 0: self.setRadioState(KISS.RADIO_STATE_OFF, interface) self.leave() - self.serial.close() def teardown_subinterfaces(self): for interface in self.subinterfaces: @@ -1005,7 +993,6 @@ class RNodeSubInterface(Interface): self.r_premable_time_ms = None self.packet_queue = [] - self.shared_medium = True self.interface_ready = False self.parent_interface = parent_interface self.announce_rate_target = None @@ -1129,9 +1116,10 @@ class RNodeSubInterface(Interface): self.bitrate = 0 def process_incoming(self, data): - if not data: return self.rxb += len(data) self.owner.inbound(data, self) + self.r_stat_rssi = None + self.r_stat_snr = None def process_outgoing(self,data): if self.online: diff --git a/RNS/Interfaces/SerialInterface.py b/RNS/Interfaces/SerialInterface.py index 0e8d7665..7c9d0da7 100755 --- a/RNS/Interfaces/SerialInterface.py +++ b/RNS/Interfaces/SerialInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -83,7 +83,6 @@ class SerialInterface(Interface): raise ValueError("No port specified for serial interface") self.HW_MTU = 564 - self.shared_medium = True self.pyserial = serial self.serial = None @@ -97,7 +96,6 @@ class SerialInterface(Interface): self.timeout = 100 self.online = False self.bitrate = self.speed - self.reconnecting = False if parity.lower() == "e" or parity.lower() == "even": self.parity = serial.PARITY_EVEN @@ -107,17 +105,15 @@ class SerialInterface(Interface): try: self.open_port() - if self.serial.is_open: self.configure_device() - else: raise IOError("Could not open serial port") - except Exception as e: - RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) - RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) - RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: - thread = threading.Thread(target=self.reconnect_port) - thread.daemon = True - thread.start() + RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) + raise e + + if self.serial.is_open: + self.configure_device() + else: + raise IOError("Could not open serial port") + def open_port(self): RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE) @@ -135,11 +131,6 @@ class SerialInterface(Interface): dsrdtr = False, ) - def detach(self): - self.detached = True - self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) def configure_device(self): sleep(0.5) @@ -149,11 +140,12 @@ class SerialInterface(Interface): self.online = True RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE) + def process_incoming(self, data): - if not data: return self.rxb += len(data) self.owner.inbound(data, self) + def process_outgoing(self,data): if self.online: data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) @@ -162,6 +154,7 @@ class SerialInterface(Interface): if written != len(data): raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) + def readLoop(self): try: in_frame = False @@ -205,28 +198,30 @@ class SerialInterface(Interface): RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) - if RNS.Reticulum.panic_on_interface_error: RNS.panic() + if RNS.Reticulum.panic_on_interface_error: + RNS.panic() RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - try: self.serial.close() - except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) - if not self.detached and not self.reconnecting: self.reconnect_port() + self.serial.close() + self.reconnect_port() def reconnect_port(self): - self.reconnecting = True - while not self.online and not self.detached: + while not self.online: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: self.configure_device() - except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: + self.configure_device() + except Exception as e: + RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - self.reconnecting = False - if self.online: RNS.log("Reconnected serial port for "+str(self)) + RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): return False + def should_ingress_limit(self): + return False - def __str__(self): return "SerialInterface["+self.name+"]" + def __str__(self): + return "SerialInterface["+self.name+"]" diff --git a/RNS/Interfaces/TCPInterface.py b/RNS/Interfaces/TCPInterface.py index 12d25c86..90c9ea3b 100644 --- a/RNS/Interfaces/TCPInterface.py +++ b/RNS/Interfaces/TCPInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -208,7 +208,6 @@ class TCPClientInterface(Interface): def detach(self): self.online = False - self.detached = True if self.socket != None: if hasattr(self.socket, "close"): if callable(self.socket.close): @@ -273,7 +272,7 @@ class TCPClientInterface(Interface): if not self.reconnecting: self.reconnecting = True attempts = 0 - while not self.online and not self.detached: + while not self.online: time.sleep(TCPClientInterface.RECONNECT_WAIT) attempts += 1 @@ -303,7 +302,6 @@ class TCPClientInterface(Interface): raise IOError("Attempt to reconnect on a non-initiator TCP interface") def process_incoming(self, data): - if not data: return if self.online and not self.detached: self.rxb += len(data) if hasattr(self, "parent_interface") and self.parent_interface != None: @@ -428,7 +426,7 @@ class TCPClientInterface(Interface): self.online = False RNS.log("An interface error occurred for "+str(self)+", the contained exception was: "+str(e), RNS.LOG_WARNING) - if self.initiator and not self.detached: + if self.initiator: RNS.log("Attempting to reconnect...", RNS.LOG_WARNING) self.reconnect() else: @@ -454,9 +452,12 @@ class TCPClientInterface(Interface): if not self.initiator: RNS.Transport.remove_interface(self) + def __str__(self): - if ":" in self.target_ip: ip_str = f"[{self.target_ip}]" - else: ip_str = f"{self.target_ip}" + if ":" in self.target_ip: + ip_str = f"[{self.target_ip}]" + else: + ip_str = f"{self.target_ip}" return "TCPInterface["+str(self.name)+"/"+ip_str+":"+str(self.target_port)+"]" @@ -636,10 +637,6 @@ class TCPServerInterface(Interface): spawned_interface.announce_rate_penalty = self.announce_rate_penalty spawned_interface.mode = self.mode spawned_interface.gravity = self.gravity - spawned_interface.recursive_prs = self.recursive_prs - spawned_interface.announces_from_internal = self.announces_from_internal - spawned_interface.announces_to_internal = self.announces_to_internal - spawned_interface.announce_cap = self.announce_cap spawned_interface.HW_MTU = self.HW_MTU spawned_interface.online = True RNS.log("Spawned new TCPClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE) @@ -649,25 +646,17 @@ class TCPServerInterface(Interface): self.spawned_interfaces.append(spawned_interface) spawned_interface.read_loop() - def received_announce(self, size=0, from_spawned=False): - if from_spawned: - self.ia_freq_deque.append(time.time()) - self.arxb += size + def received_announce(self, from_spawned=False): + if from_spawned: self.ia_freq_deque.append(time.time()) - def sent_announce(self, size=0, from_spawned=False): - if from_spawned: - self.oa_freq_deque.append(time.time()) - self.atxb += size + def sent_announce(self, from_spawned=False): + if from_spawned: self.oa_freq_deque.append(time.time()) - def received_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.ip_freq_deque.append(time.time()) - self.prxb += size + def received_path_request(self, from_spawned=False): + if from_spawned: self.ip_freq_deque.append(time.time()) - def sent_path_request(self, size=0, from_spawned=False): - if from_spawned: - self.op_freq_deque.append(time.time()) - self.ptxb += size + def sent_path_request(self, from_spawned=False): + if from_spawned: self.op_freq_deque.append(time.time()) def process_outgoing(self, data): pass @@ -689,8 +678,10 @@ class TCPServerInterface(Interface): def __str__(self): - if ":" in self.bind_ip: ip_str = f"[{self.bind_ip}]" - else: ip_str = f"{self.bind_ip}" + if ":" in self.bind_ip: + ip_str = f"[{self.bind_ip}]" + else: + ip_str = f"{self.bind_ip}" return "TCPServerInterface["+self.name+"/"+ip_str+":"+str(self.bind_port)+"]" diff --git a/RNS/Interfaces/UDPInterface.py b/RNS/Interfaces/UDPInterface.py index e2baecac..8966bbe5 100644 --- a/RNS/Interfaces/UDPInterface.py +++ b/RNS/Interfaces/UDPInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -78,7 +78,6 @@ class UDPInterface(Interface): self.name = name self.online = False self.bitrate = UDPInterface.BITRATE_GUESS - self.shared_medium = True if device != None: if bindip == None: @@ -113,17 +112,8 @@ class UDPInterface(Interface): self.forward_ip = forwardip self.forward_port = forwardport - def detach(self): - self.detached = True - self.online = False - if self.server: - try: - self.server.server_close() - self.server.shutdown() - except Exception as e: RNS.log(f"Could not shut down UDP listener for {self}: {e}", RNS.LOG_ERROR) def process_incoming(self, data): - if not data: return self.rxb += len(data) self.owner.inbound(data, self) diff --git a/RNS/Interfaces/__init__.py b/RNS/Interfaces/__init__.py index 56a541bf..9a11d8fd 100755 --- a/RNS/Interfaces/__init__.py +++ b/RNS/Interfaces/__init__.py @@ -36,7 +36,5 @@ import RNS.Interfaces.util.netinfo as netinfo py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") -so_modules = glob.glob(os.path.dirname(__file__)+"/*.so") -pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd") -modules = py_modules+pyc_modules+so_modules+pyd_modules -__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"])) \ No newline at end of file +modules = py_modules+pyc_modules +__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) \ No newline at end of file diff --git a/RNS/Interfaces/util/HDLC.py b/RNS/Interfaces/util/HDLC.py deleted file mode 100644 index bd7fd976..00000000 --- a/RNS/Interfaces/util/HDLC.py +++ /dev/null @@ -1,115 +0,0 @@ -# Reticulum License -# -# Copyright (c) 2026 Mark Qvist -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -class HDLC(): - FLAG = 0x7E - ESC = 0x7D - ESC_MASK = 0x20 - - FLAG_B = bytes([FLAG]) - ESC_B = bytes([ESC]) - ESCPD_FLAG = bytes([ESC, FLAG ^ ESC_MASK]) - ESCPD_ESC = bytes([ESC, ESC ^ ESC_MASK]) - - @staticmethod - def escape(data): - data = data.replace(HDLC.ESC_B, HDLC.ESCPD_ESC) - data = data.replace(HDLC.FLAG_B, HDLC.ESCPD_FLAG) - return data - - @staticmethod - def unescape(frame): - frame = frame.replace(HDLC.ESCPD_FLAG, HDLC.FLAG_B) - frame = frame.replace(HDLC.ESCPD_ESC, HDLC.ESC_B) - return frame - - @staticmethod - def frame(data): - esc = HDLC.escape(data) - frame = bytearray(len(esc)+2) - frame[0] = HDLC.FLAG - frame[1:-1] = esc - frame[-1] = HDLC.FLAG - return frame - -class ReceiveBuffer(): - def __init__(self, mtu, min_frame_len, max_frame_len=None, on_frame=None, on_invalid=None): - self._mtu = mtu - self._min_frame_len = min_frame_len - self._max_frame_len = max_frame_len - self._on_frame = on_frame - self._on_invalid = on_invalid - self._buf = bytearray() - self._off = 0 - - def mtu(self): - return self._mtu() if callable(self._mtu) else self._mtu - - def max(self): - m = self._max_frame_len - return m() if callable(m) else m - - def feed(self, data): - if not data: return - self._buf.extend(data) - buf = self._buf - - while True: - frame_start = buf.find(HDLC.FLAG, self._off) - if frame_start == -1: - self.reset() - return - - frame_end = buf.find(HDLC.FLAG, frame_start+1) - if frame_end == -1: - if len(buf)-self._off > self.mtu()*2: self.reset() - return - - frame = HDLC.unescape(bytes(buf[frame_start+1:frame_end])) - frame_len = len(frame) - if frame_len > 0: - max_len = self.max() - if frame_len > self._min_frame_len and (max_len is None or frame_len <= max_len): - if self._on_frame is not None: self._on_frame(frame) - elif self._on_invalid is not None: self._on_invalid(frame_len) - - self._off = frame_end - if self._off > 0 and self._off >= len(buf) // 2: - del buf[:self._off] - self._off = 0 - - # Drops any partial frames from the buffer and - # resets assembly offset - def reset(self): - self._buf.clear() - self._off = 0 - - # Buffered bytes currently unconsumed - def __len__(self): return len(self._buf)-self._off diff --git a/RNS/Interfaces/util/TransmitBuffer.py b/RNS/Interfaces/util/TransmitBuffer.py deleted file mode 100644 index 16ebc9e8..00000000 --- a/RNS/Interfaces/util/TransmitBuffer.py +++ /dev/null @@ -1,171 +0,0 @@ -# Reticulum License -# -# Copyright (c) 2026 Mark Qvist -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from collections import deque -import threading - -# Zero-copy coalesced transmit buffer for arbitrary -# frame sizes. It's even almost 100% lock-free. -class TransmitBuffer: - COALESCE_TARGET = 65536 - - def __init__(self): - self._tail_lock = threading.Lock() - - # Producer-owned state - self._cur = None - self._cur_frames = 0 - - # Consumer-owned state - self._head_offset = 0 - - # Shared chunk queue. Producer appends right, consumer - # pops from left. Appended chunks are immutable. - self._chunks = deque() - - # Accounting counters - - # Producer-owned - self._tx_total = 0 # Total bytes appended - self._tx_visible = 0 # Total bytes pushed to the chunk queue - self._tx_frames = 0 # Total frames appended - - # Consumer-owned - self._tx_sent = 0 # Total bytes sent to the kernel - self._tx_frames_sent = 0 # Total frames fully sent - - # Queue a finalized and complete on-wire frame. - # Executed by producer side only. - def append(self, frame, limit=None): - with self._tail_lock: - if limit is not None and (self._tx_total - self._tx_sent) + len(frame) > limit: return False - - # Allocate separate chunks to frames - # larger than the coalescing target. - if len(frame) >= self.COALESCE_TARGET: - self._flush() - self._chunks.append((frame, 1)) - self._tx_visible += len(frame) - self._tx_total += len(frame) - self._tx_frames += 1 - - else: - if self._cur is not None and len(self._cur) + len(frame) > self.COALESCE_TARGET: self._flush() - - if self._cur is None: - self._cur = bytearray() - self._cur_frames = 0 - - self._cur.extend(frame) - self._cur_frames += 1 - self._tx_total += len(frame) - self._tx_frames += 1 - - if not self._chunks: - # Queue is idle. Make the frame visible immediately so - # sparse traffic does not incur coalescing latency. - self._flush() - - return True - - def _flush(self): - if self._cur is not None and len(self._cur) > 0: - self._chunks.append((self._cur, self._cur_frames)) - self._tx_visible += len(self._cur) - self._cur = None - self._cur_frames = 0 - - # Make producer-side coalescing tail visible to the consumer. - # Called by producer when it has finished producing a burst, - # and wants to make the remaining bytes transmittable. Safe to - # call after every append. - def flush(self): - with self._tail_lock: self._flush() - - # Drain buffered frames to a socket. Called by consumer. If the - # chunk queue is drained completely, any potential producer-side - # coalescing tail is released under the tail lock, and drained in - # the same cycle. If you possess wizardry for doing this without - # the lock, I am all ears. - def drain_to(self, socket): - # TODO: At *some* point (no, not now), replace with a - # mechanism for send_msg to immediately drain all - # chunks directly to the kernel. - total = 0 - whole = True - while whole: - if not self._chunks: - with self._tail_lock: self._flush() - if not self._chunks: break - - chunk, _ = self._chunks[0] - remaining = len(chunk) - self._head_offset - if remaining <= 0: - self._pop_head() - continue - - try: written = socket.send(memoryview(chunk)[self._head_offset:]) - except BlockingIOError: break - except InterruptedError: continue - if written <= 0: break - - total += written - self._head_offset += written - self._tx_sent += written - - if self._head_offset >= len(chunk): self._pop_head() - else: whole = False - - return total - - def _pop_head(self): - chunk, frames = self._chunks.popleft() - self._tx_frames_sent += frames - self._head_offset = 0 - - # Total number of bytes currently buffered, including the - # producer's in-progress coalescing chunk. - def __len__(self): return self._tx_total-self._tx_sent - - # The number of bytes actually ready to send, and will be - # visible to the consumer, that is, what can be drained. - @property - def sendable(self): return self._tx_visible-self._tx_sent - - # The number of complete frames currently buffere, including - # ones contained in a potential in-progress coalescing chunk. - @property - def frames_buffered(self): return self._tx_frames-self._tx_frames_sent - - # Number of chunks currently queued and visible to the consumer - @property - def chunks_buffered(self): return len(self._chunks) - - def __repr__(self): return f"" \ No newline at end of file diff --git a/RNS/Interfaces/util/__init__.py b/RNS/Interfaces/util/__init__.py index e90e61ad..72302cb5 100644 --- a/RNS/Interfaces/util/__init__.py +++ b/RNS/Interfaces/util/__init__.py @@ -3,7 +3,5 @@ import glob py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") -so_modules = glob.glob(os.path.dirname(__file__)+"/*.so") -pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd") -modules = py_modules+pyc_modules+so_modules+pyd_modules -__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"])) +modules = py_modules+pyc_modules +__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) diff --git a/RNS/Link.py b/RNS/Link.py index 8d5079da..41f6c9e3 100644 --- a/RNS/Link.py +++ b/RNS/Link.py @@ -191,7 +191,7 @@ class Link: if len(data) == Link.ECPUBSIZE+Link.LINK_MTU_SIZE: RNS.log("Link request includes MTU signalling", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - try: link.mtu = Link.mtu_from_lr_packet(packet) or RNS.Reticulum.MTU + try: link.mtu = Link.mtu_from_lr_packet(packet) or Reticulum.MTU except Exception as e: RNS.trace_exception(e) link.mtu = RNS.Reticulum.MTU @@ -593,7 +593,7 @@ class Link: def get_expected_rate(self): """ - :returns: The expected in-flight data rate of an established link. + :returns: The packet expected in-flight data rate of an established link. """ if self.status == Link.ACTIVE: return self.expected_rate else: return None @@ -683,8 +683,8 @@ class Link: except Exception as e: pass def link_closed(self): - for resource in self.incoming_resources.copy(): resource.cancel() - for resource in self.outgoing_resources.copy(): resource.cancel() + for resource in self.incoming_resources: resource.cancel() + for resource in self.outgoing_resources: resource.cancel() if self._channel: self._channel._shutdown() self.prv = None @@ -928,13 +928,6 @@ class Link: def receive(self, packet): self.watchdog_lock = True - try: self.__receive(packet) - except Exception as e: - RNS.log(f"Error on {self} while receiving packet: {e}", RNS.LOG_ERROR) - RNS.trace_exception(e) - finally: self.watchdog_lock = False - - def __receive(self, packet): if not self.status == Link.CLOSED and not (self.initiator and packet.context == RNS.Packet.KEEPALIVE and packet.data == bytes([0xFF])): if packet.receiving_interface != self.attached_interface: RNS.log(f"Link-associated packet received on unexpected interface {packet.receiving_interface} instead of {self.attached_interface}! Someone might be trying to manipulate your communication!", RNS.LOG_ERROR) @@ -1163,6 +1156,8 @@ class Link: threading.Thread(target=job, daemon=True).start() self.__update_phy_stats(packet, query_shared=True) + self.watchdog_lock = False + def encrypt(self, plaintext): try: if not self.token: @@ -1395,7 +1390,7 @@ class RequestReceipt(): time.sleep(0.1) def request_timed_out(self, packet_receipt): - if self in self.link.pending_requests and self.status in [RequestReceipt.DELIVERED, RequestReceipt.SENT]: + if self in self.link.pending_requests and self.status == RequestReceipt.DELIVERED: self.status = RequestReceipt.FAILED self.concluded_at = time.time() self.link.pending_requests.remove(self) @@ -1405,7 +1400,7 @@ class RequestReceipt(): except Exception as e: RNS.log("Error while executing request timed out callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) def response_rejected(self): - if self in self.link.pending_requests and self.status in [RequestReceipt.DELIVERED, RequestReceipt.SENT]: + if self in self.link.pending_requests and self.status == RequestReceipt.DELIVERED: self.status = RequestReceipt.FAILED self.concluded_at = time.time() self.link.pending_requests.remove(self) diff --git a/RNS/Packet.py b/RNS/Packet.py index 991819bc..ea45b383 100755 --- a/RNS/Packet.py +++ b/RNS/Packet.py @@ -118,7 +118,6 @@ class Packet: __slots__ += "transport_id", "data", "flags", "raw", "packed", "sent", "create_receipt", "receipt", "fromPacked", "MTU" __slots__ += "sent_at", "packet_hash", "ratchet_id", "attached_interface", "receiving_interface", "rssi", "snr", "q" __slots__ += "ciphertext", "plaintext", "destination_hash", "destination_type", "link", "map_hash", "is_outbound_pr" - __slots__ += "traffic_class", "announce_signature_validated" def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True, context_flag=FLAG_UNSET): @@ -160,8 +159,6 @@ class Packet: self.packet_hash = None self.ratchet_id = None - self.traffic_class = None - self.announce_signature_validated = None self.attached_interface = attached_interface self.receiving_interface = None self.is_outbound_pr = False @@ -263,23 +260,18 @@ class Packet: self.destination_hash = self.raw[DST_LEN+2:2*DST_LEN+2] self.context = ord(self.raw[2*DST_LEN+2:2*DST_LEN+3]) self.data = self.raw[2*DST_LEN+3:] - if len(self.transport_id) != DST_LEN: raise ValueError("Malformed Transport ID field") - if len(self.destination_hash) != DST_LEN: raise ValueError("Malformed destination hash field") else: self.transport_id = None self.destination_hash = self.raw[2:DST_LEN+2] self.context = ord(self.raw[DST_LEN+2:DST_LEN+3]) self.data = self.raw[DST_LEN+3:] - if len(self.destination_hash) != DST_LEN: raise ValueError("Malformed destination hash field") - - if len(self.data) == 0: raise ValueError("Zero-length data field") self.packed = False self.update_hash() return True except Exception as e: - RNS.log(f"Received malformed packet, dropping it. The contained exception was: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None + RNS.log(f"Received malformed packet, dropping it. The contained exception was: {e}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None return False def send(self): @@ -289,7 +281,6 @@ class Packet: :returns: A :ref:`RNS.PacketReceipt` instance if *create_receipt* was set to *True* when the packet was instantiated, if not returns *None*. If the packet could not be sent *False* is returned. """ if not self.sent: - if self.hops >= RNS.Transport.PATHFINDER_M: return False if not self.packed: self.pack() if self.destination.type == RNS.Destination.LINK: if self.destination.status == RNS.Link.CLOSED: @@ -305,9 +296,7 @@ class Packet: if RNS.Transport.outbound(self): return self.receipt else: - if not self.attached_interface: - RNS.log(f"No interfaces could process the outbound {self.hops} hop, type {self.packet_type} packet for {self.destination}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - + RNS.log("No interfaces could process the outbound packet", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None self.sent = False self.receipt = None return False @@ -356,10 +345,6 @@ class Packet: def getTruncatedHash(self): return RNS.Identity.truncated_hash(self.get_hashable_part()) - @property - def truncated_packet_hash(self): - return self.packet_hash[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8] - def get_hashable_part(self): hashable_part = bytes([self.raw[0] & 0b00001111]) if self.header_type == Packet.HEADER_2: hashable_part += self.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:] @@ -372,25 +357,25 @@ class Packet: :returns: The physical layer *Received Signal Strength Indication* if available, otherwise ``None``. """ if self.rssi != None: return self.rssi - else: return RNS.Reticulum.get_instance().get_packet_rssi(self.packet_hash) + else: return reticulum.get_packet_rssi(self.packet_hash) def get_snr(self): """ :returns: The physical layer *Signal-to-Noise Ratio* if available, otherwise ``None``. """ if self.snr != None: return self.snr - else: return RNS.Reticulum.get_instance().get_packet_snr(self.packet_hash) + else: return reticulum.get_packet_snr(self.packet_hash) def get_q(self): """ :returns: The physical layer *Link Quality* if available, otherwise ``None``. """ if self.q != None: return self.q - else: return RNS.Reticulum.get_instance().get_packet_q(self.packet_hash) + else: return reticulum.get_packet_q(self.packet_hash) class ProofDestination: def __init__(self, packet): - self.hash = packet.truncated_packet_hash; + self.hash = packet.get_hash()[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]; self.type = RNS.Destination.SINGLE def encrypt(self, plaintext): return plaintext @@ -415,8 +400,8 @@ class PacketReceipt: # Creates a new packet receipt from a sent packet def __init__(self, packet): - self.hash = packet.packet_hash - self.truncated_hash = packet.truncated_packet_hash + self.hash = packet.get_hash() + self.truncated_hash = packet.getTruncatedHash() self.sent = True self.sent_at = time.time() self.proved = False diff --git a/RNS/Resource.py b/RNS/Resource.py index c7466af6..696e3f64 100644 --- a/RNS/Resource.py +++ b/RNS/Resource.py @@ -30,7 +30,6 @@ import RNS import os -import io import bz2 import math import time @@ -281,19 +280,6 @@ class Resource: if hasattr(data, "read"): if data_size == None: data_size = os.stat(data.name).st_size - if data_size == 0: - if isinstance(data, io.BufferedReader): - RNS.log(f"Attempting to proxy stream data...", RNS.LOG_DEBUG) - stream_proxy = tempfile.TemporaryFile() - stream_proxy.write(data.read()) - stream_proxy.flush(); stream_proxy.seek(0) - data_size = os.stat(stream_proxy.name).st_size - if data_size: RNS.log(f"Stream proxy succeeded, final data size is {RNS.prettysize(data_size)}", RNS.LOG_DEBUG) - else: RNS.log(f"Resource initialisation received an invalid readable input, or stream data that could not be proxied, the transfer will likely fail.", RNS.LOG_WARNING) - data = stream_proxy - - else: RNS.log(f"Got zero-sized readable at resource initialisation, the transfer will likely.", RNS.LOG_WARNING) - self.total_size = data_size + self.metadata_size if self.total_size <= Resource.MAX_EFFICIENT_SIZE: @@ -304,6 +290,12 @@ class Resource: data.close() else: + # self.total_segments = ((data_size-1)//Resource.MAX_EFFICIENT_SIZE)+1 + # self.segment_index = segment_index + # self.split = True + # seek_index = segment_index-1 + # seek_position = seek_index*Resource.MAX_EFFICIENT_SIZE + self.total_segments = ((self.total_size-1)//Resource.MAX_EFFICIENT_SIZE)+1 self.segment_index = segment_index self.split = True @@ -380,8 +372,10 @@ class Resource: self.req_hashlist = [] self.receiver_min_consecutive_height = 0 - if timeout != None: self.timeout = timeout - else: self.timeout = self.link.rtt * self.link.traffic_timeout_factor + if timeout != None: + self.timeout = timeout + else: + self.timeout = self.link.rtt * self.link.traffic_timeout_factor if data != None: self.initiator = True @@ -390,21 +384,18 @@ class Resource: compression_began = time.time() if self.auto_compress and data_size <= self.auto_compress_limit: - RNS.log("Compressing resource data...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - try: - self.compressed_data = bz2.compress(self.uncompressed_data) - RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - except Exception as e: - RNS.log(f"Could not auto-compress resource data, falling back to uncompressed transfer: {e}", RNS.LOG_DEBUG) - self.compressed_data = self.uncompressed_data - else: self.compressed_data = self.uncompressed_data + RNS.log("Compressing resource data...", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None + self.compressed_data = bz2.compress(self.uncompressed_data) + RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None + else: + self.compressed_data = self.uncompressed_data self.uncompressed_size = len(self.uncompressed_data) self.compressed_size = len(self.compressed_data) if (self.compressed_size < self.uncompressed_size and auto_compress): saved_bytes = len(self.uncompressed_data) - len(self.compressed_data) - RNS.log("Compression saved "+str(saved_bytes)+" bytes, sending compressed", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None + RNS.log("Compression saved "+str(saved_bytes)+" bytes, sending compressed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None self.data = b"" self.data += RNS.Identity.get_random_hash()[:Resource.RANDOM_HASH_SIZE] @@ -456,8 +447,8 @@ class Resource: self.hashmap = b"" collision_guard_list = [] for i in range(0,hashmap_entries): - part_data = self.data[i*self.sdu:(i+1)*self.sdu] - map_hash = self.get_map_hash(part_data) + data = self.data[i*self.sdu:(i+1)*self.sdu] + map_hash = self.get_map_hash(data) if map_hash in collision_guard_list: RNS.log("Found hash collision in resource map, remapping...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None @@ -469,7 +460,7 @@ class Resource: if len(collision_guard_list) > ResourceAdvertisement.COLLISION_GUARD_SIZE: collision_guard_list.pop(0) - part = RNS.Packet(link, part_data, context=RNS.Packet.RESOURCE) + part = RNS.Packet(link, data, context=RNS.Packet.RESOURCE) part.pack() part.map_hash = map_hash @@ -874,9 +865,9 @@ class Resource: part_data = packet.data part_hash = self.get_map_hash(part_data) - search_start = self.consecutive_completed_height+1 - i = search_start - for map_hash in self.hashmap[search_start:search_start+self.window]: + consecutive_index = self.consecutive_completed_height if self.consecutive_completed_height >= 0 else 0 + i = consecutive_index + for map_hash in self.hashmap[consecutive_index:consecutive_index+self.window]: if map_hash == part_hash: if self.parts[i] == None: @@ -887,6 +878,9 @@ class Resource: self.outstanding_parts -= 1 # Update consecutive completed pointer + if i == self.consecutive_completed_height + 1: + self.consecutive_completed_height = i + cp = self.consecutive_completed_height + 1 while cp < len(self.parts) and self.parts[cp] != None: self.consecutive_completed_height = cp @@ -1094,7 +1088,7 @@ class Resource: if self.next_segment: self.next_segment.cancel() if self.status == Resource.CORRUPT: - if self in self.link.incoming_resources: self.link.cancel_incoming_resource(self) + self.link.cancel_incoming_resource(self) self.reject(self.advertisement_packet) self.link.teardown() @@ -1106,14 +1100,14 @@ class Resource: cancel_packet = RNS.Packet(self.link, self.hash, context=RNS.Packet.RESOURCE_ICL) cancel_packet.send() except Exception as e: RNS.log("Could not send resource cancel packet, the contained exception was: "+str(e), RNS.LOG_ERROR) - if self in self.link.outgoing_resources: self.link.cancel_outgoing_resource(self) + self.link.cancel_outgoing_resource(self) else: if self.link.status == RNS.Link.ACTIVE: try: cancel_packet = RNS.Packet(self.link, self.hash, context=RNS.Packet.RESOURCE_RCL) cancel_packet.send() except Exception as e: RNS.log("Could not send resource cancel packet, the contained exception was: "+str(e), RNS.LOG_ERROR) - if self in self.link.incoming_resources: self.link.cancel_incoming_resource(self) + self.link.cancel_incoming_resource(self) if self.callback != None: try: @@ -1126,7 +1120,7 @@ class Resource: if self.status < Resource.COMPLETE: if self.initiator: self.status = Resource.REJECTED - if self in self.link.outgoing_resources: self.link.cancel_outgoing_resource(self) + self.link.cancel_outgoing_resource(self) if self.callback != None: try: self.link.resource_concluded(self) @@ -1247,9 +1241,7 @@ class Resource: return self.compressed def __str__(self): - if hasattr(self, "hash") and self.hash and hasattr(self, "link") and self.link: - return "<"+RNS.hexrep(self.hash,delimit=False)+"/"+RNS.hexrep(self.link.link_id,delimit=False)+">" - else: return "" + return "<"+RNS.hexrep(self.hash,delimit=False)+"/"+RNS.hexrep(self.link.link_id,delimit=False)+">" class ResourceAdvertisement: @@ -1373,4 +1365,4 @@ class ResourceAdvertisement: if adv.t > Resource.MAX_EFFICIENT_SIZE*3: raise ValueError("Invalid transfer size") - return adv + return adv \ No newline at end of file diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py index 7b11a351..8ed957b5 100755 --- a/RNS/Reticulum.py +++ b/RNS/Reticulum.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2026 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -108,8 +108,8 @@ class Reticulum: discovery significantly increases throughput over fast links. """ - MAX_QUEUED_ANNOUNCES = 4096 - QUEUED_ANNOUNCE_LIFE = 60*60*3 + MAX_QUEUED_ANNOUNCES = 16384 + QUEUED_ANNOUNCE_LIFE = 60*60*24 ANNOUNCE_CAP = 2 """ @@ -140,9 +140,6 @@ class Reticulum: # TODO: Let Reticulum somehow continously build a map of per-hop # latencies and use this map for global timeout calculation. DEFAULT_PER_HOP_TIMEOUT = 6 - """ - The default per-hop timeout value used in various timeout calculations. - """ # Length of truncated hashes in bits. TRUNCATED_HASHLENGTH = 128 @@ -190,6 +187,8 @@ class Reticulum: RNS.Transport.exit_handler() RNS.Identity.exit_handler() + if RNS.Profiler.ran(): RNS.Profiler.results() + RNS.loglevel = RNS.LOG_NONE RNS._detach_stdout() @@ -208,7 +207,7 @@ class Reticulum: @staticmethod def get_instance(): """ - Returns the currently running Reticulum instance. + Return the currently running Reticulum instance """ return Reticulum.__instance @@ -257,13 +256,11 @@ class Reticulum: Reticulum.__local_hops_delta = False Reticulum.__link_mtu_discovery = Reticulum.LINK_MTU_DISCOVERY Reticulum.__remote_management_enabled = False - Reticulum.__interface_management_enabled = True Reticulum.__use_implicit_proof = True Reticulum.__allow_probes = False Reticulum.__discovery_enabled = False Reticulum.__discover_interfaces = False Reticulum.__autoconnect_discovered_interfaces = False - Reticulum.__autoconnect_unverified = False Reticulum.__autoconnect_interface_mode = None Reticulum.__autoconnect_interface_gravity = None Reticulum.__autoconnect_announces_to_internal = None @@ -287,10 +284,6 @@ class Reticulum: Reticulum.__ic_held_release_interval = None Reticulum.__ec_pr_freq = None Reticulum.__egress_control = None - Reticulum.__inbound_data_queue_length = None - Reticulum.__inbound_announce_queue_length = None - Reticulum.__inbound_pr_queue_length = None - Reticulum.__inbound_il_queue_length = None Reticulum.panic_on_interface_error = False @@ -543,18 +536,12 @@ class Reticulum: if option == "link_mtu_discovery": v = self.config["reticulum"].as_bool(option) - if v == True: Reticulum.__link_mtu_discovery = True - elif v == False: Reticulum.__link_mtu_discovery = False + if v == True: Reticulum.__link_mtu_discovery = True if option == "enable_remote_management": v = self.config["reticulum"].as_bool(option) if v == True: Reticulum.__remote_management_enabled = True - if option == "enable_interface_management": - v = self.config["reticulum"].as_bool(option) - if v == False: Reticulum.__interface_management_enabled = False - else: Reticulum.__interface_management_enabled = True - if option == "remote_management_allowed": v = self.config["reticulum"].as_list(option) for hexhash in v: @@ -629,10 +616,6 @@ class Reticulum: v = self.config["reticulum"].as_int(option) if v > 0: Reticulum.__autoconnect_discovered_interfaces = v - if option == "autoconnect_unverified_implementations": - v = self.config["reticulum"].as_bool(option) - Reticulum.__autoconnect_unverified = v - if option == "autoconnect_interface_mode": v = None; dmode = str(self.config["reticulum"]["autoconnect_interface_mode"]).lower() if dmode == "full": v = Interface.Interface.MODE_FULL @@ -713,22 +696,6 @@ class Reticulum: v = self.config["reticulum"].as_float(option) if v >= 0: Reticulum.__ic_held_release_interval = v - if option == "qlen_in_data": - v = self.config["reticulum"].as_int(option) - if v > 0: Reticulum.__inbound_data_queue_length = v - - if option == "qlen_in_announce": - v = self.config["reticulum"].as_int(option) - if v > 0: Reticulum.__inbound_announce_queue_length = v - - if option == "qlen_in_pr": - v = self.config["reticulum"].as_int(option) - if v > 0: Reticulum.__inbound_pr_queue_length = v - - if option == "qlen_in_il": - v = self.config["reticulum"].as_int(option) - if v > 0: Reticulum.__inbound_il_queue_length = v - if RNS.compiled: RNS.log("Reticulum running in compiled mode", RNS.LOG_DEBUG) else: RNS.log("Reticulum running in interpreted mode", RNS.LOG_DEBUG) @@ -746,11 +713,6 @@ class Reticulum: if self.local_socket_path == None and self.use_af_unix: self.local_socket_path = "default" - dql = RNS.Reticulum.default_data_queue_length() or RNS.Transport.INBOUND_DA_QUEUE_LENGTH - BackboneInterface.BackboneInterface.DP_IC_HIGH_WM = max(4, int((BackboneInterface.BackboneInterface.DP_IC_HIGH_WM_PCT/100.0) * dql)) - BackboneInterface.BackboneInterface.DP_IC_MID_WM = max(2, int((BackboneInterface.BackboneInterface.DP_IC_MID_WM_PCT/100.0) * dql)) - BackboneInterface.BackboneInterface.DP_IC_LOW_WM = max(0, int((BackboneInterface.BackboneInterface.DP_IC_LOW_WM_PCT/100.0) * dql)) - self.__start_local_interface() if self.is_shared_instance or self.is_standalone_instance: @@ -768,79 +730,7 @@ class Reticulum: RNS.log("System interfaces are ready", RNS.LOG_VERBOSE) - def _attach_interface(self, name, internal_forced=False): - if not internal_forced and not Reticulum.__interface_management_enabled: return False - try: - interfaces = { iface.name: iface for iface in RNS.Transport.interfaces } - if name in interfaces: - RNS.log(f"Attempt to attach existing interface \"{name}\"", RNS.LOG_WARNING) - return False - - else: - try: config = ConfigObj(self.configpath) - except Exception as e: - RNS.log(f"Could not parse the configuration at {self.configpath} during interface attach", RNS.LOG_ERROR) - return None - - if "interfaces" in config: - if not name in config["interfaces"]: - RNS.log(f"Cannot attach interface \"{name}\", no configuration entry exists", RNS.LOG_ERROR) - return None - - else: - interface_config = config["interfaces"][name] - self._synthesize_interface(interface_config, name, force_attach=True) - RNS.log(f"Interface \"{name}\" was attached", RNS.LOG_NOTICE) - return True - - except Exception as e: RNS.log(f"Error while detaching interface \"{name}\": {e}", RNS.LOG_ERROR) - return False - - def _detach_interface(self, name, internal_forced=False): - if not internal_forced and not Reticulum.__interface_management_enabled: return False - try: - interfaces = { iface.name: iface for iface in RNS.Transport.interfaces } - if not name in interfaces: - RNS.log(f"Attempt to detach non-existing interface \"{name}\"", RNS.LOG_WARNING) - return None - - else: - interface = interfaces[name] - if isinstance(interface, I2PInterface.I2PInterface): return False - if isinstance(interface, LocalInterface.LocalClientInterface): return False - if isinstance(interface, LocalInterface.LocalServerInterface): return False - if interface.spawned_interfaces: - if type(interface.spawned_interfaces) == dict: spawned = list(interface.spawned_interfaces.values()) - elif type(interface.spawned_interfaces) == list: spawned = interface.spawned_interfaces.copy() - for s in spawned: - s.detach() - s.teardown() - RNS.Transport.remove_interface(s) - - interface.detach() - RNS.Transport.remove_interface(interface) - RNS.log(f"Interface \"{name}\" was detached", RNS.LOG_NOTICE) - return True - - except Exception as e: RNS.log(f"Error while detaching interface \"{name}\": {e}", RNS.LOG_ERROR); RNS.trace_exception(e) - return False - - def _reload_interface(self, name, internal_forced=False): - if not internal_forced and not Reticulum.__interface_management_enabled: return False - interfaces = { iface.name: iface for iface in RNS.Transport.interfaces } - if not name in interfaces: - RNS.log(f"Attempt to reload non-existing interface \"{name}\"", RNS.LOG_WARNING) - return None - - if self._detach_interface(name, internal_forced=internal_forced) and self._attach_interface(name, internal_forced=internal_forced): - RNS.log(f"Interface \"{name}\" was reloaded", RNS.LOG_NOTICE) - return True - - else: - RNS.log(f"Could not reload interface \"{name}\"", RNS.LOG_NOTICE) - return False - - def _synthesize_interface(self, config, name, instance_init=False, force_attach=False): + def _synthesize_interface(self, config, name, instance_init=False): c = config interface_mode = Interface.Interface.MODE_FULL @@ -887,19 +777,15 @@ class Reticulum: ifac_netname = None if "networkname" in c: - if c["networkname"] == "None": RNS.log("Ambiguous IFAC network name \"None\", this value is ignored and an IFAC network name has NOT been set", RNS.LOG_WARNING) - if c["networkname"] != "" and c["networkname"] != "None": ifac_netname = c["networkname"] + if c["networkname"] != "": ifac_netname = c["networkname"] if "network_name" in c: - if c["network_name"] == "None": RNS.log("Ambiguous IFAC network name \"None\", this value is ignored and an IFAC network name has NOT been set", RNS.LOG_WARNING) - if c["network_name"] != "" and c["network_name"] != "None": ifac_netname = c["network_name"] + if c["network_name"] != "": ifac_netname = c["network_name"] ifac_netkey = None if "passphrase" in c: - if c["passphrase"] == "None": RNS.log("Ambiguous IFAC passphrase \"None\", this value is ignored and an IFAC passphrase has NOT been set", RNS.LOG_WARNING) - if c["passphrase"] != "" and c["passphrase"] != "None": ifac_netkey = c["passphrase"] + if c["passphrase"] != "": ifac_netkey = c["passphrase"] if "pass_phrase" in c: - if c["pass_phrase"] == "None": RNS.log("Ambiguous IFAC passphrase \"None\", this value is ignored and an IFAC passphrase has NOT been set", RNS.LOG_WARNING) - if c["pass_phrase"] != "" and c["pass_phrase"] != "None": ifac_netkey = c["pass_phrase"] + if c["pass_phrase"] != "": ifac_netkey = c["pass_phrase"] ingress_control = True if "ingress_control" in c: ingress_control = c.as_bool("ingress_control") @@ -974,7 +860,6 @@ class Reticulum: discovery_announce_interval = None discovery_stamp_value = None discovery_name = None - discovery_lxmf_address = None discovery_encrypt = False reachable_on = None publish_ifac = False @@ -1006,11 +891,6 @@ class Reticulum: if "discovery_frequency" in c: discovery_frequency = c.as_int("discovery_frequency") if "discovery_bandwidth" in c: discovery_bandwidth = c.as_int("discovery_bandwidth") if "discovery_modulation" in c: discovery_modulation = c.as_int("discovery_modulation") - if "discovery_lxmf_address" in c: - if len(c["discovery_lxmf_address"]) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2: - try: discovery_lxmf_address = bytes.fromhex(c["discovery_lxmf_address"]) - except: RNS.log(f"Invalid interface discovery LXMF address: {c['discovery_lxmf_address']}", RNS.LOG_ERROR) - else: RNS.log(f"Invalid length for interface discovery LXMF address: {c['discovery_lxmf_address']}", RNS.LOG_ERROR) if not interface_mode in [Interface.Interface.MODE_GATEWAY, Interface.Interface.MODE_ACCESS_POINT, Interface.Interface.MODE_INTERNAL]: if not ignore_config_warnings: @@ -1042,7 +922,6 @@ class Reticulum: interface.discovery_publish_ifac = publish_ifac interface.reachable_on = reachable_on interface.discovery_name = discovery_name - interface.discovery_lxmf_address = discovery_lxmf_address interface.discovery_encrypt = discovery_encrypt interface.discovery_stamp_value = discovery_stamp_value interface.discovery_location = discovery_location @@ -1092,16 +971,11 @@ class Reticulum: interface.ifac_identity = RNS.Identity.from_bytes(interface.ifac_key) interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key)) - elif interface.discoverable and interface.discovery_publish_ifac: - RNS.log(f"IFAC publishing was enabled for discoverable interface {interface}, but neither IFAC netname nor passphrase is configured", RNS.LOG_WARNING) - RNS.log(f"Disabling IFAC publishing for {interface}", RNS.LOG_WARNING) - interface.discovery_publish_ifac = False - RNS.Transport.add_interface(interface) interface.final_init() interface = None - if (("interface_enabled" in c) and c.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True) or force_attach: + if (("interface_enabled" in c) and c.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True): interface_config = c interface_config["name"] = name interface_config["selected_interface_mode"] = interface_mode @@ -1205,6 +1079,7 @@ class Reticulum: except Exception as e: RNS.log(f"External interface initialisation failed for {interface_type} / {name}", RNS.LOG_ERROR) RNS.trace_exception(e) + else: RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_DEBUG) @@ -1375,27 +1250,17 @@ class Reticulum: mh = call["max_hops"] self.rpc_return(conn, self.get_path_table(max_hops=mh)) - if path == "interface_stats": self.rpc_return(conn, self.get_interface_stats()) - if path == "rate_table": self.rpc_return(conn, self.get_rate_table()) - if path == "next_hop_if_name": self.rpc_return(conn, self.get_next_hop_if_name(call["destination_hash"])) - if path == "next_hop": self.rpc_return(conn, self.get_next_hop(call["destination_hash"])) - if path == "first_hop_timeout": self.rpc_return(conn, self.get_first_hop_timeout(call["destination_hash"])) - if path == "lowest_interface_bitrate": self.rpc_return(conn, self.get_lowest_interface_bitrate()) - if path == "medium_path_timeout": self.rpc_return(conn, self.get_medium_path_timeout()) - if path == "link_count": self.rpc_return(conn, self.get_link_count()) - if path == "active_link_count": self.rpc_return(conn, self.get_active_link_count()) - if path == "packet_rssi": self.rpc_return(conn, self.get_packet_rssi(call["packet_hash"])) - if path == "packet_snr": self.rpc_return(conn, self.get_packet_snr(call["packet_hash"])) - if path == "packet_q": self.rpc_return(conn, self.get_packet_q(call["packet_hash"])) - if path == "profiling_results": self.rpc_return(conn, self.get_profiling_results()) - if path == "blackholed_identities": self.rpc_return(conn, self.get_blackholed_identities()) - if path == "is_blackholed": self.rpc_return(conn, self.is_blackholed(call["identity_hash"])) - - if "manage" in call: - path = call["manage"] - if path == "attach_interface": self.rpc_return(conn, self.attach_interface(call["name"])) - if path == "detach_interface": self.rpc_return(conn, self.detach_interface(call["name"])) - if path == "reload_interface": self.rpc_return(conn, self.reload_interface(call["name"])) + if path == "interface_stats": self.rpc_return(conn, self.get_interface_stats()) + if path == "rate_table": self.rpc_return(conn, self.get_rate_table()) + if path == "next_hop_if_name": self.rpc_return(conn, self.get_next_hop_if_name(call["destination_hash"])) + if path == "next_hop": self.rpc_return(conn, self.get_next_hop(call["destination_hash"])) + if path == "first_hop_timeout": self.rpc_return(conn, self.get_first_hop_timeout(call["destination_hash"])) + if path == "link_count": self.rpc_return(conn, self.get_link_count()) + if path == "packet_rssi": self.rpc_return(conn, self.get_packet_rssi(call["packet_hash"])) + if path == "packet_snr": self.rpc_return(conn, self.get_packet_snr(call["packet_hash"])) + if path == "packet_q": self.rpc_return(conn, self.get_packet_q(call["packet_hash"])) + if path == "blackholed_identities": self.rpc_return(conn, self.get_blackholed_identities()) + if path == "is_blackholed": self.rpc_return(conn, self.is_blackholed(call["identity_hash"])) if "drop" in call: path = call["drop"] @@ -1575,26 +1440,6 @@ class Reticulum: else: ifstats["txs"] = 0 else: ifstats["txs"] = 0 - if hasattr(interface, "current_arx_speed"): - if interface.current_arx_speed != None: ifstats["arxs"] = interface.current_arx_speed - else: ifstats["arxs"] = 0 - else: ifstats["arxs"] = 0 - - if hasattr(interface, "current_atx_speed"): - if interface.current_atx_speed != None: ifstats["atxs"] = interface.current_atx_speed - else: ifstats["atxs"] = 0 - else: ifstats["atxs"] = 0 - - if hasattr(interface, "current_prx_speed"): - if interface.current_prx_speed != None: ifstats["prxs"] = interface.current_prx_speed - else: ifstats["prxs"] = 0 - else: ifstats["prxs"] = 0 - - if hasattr(interface, "current_ptx_speed"): - if interface.current_ptx_speed != None: ifstats["ptxs"] = interface.current_ptx_speed - else: ifstats["ptxs"] = 0 - else: ifstats["ptxs"] = 0 - if hasattr(interface, "peers"): if interface.peers != None: ifstats["peers"] = len(interface.peers) else: ifstats["peers"] = None @@ -1625,21 +1470,8 @@ class Reticulum: ifstats["short_name"] = str(interface.name) ifstats["hash"] = interface.get_hash() ifstats["type"] = str(type(interface).__name__) - ifstats["mtu"] = interface.HW_MTU ifstats["rxb"] = interface.rxb ifstats["txb"] = interface.txb - ifstats["arxb"] = interface.arxb - ifstats["atxb"] = interface.atxb - ifstats["arxc"] = interface.arxc - ifstats["atxc"] = interface.atxc - ifstats["prxb"] = interface.prxb - ifstats["ptxb"] = interface.ptxb - ifstats["prxc"] = interface.prxc - ifstats["ptxc"] = interface.ptxc - ifstats["txdrp"] = interface.tx_drops - ifstats["txdrb"] = interface.tx_dropped_bytes - ifstats["txstalled"] = interface.tx_stalled - ifstats["txbuffered"] = len(interface.transmit_buffer) if interface.transmit_buffer else 0 ifstats["incoming_announce_frequency"] = interface.incoming_announce_frequency() ifstats["outgoing_announce_frequency"] = interface.outgoing_announce_frequency() ifstats["incoming_pr_frequency"] = interface.incoming_pr_frequency() @@ -1650,63 +1482,21 @@ class Reticulum: ifstats["held_announces"] = len(interface.held_announces) ifstats["burst_active"] = interface.ic_burst_active ifstats["burst_activated"] = interface.ic_burst_activated - ifstats["burst_count"] = interface.ic_burst_count ifstats["pr_burst_active"] = interface.ic_pr_burst_active ifstats["pr_burst_activated"] = interface.ic_pr_burst_activated - ifstats["pr_burst_count"] = interface.ic_pr_burst_count ifstats["status"] = interface.online ifstats["mode"] = interface.mode ifstats["gravity"] = interface.gravity ifstats["announces_to_internal"] = interface.announces_to_internal - ifstats["protocol_violations"] = interface.protocol_violations - ifstats["ifac_violations"] = interface.ifac_violations - ifstats["packet_filter_hits"] = interface.packet_filter_hits interfaces.append(ifstats) - qsnapshot = RNS.Transport.inbound_queues.snapshot() - dql = RNS.Transport.INBOUND_DA_QUEUE_LENGTH - aql = RNS.Transport.INBOUND_AN_QUEUE_LENGTH - pql = RNS.Transport.INBOUND_PR_QUEUE_LENGTH - ilql = RNS.Transport.INBOUND_IL_QUEUE_LENGTH - tql = dql+aql+pql+ilql - - stats = {} - stats["interfaces"] = interfaces - stats["rxb"] = RNS.Transport.traffic_rxb - stats["txb"] = RNS.Transport.traffic_txb - stats["rxs"] = RNS.Transport.speed_rx - stats["txs"] = RNS.Transport.speed_tx - stats["arxb"] = RNS.Transport.announce_rxb - stats["atxb"] = RNS.Transport.announce_txb - stats["arxs"] = RNS.Transport.announce_speed_rx - stats["atxs"] = RNS.Transport.announce_speed_tx - stats["arxf"] = RNS.Transport.announce_freq_rx - stats["atxf"] = RNS.Transport.announce_freq_tx - stats["prxb"] = RNS.Transport.pr_rxb - stats["ptxb"] = RNS.Transport.pr_txb - stats["prxs"] = RNS.Transport.pr_speed_rx - stats["ptxs"] = RNS.Transport.pr_speed_tx - stats["prxf"] = RNS.Transport.pr_freq_rx - stats["ptxf"] = RNS.Transport.pr_freq_tx - stats["rxpps"] = RNS.Transport.rx_pps - stats["txpps"] = RNS.Transport.tx_pps - stats["rxqt"] = qsnapshot[0] - stats["rxqd"] = qsnapshot[1][0] - stats["rxqa"] = qsnapshot[1][1] - stats["rxqp"] = qsnapshot[1][2] - stats["rxqil"] = qsnapshot[1][3] - stats["rxqtd"] = qsnapshot[2][0]+qsnapshot[2][1]+qsnapshot[2][2]+qsnapshot[2][3] - stats["rxqdd"] = qsnapshot[2][0] - stats["rxqad"] = qsnapshot[2][1] - stats["rxqpd"] = qsnapshot[2][2] - stats["rxqild"] = qsnapshot[2][3] - stats["tqpressure"] = qsnapshot[0]/tql if qsnapshot[0] else 0 - stats["dqpressure"] = qsnapshot[1][0]/dql if qsnapshot[1][0] else 0 - stats["aqpressure"] = qsnapshot[1][1]/aql if qsnapshot[1][1] else 0 - stats["pqpressure"] = qsnapshot[1][2]/pql if qsnapshot[1][2] else 0 - stats["ilqpressure"] = qsnapshot[1][3]/ilql if qsnapshot[1][3] else 0 - stats["txq"] = None + stats = {} + stats["interfaces"] = interfaces + stats["rxb"] = RNS.Transport.traffic_rxb + stats["txb"] = RNS.Transport.traffic_txb + stats["rxs"] = RNS.Transport.speed_rx + stats["txs"] = RNS.Transport.speed_tx if Reticulum.transport_enabled(): stats["transport_id"] = RNS.Transport.identity.hash @@ -1812,16 +1602,6 @@ class Reticulum: return str(RNS.Transport.next_hop_interface(destination)) def get_first_hop_timeout(self, destination): - """ - Returns a best-effort estimate of a reasonable minimum - *first-hop* timeout value for a given destination hash. - If a path is known, this calculation takes the next-hop - interface's bitrate into account. If no path is currently - known, returns ``DEFAULT_PER_HOP_TIMEOUT``. - - :param destination: A destination hash, as *bytes*. - :returns: First-hop timeout, in seconds. - """ if self.is_connected_to_shared_instance: try: rpc_connection = self.get_rpc_client() @@ -1841,45 +1621,6 @@ class Reticulum: else: return RNS.Transport.first_hop_timeout(destination) - def get_lowest_interface_bitrate(self): - """ - Returns the bitrate of the slowest currently online - interface, or None if no online interface bitrate - - :returns: Lowest online interface bitrate in bits per second, or ``None``. - """ - if self.is_connected_to_shared_instance: - try: - rpc_connection = self.get_rpc_client() - rpc_connection.send_bytes(mp.packb({"get": "lowest_interface_bitrate"})) - return mp.unpackb(rpc_connection.recv_bytes()) - except Exception as e: - RNS.log("An error occurred while getting lowest interface bitrate from shared instance: "+str(e), RNS.LOG_ERROR) - return None - - else: - return RNS.Transport.lowest_interface_bitrate - - def get_medium_path_timeout(self): - """ - Returns an estimate of a reasonable minimum path request timeout covering - a full round trip for an MTU on the slowest currently online interface - plus per hop grace - - :returns: Timeout in seconds or 0 if it's unknown. - """ - if self.is_connected_to_shared_instance: - try: - rpc_connection = self.get_rpc_client() - rpc_connection.send_bytes(mp.packb({"get": "medium_path_timeout"})) - return mp.unpackb(rpc_connection.recv_bytes()) - except Exception as e: - RNS.log("An error occurred while getting medium path timeout from shared instance: "+str(e), RNS.LOG_ERROR) - return 0 - - else: - return RNS.Transport.medium_path_timeout() - def get_next_hop(self, destination): if self.is_connected_to_shared_instance: rpc_connection = self.get_rpc_client() @@ -1899,17 +1640,7 @@ class Reticulum: return response else: - return RNS.Transport.link_count() - - def get_active_link_count(self): - if self.is_connected_to_shared_instance: - rpc_connection = self.get_rpc_client() - rpc_connection.send_bytes(mp.packb({"get": "active_link_count"})) - response = mp.unpackb(rpc_connection.recv_bytes()) - return response - - else: - return RNS.Transport.active_link_count() + return len(RNS.Transport.link_table) def get_packet_rssi(self, packet_hash): if self.is_connected_to_shared_instance: @@ -1953,50 +1684,21 @@ class Reticulum: return None - def get_profiling_results(self): - if self.is_connected_to_shared_instance: - rpc_connection = self.get_rpc_client() - rpc_connection.send_bytes(mp.packb({"get": "profiling_results"})) - response = mp.unpackb(rpc_connection.recv_bytes()) - return response + def halt_interface(self, interface): + pass - else: - if RNS.Profiler.ran(): return RNS.Profiler.results() - else: return None + def resume_interface(self, interface): + pass - def attach_interface(self, interface_name): - if self.is_connected_to_shared_instance: - rpc_connection = self.get_rpc_client() - rpc_connection.send_bytes(mp.packb({"manage": "attach_interface", "name": interface_name})) - response = mp.unpackb(rpc_connection.recv_bytes()) - return response - - else: return self._attach_interface(interface_name) - - def detach_interface(self, interface_name): - if self.is_connected_to_shared_instance: - rpc_connection = self.get_rpc_client() - rpc_connection.send_bytes(mp.packb({"manage": "detach_interface", "name": interface_name})) - response = mp.unpackb(rpc_connection.recv_bytes()) - return response - - else: return self._detach_interface(interface_name) - - def reload_interface(self, interface_name): - if self.is_connected_to_shared_instance: - rpc_connection = self.get_rpc_client() - rpc_connection.send_bytes(mp.packb({"manage": "reload_interface", "name": interface_name})) - response = mp.unpackb(rpc_connection.recv_bytes()) - return response - - else: return self._reload_interface(interface_name) + def reload_interface(self, interface): + pass def get_blackholed_identities(self): if self.is_connected_to_shared_instance: - rpc_connection = self.get_rpc_client() - rpc_connection.send_bytes(mp.packb({"get": "blackholed_identities"})) - response = mp.unpackb(rpc_connection.recv_bytes()) - return response + rpc_connection = self.get_rpc_client() + rpc_connection.send_bytes(mp.packb({"get": "blackholed_identities"})) + response = mp.unpackb(rpc_connection.recv_bytes()) + return response else: return RNS.Transport.blackholed_identities @@ -2007,10 +1709,10 @@ class Reticulum: if len(identity_hash) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8: raise ValueError("Invalid identity hash length for blackhole check") if self.is_connected_to_shared_instance: - rpc_connection = self.get_rpc_client() - rpc_connection.send_bytes(mp.packb({"get": "is_blackholed", "identity_hash": identity_hash})) - response = mp.unpackb(rpc_connection.recv_bytes()) - return response + rpc_connection = self.get_rpc_client() + rpc_connection.send_bytes(mp.packb({"get": "is_blackholed", "identity_hash": identity_hash})) + response = mp.unpackb(rpc_connection.recv_bytes()) + return response else: return identity_hash in RNS.Transport.blackholed_identities @@ -2147,10 +1849,6 @@ class Reticulum: def should_autoconnect_discovered_interfaces(): return Reticulum.__autoconnect_discovered_interfaces > 0 - @staticmethod - def should_autoconnect_unverified_implementations(): - return Reticulum.__autoconnect_unverified - @staticmethod def autoconnect_interface_mode(): return Reticulum.__autoconnect_interface_mode @@ -2175,22 +1873,6 @@ class Reticulum: def local_hops_delta(): return Reticulum.__local_hops_delta - @staticmethod - def default_data_queue_length(): - return Reticulum.__inbound_data_queue_length - - @staticmethod - def default_announce_queue_length(): - return Reticulum.__inbound_announce_queue_length - - @staticmethod - def default_pr_queue_length(): - return Reticulum.__inbound_pr_queue_length - - @staticmethod - def default_il_queue_length(): - return Reticulum.__inbound_il_queue_length - # Default configuration file: __default_rns_config__ = '''# This is the default Reticulum config file. # You should probably edit it to include any additional, diff --git a/RNS/Transport.py b/RNS/Transport.py index 2a46569f..1453f78d 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -37,61 +37,11 @@ import struct import inspect import threading from time import sleep +from threading import Lock from collections import deque -from queue import Full, Empty -from threading import Lock, Condition from .vendor import umsgpack as umsgpack from RNS.Interfaces.BackboneInterface import BackboneInterface -class InboundQueues: - __slots__ = ("_cond", "_queues", "_sizes", "_dropped", "_hw_mark") - - def __init__(self, sizes, hw_mark=None): - self._cond = Condition() - self._queues = [deque() for _ in sizes] - self._sizes = sizes - self._dropped = [0 for _ in sizes] - self._hw_mark = hw_mark or max(128, BackboneInterface.DP_IC_HIGH_WM) - - def put(self, traffic_class, item, block=False, timeout=None): - with self._cond: - q = self._queues[traffic_class]; ql = len(q) - if traffic_class == Transport.TC_DATA and ql >= self._hw_mark: - BackboneInterface._throttle_immediate(ql) - if ql >= self._sizes[traffic_class]: - self._dropped[traffic_class] += 1 - raise Full - q.append(item) - # For single drainer, notify() suffices, but will - # need to move this to notify_all for the future - # transition to free-threaded drainers. - self._cond.notify() - - def get(self, block=True, timeout=None): - deadline = None if timeout is None else time.monotonic() + timeout - with self._cond: - while True: - # Scan queues in priority order - for q in self._queues: - if q: return q.popleft() - if not block: raise Empty - remaining = None if deadline is None else deadline - time.monotonic() - if remaining is not None and remaining <= 0: raise Empty - self._cond.wait(remaining) - - # Approximate queue height, this is an approximate value, - # reads without locking and never blocks. - def qsize(self, traffic_class=None): - if traffic_class is None: return sum(len(q) for q in self._queues) - return len(self._queues[traffic_class]) - - # Consistent-time, single-instant height snapshot. - def snapshot(self): - with self._cond: - heights = tuple(len(q) for q in self._queues) - dropped = tuple(d for d in self._dropped) - return (sum(heights), heights, dropped) - class Transport: """ Through static methods of this class you can interact with the @@ -108,11 +58,6 @@ class Transport: REACHABILITY_DIRECT = 0x01 REACHABILITY_TRANSPORT = 0x02 - TC_DATA = 0x00 - TC_ANNOUNCE = 0x01 - TC_PATH_REQUEST = 0x02 - TC_INGRESS_LIMITED = 0x03 - APP_NAME = "rnstransport" PATHFINDER_M = 128 # Max hops @@ -132,19 +77,11 @@ class Transport: LOCAL_REBROADCASTS_MAX = 2 # How many local rebroadcasts of an announce is allowed PATH_REQUEST_TIMEOUT = 15 # Default timeout for client path requests in seconds - PATH_REQUEST_GATE_TIMEOUT = 45 # Default timeout for client path request gate control in seconds + PATH_REQUEST_GATE_TIMEOUT = 120 # Default timeout for client path request gate control in seconds PATH_REQUEST_GRACE = 0.4 # Grace time before a path announcement is made, allows directly reachable peers to respond first PATH_REQUEST_RG = 1.5 # Extra grace time for roaming-mode interfaces to allow more suitable peers to respond first PATH_REQUEST_MI = 20 # Minimum interval in seconds for automated path requests - TRANSPORT_WORKERS = 1 - USE_INBOUND_QUEUE = True - USE_OUTBOUND_QUEUE = False - INBOUND_DA_QUEUE_LENGTH = 1024 - INBOUND_AN_QUEUE_LENGTH = 128 - INBOUND_PR_QUEUE_LENGTH = 128 - INBOUND_IL_QUEUE_LENGTH = 8 - STATE_UNKNOWN = 0x00 STATE_UNRESPONSIVE = 0x01 STATE_RESPONSIVE = 0x02 @@ -167,8 +104,6 @@ class Transport: destinations_map = {} # Destination hash map of active destinations pending_links = [] # Links that are being established active_links = [] # Links that are active - pending_links_map = {} # Link ID hash map of pending links - active_links_map = {} # Link ID hash map of active links packet_hashlist = set() # A list of packet hashes for duplicate detection packet_hashlist_prev = set() receipts = [] # Receipts of all outgoing packets for proof processing @@ -185,18 +120,14 @@ class Transport: tunnels = {} # A table storing tunnels to other transport instances announce_rate_table = {} # A table for keeping track of announce rates path_requests = {} # A table for storing path request timestamps - inflight_path_requests = {} # A table for tracking in-flight path request path_states = {} # A table for keeping track of path states blackholed_identities = {} # A table for keeping track of blackholed identities discovery_path_requests = {} # A table for keeping track of path requests on behalf of other nodes - discovery_pr_tags = set() # A table for keeping track of tagged path requests - discovery_pr_tags_prev = set() - max_pr_tags = 16000 # Maximum amount of unique path request tags to remember + discovery_pr_tags = [] # A table for keeping track of tagged path requests + max_pr_tags = 32000 # Maximum amount of unique path request tags to remember max_queued_discovery_prs = 32 # Maximum amount of queued discovery path requests - pr_destination_hash = None # Destination hash of the local path request destination - inbound_queues = None interfaces_lock = Lock() destinations_lock = Lock() destinations_map_lock = Lock() @@ -214,7 +145,6 @@ class Transport: discovery_pr_lock = Lock() discovery_pr_tags_lock = Lock() path_requests_lock = Lock() - inflight_path_requests_lock = Lock() pending_local_prs_lock = Lock() path_states_lock = Lock() jobs_lock = Lock() @@ -272,27 +202,9 @@ class Transport: traffic_rxb = 0 traffic_txb = 0 - announce_rxb = 0 - announce_txb = 0 - pr_rxb = 0 - pr_txb = 0 speed_rx = 0 speed_tx = 0 - announce_speed_rx = 0 - announce_speed_tx = 0 - announce_freq_rx = 0 - announce_freq_tx = 0 - pr_speed_rx = 0 - pr_speed_tx = 0 - pr_freq_rx = 0 - pr_freq_tx = 0 - rx_packets = 0 - tx_packets = 0 - rx_pps = 0 - tx_pps = 0 traffic_captured = None - lowest_interface_bitrate = None - highest_interface_bitrate = None identity = None network_identity = None @@ -307,15 +219,6 @@ class Transport: def start(reticulum_instance): Transport.owner = reticulum_instance - Transport.INBOUND_DA_QUEUE_LENGTH = RNS.Reticulum.default_data_queue_length() or Transport.INBOUND_DA_QUEUE_LENGTH - Transport.INBOUND_AN_QUEUE_LENGTH = RNS.Reticulum.default_announce_queue_length() or Transport.INBOUND_AN_QUEUE_LENGTH - Transport.INBOUND_PR_QUEUE_LENGTH = RNS.Reticulum.default_pr_queue_length() or Transport.INBOUND_PR_QUEUE_LENGTH - Transport.INBOUND_IL_QUEUE_LENGTH = RNS.Reticulum.default_il_queue_length() or Transport.INBOUND_IL_QUEUE_LENGTH - Transport.inbound_queues = InboundQueues((Transport.INBOUND_DA_QUEUE_LENGTH, - Transport.INBOUND_AN_QUEUE_LENGTH, - Transport.INBOUND_PR_QUEUE_LENGTH, - Transport.INBOUND_IL_QUEUE_LENGTH)) - if Transport.identity == None: transport_identity_path = RNS.Reticulum.storagepath+"/transport_identity" if os.path.isfile(transport_identity_path): @@ -357,7 +260,6 @@ class Transport: Transport.path_request_destination.set_packet_callback(Transport.path_request_handler) Transport.control_destinations.append(Transport.path_request_destination) Transport.control_hashes.append(Transport.path_request_destination.hash) - Transport.pr_destination_hash = Transport.path_request_destination.hash Transport.tunnel_synthesize_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, Transport.APP_NAME, "tunnel", "synthesize") Transport.tunnel_synthesize_destination.set_packet_callback(Transport.tunnel_synthesize_handler) @@ -478,7 +380,7 @@ class Transport: timestamp = serialised_entry[1] received_from = serialised_entry[2] hops = serialised_entry[3] - path_expires = serialised_entry[4] + expires = serialised_entry[4] random_blobs = list(set(serialised_entry[5])) receiving_interface = Transport.find_interface_from_hash(serialised_entry[6]) announce_packet = Transport.get_cached_packet(serialised_entry[7], packet_type="announce") @@ -491,7 +393,7 @@ class Transport: # increased hop-count. announce_packet.hops += 1 - tunnel_path = [timestamp, received_from, hops, path_expires, random_blobs, receiving_interface, announce_packet.packet_hash] + tunnel_path = [timestamp, received_from, hops, expires, random_blobs, receiving_interface, announce_packet.packet_hash] tunnel_paths[destination_hash] = tunnel_path if len(tunnel_paths) > 0: @@ -518,16 +420,10 @@ class Transport: Transport.probe_destination = None RNS.log("Transport instance "+str(Transport.identity)+" started", RNS.LOG_VERBOSE) if RNS.sl(RNS.LOG_VERBOSE) else None - Transport.start_time = time.time() + Transport.start_time = time.time() # Sort interfaces according to bitrate Transport.prioritize_interfaces() - - # Spawn queue workers - for n in range(Transport.TRANSPORT_WORKERS): - if Transport.USE_INBOUND_QUEUE: threading.Thread(target=Transport.inbound_job, daemon=True).start() - if Transport.USE_OUTBOUND_QUEUE: threading.Thread(target=Transport.outbound_job, daemon=True).start() - Transport.ready = True # Synthesize tunnels for any interfaces wanting it @@ -565,10 +461,6 @@ class Transport: with Transport.interfaces_lock: try: Transport.interfaces.sort(key=lambda interface: interface.bitrate, reverse=True) except Exception as e: RNS.log(f"Could not prioritize interfaces according to bitrate. The contained exception was: {e}", RNS.LOG_ERROR) - try: Transport.lowest_interface_bitrate = min((interface.bitrate for interface in Transport.interfaces if interface.online and interface.bitrate)) - except Exception as e: RNS.log(f"Could not get lowest interface bitrate. The contained exception was: {e}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - try: Transport.highest_interface_bitrate = max((interface.bitrate for interface in Transport.interfaces if interface.online and interface.bitrate)) - except Exception as e: RNS.log(f"Could not get highest interface bitrate. The contained exception was: {e}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None @staticmethod def enable_discovery(): @@ -589,89 +481,34 @@ class Transport: @staticmethod def count_traffic_loop(): - rxp = 0; txp = 0; cts = None - while True: time.sleep(1) try: - rxb = 0; txb = 0; rxs = 0; txs = 0; - arxb = 0; atxb = 0; arxs = 0; atxs = 0; - prxb = 0; ptxb = 0; prxs = 0; ptxs = 0; - iafreq = 0; oafreq = 0; ipfreq = 0; opfreq = 0 - + rxb = 0; txb = 0; + rxs = 0; txs = 0; for interface in Transport.interfaces: if not hasattr(interface, "parent_interface") or interface.parent_interface == None: - if hasattr(interface, "is_local_shared_instance"): continue if hasattr(interface, "transport_traffic_counter"): - now = time.time() - - irxb = interface.rxb; itxb = interface.txb - iarxb = interface.arxb; iatxb = interface.atxb - iprxb = interface.prxb; iptxb = interface.ptxb - + now = time.time(); irxb = interface.rxb; itxb = interface.txb tc = interface.transport_traffic_counter + rx_diff = irxb - tc["rxb"] + tx_diff = itxb - tc["txb"] ts_diff = now - tc["ts"] - - rx_diff = irxb - tc["rxb"]; tx_diff = itxb - tc["txb"] - arx_diff = iarxb - tc["arxb"]; atx_diff = iatxb - tc["atxb"] - prx_diff = iprxb - tc["prxb"]; ptx_diff = iptxb - tc["ptxb"] - - rxb += rx_diff; crxs = (rx_diff*8)/ts_diff - txb += tx_diff; ctxs = (tx_diff*8)/ts_diff - arxb += arx_diff; carxs = (arx_diff*8)/ts_diff - atxb += atx_diff; catxs = (atx_diff*8)/ts_diff - prxb += prx_diff; cprxs = (prx_diff*8)/ts_diff - ptxb += ptx_diff; cptxs = (ptx_diff*8)/ts_diff - - iafreq += interface.incoming_announce_frequency() - oafreq += interface.outgoing_announce_frequency() - ipfreq += interface.incoming_pr_frequency() - opfreq += interface.outgoing_pr_frequency() - - interface.current_rx_speed = crxs; rxs += crxs - interface.current_tx_speed = ctxs; txs += ctxs - interface.current_arx_speed = carxs; arxs += carxs - interface.current_atx_speed = catxs; atxs += catxs - interface.current_prx_speed = cprxs; prxs += cprxs - interface.current_ptx_speed = cptxs; ptxs += cptxs - - tc["ts"] = now - tc["rxb"] = irxb; tc["txb"] = itxb - tc["arxb"] = iarxb; tc["atxb"] = iatxb - tc["prxb"] = iprxb; tc["ptxb"] = iptxb + rxb += rx_diff; crxs = (rx_diff*8)/ts_diff + txb += tx_diff; ctxs = (tx_diff*8)/ts_diff + interface.current_rx_speed = crxs; rxs += crxs + interface.current_tx_speed = ctxs; txs += ctxs + tc["rxb"] = irxb; + tc["txb"] = itxb; + tc["ts"] = now; else: - interface.transport_traffic_counter = { "ts": time.time(), "rxb": interface.rxb, "txb": interface.txb, - "arxb": interface.arxb, "atxb": interface.atxb, - "prxb": interface.prxb, "ptxb": interface.ptxb } - if not Transport.start_time: rpps = 0; tpps = 0 - else: - if not cts: ts = Transport.start_time - else: ts = cts - cts = time.time(); td = cts - ts - rpps = (Transport.rx_packets - rxp) / td - tpps = (Transport.tx_packets - txp) / td - rxp = Transport.rx_packets - txp = Transport.tx_packets + interface.transport_traffic_counter = {"ts": time.time(), "rxb": interface.rxb, "txb": interface.txb} - Transport.rx_pps = int(round(rpps)) - Transport.tx_pps = int(round(tpps)) - Transport.traffic_rxb += rxb - Transport.traffic_txb += txb - Transport.speed_rx = rxs - Transport.speed_tx = txs - Transport.announce_rxb += arxb - Transport.announce_txb += atxb - Transport.announce_speed_rx = arxs - Transport.announce_speed_tx = atxs - Transport.announce_freq_rx = iafreq - Transport.announce_freq_tx = oafreq - Transport.pr_rxb += prxb - Transport.pr_txb += ptxb - Transport.pr_speed_rx = prxs - Transport.pr_speed_tx = ptxs - Transport.pr_freq_rx = ipfreq - Transport.pr_freq_tx = opfreq + Transport.traffic_rxb += rxb + Transport.traffic_txb += txb + Transport.speed_rx = rxs + Transport.speed_tx = txs except Exception as e: RNS.log(f"An error occurred while counting interface traffic: {e}", RNS.LOG_ERROR) @@ -695,7 +532,6 @@ class Transport: if time.time() > Transport.links_last_checked+Transport.links_check_interval: with Transport.pending_links_lock: - closed_pending_links = [] for link in Transport.pending_links: if link.status == RNS.Link.CLOSED: # If we are not a Transport Instance, finding a pending link @@ -719,28 +555,19 @@ class Transport: blocked_if = None path_requests[link.destination.hash] = blocked_if - closed_pending_links.append(link) - - for closed_link in closed_pending_links: - Transport.pending_links.remove(closed_link) - Transport.pending_links_map.pop(closed_link.link_id, None) + Transport.pending_links.remove(link) with Transport.active_links_lock: closed_links = [] for link in Transport.active_links: if link.status == RNS.Link.CLOSED: closed_links.append(link) - for closed_link in closed_links: - Transport.active_links.remove(closed_link) - Transport.active_links_map.pop(closed_link.link_id, None) + for closed_link in closed_links: Transport.active_links.remove(closed_link) Transport.links_last_checked = time.time() # Process receipts list for timed-out packets if time.time() > Transport.receipts_last_checked+Transport.receipts_check_interval: - # TODO: The pop(0) is expensive under the lock. - # Could improve contention via an initial reverse(), - # then pop() trick or similar. with Transport.receipts_lock: while len(Transport.receipts) > Transport.MAX_RECEIPTS: culled_receipt = Transport.receipts.pop(0) @@ -748,9 +575,6 @@ class Transport: culled_receipt.check_timeout() should_collect = True - # TODO: Actually, this whole thing might need re- - # thinking a bit, but may require moving receipts - # to a dict or something else entirely. with Transport.receipts_lock: expired_receipts = [] for receipt in Transport.receipts: @@ -849,8 +673,7 @@ class Transport: # Cull the path request tags list if it has reached its max size if len(Transport.discovery_pr_tags) > Transport.max_pr_tags: with Transport.discovery_pr_tags_lock: - Transport.discovery_pr_tags_prev = Transport.discovery_pr_tags - Transport.discovery_pr_tags = set() + Transport.discovery_pr_tags = Transport.discovery_pr_tags[len(Transport.discovery_pr_tags)-Transport.max_pr_tags:len(Transport.discovery_pr_tags)-1] if time.time() > Transport.tables_last_culled + Transport.tables_cull_interval: # Remove unneeded path state entries @@ -979,27 +802,15 @@ class Transport: # Cull the pending path requests table stale_path_requests = [] - try: - path_requests_snapshot = Transport.path_requests.copy() - for destination_hash in path_requests_snapshot: - if time.time() > path_requests_snapshot[destination_hash] + Transport.PATH_REQUEST_GATE_TIMEOUT: - stale_path_requests.append(destination_hash) - RNS.log("Path request entry for "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None + with Transport.path_requests_lock: + try: + for destination_hash in Transport.path_requests: + if time.time() > Transport.path_requests[destination_hash] + Transport.PATH_REQUEST_GATE_TIMEOUT: + stale_path_requests.append(destination_hash) + RNS.log("Path request entry for "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - except Exception as e: - RNS.log(f"Could not complete stale path request enumeration in this job round, retrying later: {e}", RNS.LOG_WARNING) - - # Cull the pending in-flight path requests table - stale_inflight_path_requests = [] - try: - inflight_path_requests_snapshot = Transport.inflight_path_requests.copy() - for destination_hash in inflight_path_requests_snapshot: - if time.time() > inflight_path_requests_snapshot[destination_hash] + Transport.PATH_REQUEST_GATE_TIMEOUT: - stale_inflight_path_requests.append(destination_hash) - RNS.log("In-flight path request entry for "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - - except Exception as e: - RNS.log(f"Could not complete stale in-flight path request enumeration in this job round, retrying later: {e}", RNS.LOG_WARNING) + except Exception as e: + RNS.log(f"Could not complete stale path request enumeration in this job round, retrying later: {e}", RNS.LOG_WARNING) # Cull the pending discovery path requests table stale_discovery_path_requests = [] @@ -1104,16 +915,6 @@ class Transport: if i == 1: RNS.log("Removed "+str(i)+" path request entry", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None else: RNS.log("Removed "+str(i)+" path request entries", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - i = 0 - with Transport.inflight_path_requests_lock: - for destination_hash in stale_inflight_path_requests: - Transport.inflight_path_requests.pop(destination_hash) - i += 1 - - if i > 0: - if i == 1: RNS.log("Removed "+str(i)+" in-flight path request entry", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - else: RNS.log("Removed "+str(i)+" in-flight path request entries", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - i = 0 with Transport.discovery_pr_lock: for destination_hash in stale_discovery_path_requests: @@ -1217,27 +1018,23 @@ class Transport: except Exception as e: RNS.log("An exception occurred while running Transport jobs.", RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.trace_exception(e) # TODO: Remove - try: - if outgoing: - def job(): Transport.handle_outgoing_announces(outgoing) - threading.Thread(target=job).start() + if outgoing: + def job(): Transport.handle_outgoing_announces(outgoing) + threading.Thread(target=job).start() - if path_requests: - with Transport.discovery_pr_tx_lock: - queued_destinations = [entry[0] for entry in Transport.pending_discovery_prs] - for destination_hash in path_requests: - if destination_hash not in queued_destinations: - if not len(Transport.pending_discovery_prs) >= Transport.max_queued_discovery_prs: - Transport.pending_discovery_prs.append([destination_hash, path_requests[destination_hash]]) + if path_requests: + with Transport.discovery_pr_tx_lock: + for destination_hash in path_requests: + if not destination_hash in Transport.pending_discovery_prs: + if not len(Transport.pending_discovery_prs) >= Transport.max_queued_discovery_prs: + Transport.pending_discovery_prs.append([destination_hash, path_requests[destination_hash]]) - if len(Transport.pending_discovery_prs): - def job(): Transport.handle_disovery_path_requests() - threading.Thread(target=job).start() + if len(Transport.pending_discovery_prs): + def job(): Transport.handle_disovery_path_requests() + threading.Thread(target=job).start() - except Exception as e: - RNS.log(f"Error while finalizing transport jobs: {e}", RNS.LOG_ERROR) - RNS.trace_exception(e) discovery_pr_tx_throttle = 0.5 discovery_pr_tx_lock = Lock() @@ -1268,92 +1065,50 @@ class Transport: def handle_outgoing_announces(outgoing): for packet in sorted(outgoing, key=lambda p: p.hops): packet.send() - @staticmethod - def handle_outgoing_ifac(interface, raw): - # Calculate packet access code - ifac_size = interface.ifac_size - ifac = interface.ifac_identity.sign(raw)[-ifac_size:] - - # Generate mask - mask = RNS.Cryptography.hkdf(length=len(raw) + ifac_size, derive_from=ifac, - salt=interface.ifac_key, context=None) - - # Set IFAC flag and assemble new payload with IFAC - new_raw = bytes([raw[0] | 0x80, raw[1]]) + ifac + raw[2:] - - # Mask header bytes and payload with a single bignum XOR, leaving - # the IFAC itself untouched, then ensure the IFAC flag is set. - n = len(new_raw) - x = int.from_bytes(new_raw, "big") ^ int.from_bytes(mask, "big") - if ifac_size: x ^= int.from_bytes(mask[2:2 + ifac_size], "big") << (8 * (n - 2 - ifac_size)) - x |= 1 << (8 * n - 1) - return x.to_bytes(n, "big") - - @staticmethod - def handle_outgoing_ifac_legacy(interface, raw): - # Calculate packet access code - ifac = interface.ifac_identity.sign(raw)[-interface.ifac_size:] - - # Generate mask - mask = RNS.Cryptography.hkdf(length=len(raw)+interface.ifac_size, derive_from=ifac, - salt=interface.ifac_key, context=None) - # Set IFAC flag - new_header = bytes([raw[0] | 0x80, raw[1]]) - - # Assemble new payload with IFAC - new_raw = new_header+ifac+raw[2:] - - # Mask payload - i = 0; masked_raw = b"" - for byte in new_raw: - # Mask first header byte, but make sure the - # IFAC flag is still set - if i == 0: masked_raw += bytes([byte ^ mask[i] | 0x80]) - - # Mask second header byte and payload - elif i == 1 or i > interface.ifac_size+1: masked_raw += bytes([byte ^ mask[i]]) - - # Don't mask the IFAC itself - else: masked_raw += bytes([byte]) - - i += 1 - - return masked_raw - @staticmethod def transmit(interface, raw): try: if hasattr(interface, "ifac_identity") and interface.ifac_identity != None: - interface.process_outgoing(Transport.handle_outgoing_ifac(interface, raw)) + # Calculate packet access code + ifac = interface.ifac_identity.sign(raw)[-interface.ifac_size:] + + # Generate mask + mask = RNS.Cryptography.hkdf(length=len(raw)+interface.ifac_size, + derive_from=ifac, + salt=interface.ifac_key, + context=None) + # Set IFAC flag + new_header = bytes([raw[0] | 0x80, raw[1]]) + + # Assemble new payload with IFAC + new_raw = new_header+ifac+raw[2:] + + # Mask payload + i = 0; masked_raw = b"" + for byte in new_raw: + # Mask first header byte, but make sure the + # IFAC flag is still set + if i == 0: masked_raw += bytes([byte ^ mask[i] | 0x80]) + + # Mask second header byte and payload + elif i == 1 or i > interface.ifac_size+1: masked_raw += bytes([byte ^ mask[i]]) + + # Don't mask the IFAC itself + else: masked_raw += bytes([byte]) + + i += 1 + + # Send it + interface.process_outgoing(masked_raw) + else: interface.process_outgoing(raw) - Transport.tx_packets += 1 except Exception as e: RNS.log("Error while transmitting on "+str(interface)+". The contained exception was: "+str(e), RNS.LOG_ERROR) @staticmethod def outbound(packet): - if not Transport.USE_OUTBOUND_QUEUE: return Transport._outbound(packet) - else: - raise NotImplementedError("Outbound queue-based processing not available") - # try: Transport.outbound_queue.put(packet, block=False) - # except Full: RNS.log(f"Dropping outbound packet, TX queue is full", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - # except Exception as e: RNS.log(f"Error while inserting into outbound queue: {e}", RNS.LOG_ERROR) - # return False - - @staticmethod - def outbound_job(): - raise NotImplementedError("Outbound queue-based processing not available") - while Transport._should_run: - try: Transport._outbound(Transport.outbound_queue.get()) - except Exception as e: - RNS.log(f"Error while draining outbound queue: {e}", RNS.LOG_ERROR) - RNS.trace_exception(e) - - @staticmethod - def _outbound(packet): sent = False outbound_time = time.time() - if packet.hops > Transport.PATHFINDER_M-1: return False generate_receipt = False if (packet.create_receipt == True and @@ -1381,10 +1136,11 @@ class Transport: # Check if we have a known path for the destination in the path table if packet.packet_type != RNS.Packet.ANNOUNCE and packet.destination.type != RNS.Destination.PLAIN and packet.destination.type != RNS.Destination.GROUP and packet.destination_hash in Transport.path_table: - path_entry = Transport.path_table.get(packet.destination_hash) - if not path_entry: - RNS.log(f"Dropped packet since path table entry disappeared during outbound processing", RNS.LOG_WARNING) - return False + with Transport.path_table_lock: + if not packet.destination_hash in Transport.path_table: + RNS.log(f"Dropped packet since path table entry disappeared during outbound processing", RNS.LOG_WARNING) + return False + else: path_entry = Transport.path_table[packet.destination_hash] outbound_interface = path_entry[IDX_PT_RVCD_IF] @@ -1442,11 +1198,8 @@ class Transport: # interface, or belongs to a link. else: stored_hash = False - if packet.attached_interface: potential_interfaces = [packet.attached_interface] - else: potential_interfaces = Transport.interfaces - - for interface in potential_interfaces: - if interface.OUT and interface.online: + for interface in Transport.interfaces: + if interface.OUT: should_transmit = True if packet.destination.type == RNS.Destination.LINK: @@ -1521,7 +1274,7 @@ class Transport: # TODO: Rethink whether this is actually optimal. if packet.hops > 0: - if not hasattr(interface, "announce_cap"): interface.announce_cap = RNS.Reticulum.ANNOUNCE_CAP/100.0 + if not hasattr(interface, "announce_cap"): interface.announce_cap = RNS.Reticulum.ANNOUNCE_CAP if not hasattr(interface, "announce_allowed_at"): interface.announce_allowed_at = 0 if not hasattr(interface, "announce_queue"): interface.announce_queue = [] @@ -1541,17 +1294,16 @@ class Transport: if e["destination"] == packet.destination_hash: already_queued = True existing_entry = e - break emission_timestamp = Transport.announce_emitted(packet) if already_queued: should_queue = False if emission_timestamp > existing_entry["emitted"]: - existing_entry["time"] = outbound_time - existing_entry["hops"] = packet.hops - existing_entry["emitted"] = emission_timestamp - existing_entry["raw"] = packet.raw + e["time"] = outbound_time + e["hops"] = packet.hops + e["emitted"] = emission_timestamp + e["raw"] = packet.raw if should_queue: entry = { "destination": packet.destination_hash, @@ -1568,19 +1320,20 @@ class Transport: timer = threading.Timer(wait_time, interface.process_announce_queue) timer.start() - if RNS.sl(RNS.LOG_EXTREME): - if wait_time < 1: wait_time_str = str(round(wait_time*1000,2))+"ms" - else: wait_time_str = str(round(wait_time*1,2))+"s" - ql_str = str(len(interface.announce_queue)) - RNS.log("Added announce to queue (height "+ql_str+") on "+str(interface)+" for processing in "+wait_time_str, RNS.LOG_EXTREME) + if wait_time < 1: wait_time_str = str(round(wait_time*1000,2))+"ms" + else: wait_time_str = str(round(wait_time*1,2))+"s" + + ql_str = str(len(interface.announce_queue)) + RNS.log("Added announce to queue (height "+ql_str+") on "+str(interface)+" for processing in "+wait_time_str, RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None else: - if RNS.sl(RNS.LOG_EXTREME): - wait_time = max(interface.announce_allowed_at - time.time(), 0) - if wait_time < 1: wait_time_str = str(round(wait_time*1000,2))+"ms" - else: wait_time_str = str(round(wait_time*1,2))+"s" - ql_str = str(len(interface.announce_queue)) - RNS.log("Added announce to queue (height "+ql_str+") on "+str(interface)+" for processing in "+wait_time_str, RNS.LOG_EXTREME) + wait_time = max(interface.announce_allowed_at - time.time(), 0) + + if wait_time < 1: wait_time_str = str(round(wait_time*1000,2))+"ms" + else: wait_time_str = str(round(wait_time*1,2))+"s" + + ql_str = str(len(interface.announce_queue)) + RNS.log("Added announce to queue (height "+ql_str+") on "+str(interface)+" for processing in "+wait_time_str, RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None else: pass @@ -1597,8 +1350,8 @@ class Transport: else: raw = Transport.mangle_hops(packet.raw, Transport.local_hops_delta, transport_insert=packet.header_type==RNS.Packet.HEADER_1) Transport.transmit(interface, raw) - if packet.packet_type == RNS.Packet.ANNOUNCE: interface.sent_announce(size=len(raw)) - if packet.destination.type == RNS.Destination.PLAIN and packet.is_outbound_pr: interface.sent_path_request(size=len(raw)) + if packet.packet_type == RNS.Packet.ANNOUNCE: interface.sent_announce() + if packet.destination.type == RNS.Destination.PLAIN and packet.is_outbound_pr: interface.sent_path_request() packet_sent(packet) sent = True @@ -1643,26 +1396,22 @@ class Transport: if packet.packet_type != RNS.Packet.ANNOUNCE: if packet.hops > 1: RNS.log("Dropped PLAIN packet "+RNS.prettyhexrep(packet.packet_hash)+" with "+str(packet.hops)+" hops", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - if packet.receiving_interface: packet.receiving_interface.protocol_violation(f"Transported PLAIN packet with {str(packet.hops)} hops") return False else: return True else: RNS.log("Dropped invalid PLAIN announce packet", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - if packet.receiving_interface: packet.receiving_interface.protocol_violation("Announce packet with PLAIN type") return False if packet.destination_type == RNS.Destination.GROUP: if packet.packet_type != RNS.Packet.ANNOUNCE: if packet.hops > 1: RNS.log("Dropped GROUP packet "+RNS.prettyhexrep(packet.packet_hash)+" with "+str(packet.hops)+" hops", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - if packet.receiving_interface: packet.receiving_interface.protocol_violation(f"Transported GROUP packet with {str(packet.hops)} hops") return False else: return True else: RNS.log("Dropped invalid GROUP announce packet", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - if packet.receiving_interface: packet.receiving_interface.protocol_violation("Announce packet with GROUP type") return False if not packet.packet_hash in Transport.packet_hashlist and not packet.packet_hash in Transport.packet_hashlist_prev: return True @@ -1672,85 +1421,13 @@ class Transport: return True else: RNS.log("Dropped invalid announce packet", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - if packet.receiving_interface: packet.receiving_interface.protocol_violation("Announce packet with invalid destination type") return False RNS.log("Filtered packet with hash "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None return False @staticmethod - def inbound(raw, interface=None, tc=None, ifac_handled=False): - try: Transport.preprocess_inbound(raw=raw, interface=interface, tc=tc, ifac_handled=ifac_handled) - except Exception as e: - RNS.log(f"Error while processing inbound on {interface}: {e}", RNS.LOG_ERROR) - RNS.trace_exception(e) - - @staticmethod - def handle_ifac(raw, interface): - # Extract IFAC - ifac_size = interface.ifac_size - ifac = raw[2:2 + ifac_size] - - # Generate mask - mask = RNS.Cryptography.hkdf(length=len(raw), derive_from=ifac, - salt=interface.ifac_key, context=None) - - # Unmask header bytes and payload with a single bignum XOR, - # leaving the IFAC tag itself untouched, then drop the IFAC - # flag and strip to actual frame payload. - n = len(raw) - x = int.from_bytes(raw, "big") ^ int.from_bytes(mask, "big") - if ifac_size: x ^= int.from_bytes(mask[2:2 + ifac_size], "big") << (8 * (n - 2 - ifac_size)) - unmasked = x.to_bytes(n, "big") - - # Unset IFAC flag and re-assemble packet - new_raw = bytes([unmasked[0] & 0x7f, unmasked[1]]) + unmasked[2 + ifac_size:] - - # Calculate expected IFAC - expected_ifac = interface.ifac_identity.sign(new_raw)[-ifac_size:] - - # Check it - if ifac == expected_ifac: return True, new_raw - else: - interface.ifac_violation("Invalid IFAC on packet") - return False, None - - @staticmethod - def handle_ifac_legacy(raw, interface): - # Extract IFAC - ifac = raw[2:2+interface.ifac_size] - - # Generate mask - mask = RNS.Cryptography.hkdf(length=len(raw), derive_from=ifac, - salt=interface.ifac_key, context=None) - # Unmask payload - i = 0; unmasked_raw = b"" - for byte in raw: - # Unmask header bytes and payload - if i <= 1 or i > interface.ifac_size+1: unmasked_raw += bytes([byte ^ mask[i]]) - # Don't unmask IFAC itself - else: unmasked_raw += bytes([byte]) - i += 1 - - raw = unmasked_raw - - # Unset IFAC flag - new_header = bytes([raw[0] & 0x7f, raw[1]]) - - # Re-assemble packet - new_raw = new_header+raw[2+interface.ifac_size:] - - # Calculate expected IFAC - expected_ifac = interface.ifac_identity.sign(new_raw)[-interface.ifac_size:] - - # Check it - if ifac == expected_ifac: return True, new_raw - else: - interface.ifac_violation("Invalid IFAC on packet") - return False, None - - @staticmethod - def preprocess_inbound(raw, interface=None, tc=None, ifac_handled=False): + def inbound(raw, interface=None): if not Transport.ready: wait_start = time.time() while not Transport.ready: @@ -1763,681 +1440,597 @@ class Transport: # we must authenticate each packet. if len(raw) > 2: if interface != None and hasattr(interface, "ifac_identity") and interface.ifac_identity != None: - if not ifac_handled: - # Check that IFAC flag is set - if raw[0] & 0x80 == 0x80: - if len(raw) > 2+interface.ifac_size: - ifac_valid, raw = Transport.handle_ifac(raw, interface) - if not ifac_valid: return + # Check that IFAC flag is set + if raw[0] & 0x80 == 0x80: + if len(raw) > 2+interface.ifac_size: + # Extract IFAC + ifac = raw[2:2+interface.ifac_size] - else: return interface.ifac_violation("Insufficient packet size for IFAC packet") + # Generate mask + mask = RNS.Cryptography.hkdf(length=len(raw), derive_from=ifac, + salt=interface.ifac_key, context=None) + # Unmask payload + i = 0; unmasked_raw = b"" + for byte in raw: + # Unmask header bytes and payload + if i <= 1 or i > interface.ifac_size+1: unmasked_raw += bytes([byte ^ mask[i]]) + # Don't unmask IFAC itself + else: unmasked_raw += bytes([byte]) + i += 1 + + raw = unmasked_raw - # If the IFAC flag is not set, but should be, - # drop the packet. - else: return interface.ifac_violation("Missing IFAC flag on packet for IFAC-enabled interface") + # Unset IFAC flag + new_header = bytes([raw[0] & 0x7f, raw[1]]) + + # Re-assemble packet + new_raw = new_header+raw[2+interface.ifac_size:] + + # Calculate expected IFAC + expected_ifac = interface.ifac_identity.sign(new_raw)[-interface.ifac_size:] + + # Check it + if ifac == expected_ifac: raw = new_raw + else: return + + else: return + + # If the IFAC flag is not set, but should be, + # drop the packet. + else: return else: # If the interface does not have IFAC enabled, # check the received packet IFAC flag. # If the flag is set, drop the packet - if raw[0] & 0x80 == 0x80: - return interface.ifac_violation("IFAC flag set on packet for interface without IFAC enabled") if interface else None + if raw[0] & 0x80 == 0x80: return - else: return interface.protocol_violation("Insufficient packet size for IFAC processing") if interface else None + else: return if Transport.identity == None: return - - if interface and len(raw) > interface.HW_MTU + (interface.ifac_size or 0): - return interface.protocol_violation(f"Frame size exceeded MTU of {interface.HW_MTU} on {interface}") - + packet = RNS.Packet(None, raw) - if not packet.unpack(): return interface.protocol_violation(f"Malformed packet ({len(raw)} bytes)") if interface else None - if not Transport.packet_filter(packet): return interface.packet_filter_hit() - traffic_class = tc or Transport.TC_DATA - - Transport.rx_packets += 1 + if not packet.unpack(): return + packet.receiving_interface = interface packet.hops += 1 - # Ingress limit announces early - if packet.packet_type == RNS.Packet.ANNOUNCE: - if len(raw) > RNS.Reticulum.MTU: return packet.receiving_interface.protocol_violation(f"Excessive announce packet frame size of {len(raw)} bytes") if packet.receiving_interface else None - if traffic_class < Transport.TC_ANNOUNCE: traffic_class = Transport.TC_ANNOUNCE - announce_signature_valid = RNS.Identity.validate_announce(packet, only_validate_signature=True, signal_blackholed=True) - if announce_signature_valid == "blackholed": return None - elif not announce_signature_valid: - return packet.receiving_interface.protocol_violation(f"Invalid announce signature for {RNS.prettyhexrep(packet.destination_hash)}") if packet.receiving_interface else None - - elif packet.receiving_interface != None: packet.receiving_interface.received_announce(size=len(packet.raw)) - announced_destination_known = packet.destination_hash in Transport.path_table - - if not announced_destination_known: - # This is an unknown destination, and we'll apply - # potential ingress limiting. Already known - # destinations will have re-announces controlled - # by normal announce rate limiting. - if packet.destination_hash in Transport.path_requests or packet.destination_hash in Transport.discovery_path_requests: - # RNS.log(f"Skipping ingress limit check for {RNS.prettyhexrep(packet.destination_hash)} due to waiting path requests", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - pass - - elif packet.receiving_interface.should_ingress_limit(): - packet.receiving_interface.hold_announce(packet) - return - - # Ingress limit path requests early - elif packet.destination_hash == Transport.pr_destination_hash: - if traffic_class < Transport.TC_PATH_REQUEST: traffic_class = Transport.TC_PATH_REQUEST - if not len(packet.data) >= RNS.Identity.TRUNCATED_HASHLENGTH//8: return - else: - tag_bytes = None - tag_valid = False - destination_hash = packet.data[:RNS.Identity.TRUNCATED_HASHLENGTH//8] - if len(packet.data) > (RNS.Identity.TRUNCATED_HASHLENGTH//8)*2: tag_bytes = packet.data[RNS.Identity.TRUNCATED_HASHLENGTH//8*2:] - elif len(packet.data) > (RNS.Identity.TRUNCATED_HASHLENGTH//8): tag_bytes = packet.data[RNS.Identity.TRUNCATED_HASHLENGTH//8:] - - if tag_bytes == None: - RNS.log("Ignoring tagless path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - return packet.receiving_interface.protocol_violation("Tagless path request") if packet.receiving_interface else None - - else: - if len(tag_bytes) > RNS.Identity.TRUNCATED_HASHLENGTH//8: - tag_bytes = tag_bytes[:RNS.Identity.TRUNCATED_HASHLENGTH//8] - if packet.receiving_interface: packet.receiving_interface.protocol_violation("Excessive path request tag size") - - unique_tag = destination_hash+tag_bytes - - with Transport.discovery_pr_tags_lock: - if not unique_tag in Transport.discovery_pr_tags and not unique_tag in Transport.discovery_pr_tags_prev: - Transport.discovery_pr_tags.add(unique_tag) - tag_valid = True - - if not tag_valid: - RNS.log("Ignoring duplicate path request for "+RNS.prettyhexrep(destination_hash)+" with tag "+RNS.prettyhexrep(unique_tag), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - return - - if packet.receiving_interface: packet.receiving_interface.received_path_request(size=len(raw)) - if interface.should_ingress_limit_pr(): - traffic_class = Transport.TC_INGRESS_LIMITED - - path_request_inflight = False - with Transport.inflight_path_requests_lock: - if destination_hash in Transport.inflight_path_requests: path_request_inflight = True - - if not path_request_inflight: - # Early registration to immediately batch duplicates - with Transport.inflight_path_requests_lock: Transport.inflight_path_requests[destination_hash] = time.time() - else: - if not traffic_class == Transport.TC_INGRESS_LIMITED: - RNS.log(f"Path request on {packet.receiving_interface} for {RNS.prettyhexrep(destination_hash)} already in-flight, batching to existing", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - with Transport.discovery_pr_lock: - if destination_hash in Transport.discovery_path_requests: - if not packet.receiving_interface in Transport.discovery_path_requests[destination_hash]["requesting_interfaces"]: - Transport.discovery_path_requests[destination_hash]["requesting_interfaces"].append(packet.receiving_interface) - - else: - discovery_timeout = max(Transport.PATH_REQUEST_TIMEOUT, Transport.medium_path_timeout()) - pr_entry = { "destination_hash": destination_hash, "timeout": time.time()+discovery_timeout, - "requesting_interfaces": [packet.receiving_interface], "engaged": False } - - Transport.discovery_path_requests[destination_hash] = pr_entry - - return - - # TODO: Add early filtering here for non-transport nodes; there's no - # reason to process further if we know we don't have the destination - # locally on this system. Might need adding a local_client_dest_map, - # but this will need to be kept *tightly* in-sync. - - if not Transport.USE_INBOUND_QUEUE: return Transport._inbound(packet) - else: - if traffic_class == Transport.TC_INGRESS_LIMITED: packet.traffic_class = Transport.TC_INGRESS_LIMITED - try: Transport.inbound_queues.put(traffic_class, packet, block=False) - except Full: RNS.log(f"Dropping inbound packet, queue is full (tc={traffic_class})", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - except Exception as e: RNS.log(f"Error while inserting into inbound queue: {e}", RNS.LOG_ERROR) - - @staticmethod - def inbound_job(): - while Transport._should_run: - try: packet = Transport.inbound_queues.get() - except Exception as e: - RNS.log(f"Error while draining inbound queue: {e}", RNS.LOG_ERROR) - RNS.trace_exception(e) - continue - - try: Transport._inbound(packet) - except Exception as e: - packet.receiving_interface.protocol_violation("Processing caused exception") if packet.receiving_interface else None - RNS.log(f"Error while processing inbound packet: {e}", RNS.LOG_ERROR) - RNS.trace_exception(e) - - @staticmethod - def _inbound(packet): - if packet.receiving_interface != None: - if not packet.receiving_interface.online: return - - if packet.receiving_interface.reports_phy_stats: - if packet.receiving_interface.r_stat_rssi != None: - packet.rssi = packet.receiving_interface.r_stat_rssi + if interface != None: + if hasattr(interface, "r_stat_rssi"): + if interface.r_stat_rssi != None: + packet.rssi = interface.r_stat_rssi Transport.local_client_rssi_cache.append([packet.packet_hash, packet.rssi]) while len(Transport.local_client_rssi_cache) > Transport.LOCAL_CLIENT_CACHE_MAXSIZE: Transport.local_client_rssi_cache.pop(0) - if packet.receiving_interface.r_stat_snr != None: - packet.snr = packet.receiving_interface.r_stat_snr + if hasattr(interface, "r_stat_snr"): + if interface.r_stat_rssi != None: + packet.snr = interface.r_stat_snr Transport.local_client_snr_cache.append([packet.packet_hash, packet.snr]) while len(Transport.local_client_snr_cache) > Transport.LOCAL_CLIENT_CACHE_MAXSIZE: Transport.local_client_snr_cache.pop(0) - if packet.receiving_interface.r_stat_q != None: - packet.q = packet.receiving_interface.r_stat_q + if hasattr(interface, "r_stat_q"): + if interface.r_stat_q != None: + packet.q = interface.r_stat_q Transport.local_client_q_cache.append([packet.packet_hash, packet.q]) while len(Transport.local_client_q_cache) > Transport.LOCAL_CLIENT_CACHE_MAXSIZE: Transport.local_client_q_cache.pop(0) if len(Transport.local_client_interfaces) > 0: - if Transport.is_local_client_interface(packet.receiving_interface): packet.hops -= 1 + if Transport.is_local_client_interface(interface): packet.hops -= 1 - elif Transport.interface_to_shared_instance(packet.receiving_interface): packet.hops -= 1 + elif Transport.interface_to_shared_instance(interface): packet.hops -= 1 - # By default, remember packet hashes to avoid routing - # loops in the network, using the packet filter. - remember_packet_hash = True + if Transport.packet_filter(packet): + # By default, remember packet hashes to avoid routing + # loops in the network, using the packet filter. + remember_packet_hash = True - # If this packet belongs to a link in our link table, - # we'll have to defer adding it to the filter list. - # In some cases, we might see a packet over a shared- - # medium interface, belonging to a link that transports - # or terminates with this instance, but before it would - # normally reach us. If the packet is appended to the - # filter list at this point, link transport will break. - if packet.destination_hash in Transport.link_table: remember_packet_hash = False + # If this packet belongs to a link in our link table, + # we'll have to defer adding it to the filter list. + # In some cases, we might see a packet over a shared- + # medium interface, belonging to a link that transports + # or terminates with this instance, but before it would + # normally reach us. If the packet is appended to the + # filter list at this point, link transport will break. + if packet.destination_hash in Transport.link_table: remember_packet_hash = False - # If this is a link request proof, don't add it until - # we are sure it's not actually somewhere else in the - # routing chain. - if packet.packet_type == RNS.Packet.PROOF and packet.context == RNS.Packet.LRPROOF: remember_packet_hash = False + # If this is a link request proof, don't add it until + # we are sure it's not actually somewhere else in the + # routing chain. + if packet.packet_type == RNS.Packet.PROOF and packet.context == RNS.Packet.LRPROOF: remember_packet_hash = False - if remember_packet_hash: - Transport.add_packet_hash(packet.packet_hash) - # TODO: Enable when caching has been redesigned - # Transport.cache(packet) - - # Check special conditions for local clients connected - # through a shared Reticulum instance - from_local_client = (packet.receiving_interface in Transport.local_client_interfaces) - for_local_client = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.path_table and Transport.path_table[packet.destination_hash][IDX_PT_HOPS] == 0) - local_client_nh = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.link_table and Transport.link_table[packet.destination_hash][IDX_LT_NH_IF] in Transport.local_client_interfaces) - local_client_rh = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.link_table and Transport.link_table[packet.destination_hash][IDX_LT_RCVD_IF] in Transport.local_client_interfaces) - proof_for_local_client = (packet.destination_hash in Transport.reverse_table) and (Transport.reverse_table[packet.destination_hash][IDX_RT_RCVD_IF] in Transport.local_client_interfaces) - to_local_client = for_local_client or proof_for_local_client - instance_local_link = local_client_nh and local_client_rh - for_local_client_link = local_client_nh or local_client_rh - link_request_handled = False + if remember_packet_hash: + Transport.add_packet_hash(packet.packet_hash) + # TODO: Enable when caching has been redesigned + # Transport.cache(packet) + + # Check special conditions for local clients connected + # through a shared Reticulum instance + from_local_client = (packet.receiving_interface in Transport.local_client_interfaces) + for_local_client = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.path_table and Transport.path_table[packet.destination_hash][IDX_PT_HOPS] == 0) + local_client_nh = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.link_table and Transport.link_table[packet.destination_hash][IDX_LT_NH_IF] in Transport.local_client_interfaces) + local_client_rh = (packet.packet_type != RNS.Packet.ANNOUNCE) and (packet.destination_hash in Transport.link_table and Transport.link_table[packet.destination_hash][IDX_LT_RCVD_IF] in Transport.local_client_interfaces) + proof_for_local_client = (packet.destination_hash in Transport.reverse_table) and (Transport.reverse_table[packet.destination_hash][IDX_RT_RCVD_IF] in Transport.local_client_interfaces) + to_local_client = for_local_client or proof_for_local_client + instance_local_link = local_client_nh and local_client_rh + for_local_client_link = local_client_nh or local_client_rh + link_request_handled = False - # Plain broadcast packets from local clients are sent - # directly on all attached interfaces, since they are - # never injected into transport. - if not packet.destination_hash in Transport.control_hashes: - if packet.destination_type == RNS.Destination.PLAIN and packet.transport_type == Transport.BROADCAST: - # Send to all interfaces except the originator - if from_local_client: - for interface in Transport.interfaces: - if interface != packet.receiving_interface: + # Plain broadcast packets from local clients are sent + # directly on all attached interfaces, since they are + # never injected into transport. + if not packet.destination_hash in Transport.control_hashes: + if packet.destination_type == RNS.Destination.PLAIN and packet.transport_type == Transport.BROADCAST: + # Send to all interfaces except the originator + if from_local_client: + for interface in Transport.interfaces: + if interface != packet.receiving_interface: + Transport.transmit(interface, packet.raw) + # If the packet was not from a local client, send + # it directly to all local clients + else: + for interface in Transport.local_client_interfaces: Transport.transmit(interface, packet.raw) - # If the packet was not from a local client, send - # it directly to all local clients - else: - for interface in Transport.local_client_interfaces: - Transport.transmit(interface, packet.raw) - # General transport handling. Takes care of directing - # packets according to transport tables and recording - # entries in reverse and link tables. - if RNS.Reticulum.transport_enabled() or from_local_client or for_local_client or for_local_client_link: + # General transport handling. Takes care of directing + # packets according to transport tables and recording + # entries in reverse and link tables. + if RNS.Reticulum.transport_enabled() or from_local_client or for_local_client or for_local_client_link: - # If there is no transport id, but the packet is - # for a local client, we generate the transport - # id (it was stripped on the previous hop, since - # we "spoof" the hop count for clients behind a - # shared instance, so they look directly reach- - # able), and reinsert, so the normal transport - # implementation can handle the packet. - if packet.transport_id == None and for_local_client: - packet.transport_id = Transport.identity.hash + # If there is no transport id, but the packet is + # for a local client, we generate the transport + # id (it was stripped on the previous hop, since + # we "spoof" the hop count for clients behind a + # shared instance, so they look directly reach- + # able), and reinsert, so the normal transport + # implementation can handle the packet. + if packet.transport_id == None and for_local_client: + packet.transport_id = Transport.identity.hash - # If this is a cache request, and we can fullfill - # it, do so and stop processing. Otherwise resume - # normal processing. - if packet.context == RNS.Packet.CACHE_REQUEST: - if Transport.cache_request_packet(packet): return + # If this is a cache request, and we can fullfill + # it, do so and stop processing. Otherwise resume + # normal processing. + if packet.context == RNS.Packet.CACHE_REQUEST: + if Transport.cache_request_packet(packet): return - # If the packet is in transport, check whether we - # are the designated next hop, and process it - # accordingly if we are. - if packet.transport_id != None and packet.packet_type != RNS.Packet.ANNOUNCE: - if not packet.transport_id == Transport.identity.hash: return - else: - path_entry = Transport.path_table.get(packet.destination_hash) - if path_entry != None: - next_hop = path_entry[IDX_PT_NEXT_HOP] - remaining_hops = path_entry[IDX_PT_HOPS] - - if remaining_hops > 1: - # Just increase hop count and transmit - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops) - new_raw += next_hop - new_raw += packet.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:] - elif remaining_hops == 1: - # Strip transport headers and transmit - new_flags = (RNS.Packet.HEADER_1) << 6 | (Transport.BROADCAST) << 4 | (packet.flags & 0b00001111) - new_raw = struct.pack("!B", new_flags) - new_raw += struct.pack("!B", packet.hops) - new_raw += packet.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:] - elif remaining_hops == 0: - if to_local_client and RNS.Transport.local_hops_delta != 0: - if packet.header_type == RNS.Packet.HEADER_2: - # Strip transport headers and transmit - new_flags = (RNS.Packet.HEADER_1) << 6 | (Transport.BROADCAST) << 4 | (packet.flags & 0b00001111) - new_raw = struct.pack("!B", new_flags) - new_raw += struct.pack("!B", packet.hops) - new_raw += packet.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:] + # If the packet is in transport, check whether we + # are the designated next hop, and process it + # accordingly if we are. + if packet.transport_id != None and packet.packet_type != RNS.Packet.ANNOUNCE: + if packet.transport_id == Transport.identity.hash: + if packet.destination_hash in Transport.path_table: + next_hop = Transport.path_table[packet.destination_hash][IDX_PT_NEXT_HOP] + remaining_hops = Transport.path_table[packet.destination_hash][IDX_PT_HOPS] + + if remaining_hops > 1: + # Just increase hop count and transmit + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops) + new_raw += next_hop + new_raw += packet.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:] + elif remaining_hops == 1: + # Strip transport headers and transmit + new_flags = (RNS.Packet.HEADER_1) << 6 | (Transport.BROADCAST) << 4 | (packet.flags & 0b00001111) + new_raw = struct.pack("!B", new_flags) + new_raw += struct.pack("!B", packet.hops) + new_raw += packet.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:] + elif remaining_hops == 0: + if to_local_client and RNS.Transport.local_hops_delta != 0: + if packet.header_type == RNS.Packet.HEADER_2: + # Strip transport headers and transmit + new_flags = (RNS.Packet.HEADER_1) << 6 | (Transport.BROADCAST) << 4 | (packet.flags & 0b00001111) + new_raw = struct.pack("!B", new_flags) + new_raw += struct.pack("!B", packet.hops) + new_raw += packet.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:] + else: + # Just increase hop count and transmit + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops) + new_raw += packet.raw[2:] else: # Just increase hop count and transmit new_raw = packet.raw[0:1] new_raw += struct.pack("!B", packet.hops) new_raw += packet.raw[2:] + + outbound_interface = Transport.path_table[packet.destination_hash][IDX_PT_RVCD_IF] + + if packet.packet_type == RNS.Packet.LINKREQUEST: + now = time.time() + proof_timeout = Transport.extra_link_proof_timeout(packet.receiving_interface) + proof_timeout += now + RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, remaining_hops) + + path_mtu = RNS.Link.mtu_from_lr_packet(packet) + mode = RNS.Link.mode_from_lr_packet(packet) + ph_mtu = interface.HW_MTU if interface else None + nh_mtu = outbound_interface.HW_MTU + if path_mtu: + if outbound_interface.HW_MTU == None: + RNS.log(f"No next-hop HW MTU, disabling link MTU upgrade", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + path_mtu = None + new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE] + elif not outbound_interface.AUTOCONFIGURE_MTU and not outbound_interface.FIXED_MTU: + RNS.log(f"Outbound interface doesn't support MTU autoconfiguration, disabling link MTU upgrade", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + path_mtu = None + new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE] + else: + if nh_mtu < path_mtu or (ph_mtu and ph_mtu < path_mtu): + try: + path_mtu = min(nh_mtu, ph_mtu) + clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode) + RNS.log(f"Clamping link MTU to {RNS.prettysize(path_mtu)}", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu + except Exception as e: + RNS.log(f"Dropping link request packet. The contained exception was: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None + return + + # Entry format is + link_entry = [ now, # 0: Timestamp, + next_hop, # 1: Next-hop transport ID + outbound_interface, # 2: Next-hop interface + remaining_hops, # 3: Remaining hops + packet.receiving_interface, # 4: Received on interface + packet.hops, # 5: Taken hops + packet.destination_hash, # 6: Original destination hash + False, # 7: Validated + proof_timeout ] # 8: Proof timeout timestamp + + with Transport.link_table_lock: Transport.link_table[RNS.Link.link_id_from_lr_packet(packet)] = link_entry + link_request_handled = True + else: - # Just increase hop count and transmit - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops) - new_raw += packet.raw[2:] + # Entry format is + reverse_entry = [ packet.receiving_interface, # 0: Received on interface + outbound_interface, # 1: Outbound interface + time.time() ] # 2: Timestamp - outbound_interface = path_entry[IDX_PT_RVCD_IF] + with Transport.reverse_table_lock: Transport.reverse_table[packet.getTruncatedHash()] = reverse_entry - if packet.packet_type == RNS.Packet.LINKREQUEST: - now = time.time() - proof_timeout = Transport.extra_link_proof_timeout(outbound_interface) - proof_timeout += now + RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, remaining_hops) - - path_mtu = RNS.Link.mtu_from_lr_packet(packet) - mode = RNS.Link.mode_from_lr_packet(packet) - ph_mtu = packet.receiving_interface.HW_MTU if packet.receiving_interface else None - nh_mtu = outbound_interface.HW_MTU - if path_mtu: - if outbound_interface.HW_MTU == None: - RNS.log(f"No next-hop HW MTU, disabling link MTU upgrade", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - path_mtu = None - new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE] - elif not outbound_interface.AUTOCONFIGURE_MTU and not outbound_interface.FIXED_MTU: - RNS.log(f"Outbound interface doesn't support MTU autoconfiguration, disabling link MTU upgrade", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - path_mtu = None - new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE] - else: - if nh_mtu < path_mtu or (ph_mtu and ph_mtu < path_mtu): - try: - path_mtu = min(nh_mtu, ph_mtu) - clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode) - RNS.log(f"Clamping link MTU to {RNS.prettysize(path_mtu)}", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu - except Exception as e: - RNS.log(f"Dropping link request packet. The contained exception was: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - if packet.receiving_interface: packet.receiving_interface.protocol_violation("Undecodable path MTU signalling bytes") - return - - # Entry format is - link_entry = [ now, # 0: Timestamp, - next_hop, # 1: Next-hop transport ID - outbound_interface, # 2: Next-hop interface - remaining_hops, # 3: Remaining hops - packet.receiving_interface, # 4: Received on interface - packet.hops, # 5: Taken hops - packet.destination_hash, # 6: Original destination hash - False, # 7: Validated - proof_timeout ] # 8: Proof timeout timestamp - - with Transport.link_table_lock: Transport.link_table[RNS.Link.link_id_from_lr_packet(packet)] = link_entry - link_request_handled = True + if Transport.local_hops_delta != 0 and from_local_client and not to_local_client: new_raw = Transport.mangle_hops(new_raw, Transport.local_hops_delta) + Transport.transmit(outbound_interface, new_raw) + with Transport.path_table_lock: Transport.path_table[packet.destination_hash][IDX_PT_TIMESTAMP] = time.time() else: - # Entry format is - reverse_entry = [ packet.receiving_interface, # 0: Received on interface - outbound_interface, # 1: Outbound interface - time.time() ] # 2: Timestamp + # TODO: There should probably be some kind of REJECT + # mechanism here, to signal to the source that their + # expected path failed. + RNS.log("Got packet in transport, but no known path to final destination "+RNS.prettyhexrep(packet.destination_hash)+". Dropping packet.", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - with Transport.reverse_table_lock: Transport.reverse_table[packet.truncated_packet_hash] = reverse_entry - - if Transport.local_hops_delta != 0 and from_local_client and not to_local_client: new_raw = Transport.mangle_hops(new_raw, Transport.local_hops_delta) - Transport.transmit(outbound_interface, new_raw) - path_entry[IDX_PT_TIMESTAMP] = time.time() - - else: - # TODO: There should probably be some kind of REJECT - # mechanism here, to signal to the source that their - # expected path failed. - RNS.log("Got packet in transport, but no known path to final destination "+RNS.prettyhexrep(packet.destination_hash)+". Dropping packet.", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - - # Link transport handling. Directs packets according - # to entries in the link tables - if packet.packet_type != RNS.Packet.ANNOUNCE and packet.packet_type != RNS.Packet.LINKREQUEST and packet.context != RNS.Packet.LRPROOF: - if packet.destination_hash in Transport.link_table: - link_entry = Transport.link_table[packet.destination_hash] - if not link_entry[IDX_LT_VALIDATED]: - RNS.log(f"Pre-validation link packet from {packet.receiving_interface}", RNS.LOG_WARNING) # TODO: Remove - return packet.receiving_interface.protocol_violation("Link packet received before link validation") if packet.receiving_interface else None - - # If receiving and outbound interface is - # the same for this link, direction doesn't - # matter, and we simply repeat the packet. - outbound_interface = None - if link_entry[IDX_LT_NH_IF] == link_entry[IDX_LT_RCVD_IF]: - # But check that taken hops matches one - # of the expectede values. - if packet.hops == link_entry[IDX_LT_REM_HOPS] or packet.hops == link_entry[IDX_LT_HOPS]: - outbound_interface = link_entry[IDX_LT_NH_IF] - else: - # If interfaces differ, we transmit on - # the opposite interface of what the - # packet was received on. - if packet.receiving_interface == link_entry[IDX_LT_NH_IF]: - # Also check that expected hop count matches - if packet.hops == link_entry[IDX_LT_REM_HOPS]: - outbound_interface = link_entry[IDX_LT_RCVD_IF] - elif packet.receiving_interface == link_entry[IDX_LT_RCVD_IF]: - # Also check that expected hop count matches - if packet.hops == link_entry[IDX_LT_HOPS]: + # Link transport handling. Directs packets according + # to entries in the link tables + if packet.packet_type != RNS.Packet.ANNOUNCE and packet.packet_type != RNS.Packet.LINKREQUEST and packet.context != RNS.Packet.LRPROOF: + if packet.destination_hash in Transport.link_table: + link_entry = Transport.link_table[packet.destination_hash] + # If receiving and outbound interface is + # the same for this link, direction doesn't + # matter, and we simply repeat the packet. + outbound_interface = None + if link_entry[IDX_LT_NH_IF] == link_entry[IDX_LT_RCVD_IF]: + # But check that taken hops matches one + # of the expectede values. + if packet.hops == link_entry[IDX_LT_REM_HOPS] or packet.hops == link_entry[IDX_LT_HOPS]: outbound_interface = link_entry[IDX_LT_NH_IF] + else: + # If interfaces differ, we transmit on + # the opposite interface of what the + # packet was received on. + if packet.receiving_interface == link_entry[IDX_LT_NH_IF]: + # Also check that expected hop count matches + if packet.hops == link_entry[IDX_LT_REM_HOPS]: + outbound_interface = link_entry[IDX_LT_RCVD_IF] + elif packet.receiving_interface == link_entry[IDX_LT_RCVD_IF]: + # Also check that expected hop count matches + if packet.hops == link_entry[IDX_LT_HOPS]: + outbound_interface = link_entry[IDX_LT_NH_IF] - if outbound_interface != None: - # Add this packet to the filter hashlist if we - # have determined that it's actually our turn - # to process it. - Transport.add_packet_hash(packet.packet_hash) + if outbound_interface != None: + # Add this packet to the filter hashlist if we + # have determined that it's actually our turn + # to process it. + Transport.add_packet_hash(packet.packet_hash) - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops if not from_local_client or instance_local_link or Transport.local_hops_delta == 0 else Transport.local_hops_delta) - new_raw += packet.raw[2:] - Transport.transmit(outbound_interface, new_raw) - Transport.link_table[packet.destination_hash][IDX_LT_TIMESTAMP] = time.time() - - else: RNS.log(f"No-outbound return on link packet from {packet.receiving_interface}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - - return - - # Announce handling. Handles logic related to incoming - # announces, queueing rebroadcasts of these, and removal - # of queued announce rebroadcasts once handed to the next node. - if packet.packet_type == RNS.Packet.ANNOUNCE: - announce_valid = RNS.Identity.validate_announce(packet) - if not announce_valid: return packet.receiving_interface.protocol_violation("Invalid announce") if packet.receiving_interface else None - - local_destination = Transport.destinations_map.get(packet.destination_hash) - if local_destination == None and announce_valid: - if packet.transport_id != None: - received_from = packet.transport_id - - # Check if this is a next retransmission from - # another node. If it is, we're removing the - # announce in question from our pending table - if RNS.Reticulum.transport_enabled() and packet.destination_hash in Transport.announce_table: - announce_entry = Transport.announce_table[packet.destination_hash] + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops if not from_local_client or instance_local_link or Transport.local_hops_delta == 0 else Transport.local_hops_delta) + new_raw += packet.raw[2:] + Transport.transmit(outbound_interface, new_raw) + Transport.link_table[packet.destination_hash][IDX_LT_TIMESTAMP] = time.time() - if packet.hops-1 == announce_entry[IDX_AT_HOPS]: - RNS.log(f"Heard a rebroadcast of announce for {RNS.prettyhexrep(packet.destination_hash)} on {packet.receiving_interface}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - announce_entry[IDX_AT_LCL_RBRD] += 1 - if announce_entry[IDX_AT_RETRIES] > 0: - if announce_entry[IDX_AT_LCL_RBRD] >= Transport.LOCAL_REBROADCASTS_MAX: - RNS.log("Completed announce processing for "+RNS.prettyhexrep(packet.destination_hash)+", local rebroadcast limit reached", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None + # TODO: Can we return safely here? Test and possibly enable this at some point. + # return + + + # Announce handling. Handles logic related to incoming + # announces, queueing rebroadcasts of these, and removal + # of queued announce rebroadcasts once handed to the next node. + if packet.packet_type == RNS.Packet.ANNOUNCE: + announce_signature_valid = RNS.Identity.validate_announce(packet, only_validate_signature=True) + if not announce_signature_valid: return + elif interface != None: interface.received_announce() + announced_destination_known = packet.destination_hash in Transport.path_table + + if not announced_destination_known: + # This is an unknown destination, and we'll apply + # potential ingress limiting. Already known + # destinations will have re-announces controlled + # by normal announce rate limiting. + if packet.destination_hash in Transport.path_requests or packet.destination_hash in Transport.discovery_path_requests: + # RNS.log(f"Skipping ingress limit check for {RNS.prettyhexrep(packet.destination_hash)} due to waiting path requests", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None + pass + + elif interface.should_ingress_limit(): + interface.hold_announce(packet) + return + + local_destination = None + with Transport.destinations_map_lock: + if packet.destination_hash in Transport.destinations_map: + local_destination = Transport.destinations_map[packet.destination_hash] + + if local_destination == None and RNS.Identity.validate_announce(packet): + if packet.transport_id != None: + received_from = packet.transport_id + + # Check if this is a next retransmission from + # another node. If it is, we're removing the + # announce in question from our pending table + if RNS.Reticulum.transport_enabled() and packet.destination_hash in Transport.announce_table: + announce_entry = Transport.announce_table[packet.destination_hash] + + if packet.hops-1 == announce_entry[IDX_AT_HOPS]: + RNS.log(f"Heard a rebroadcast of announce for {RNS.prettyhexrep(packet.destination_hash)} on {packet.receiving_interface}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None + announce_entry[IDX_AT_LCL_RBRD] += 1 + if announce_entry[IDX_AT_RETRIES] > 0: + if announce_entry[IDX_AT_LCL_RBRD] >= Transport.LOCAL_REBROADCASTS_MAX: + RNS.log("Completed announce processing for "+RNS.prettyhexrep(packet.destination_hash)+", local rebroadcast limit reached", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None + with Transport.announce_table_lock: + if packet.destination_hash in Transport.announce_table: Transport.announce_table.pop(packet.destination_hash) + + if packet.hops-1 == announce_entry[IDX_AT_HOPS]+1 and announce_entry[IDX_AT_RETRIES] > 0: + now = time.time() + if now < announce_entry[IDX_AT_RTRNS_TMO]: + RNS.log("Rebroadcasted announce for "+RNS.prettyhexrep(packet.destination_hash)+" has been passed on to another node, no further tries needed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None with Transport.announce_table_lock: if packet.destination_hash in Transport.announce_table: Transport.announce_table.pop(packet.destination_hash) - if packet.hops-1 == announce_entry[IDX_AT_HOPS]+1 and announce_entry[IDX_AT_RETRIES] > 0: - now = time.time() - if now < announce_entry[IDX_AT_RTRNS_TMO]: - RNS.log("Rebroadcasted announce for "+RNS.prettyhexrep(packet.destination_hash)+" has been passed on to another node, no further tries needed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - with Transport.announce_table_lock: - if packet.destination_hash in Transport.announce_table: Transport.announce_table.pop(packet.destination_hash) + else: received_from = packet.destination_hash - else: received_from = packet.destination_hash + # Check if this announce should be inserted into + # announce and destination tables + should_add = False - # Check if this announce should be inserted into - # announce and destination tables - should_add = False + # First, check that the announce is not for a destination + # local to this system, and that hops are less than the max + with Transport.destinations_map_lock: + local_and_hops_condition = (packet.hops < Transport.PATHFINDER_M+1) and (not packet.destination_hash in Transport.destinations_map) - # First, check that the announce is not for a destination - # local to this system, and that hops are less than the max - with Transport.destinations_map_lock: - local_and_hops_condition = (packet.hops < Transport.PATHFINDER_M+1) and (not packet.destination_hash in Transport.destinations_map) + if local_and_hops_condition: + announce_emitted = Transport.announce_emitted(packet) + + random_blob = packet.data[RNS.Identity.KEYSIZE//8+RNS.Identity.NAME_HASH_LENGTH//8:RNS.Identity.KEYSIZE//8+RNS.Identity.NAME_HASH_LENGTH//8+10] + random_blobs = [] + with Transport.inbound_announce_lock: + announced_destination_known |= packet.destination_hash in Transport.path_table + if not announced_destination_known: + # If this destination is unknown in our table + # we should add it + should_add = True - if local_and_hops_condition: - announce_emitted = Transport.announce_emitted(packet) - - random_blobs = [] - random_blob = packet.data[RNS.Identity.KEYSIZE//8+RNS.Identity.NAME_HASH_LENGTH//8:RNS.Identity.KEYSIZE//8+RNS.Identity.NAME_HASH_LENGTH//8+10] - if not random_blob: return packet.receiving_interface.protocol_violation("No random blob in data") if packet.receiving_interface else None - - with Transport.inbound_announce_lock: - announced_destination_known = packet.destination_hash in Transport.path_table - if not announced_destination_known: - # If this destination is unknown in our table - # we should add it - should_add = True - - else: - random_blobs = Transport.path_table[packet.destination_hash][IDX_PT_RANDBLOBS] - current_gravity = Transport.path_table[packet.destination_hash][IDX_PT_RVCD_IF].gravity - announce_gravity = packet.receiving_interface.gravity if packet.receiving_interface != None else None - - # If we already have a path to the announced destination, - # but a more recently emitted announce arrives with a hop - # count equal to or less than the existing path, we will - # update our tables. - if packet.hops <= Transport.path_table[packet.destination_hash][IDX_PT_HOPS]: - path_timebase = Transport.timebase_from_random_blobs(random_blobs) - if not random_blob in random_blobs and announce_emitted > path_timebase: - Transport.mark_path_unknown_state(packet.destination_hash) - should_add = True - else: - # If the same announce is received later on an interface - # with higher gravity, allow updating the path table to - # use this interface instead. - if announce_emitted != path_timebase: should_add = False - elif announce_gravity == None or current_gravity == None: should_add = False - else: - if announce_gravity <= current_gravity: should_add = False - else: - RNS.log(f"Replacing path table entry for {RNS.prettyhexrep(packet.destination_hash)} with new announce due to higher gravity ({current_gravity}->{announce_gravity})", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - should_add = True else: - # If an announce arrives with a larger hop - # count than we already have in the table, - # ignore it, unless the path is expired, or - # the emission timestamp is more recent. - now = time.time() - path_expires = Transport.path_table[packet.destination_hash][IDX_PT_EXPIRES] - - path_announce_emitted = 0 - for path_random_blob in random_blobs: - path_announce_emitted = max(path_announce_emitted, int.from_bytes(path_random_blob[5:10], "big")) - if path_announce_emitted >= announce_emitted: break + random_blobs = Transport.path_table[packet.destination_hash][IDX_PT_RANDBLOBS] + current_gravity = Transport.path_table[packet.destination_hash][IDX_PT_RVCD_IF].gravity + announce_gravity = packet.receiving_interface.gravity if packet.receiving_interface != None else None - # If the path has expired, consider this - # announce for adding to the path table. - if (now >= path_expires): - # We check that the announce is - # different from ones we've already heard, - # to avoid loops in the network - if not random_blob in random_blobs: - RNS.log("Replacing path table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce due to expired path", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + # If we already have a path to the announced destination, + # but a more recently emitted announce arrives with a hop + # count equal to or less than the existing path, we will + # update our tables. + if packet.hops <= Transport.path_table[packet.destination_hash][IDX_PT_HOPS]: + path_timebase = Transport.timebase_from_random_blobs(random_blobs) + if not random_blob in random_blobs and announce_emitted > path_timebase: Transport.mark_path_unknown_state(packet.destination_hash) should_add = True - else: should_add = False - + else: + # If the same announce is received later on an interface + # with higher gravity, allow updating the path table to + # use this interface instead. + if announce_emitted != path_timebase: should_add = False + elif announce_gravity == None or current_gravity == None: should_add = False + else: + if announce_gravity <= current_gravity: should_add = False + else: + RNS.log(f"Replacing path table entry for {RNS.prettyhexrep(packet.destination_hash)} with new announce due to higher gravity ({current_gravity}->{announce_gravity})", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + should_add = True else: - # If the path is not expired, but the emission - # is more recent, and we haven't already heard - # this announce before, update the path table. - if (announce_emitted > path_announce_emitted): + # If an announce arrives with a larger hop + # count than we already have in the table, + # ignore it, unless the path is expired, or + # the emission timestamp is more recent. + now = time.time() + path_expires = Transport.path_table[packet.destination_hash][IDX_PT_EXPIRES] + + path_announce_emitted = 0 + for path_random_blob in random_blobs: + path_announce_emitted = max(path_announce_emitted, int.from_bytes(path_random_blob[5:10], "big")) + if path_announce_emitted >= announce_emitted: break + + # If the path has expired, consider this + # announce for adding to the path table. + if (now >= path_expires): + # We check that the announce is + # different from ones we've already heard, + # to avoid loops in the network if not random_blob in random_blobs: - RNS.log("Replacing path table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce, since it was more recently emitted", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + RNS.log("Replacing path table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce due to expired path", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None Transport.mark_path_unknown_state(packet.destination_hash) should_add = True else: should_add = False - - # If we have already heard this announce before, - # but the path has been marked as unresponsive - # by a failed communications attempt or similar, - # allow updating the path table to this one. - elif announce_emitted == path_announce_emitted: - if Transport.path_is_unresponsive(packet.destination_hash): - RNS.log("Replacing path table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce, since previously tried path was unresponsive", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - should_add = True - else: should_add = False - - if should_add: - now = time.time() - is_from_local_client = Transport.from_local_client(packet) - - rate_blocked = False - if packet.context != RNS.Packet.PATH_RESPONSE and packet.receiving_interface.announce_rate_target != None: - with Transport.announce_rate_table_lock: - if not packet.destination_hash in Transport.announce_rate_table: - rate_entry = { "last": now, "rate_violations": 0, "blocked_until": 0, "timestamps": [now]} - Transport.announce_rate_table[packet.destination_hash] = rate_entry else: - rate_entry = Transport.announce_rate_table[packet.destination_hash] - rate_entry["timestamps"].append(now) + # If the path is not expired, but the emission + # is more recent, and we haven't already heard + # this announce before, update the path table. + if (announce_emitted > path_announce_emitted): + if not random_blob in random_blobs: + RNS.log("Replacing path table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce, since it was more recently emitted", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + Transport.mark_path_unknown_state(packet.destination_hash) + should_add = True + else: should_add = False + + # If we have already heard this announce before, + # but the path has been marked as unresponsive + # by a failed communications attempt or similar, + # allow updating the path table to this one. + elif announce_emitted == path_announce_emitted: + if Transport.path_is_unresponsive(packet.destination_hash): + RNS.log("Replacing path table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce, since previously tried path was unresponsive", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + should_add = True + else: should_add = False - while len(rate_entry["timestamps"]) > Transport.MAX_RATE_TIMESTAMPS: - rate_entry["timestamps"].pop(0) + if should_add: + now = time.time() + is_from_local_client = Transport.from_local_client(packet) - current_rate = now - rate_entry["last"] + rate_blocked = False + if packet.context != RNS.Packet.PATH_RESPONSE and packet.receiving_interface.announce_rate_target != None: + with Transport.announce_rate_table_lock: + if not packet.destination_hash in Transport.announce_rate_table: + rate_entry = { "last": now, "rate_violations": 0, "blocked_until": 0, "timestamps": [now]} + Transport.announce_rate_table[packet.destination_hash] = rate_entry - if now > rate_entry["blocked_until"]: - if current_rate < packet.receiving_interface.announce_rate_target: rate_entry["rate_violations"] += 1 - else: rate_entry["rate_violations"] = max(0, rate_entry["rate_violations"]-1) + else: + rate_entry = Transport.announce_rate_table[packet.destination_hash] + rate_entry["timestamps"].append(now) - if rate_entry["rate_violations"] > packet.receiving_interface.announce_rate_grace: - rate_target = packet.receiving_interface.announce_rate_target - rate_penalty = packet.receiving_interface.announce_rate_penalty - rate_entry["blocked_until"] = rate_entry["last"] + rate_target + rate_penalty - rate_blocked = True - else: - rate_entry["last"] = now + while len(rate_entry["timestamps"]) > Transport.MAX_RATE_TIMESTAMPS: + rate_entry["timestamps"].pop(0) - else: rate_blocked = True + current_rate = now - rate_entry["last"] - retries = 0 - announce_hops = packet.hops - local_rebroadcasts = 0 - block_rebroadcasts = False - attached_interface = None - - retransmit_timeout = now + (RNS.rand() * Transport.PATHFINDER_RW) + if now > rate_entry["blocked_until"]: + if current_rate < packet.receiving_interface.announce_rate_target: rate_entry["rate_violations"] += 1 + else: rate_entry["rate_violations"] = max(0, rate_entry["rate_violations"]-1) - if hasattr(packet.receiving_interface, "mode") and packet.receiving_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT: - expires = now + Transport.AP_PATH_TIME - elif hasattr(packet.receiving_interface, "mode") and packet.receiving_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ROAMING: - expires = now + Transport.ROAMING_PATH_TIME - else: - expires = now + Transport.PATHFINDER_E - - if not random_blob in random_blobs: - random_blobs.append(random_blob) - random_blobs = random_blobs[-Transport.MAX_RANDOM_BLOBS:] + if rate_entry["rate_violations"] > packet.receiving_interface.announce_rate_grace: + rate_target = packet.receiving_interface.announce_rate_target + rate_penalty = packet.receiving_interface.announce_rate_penalty + rate_entry["blocked_until"] = rate_entry["last"] + rate_target + rate_penalty + rate_blocked = True + else: + rate_entry["last"] = now - if (RNS.Reticulum.transport_enabled() or is_from_local_client) and packet.context != RNS.Packet.PATH_RESPONSE: - # Insert announce into announce table for retransmission + else: rate_blocked = True - if rate_blocked: RNS.log("Blocking rebroadcast of announce from "+RNS.prettyhexrep(packet.destination_hash)+" due to excessive announce rate", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + retries = 0 + announce_hops = packet.hops + local_rebroadcasts = 0 + block_rebroadcasts = False + attached_interface = None + + retransmit_timeout = now + (RNS.rand() * Transport.PATHFINDER_RW) + + if hasattr(packet.receiving_interface, "mode") and packet.receiving_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT: + expires = now + Transport.AP_PATH_TIME + elif hasattr(packet.receiving_interface, "mode") and packet.receiving_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ROAMING: + expires = now + Transport.ROAMING_PATH_TIME else: - if is_from_local_client: - # If the announce is from a local client, - # it is announced immediately, but only one time. - retransmit_timeout = now - retries = Transport.PATHFINDER_R + expires = now + Transport.PATHFINDER_E + + if not random_blob in random_blobs: + random_blobs.append(random_blob) + random_blobs = random_blobs[-Transport.MAX_RANDOM_BLOBS:] - with Transport.announce_table_lock: - Transport.announce_table[packet.destination_hash] = [ - now, # 0: IDX_AT_TIMESTAMP - retransmit_timeout, # 1: IDX_AT_RTRNS_TMO - retries, # 2: IDX_AT_RETRIES - received_from, # 3: IDX_AT_RCVD_IF - announce_hops, # 4: IDX_AT_HOPS - packet, # 5: IDX_AT_PACKET - local_rebroadcasts, # 6: IDX_AT_LCL_RBRD - block_rebroadcasts, # 7: IDX_AT_BLCK_RBRD - attached_interface, # 8: IDX_AT_ATTCHD_IF - ] + if (RNS.Reticulum.transport_enabled() or is_from_local_client) and packet.context != RNS.Packet.PATH_RESPONSE: + # Insert announce into announce table for retransmission - elif is_from_local_client and packet.context == RNS.Packet.PATH_RESPONSE: - # If this is a path response from a local client, - # check if any external interfaces have pending - # path requests. - with Transport.pending_local_prs_lock: - if packet.destination_hash in Transport.pending_local_path_requests: - desiring_interface = Transport.pending_local_path_requests.pop(packet.destination_hash) - retransmit_timeout = now - retries = Transport.PATHFINDER_R + if rate_blocked: RNS.log("Blocking rebroadcast of announce from "+RNS.prettyhexrep(packet.destination_hash)+" due to excessive announce rate", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + else: + if is_from_local_client: + # If the announce is from a local client, + # it is announced immediately, but only one time. + retransmit_timeout = now + retries = Transport.PATHFINDER_R with Transport.announce_table_lock: Transport.announce_table[packet.destination_hash] = [ - now, - retransmit_timeout, - retries, - received_from, - announce_hops, - packet, - local_rebroadcasts, - block_rebroadcasts, - attached_interface + now, # 0: IDX_AT_TIMESTAMP + retransmit_timeout, # 1: IDX_AT_RTRNS_TMO + retries, # 2: IDX_AT_RETRIES + received_from, # 3: IDX_AT_RCVD_IF + announce_hops, # 4: IDX_AT_HOPS + packet, # 5: IDX_AT_PACKET + local_rebroadcasts, # 6: IDX_AT_LCL_RBRD + block_rebroadcasts, # 7: IDX_AT_BLCK_RBRD + attached_interface, # 8: IDX_AT_ATTCHD_IF ] - # If we have any local clients connected, we re- - # transmit the announce to them immediately - if (len(Transport.local_client_interfaces)): - announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True) - announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown"); - announce_destination.hash = packet.destination_hash - announce_destination.hexhash = announce_destination.hash.hex() - announce_context = RNS.Packet.NONE - announce_data = packet.data + elif is_from_local_client and packet.context == RNS.Packet.PATH_RESPONSE: + # If this is a path response from a local client, + # check if any external interfaces have pending + # path requests. + with Transport.pending_local_prs_lock: + if packet.destination_hash in Transport.pending_local_path_requests: + desiring_interface = Transport.pending_local_path_requests.pop(packet.destination_hash) + retransmit_timeout = now + retries = Transport.PATHFINDER_R - # TODO: Shouldn't the context be PATH_RESPONSE in the first case here? - if is_from_local_client and packet.context == RNS.Packet.PATH_RESPONSE: - for local_interface in Transport.local_client_interfaces: - if packet.receiving_interface != local_interface: - new_announce = RNS.Packet(announce_destination, announce_data, RNS.Packet.ANNOUNCE, # <-- This one? - context = announce_context, header_type = RNS.Packet.HEADER_2, - transport_type = Transport.TRANSPORT, transport_id = Transport.identity.hash, - attached_interface = local_interface, context_flag = packet.context_flag) - - new_announce.hops = packet.hops - new_announce.send() + with Transport.announce_table_lock: + Transport.announce_table[packet.destination_hash] = [ + now, + retransmit_timeout, + retries, + received_from, + announce_hops, + packet, + local_rebroadcasts, + block_rebroadcasts, + attached_interface + ] - else: - for local_interface in Transport.local_client_interfaces: - if packet.receiving_interface != local_interface: - new_announce = RNS.Packet(announce_destination, announce_data, RNS.Packet.ANNOUNCE, - context = announce_context, header_type = RNS.Packet.HEADER_2, - transport_type = Transport.TRANSPORT, transport_id = Transport.identity.hash, - attached_interface = local_interface, context_flag = packet.context_flag) + # If we have any local clients connected, we re- + # transmit the announce to them immediately + if (len(Transport.local_client_interfaces)): + announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True) + announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown"); + announce_destination.hash = packet.destination_hash + announce_destination.hexhash = announce_destination.hash.hex() + announce_context = RNS.Packet.NONE + announce_data = packet.data - new_announce.hops = packet.hops - new_announce.send() + # TODO: Shouldn't the context be PATH_RESPONSE in the first case here? + if is_from_local_client and packet.context == RNS.Packet.PATH_RESPONSE: + for local_interface in Transport.local_client_interfaces: + if packet.receiving_interface != local_interface: + new_announce = RNS.Packet(announce_destination, announce_data, RNS.Packet.ANNOUNCE, # <-- This one? + context = announce_context, header_type = RNS.Packet.HEADER_2, + transport_type = Transport.TRANSPORT, transport_id = Transport.identity.hash, + attached_interface = local_interface, context_flag = packet.context_flag) + + new_announce.hops = packet.hops + new_announce.send() - # If we have any waiting discovery path requests - # for this destination, we retransmit to that - # interface immediately - with Transport.discovery_pr_lock: - if not packet.destination_hash in Transport.discovery_path_requests: discovery_pr_entry = None - else: discovery_pr_entry = Transport.discovery_path_requests.pop(packet.destination_hash) + else: + for local_interface in Transport.local_client_interfaces: + if packet.receiving_interface != local_interface: + new_announce = RNS.Packet(announce_destination, announce_data, RNS.Packet.ANNOUNCE, + context = announce_context, header_type = RNS.Packet.HEADER_2, + transport_type = Transport.TRANSPORT, transport_id = Transport.identity.hash, + attached_interface = local_interface, context_flag = packet.context_flag) + + new_announce.hops = packet.hops + new_announce.send() + + # If we have any waiting discovery path requests + # for this destination, we retransmit to that + # interface immediately + if packet.destination_hash in Transport.discovery_path_requests: + pr_entry = Transport.discovery_path_requests[packet.destination_hash] + attached_interface = pr_entry["requesting_interface"] - if discovery_pr_entry: - for attached_interface in discovery_pr_entry["requesting_interfaces"]: interface_str = " on "+str(attached_interface) + RNS.log("Got matching announce, answering waiting discovery path request for "+RNS.prettyhexrep(packet.destination_hash)+interface_str, RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=False) announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown"); @@ -2454,311 +2047,320 @@ class Transport: new_announce.hops = packet.hops new_announce.send() - if not Transport.owner.is_connected_to_shared_instance: Transport.cache(packet, force_cache=True, packet_type="announce") - path_table_entry = [now, received_from, announce_hops, expires, random_blobs, packet.receiving_interface, packet.packet_hash] - with Transport.path_table_lock: Transport.path_table[packet.destination_hash] = path_table_entry - Transport.mark_path_unknown_state(packet.destination_hash) - RNS.log("Destination "+RNS.prettyhexrep(packet.destination_hash)+" is now "+str(announce_hops)+" hops away via "+RNS.prettyhexrep(received_from)+" on "+str(packet.receiving_interface), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - if packet.destination_hash in Transport.path_requests: - RNS.Reticulum.get_instance()._used_destination_data(packet.destination_hash) + if not Transport.owner.is_connected_to_shared_instance: Transport.cache(packet, force_cache=True, packet_type="announce") + path_table_entry = [now, received_from, announce_hops, expires, random_blobs, packet.receiving_interface, packet.packet_hash] + with Transport.path_table_lock: Transport.path_table[packet.destination_hash] = path_table_entry + Transport.mark_path_unknown_state(packet.destination_hash) + RNS.log("Destination "+RNS.prettyhexrep(packet.destination_hash)+" is now "+str(announce_hops)+" hops away via "+RNS.prettyhexrep(received_from)+" on "+str(packet.receiving_interface), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None + if packet.destination_hash in Transport.path_requests: + RNS.Reticulum.get_instance()._used_destination_data(packet.destination_hash) - # If the receiving interface is a tunnel, we add the - # announce to the tunnels table - if hasattr(packet.receiving_interface, "tunnel_id") and packet.receiving_interface.tunnel_id != None: - with Transport.tunnels_lock: - if not packet.receiving_interface.tunnel_id in Transport.tunnels: RNS.log(f"Tunnel ID for {packet.receiving_interface} was not found in tunnel table", RNS.LOG_WARNING) - else: - tunnel_entry = Transport.tunnels[packet.receiving_interface.tunnel_id] - paths = tunnel_entry[IDX_TT_PATHS] - paths[packet.destination_hash] = [now, received_from, announce_hops, expires, random_blobs, None, packet.packet_hash] - expires = time.time() + Transport.TUNNEL_TIMEOUT - tunnel_entry[IDX_TT_EXPIRES] = expires - RNS.log("Path to "+RNS.prettyhexrep(packet.destination_hash)+" associated with tunnel "+RNS.prettyhexrep(packet.receiving_interface.tunnel_id), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - - # Resolve potential in-flight path requests - with Transport.inflight_path_requests_lock: - if packet.destination_hash in Transport.inflight_path_requests: - Transport.inflight_path_requests.pop(packet.destination_hash) - - # Call externally registered callbacks from apps - # wanting to know when an announce arrives - with Transport.announce_handler_lock: - for handler in Transport.announce_handlers: - try: - # Check that the announced destination matches - # the handlers aspect filter - execute_callback = False - announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True) - - # If the handlers aspect filter is set to - # None, we execute the callback in all cases - if handler.aspect_filter == None: execute_callback = True + # If the receiving interface is a tunnel, we add the + # announce to the tunnels table + if hasattr(packet.receiving_interface, "tunnel_id") and packet.receiving_interface.tunnel_id != None: + with Transport.tunnels_lock: + if not packet.receiving_interface.tunnel_id in Transport.tunnels: RNS.log(f"Tunnel ID for {packet.receiving_interface} was not found in tunnel table", RNS.LOG_WARNING) else: - handler_expected_hash = RNS.Destination.hash_from_name_and_identity(handler.aspect_filter, announce_identity) - if packet.destination_hash == handler_expected_hash: execute_callback = True + tunnel_entry = Transport.tunnels[packet.receiving_interface.tunnel_id] + paths = tunnel_entry[IDX_TT_PATHS] + paths[packet.destination_hash] = [now, received_from, announce_hops, expires, random_blobs, None, packet.packet_hash] + expires = time.time() + Transport.TUNNEL_TIMEOUT + tunnel_entry[IDX_TT_EXPIRES] = expires + RNS.log("Path to "+RNS.prettyhexrep(packet.destination_hash)+" associated with tunnel "+RNS.prettyhexrep(packet.receiving_interface.tunnel_id), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - # If this is a path response, check whether the - # handler wants to receive it. - if packet.context == RNS.Packet.PATH_RESPONSE: - if hasattr(handler, "receive_path_responses") and handler.receive_path_responses == True: pass - else: execute_callback = False - - if execute_callback: - if len(inspect.signature(handler.received_announce).parameters) == 3: - def job(handler=handler, packet=packet, announce_identity=announce_identity): - handler.received_announce(destination_hash=packet.destination_hash, - announced_identity=announce_identity, - app_data=RNS.Identity.recall_app_data(packet.destination_hash, _no_use=True)) - threading.Thread(target=job, daemon=True).start() - - elif len(inspect.signature(handler.received_announce).parameters) == 4: - def job(handler=handler, packet=packet, announce_identity=announce_identity): - handler.received_announce(destination_hash=packet.destination_hash, - announced_identity=announce_identity, - app_data=RNS.Identity.recall_app_data(packet.destination_hash, _no_use=True), - announce_packet_hash = packet.packet_hash) - threading.Thread(target=job, daemon=True).start() - - elif len(inspect.signature(handler.received_announce).parameters) == 5: - def job(handler=handler, packet=packet, announce_identity=announce_identity): - handler.received_announce(destination_hash=packet.destination_hash, - announced_identity=announce_identity, - app_data=RNS.Identity.recall_app_data(packet.destination_hash, _no_use=True), - announce_packet_hash = packet.packet_hash, - is_path_response = packet.context == RNS.Packet.PATH_RESPONSE) - threading.Thread(target=job, daemon=True).start() + # Call externally registered callbacks from apps + # wanting to know when an announce arrives + with Transport.announce_handler_lock: + for handler in Transport.announce_handlers: + try: + # Check that the announced destination matches + # the handlers aspect filter + execute_callback = False + announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True) + # If the handlers aspect filter is set to + # None, we execute the callback in all cases + if handler.aspect_filter == None: execute_callback = True else: - raise TypeError("Invalid signature for announce handler callback") + handler_expected_hash = RNS.Destination.hash_from_name_and_identity(handler.aspect_filter, announce_identity) + if packet.destination_hash == handler_expected_hash: execute_callback = True - except Exception as e: - RNS.log("Error while processing external announce callback.", RNS.LOG_ERROR) - RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.trace_exception(e) + # If this is a path response, check whether the + # handler wants to receive it. + if packet.context == RNS.Packet.PATH_RESPONSE: + if hasattr(handler, "receive_path_responses") and handler.receive_path_responses == True: pass + else: execute_callback = False - # Handling for link requests to local destinations - elif packet.packet_type == RNS.Packet.LINKREQUEST and not link_request_handled: - if packet.transport_id == None or packet.transport_id == Transport.identity.hash: - destination = Transport.destinations_map.get(packet.destination_hash) - if destination and destination.type == packet.destination_type: - path_mtu = RNS.Link.mtu_from_lr_packet(packet) - mode = RNS.Link.mode_from_lr_packet(packet) - if packet.receiving_interface.AUTOCONFIGURE_MTU or packet.receiving_interface.FIXED_MTU: - nh_mtu = packet.receiving_interface.HW_MTU - else: - nh_mtu = RNS.Reticulum.MTU + if execute_callback: + if len(inspect.signature(handler.received_announce).parameters) == 3: + def job(handler=handler, packet=packet, announce_identity=announce_identity): + handler.received_announce(destination_hash=packet.destination_hash, + announced_identity=announce_identity, + app_data=RNS.Identity.recall_app_data(packet.destination_hash, _no_use=True)) + threading.Thread(target=job, daemon=True).start() + + elif len(inspect.signature(handler.received_announce).parameters) == 4: + def job(handler=handler, packet=packet, announce_identity=announce_identity): + handler.received_announce(destination_hash=packet.destination_hash, + announced_identity=announce_identity, + app_data=RNS.Identity.recall_app_data(packet.destination_hash, _no_use=True), + announce_packet_hash = packet.packet_hash) + threading.Thread(target=job, daemon=True).start() + + elif len(inspect.signature(handler.received_announce).parameters) == 5: + def job(handler=handler, packet=packet, announce_identity=announce_identity): + handler.received_announce(destination_hash=packet.destination_hash, + announced_identity=announce_identity, + app_data=RNS.Identity.recall_app_data(packet.destination_hash, _no_use=True), + announce_packet_hash = packet.packet_hash, + is_path_response = packet.context == RNS.Packet.PATH_RESPONSE) + threading.Thread(target=job, daemon=True).start() - if path_mtu: - if packet.receiving_interface.HW_MTU == None: - path_mtu = None - packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE] + else: + raise TypeError("Invalid signature for announce handler callback") + + except Exception as e: + RNS.log("Error while processing external announce callback.", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.trace_exception(e) + + # Handling for link requests to local destinations + elif packet.packet_type == RNS.Packet.LINKREQUEST and not link_request_handled: + if packet.transport_id == None or packet.transport_id == Transport.identity.hash: + destination = None + with Transport.destinations_map_lock: + if packet.destination_hash in Transport.destinations_map: + destination = Transport.destinations_map[packet.destination_hash] + + if destination and destination.type == packet.destination_type: + path_mtu = RNS.Link.mtu_from_lr_packet(packet) + mode = RNS.Link.mode_from_lr_packet(packet) + if packet.receiving_interface.AUTOCONFIGURE_MTU or packet.receiving_interface.FIXED_MTU: + nh_mtu = packet.receiving_interface.HW_MTU else: - if nh_mtu < path_mtu: - try: - path_mtu = nh_mtu - clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode) - packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu - except Exception as e: - RNS.log(f"Dropping link request packet to local destination. The contained exception was: {e}", RNS.LOG_WARNING) - if packet.receiving_interface: packet.receiving_interface.protocol_violation("Undecodable path MTU signalling bytes") - return + nh_mtu = RNS.Reticulum.MTU - packet.destination = destination - destination.receive(packet) - - # Handling for local data packets - elif packet.packet_type == RNS.Packet.DATA: - if packet.destination_type == RNS.Destination.LINK: - link = Transport.active_links_map.get(packet.destination_hash) - if link != None: - if link.attached_interface == packet.receiving_interface: - packet.link = link - if packet.context == RNS.Packet.CACHE_REQUEST: - cached_packet = Transport.get_cached_packet(packet.data) - if cached_packet != None: - if not cached_packet.unpack(): return - RNS.Packet(destination=link, data=cached_packet.data, - packet_type=cached_packet.packet_type, context=cached_packet.context).send() + if path_mtu: + if packet.receiving_interface.HW_MTU == None: + path_mtu = None + packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE] + else: + if nh_mtu < path_mtu: + try: + path_mtu = nh_mtu + clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode) + packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu + except Exception as e: + RNS.log(f"Dropping link request packet to local destination. The contained exception was: {e}", RNS.LOG_WARNING) + return - else: link.receive(packet) - - else: - # In the strange and rare case that an interface - # is partly malfunctioning, and a link-associated - # packet is being received on an interface that - # has failed sending, and transport has failed over - # to another path, we remove this packet hash from - # the filter hashlist so the link can receive the - # packet when it finally arrives over another path. - while packet.packet_hash in Transport.packet_hashlist: Transport.packet_hashlist.remove(packet.packet_hash) - while packet.packet_hash in Transport.packet_hashlist_prev: Transport.packet_hashlist_prev.remove(packet.packet_hash) - else: - destination = Transport.destinations_map.get(packet.destination_hash) - if destination and destination.type == packet.destination_type: - packet.destination = destination - if destination.receive(packet): - if destination.proof_strategy == RNS.Destination.PROVE_ALL: packet.prove() - elif destination.proof_strategy == RNS.Destination.PROVE_APP: - if destination.callbacks.proof_requested: - try: - if destination.callbacks.proof_requested(packet): packet.prove() - except Exception as e: RNS.log("Error while executing proof request callback. The contained exception was: "+str(e), RNS.LOG_ERROR) - - # Handling for proofs and link-request proofs - elif packet.packet_type == RNS.Packet.PROOF: - if packet.context == RNS.Packet.LRPROOF: - # This is a link request proof, check if it needs to be transported - REBALANCE_LOGLEVEL = RNS.LOG_PATHING - if (RNS.Reticulum.transport_enabled() or for_local_client_link or from_local_client) and packet.destination_hash in Transport.link_table: - link_entry = Transport.link_table[packet.destination_hash] - if packet.hops != link_entry[IDX_LT_REM_HOPS] and Transport.ALLOW_LINK_PATH_REBALANCE: - if packet.receiving_interface == link_entry[IDX_LT_NH_IF]: - try: - if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2 or len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - signalling_bytes = b"" - if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - signalling_bytes = RNS.Link.signalling_bytes(RNS.Link.mtu_from_lp_packet(packet), RNS.Link.mode_from_lp_packet(packet)) - - peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] - peer_identity = RNS.Identity.recall(link_entry[IDX_LT_DSTHASH], _no_use=True) - peer_sig_pub_bytes = peer_identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE] - - signed_data = packet.destination_hash+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes - signature = packet.data[:RNS.Identity.SIGLENGTH//8] - link_destination = link_entry[IDX_LT_DSTHASH] - - if peer_identity.validate(signature, signed_data) and not link_entry[IDX_LT_VALIDATED]: - RNS.log(f"Re-balancing path to {RNS.prettyhexrep(link_destination)} from link-request proof ({link_entry[IDX_LT_REM_HOPS]}->{packet.hops})", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - link_entry[IDX_LT_REM_HOPS] = packet.hops - path_entry = Transport.path_table.get(link_destination) - if path_entry: path_entry[IDX_PT_HOPS] = packet.hops - - elif not link_entry[IDX_LT_VALIDATED]: RNS.log(f"Aborting link request proof path re-balancing for {RNS.prettyhexrep(link_destination)} on link {RNS.prettyhexrep(packet.destination_hash)} due to invalid signature", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - else: pass - - except Exception as e: RNS.log(f"Error while re-balancing path from link request proof. The contained exception was: {e}", RNS.LOG_ERROR) if RNS.sl(RNS.LOG_ERROR) else None # TODO: Drop to DEBUG at some point - - if packet.hops == link_entry[IDX_LT_REM_HOPS]: - if packet.receiving_interface == link_entry[IDX_LT_NH_IF]: - try: - if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2 or len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - signalling_bytes = b"" - if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - signalling_bytes = RNS.Link.signalling_bytes(RNS.Link.mtu_from_lp_packet(packet), RNS.Link.mode_from_lp_packet(packet)) - - peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] - peer_identity = RNS.Identity.recall(link_entry[IDX_LT_DSTHASH], _no_use=True) - peer_sig_pub_bytes = peer_identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE] - - signed_data = packet.destination_hash+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes - signature = packet.data[:RNS.Identity.SIGLENGTH//8] - - if peer_identity.validate(signature, signed_data): - RNS.log("Link request proof validated for transport via "+str(link_entry[IDX_LT_RCVD_IF]), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops if not from_local_client or instance_local_link or Transport.local_hops_delta == 0 else Transport.local_hops_delta) - new_raw += packet.raw[2:] - Transport.link_table[packet.destination_hash][IDX_LT_VALIDATED] = True - Transport.transmit(link_entry[IDX_LT_RCVD_IF], new_raw) - if not Transport.owner.is_connected_to_shared_instance: - RNS.Identity._used_destination_data(link_entry[IDX_LT_DSTHASH]) - - else: - RNS.log("Invalid link request proof in transport for link "+RNS.prettyhexrep(packet.destination_hash)+", dropping proof.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - return packet.receiving_interface.protocol_violation("Invalid link request proof in transport") if packet.receiving_interface else None - - except Exception as e: RNS.log("Could not transport link request proof. The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - else: RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - else: RNS.log(f"Received link request proof with hop mismatch ({packet.hops}/{link_entry[IDX_LT_REM_HOPS]}:{link_entry[IDX_LT_NH_IF]}->{link_entry[IDX_LT_RCVD_IF]}), not transporting it", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - + packet.destination = destination + destination.receive(packet) + + # Handling for local data packets + elif packet.packet_type == RNS.Packet.DATA: + if packet.destination_type == RNS.Destination.LINK: + with Transport.active_links_lock: + for link in Transport.active_links: + if link.link_id == packet.destination_hash: + if link.attached_interface == packet.receiving_interface: + packet.link = link + if packet.context == RNS.Packet.CACHE_REQUEST: + cached_packet = Transport.get_cached_packet(packet.data) + if cached_packet != None: + if not cached_packet.unpack(): return + RNS.Packet(destination=link, data=cached_packet.data, + packet_type=cached_packet.packet_type, context=cached_packet.context).send() + + else: link.receive(packet) + break + + else: + # In the strange and rare case that an interface + # is partly malfunctioning, and a link-associated + # packet is being received on an interface that + # has failed sending, and transport has failed over + # to another path, we remove this packet hash from + # the filter hashlist so the link can receive the + # packet when it finally arrives over another path. + while packet.packet_hash in Transport.packet_hashlist: + Transport.packet_hashlist.remove(packet.packet_hash) else: - # Check if we can deliver it to a local pending link - pending_link = None - with Transport.pending_links_lock: - link = Transport.pending_links_map.get(packet.destination_hash) - if link != None: - if packet.hops != link.expected_hops and link.status == RNS.Link.PENDING and Transport.ALLOW_LINK_PATH_REBALANCE: - RNS.log(f"Unbalanced link path ({packet.hops}/{link.expected_hops}) detected on link {link}, validating signature for re-balancing...", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + destination = None + with Transport.destinations_map_lock: + if packet.destination_hash in Transport.destinations_map: + destination = Transport.destinations_map[packet.destination_hash] + + if destination and destination.type == packet.destination_type: + packet.destination = destination + if destination.receive(packet): + if destination.proof_strategy == RNS.Destination.PROVE_ALL: packet.prove() + elif destination.proof_strategy == RNS.Destination.PROVE_APP: + if destination.callbacks.proof_requested: + try: + if destination.callbacks.proof_requested(packet): packet.prove() + except Exception as e: RNS.log("Error while executing proof request callback. The contained exception was: "+str(e), RNS.LOG_ERROR) + + # Handling for proofs and link-request proofs + elif packet.packet_type == RNS.Packet.PROOF: + if packet.context == RNS.Packet.LRPROOF: + # This is a link request proof, check if it needs to be transported + REBALANCE_LOGLEVEL = RNS.LOG_DEBUG + if (RNS.Reticulum.transport_enabled() or for_local_client_link or from_local_client) and packet.destination_hash in Transport.link_table: + link_entry = Transport.link_table[packet.destination_hash] + if packet.hops != link_entry[IDX_LT_REM_HOPS] and Transport.ALLOW_LINK_PATH_REBALANCE: + if packet.receiving_interface == link_entry[IDX_LT_NH_IF]: try: if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2 or len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - packet_data = packet.data signalling_bytes = b"" - confirmed_mtu = None - mode = RNS.Link.mode_from_lp_packet(packet) - if mode != link.mode: raise TypeError(f"Invalid link mode {mode} in link request proof") - if len(packet_data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - confirmed_mtu = RNS.Link.mtu_from_lp_packet(packet) - signalling_bytes = RNS.Link.signalling_bytes(confirmed_mtu, mode) - packet_data = packet_data[:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] + if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: + signalling_bytes = RNS.Link.signalling_bytes(RNS.Link.mtu_from_lp_packet(packet), RNS.Link.mode_from_lp_packet(packet)) - peer_pub_bytes = packet_data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] - peer_sig_pub_bytes = link.destination.identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE] + peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] + peer_identity = RNS.Identity.recall(link_entry[IDX_LT_DSTHASH], _no_use=True) + peer_sig_pub_bytes = peer_identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE] - signed_data = link.link_id+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes - signature = packet_data[:RNS.Identity.SIGLENGTH//8] + signed_data = packet.destination_hash+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes + signature = packet.data[:RNS.Identity.SIGLENGTH//8] + link_destination = link_entry[IDX_LT_DSTHASH] - if link.destination.identity.validate(signature, signed_data): - if not link.rebalanced: - RNS.log(f"Re-balancing path to {RNS.prettyhexrep(link.destination.hash)} at link terminus ({link.expected_hops}->{packet.hops})", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - link.rebalanced = time.time() - link.expected_hops = packet.hops - path_entry = Transport.path_table.get(link.destination.hash) - if path_entry: + if peer_identity.validate(signature, signed_data) and not link_entry[IDX_LT_VALIDATED]: + RNS.log(f"Re-balancing path to {RNS.prettyhexrep(link_destination)} from link-request proof ({link_entry[IDX_LT_REM_HOPS]}->{packet.hops})", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + link_entry[IDX_LT_REM_HOPS] = packet.hops + with Transport.path_table_lock: + if link_destination in Transport.path_table: + path_entry = Transport.path_table[link_destination] path_entry[IDX_PT_HOPS] = packet.hops - RNS.log(f"Path table re-balanced for {RNS.prettyhexrep(link.destination.hash)}", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - else: RNS.log(f"Aborting path re-balancing at link terminus for {RNS.prettyhexrep(link.destination.hash)} on link {link} due to invalid signature", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - except Exception as e: RNS.log("Error while validating link request proof for path re-balancing at link terminus. The contained exception was: "+str(e), REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + elif not link_entry[IDX_LT_VALIDATED]: RNS.log(f"Aborting link request proof path re-balancing for {RNS.prettyhexrep(link_destination)} on link {RNS.prettyhexrep(packet.destination_hash)} due to invalid signature", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + else: pass - if packet.hops == link.expected_hops: - # Add this packet to the filter hashlist if we - # have determined that it's actually destined - # for this system, and then validate the proof - Transport.add_packet_hash(packet.packet_hash) - pending_link = link + except Exception as e: RNS.log(f"Error while re-balancing path from link request proof. The contained exception was: {e}", RNS.LOG_ERROR) if RNS.sl(RNS.LOG_ERROR) else None # TODO: Drop to DEBUG at some point - if pending_link: pending_link.validate_proof(packet) + if packet.hops == link_entry[IDX_LT_REM_HOPS]: + if packet.receiving_interface == link_entry[IDX_LT_NH_IF]: + try: + if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2 or len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: + signalling_bytes = b"" + if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: + signalling_bytes = RNS.Link.signalling_bytes(RNS.Link.mtu_from_lp_packet(packet), RNS.Link.mode_from_lp_packet(packet)) - elif packet.context == RNS.Packet.RESOURCE_PRF: - link = Transport.active_links_map.get(packet.destination_hash) - if link != None: link.receive(packet) - else: - if packet.destination_type == RNS.Destination.LINK: - link = Transport.active_links_map.get(packet.destination_hash) - if link != None: packet.link = link - - if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH: proof_hash = packet.data[:RNS.Identity.HASHLENGTH//8] - else: proof_hash = None + peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] + peer_identity = RNS.Identity.recall(link_entry[IDX_LT_DSTHASH], _no_use=True) + peer_sig_pub_bytes = peer_identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE] - # Check if this proof needs to be transported - if (RNS.Reticulum.transport_enabled() or from_local_client or proof_for_local_client) and packet.destination_hash in Transport.reverse_table: - with Transport.reverse_table_lock: reverse_entry = Transport.reverse_table.pop(packet.destination_hash) - if packet.receiving_interface == reverse_entry[IDX_RT_OUTB_IF]: - RNS.log("Proof received on correct interface, transporting it via "+str(reverse_entry[IDX_RT_RCVD_IF]), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops if not from_local_client or proof_for_local_client or Transport.local_hops_delta == 0 else Transport.local_hops_delta) - new_raw += packet.raw[2:] - Transport.transmit(reverse_entry[IDX_RT_RCVD_IF], new_raw) + signed_data = packet.destination_hash+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes + signature = packet.data[:RNS.Identity.SIGLENGTH//8] + + if peer_identity.validate(signature, signed_data): + RNS.log("Link request proof validated for transport via "+str(link_entry[IDX_LT_RCVD_IF]), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops if not from_local_client or instance_local_link or Transport.local_hops_delta == 0 else Transport.local_hops_delta) + new_raw += packet.raw[2:] + Transport.link_table[packet.destination_hash][IDX_LT_VALIDATED] = True + Transport.transmit(link_entry[IDX_LT_RCVD_IF], new_raw) + if not Transport.owner.is_connected_to_shared_instance: + RNS.Identity._used_destination_data(link_entry[IDX_LT_DSTHASH]) + + else: RNS.log("Invalid link request proof in transport for link "+RNS.prettyhexrep(packet.destination_hash)+", dropping proof.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None + except Exception as e: RNS.log("Could not transport link request proof. The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None + else: RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None + else: RNS.log(f"Received link request proof with hop mismatch ({packet.hops}/{link_entry[IDX_LT_REM_HOPS]}:{link_entry[IDX_LT_NH_IF]}->{link_entry[IDX_LT_RCVD_IF]}), not transporting it", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + else: - RNS.log("Proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None + # Check if we can deliver it to a local pending link + pending_link = None + with Transport.pending_links_lock: + for link in Transport.pending_links: + if link.link_id == packet.destination_hash: + if packet.hops != link.expected_hops and link.status == RNS.Link.PENDING and Transport.ALLOW_LINK_PATH_REBALANCE: + RNS.log(f"Unbalanced link path ({packet.hops}/{link.expected_hops}) detected on link {link}, validating signature for re-balancing...", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + try: + if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2 or len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: + packet_data = packet.data + signalling_bytes = b"" + confirmed_mtu = None + mode = RNS.Link.mode_from_lp_packet(packet) + if mode != link.mode: raise TypeError(f"Invalid link mode {mode} in link request proof") + if len(packet_data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: + confirmed_mtu = RNS.Link.mtu_from_lp_packet(packet) + signalling_bytes = RNS.Link.signalling_bytes(confirmed_mtu, mode) + packet_data = packet_data[:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] - # TODO: Lock contention could be improved here - # by a copy(), instead of running the list - # comprehension under lock. - with Transport.receipts_lock: - if proof_hash != None: - # Only test validation if hash matches - candidate_receipts = [r for r in Transport.receipts if r.hash == proof_hash] - else: - # In case of an implicit proof, we have - # to check every single outstanding receipt - candidate_receipts = Transport.receipts.copy() + peer_pub_bytes = packet_data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] + peer_sig_pub_bytes = link.destination.identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE] - for receipt in candidate_receipts: - if receipt.status != RNS.PacketReceipt.SENT: continue - if receipt.validate_proof_packet(packet): - with Transport.receipts_lock: - if receipt in Transport.receipts: Transport.receipts.remove(receipt) + signed_data = link.link_id+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes + signature = packet_data[:RNS.Identity.SIGLENGTH//8] + + if link.destination.identity.validate(signature, signed_data): + with Transport.path_table_lock: + if not link.rebalanced: + RNS.log(f"Re-balancing path to {RNS.prettyhexrep(link.destination.hash)} at link terminus ({link.expected_hops}->{packet.hops})", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + link.rebalanced = time.time() + link.expected_hops = packet.hops + if link.destination.hash in Transport.path_table: + path_entry = Transport.path_table[link.destination.hash] + path_entry[IDX_PT_HOPS] = packet.hops + RNS.log(f"Path table re-balanced for {RNS.prettyhexrep(link.destination.hash)}", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + + else: RNS.log(f"Aborting path re-balancing at link terminus for {RNS.prettyhexrep(link.destination.hash)} on link {link} due to invalid signature", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + except Exception as e: RNS.log("Error while validating link request proof for path re-balancing at link terminus. The contained exception was: "+str(e), REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + + if packet.hops == link.expected_hops: + # Add this packet to the filter hashlist if we + # have determined that it's actually destined + # for this system, and then validate the proof + Transport.add_packet_hash(packet.packet_hash) + pending_link = link + break + + if pending_link: pending_link.validate_proof(packet) + + elif packet.context == RNS.Packet.RESOURCE_PRF: + with Transport.active_links_lock: + for link in Transport.active_links: + if link.link_id == packet.destination_hash: + link.receive(packet) + break + else: + if packet.destination_type == RNS.Destination.LINK: + with Transport.active_links_lock: + for link in Transport.active_links: + if link.link_id == packet.destination_hash: + packet.link = link + break + + if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH: proof_hash = packet.data[:RNS.Identity.HASHLENGTH//8] + else: proof_hash = None + + # Check if this proof needs to be transported + if (RNS.Reticulum.transport_enabled() or from_local_client or proof_for_local_client) and packet.destination_hash in Transport.reverse_table: + with Transport.reverse_table_lock: reverse_entry = Transport.reverse_table.pop(packet.destination_hash) + if packet.receiving_interface == reverse_entry[IDX_RT_OUTB_IF]: + RNS.log("Proof received on correct interface, transporting it via "+str(reverse_entry[IDX_RT_RCVD_IF]), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", packet.hops if not from_local_client or proof_for_local_client or Transport.local_hops_delta == 0 else Transport.local_hops_delta) + new_raw += packet.raw[2:] + Transport.transmit(reverse_entry[IDX_RT_RCVD_IF], new_raw) + else: + RNS.log("Proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None + + with Transport.receipts_lock: + for receipt in Transport.receipts: + receipt_validated = False + if proof_hash != None: + # Only test validation if hash matches + if receipt.hash == proof_hash: + receipt_validated = receipt.validate_proof_packet(packet) + else: + # In case of an implicit proof, we have + # to check every single outstanding receipt + receipt_validated = receipt.validate_proof_packet(packet) + + if receipt_validated: + if receipt in Transport.receipts: + Transport.receipts.remove(receipt) @staticmethod def synthesize_tunnel(interface): @@ -2807,7 +2409,6 @@ class Transport: except Exception as e: RNS.log("An error occurred while validating tunnel establishment packet.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None - if packet.receiving_interface: packet.receiving_interface.protocol_violation("Invalid tunnel synthesis packet") @staticmethod def void_tunnel_interface(tunnel_id): @@ -2928,13 +2529,9 @@ class Transport: def register_link(link): RNS.log("Registering link "+str(link), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None if link.initiator: - with Transport.pending_links_lock: - Transport.pending_links.append(link) - Transport.pending_links_map[link.link_id] = link + with Transport.pending_links_lock: Transport.pending_links.append(link) else: - with Transport.active_links_lock: - Transport.active_links.append(link) - Transport.active_links_map[link.link_id] = link + with Transport.active_links_lock: Transport.active_links.append(link) @staticmethod def activate_link(link): @@ -2943,14 +2540,10 @@ class Transport: if link in Transport.pending_links: if link.status != RNS.Link.ACTIVE: raise IOError("Invalid link state for link activation: "+str(link.status)) Transport.pending_links.remove(link) - Transport.pending_links_map.pop(link.link_id, None) - with Transport.active_links_lock: - Transport.active_links.append(link) - Transport.active_links_map[link.link_id] = link - + with Transport.active_links_lock: Transport.active_links.append(link) link.status = RNS.Link.ACTIVE - - else: RNS.log("Attempted to activate a link that was not in the pending table", RNS.LOG_ERROR) + else: + RNS.log("Attempted to activate a link that was not in the pending table", RNS.LOG_ERROR) @staticmethod def register_announce_handler(handler): @@ -3104,11 +2697,12 @@ class Transport: # cache, replay it to the Transport instance, # so that it can be directed towards it original # destination. - Transport.inbound(packet.raw, packet.receiving_interface, ifac_handled=True) + Transport.inbound(packet.raw, packet.receiving_interface) return True - - else: return False - else: return False + else: + return False + else: + return False @staticmethod def cache_request(packet_hash, destination): @@ -3116,7 +2710,7 @@ class Transport: if cached_packet: # The packet was found in the local cache, # replay it to the Transport instance. - Transport.inbound(cached_packet.raw, cached_packet.receiving_interface, ifac_handled=True) + Transport.inbound(cached_packet.raw, cached_packet.receiving_interface) else: # The packet is not in the local cache, # query the network. @@ -3198,23 +2792,9 @@ class Transport: @staticmethod def extra_link_proof_timeout(interface): - if interface != None and interface.bitrate: return ((1/interface.bitrate)*8)*RNS.Reticulum.MTU + if interface != None: return ((1/interface.bitrate)*8)*RNS.Reticulum.MTU else: return 0 - @staticmethod - def medium_path_timeout(): - # A full round trip for an MTU on the slowest online interface - if not Transport.lowest_interface_bitrate: return 0 - return 2*(RNS.Reticulum.MTU*8/max(Transport.lowest_interface_bitrate, RNS.Reticulum.MINIMUM_BITRATE)) + RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT - - @staticmethod - def link_count(): - return len(Transport.link_table) - - @staticmethod - def active_link_count(): - return sum(1 for e in (True for entry in Transport.link_table if entry[IDX_LT_VALIDATED])) - @staticmethod def expire_path(destination_hash): with Transport.path_table_lock: @@ -3285,10 +2865,6 @@ class Transport: :param destination_hash: A destination hash as *bytes*. :param on_interface: If specified, the path request will only be sent on this interface. In normal use, Reticulum handles this automatically, and this parameter should not be used. """ - if on_interface and recursive and on_interface.should_egress_limit_pr(): - RNS.log(f"Blocking recursive path request on {on_interface} due to active egress limiting", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - return - if tag == None: request_tag = RNS.Identity.get_random_hash() else: request_tag = tag @@ -3299,7 +2875,7 @@ class Transport: packet = RNS.Packet(path_request_dst, path_request_data, packet_type = RNS.Packet.DATA, transport_type = RNS.Transport.BROADCAST, header_type = RNS.Packet.HEADER_1, attached_interface = on_interface) if on_interface != None and recursive: - if not hasattr(on_interface, "announce_cap"): on_interface.announce_cap = RNS.Reticulum.ANNOUNCE_CAP/100.0 + if not hasattr(on_interface, "announce_cap"): on_interface.announce_cap = RNS.Reticulum.ANNOUNCE_CAP if not hasattr(on_interface, "announce_allowed_at"): on_interface.announce_allowed_at = 0 if not hasattr(on_interface, "announce_queue"): on_interface.announce_queue = [] @@ -3318,9 +2894,10 @@ class Transport: on_interface.announce_allowed_at = now + wait_time packet.is_outbound_pr = True - with Transport.path_requests_lock: Transport.path_requests[destination_hash] = time.time() packet.send() + with Transport.path_requests_lock: Transport.path_requests[destination_hash] = time.time() + @staticmethod def remote_status_handler(path, data, request_id, link_id, remote_identity, requested_at): if remote_identity != None: @@ -3331,9 +2908,6 @@ class Transport: response.append(Transport.owner.get_interface_stats()) if data[0] == True: response.append(Transport.owner.get_link_count()) - if len(data) >= 2: - if data[1] == True: response.append(Transport.owner.get_profiling_results()) - return response except Exception as e: @@ -3406,25 +2980,29 @@ class Transport: unique_tag = destination_hash+tag_bytes - Transport.path_request(destination_hash, - Transport.from_local_client(packet), - packet.receiving_interface, - requestor_transport_id = requesting_transport_instance, - tag=tag_bytes, - ingress_limited=packet.traffic_class == Transport.TC_INGRESS_LIMITED) + if packet.receiving_interface: packet.receiving_interface.received_path_request() + with Transport.discovery_pr_tags_lock: + if not unique_tag in Transport.discovery_pr_tags: + Transport.discovery_pr_tags.append(unique_tag) + Transport.path_request(destination_hash, + Transport.from_local_client(packet), + packet.receiving_interface, + requestor_transport_id = requesting_transport_instance, + tag=tag_bytes) + + else: RNS.log("Ignoring duplicate path request for "+RNS.prettyhexrep(destination_hash)+" with tag "+RNS.prettyhexrep(unique_tag), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None else: RNS.log("Ignoring tagless path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None except Exception as e: RNS.log(f"Error while handling path request. The contained exception was: {e}", RNS.LOG_ERROR) @staticmethod - def path_request(destination_hash, is_from_local_client, attached_interface, requestor_transport_id=None, tag=None, ingress_limited=False): + def path_request(destination_hash, is_from_local_client, attached_interface, requestor_transport_id=None, tag=None): should_search_for_unknown = False should_ingress_limit = False search_mode_filter = None - answered = False if attached_interface != None: - should_ingress_limit = ingress_limited or attached_interface.should_ingress_limit_pr() + should_ingress_limit = attached_interface.should_ingress_limit_pr() if RNS.Reticulum.transport_enabled(): if attached_interface.recursive_prs: should_search_for_unknown = True elif attached_interface.mode in RNS.Interfaces.Interface.Interface.DISCOVER_PATHS_FOR: should_search_for_unknown = True @@ -3452,7 +3030,6 @@ class Transport: if destination_hash in Transport.destinations_map: local_destination = Transport.destinations_map[destination_hash] if local_destination != None: - answered = True local_destination.announce(path_response=True, tag=tag, attached_interface=attached_interface) RNS.log("Answering path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", destination is local to this system", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None @@ -3460,7 +3037,6 @@ class Transport: packet = Transport.get_cached_packet(Transport.path_table[destination_hash][IDX_PT_PACKET], packet_type="announce") next_hop = Transport.path_table[destination_hash][IDX_PT_NEXT_HOP] received_from = Transport.path_table[destination_hash][IDX_PT_RVCD_IF] - answered = True if packet == None: RNS.log(f"Could not retrieve announce packet from cache while answering path request for {RNS.prettyhexrep(destination_hash)}, ignoring path request", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None @@ -3530,15 +3106,7 @@ class Transport: Transport.request_path(destination_hash, interface, tag = request_tag) elif should_search_for_unknown: - with Transport.discovery_pr_lock: - if destination_hash in Transport.discovery_path_requests: - discovery_path_request_exists = True - path_request_engaged = Transport.discovery_path_requests[destination_hash]["engaged"] - else: - discovery_path_request_exists = False - path_request_engaged = False - - if discovery_path_request_exists and path_request_engaged: + if destination_hash in Transport.discovery_path_requests: RNS.log("There is already a waiting path request for "+RNS.prettyhexrep(destination_hash)+" on behalf of path request"+interface_str, RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None else: # Abort recursive path request if receiving @@ -3551,25 +3119,10 @@ class Transport: return # Forward path request on all interfaces - # except the requestor interface. The discovery - # timeout must also cover a full round trip for - # an MTU on the slowest online interface. - discovery_timeout = max(Transport.PATH_REQUEST_TIMEOUT, Transport.medium_path_timeout()) - + # except the requestor interface RNS.log("Attempting to discover unknown path to "+RNS.prettyhexrep(destination_hash)+" on behalf of path request"+interface_str, RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - pr_entry = { "destination_hash": destination_hash, "timeout": time.time()+discovery_timeout, - "requesting_interfaces": None, "engaged": True } - - with Transport.discovery_pr_lock: - existing_requesting_interfaces = [] - if destination_hash in Transport.discovery_path_requests: - existing_requesting_interfaces = Transport.discovery_path_requests[destination_hash]["requesting_interfaces"] - - if not attached_interface in existing_requesting_interfaces: - existing_requesting_interfaces.append(attached_interface) - pr_entry["requesting_interfaces"] = existing_requesting_interfaces - - Transport.discovery_path_requests[destination_hash] = pr_entry + pr_entry = { "destination_hash": destination_hash, "timeout": time.time()+Transport.PATH_REQUEST_TIMEOUT, "requesting_interface": attached_interface } + with Transport.discovery_pr_lock: Transport.discovery_path_requests[destination_hash] = pr_entry for interface in Transport.interfaces: if search_mode_filter and not interface.mode in search_mode_filter: continue @@ -3592,12 +3145,6 @@ class Transport: else: RNS.log("Ignoring path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", no path known", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None - if answered: - # Resolve potential in-flight path requests - with Transport.inflight_path_requests_lock: - if destination_hash in Transport.inflight_path_requests: - Transport.inflight_path_requests.pop(destination_hash) - @staticmethod def from_local_client(packet): if hasattr(packet.receiving_interface, "parent_interface"): @@ -3818,7 +3365,6 @@ class Transport: # Throttle interface hash lookup to 2 seconds if time.time() > interface_hashes_updated_at + 2: interface_hashes = Transport.interface_hashes() - interface_hashes_updated_at = time.time() # Get the destination entry from the destination table de = path_table[destination_hash] @@ -3919,7 +3465,7 @@ class Transport: timestamp = de[0] received_from = de[1] hops = de[2] - path_expires = de[3] + expires = de[3] random_blobs = de[4][-Transport.PERSIST_RANDOM_BLOBS:] packet_hash = de[6] @@ -3928,7 +3474,7 @@ class Transport: timestamp, received_from, hops, - path_expires, + expires, random_blobs, interface_hash, packet_hash diff --git a/RNS/Utilities/__init__.py b/RNS/Utilities/__init__.py index 454b31ff..a9a74ff3 100644 --- a/RNS/Utilities/__init__.py +++ b/RNS/Utilities/__init__.py @@ -33,7 +33,5 @@ import glob py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") -so_modules = glob.glob(os.path.dirname(__file__)+"/*.so") -pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd") -modules = py_modules+pyc_modules+so_modules+pyd_modules -__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"])) \ No newline at end of file +modules = py_modules+pyc_modules +__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) \ No newline at end of file diff --git a/RNS/Utilities/rncp.py b/RNS/Utilities/rncp.py index 151a5723..fb71a04a 100644 --- a/RNS/Utilities/rncp.py +++ b/RNS/Utilities/rncp.py @@ -401,7 +401,7 @@ def fetch(configdir, identitypath = None, verbosity = 0, quietness = 0, destinat i = 0 syms = "⢄⢂⢁⡁⡈⡐⡠" - estab_timeout = time.time()+max(timeout, reticulum.get_medium_path_timeout()) + estab_timeout = time.time()+timeout while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout: if not silent: time.sleep(0.1) @@ -431,7 +431,6 @@ def fetch(configdir, identitypath = None, verbosity = 0, quietness = 0, destinat ) link = RNS.Link(listener_destination) - estab_timeout = max(estab_timeout, time.time()+link.establishment_timeout) while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout: if not silent: time.sleep(0.1) @@ -657,7 +656,7 @@ def send(configdir, identitypath = None, verbosity = 0, quietness = 0, destinati i = 0 syms = "⢄⢂⢁⡁⡈⡐⡠" - estab_timeout = time.time()+max(timeout, reticulum.get_medium_path_timeout()) + estab_timeout = time.time()+timeout while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout: if not silent: time.sleep(0.1) @@ -687,7 +686,6 @@ def send(configdir, identitypath = None, verbosity = 0, quietness = 0, destinati ) link = RNS.Link(receiver_destination) - estab_timeout = max(estab_timeout, time.time()+link.establishment_timeout) while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout: if not silent: time.sleep(0.1) @@ -880,8 +878,8 @@ def main(): except KeyboardInterrupt: print("") - if current_resource != None: - current_resource.cancel() + if resource != None: + resource.cancel() if link != None: link.teardown() RNS.exit() diff --git a/RNS/Utilities/rngit/__init__.py b/RNS/Utilities/rngit/__init__.py index 2a04167a..56fbb793 100644 --- a/RNS/Utilities/rngit/__init__.py +++ b/RNS/Utilities/rngit/__init__.py @@ -35,7 +35,5 @@ import glob py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") -so_modules = glob.glob(os.path.dirname(__file__)+"/*.so") -pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd") -modules = py_modules+pyc_modules+so_modules+pyd_modules -__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"])) \ No newline at end of file +modules = py_modules+pyc_modules +__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) \ No newline at end of file diff --git a/RNS/Utilities/rngit/client.py b/RNS/Utilities/rngit/client.py index 454bea20..1cf8f439 100644 --- a/RNS/Utilities/rngit/client.py +++ b/RNS/Utilities/rngit/client.py @@ -40,7 +40,6 @@ import subprocess from RNS._version import __version__ from RNS.Utilities.rngit import APP_NAME -from RNS.Utilities.rngit.util import medium_path_timeout from RNS.vendor.configobj import ConfigObj from tempfile import TemporaryDirectory @@ -224,7 +223,7 @@ class ReticulumGitClient(): RNS.log(f"Requesting path to {RNS.prettyhexrep(destination_hash)}", RNS.LOG_DEBUG) sys.stderr.write(f"Requesting path..."); sys.stderr.flush() - if not RNS.Transport.await_path(destination_hash, timeout=medium_path_timeout(self.path_timeout)): + if not RNS.Transport.await_path(destination_hash, timeout=self.path_timeout): sys.stderr.write(f"\n"); sys.stderr.flush() self.abort(f"Could not resolve path to {RNS.prettyhexrep(destination_hash)}") @@ -292,8 +291,6 @@ class ReticulumGitClient(): if hasattr(self.request_response, "read") and callable(self.request_response.read): response_path = self.request_response.name - try: self.request_response.close() - except Exception as e: RNS.log(f"Could not close resource file descriptor before retain: {e}", RNS.LOG_ERROR) base_name = os.path.basename(response_path) retained_path = os.path.join(self.tmp_dir.name, base_name) shutil.move(response_path, retained_path) @@ -336,10 +333,10 @@ class ReticulumGitClient(): try: self.connect_server() except Exception as e: self.abort(str(e)) - timeout = max(self.link_timeout, self.link.establishment_timeout) + timeout = self.link_timeout while not self.link_ready and not self.link_failed and timeout > 0: time.sleep(0.5) - timeout -= 0.5 + timeout -= 1 if not self.link_ready: self.abort("Failed to establish link") diff --git a/RNS/Utilities/rngit/media.py b/RNS/Utilities/rngit/media.py deleted file mode 100644 index 75008c29..00000000 --- a/RNS/Utilities/rngit/media.py +++ /dev/null @@ -1,239 +0,0 @@ -# Reticulum License -# -# Copyright (c) 2016-2026 Mark Qvist -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import os -import time -import shutil -import subprocess -import tempfile -import RNS - -CONVERSION_TIMEOUT = 8 - -# WebP encoding backends, in preference order. All supported -# encoder families handle the common still formats (PNG, JPEG, -# TIFF, BMP, GIF, WebP). -# -# Environment overrides: -# RNGIT_MEDIA_BACKEND force a specific backend - -BACKENDS = ( ("magick", ["magick", "-", "webp:-"]), - ("convert", ["convert", "-", "webp:-"]), - ("gm", ["gm", "convert", "-", "webp:-"]), - ("ffmpeg", ["ffmpeg", "-y", "-loglevel", "error", "-i", "-", "-f", "webp", "pipe:1"]), - ("avconv", ["avconv", "-y", "-loglevel", "error", "-i", "-", "-f", "webp", "pipe:1"]) ) - -_ENV_BACKEND = os.environ.get("RNGIT_MEDIA_BACKEND") -_winner = None -_no_backend_logged = False - -def available_backends(): - out = [] - for name, argv in BACKENDS: out.append((name, shutil.which(argv[0]) is not None)) - return out - -def _selected_backend(): - global _winner - if _ENV_BACKEND: - for name, argv in BACKENDS: - if name == _ENV_BACKEND and shutil.which(argv[0]) is not None: - return (name, argv) - return None - - order = list(BACKENDS) - if _winner is not None: order.sort(key=lambda backend: backend[0] != _winner) - - for name, argv in order: - if shutil.which(argv[0]) is not None: - _winner = name - return (name, argv) - - return None - -def _terminate(proc): - try: proc.kill() - except Exception: pass - try: proc.wait() - except Exception: pass - -def _stderr_tail(proc, limit=1024): - try: - data = proc.stderr.read(limit) - return data.decode("utf-8", "replace").strip() - except Exception: return "" - -def _webp_info(data): - if len(data) < 30 or data[:4] != b"RIFF" or data[8:12] != b"WEBP": return None - fourcc = data[12:16] - if fourcc == b"VP8X": - width = int.from_bytes(data[24:27], "little") + 1 - height = int.from_bytes(data[27:30], "little") + 1 - elif fourcc == b"VP8 ": - width = int.from_bytes(data[26:28], "little") & 0x3FFF - height = int.from_bytes(data[28:30], "little") & 0x3FFF - elif fourcc == b"VP8L": - bits = int.from_bytes(data[21:25], "little") - width = (bits & 0x3FFF) + 1 - height = ((bits >> 14) & 0x3FFF) + 1 - else: return None - if width > 0 and height > 0: return (width, height) - return None - -def _valid_webp(path): - try: - with open(path, "rb") as fh: return _webp_info(fh.read(30)) is not None - except Exception: return False - -def _unlink_output(path): - try: os.unlink(path) - except Exception: pass - -def _configured_backend(quality=None, max_dimension=None): - global _no_backend_logged - backend = _selected_backend() - if backend is None: - if not _no_backend_logged: - _no_backend_logged = True - RNS.log("No WebP encoding backend available for media conversion. You can Install ImageMagick or ffmpeg to enable image conversion.", RNS.LOG_WARNING) - return None - - backend_name, encoder_argv = backend - _no_backend_logged = False - - quality_arg = None - dimension_arg = None - if quality is not None: - try: quality_arg = max(1, min(100, int(quality))) - except (ValueError, TypeError): pass - if max_dimension is not None: - try: dimension_arg = int(max_dimension) - except (ValueError, TypeError): pass - if dimension_arg is not None and dimension_arg < 1: dimension_arg = None - - encoder_argv = list(encoder_argv) - if backend_name in ("magick", "convert", "gm"): - options = [] - if quality_arg is not None: options += ["-quality", str(quality_arg)] - if dimension_arg is not None: options += ["-resize", f"{dimension_arg}x{dimension_arg}>"] - encoder_argv = encoder_argv[:-1] + options + encoder_argv[-1:] - elif backend_name in ("ffmpeg", "avconv"): - options = [] - if quality_arg is not None: options += ["-quality", str(quality_arg)] - if dimension_arg is not None: options += ["-vf", f"scale='min(iw,{dimension_arg})':'min(ih,{dimension_arg})':force_original_aspect_ratio=decrease"] - try: format_index = encoder_argv.index("-f") - except ValueError: format_index = len(encoder_argv) - encoder_argv = encoder_argv[:format_index] + options + encoder_argv[format_index:] - - return backend_name, encoder_argv - -def _await(backend_name, encoder_proc, input_proc=None, timeout=None): - if timeout is None: timeout = CONVERSION_TIMEOUT - - deadline = time.time() + timeout - try: - encoder_proc.wait(timeout=max(0.0, deadline - time.time())) - if input_proc is not None: input_proc.wait(timeout=max(0.0, deadline - time.time())) - except subprocess.TimeoutExpired: - _terminate(input_proc) - _terminate(encoder_proc) - RNS.log(f"Media conversion via {backend_name} timed out after {timeout} seconds", RNS.LOG_WARNING) - return False - - if encoder_proc.returncode != 0: - detail = "" - encoder_tail = _stderr_tail(encoder_proc) - input_tail = _stderr_tail(input_proc) if input_proc is not None else "" - if encoder_tail: detail += f" Encoder: {encoder_tail}" - if input_tail: detail += f" Input: {input_tail}" - RNS.log(f"Media conversion via {backend_name} failed.{detail}", RNS.LOG_WARNING) - return False - - return True - -def convert_to_webp(input_argv, output_path, cwd=None, timeout=None, quality=None, max_dimension=None): - configured = _configured_backend(quality=quality, max_dimension=max_dimension) - if configured is None: return False - - backend_name, encoder_argv = configured - - try: - with open(output_path, "wb") as output_fh: - input_proc = subprocess.Popen(input_argv, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - encoder_proc = subprocess.Popen(encoder_argv, stdin=input_proc.stdout, stdout=output_fh, stderr=subprocess.PIPE) - - # The parent must not retain a copy of the input pipe's write - # end, or the input process would never see EOF. - input_proc.stdout.close() - if not _await(backend_name, encoder_proc, input_proc=input_proc, timeout=timeout): return False - - if not _valid_webp(output_path): - RNS.log(f"Media conversion via {backend_name} produced invalid WebP output", RNS.LOG_WARNING) - return False - - RNS.log(f"Media converted to WebP with {backend_name}", RNS.LOG_DEBUG) - return True - - except Exception as e: - RNS.log(f"Error during media conversion: {e}", RNS.LOG_WARNING) - return False - -def convert_file_to_webp(source_path, quality=85, max_dimension=None, timeout=None): - configured = _configured_backend(quality=quality, max_dimension=max_dimension) - if configured is None: return False - backend_name, encoder_argv = configured - - tmp_path = None - success = False - try: - if not os.path.isfile(source_path): - RNS.log(f"Cannot convert media: source file does not exist: {source_path}", RNS.LOG_WARNING) - return False - - fd, tmp_path = tempfile.mkstemp(prefix="rns_media_", suffix=".webp") - os.close(fd) - - with open(source_path, "rb") as input_fh, open(tmp_path, "wb") as output_fh: - encoder_proc = subprocess.Popen(encoder_argv, stdin=input_fh, stdout=output_fh, stderr=subprocess.PIPE) - if not _await(backend_name, encoder_proc, timeout=timeout): return False - - if not _valid_webp(tmp_path): - RNS.log(f"Invalid media conversion output from {backend_name}", RNS.LOG_WARNING) - return False - - success = True - RNS.log(f"Media converted to WebP with {backend_name}", RNS.LOG_DEBUG) - return tmp_path - - except Exception as e: - RNS.log(f"Error during media conversion: {e}", RNS.LOG_WARNING) - return False - - finally: - if not success and tmp_path is not None: _unlink_output(tmp_path) diff --git a/RNS/Utilities/rngit/pages.py b/RNS/Utilities/rngit/pages.py index 7df5863b..19664b94 100644 --- a/RNS/Utilities/rngit/pages.py +++ b/RNS/Utilities/rngit/pages.py @@ -32,14 +32,13 @@ import os import time import threading import subprocess -import tempfile import urllib.parse import RNS import struct import base64 from collections import deque from datetime import datetime -from RNS.Utilities.rngit import APP_NAME, media +from RNS.Utilities.rngit import APP_NAME from RNS.Utilities.rngit.util import MarkdownToMicron, san_sha from RNS.Utilities.rngit.highlight import SyntaxHighlighter from RNS.vendor.configobj import ConfigObj @@ -51,7 +50,6 @@ from RNS.Utilities.rngit.commitsigs import unarmor_ssh_signature, parse_ssh_sign class NomadNetworkNode(): APP_NAME = "nomadnetwork" JOBS_INTERVAL = 5 - LINK_CLEAN_INTERVAL = 60 PATH_INDEX = "/page/index.mu" PATH_GROUP = "/page/group.mu" @@ -66,7 +64,6 @@ class NomadNetworkNode(): PATH_RELEASE = "/page/release.mu" PATH_WORK = "/page/work.mu" PATH_WORK_DOC = "/page/work_doc.mu" - PATH_MEDIA = "/media" FILE_ARTIFACT = "/file/artifact" FILE_DOWNLOAD = "/file/download" FILE_WORKDOC = "/file/workdoc" @@ -123,10 +120,8 @@ class NomadNetworkNode(): # want to use tabs, three spaces is all you get. TAB_WIDTH = " " - CONVERTABLE_EXTS = [".md"] - RENDERABLE_EXTS = [".md", ".mu"] - RENDER_DEFAULT = [".md", ".mu"] - IMAGE_EXTS = [".webp", ".png", ".jpg", ".jpeg", ".gif", ".tiff", ".tif", ".bmp"] + RENDERABLE_EXTS = [".md", ".mu"] + RENDER_DEFAULT = [".md", ".mu"] def __init__(self, owner=None): if not owner: raise TypeError(f"Invalid owner {owner} for {self}") @@ -155,16 +150,12 @@ class NomadNetworkNode(): self.templates["stats"] = DEFAULT_STATS_TEMPLATE self.templates["work"] = DEFAULT_WORK_TEMPLATE self.templates["work_doc"] = DEFAULT_WORK_DOC_TEMPLATE - self.templates["no_ident"] = DEFAULT_NO_IDENT_TEMPLATE self.templatesdir = self.owner.configdir+"/templates" self.use_nerdfonts = self.USE_NERDFONTS - self.media_conversion = True self.highlight_syntax = True self.highlighter = SyntaxHighlighter() self.mdc = MarkdownToMicron(max_width=self.MAX_RENDER_WIDTH, syntax_highlighter=self.highlighter) self.thanks_deque = deque(maxlen=256) - self.active_links = {} - self.last_link_clean = 0 if not os.path.isdir(self.templatesdir): try: os.makedirs(self.templatesdir) @@ -173,8 +164,6 @@ class NomadNetworkNode(): if "pages" in self.owner.config: if "unicode_icons" in self.owner.config["pages"]: if self.owner.config["pages"].as_bool("unicode_icons"): self.use_nerdfonts = False - if "media_conversion" in self.owner.config["pages"]: - self.media_conversion = self.owner.config["pages"].as_bool("media_conversion") self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, self.APP_NAME, "node") self.destination.set_link_established_callback(self.remote_connected) @@ -221,10 +210,6 @@ class NomadNetworkNode(): try: if self.announce_interval and time.time() > self.last_announce + self.announce_interval: self.announce() - if time.time() > self.last_link_clean + self.LINK_CLEAN_INTERVAL: - self.clean_links() - self.last_link_clean = time.time() - except Exception as e: RNS.log(f"Error while running periodic jobs: {e}", RNS.LOG_ERROR) def get_announce_app_data(self): return self.node_name.encode("utf-8") @@ -261,7 +246,6 @@ class NomadNetworkNode(): self.destination.register_request_handler(self.PATH_RELEASE, response_generator=self.serve_release_page, allow=RNS.Destination.ALLOW_ALL) self.destination.register_request_handler(self.PATH_WORK, response_generator=self.serve_work_page, allow=RNS.Destination.ALLOW_ALL) self.destination.register_request_handler(self.PATH_WORK_DOC, response_generator=self.serve_work_doc_page, allow=RNS.Destination.ALLOW_ALL) - self.destination.register_request_handler(self.PATH_MEDIA, response_generator=self.serve_media, allow=RNS.Destination.ALLOW_ALL, auto_compress=False) self.destination.register_request_handler(self.FILE_ARTIFACT, response_generator=self.serve_artifact, allow=RNS.Destination.ALLOW_ALL) self.destination.register_request_handler(self.FILE_DOWNLOAD, response_generator=self.serve_download, allow=RNS.Destination.ALLOW_ALL) self.destination.register_request_handler(self.FILE_WORKDOC, response_generator=self.serve_wd_download, allow=RNS.Destination.ALLOW_ALL) @@ -388,10 +372,6 @@ class NomadNetworkNode(): self.owner.view_succeeded(None, None, remote_identity) page_content = "".join(content_parts) nav_content = "".join(nav_parts) - - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", nav_content=nav_content, st=st) - return self.render_template(page_content, nav_content=nav_content, template="front", st=st) def serve_group_page(self, path, data, request_id, link_id, remote_identity, requested_at): @@ -412,9 +392,6 @@ class NomadNetworkNode(): nav_parts.append(breadcrumb + "\n") nav_content = "".join(nav_parts) - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", nav_content=nav_content, st=st) - accessible_repos = self.get_accessible_repositories(remote_identity, group_name) if not group_name or group_name not in self.owner.groups or not accessible_repos: @@ -464,9 +441,6 @@ class NomadNetworkNode(): repo = self.get_accessible_repository(remote_identity, group_name, repo_name) - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", nav_content="".join(nav_parts), st=st) - if not repo: content = self.m_heading("Not Found", 1) + "\nThe requested repository was not found.\n" return self.render_template(content, nav_content="".join(nav_parts), st=st) @@ -580,9 +554,6 @@ class NomadNetworkNode(): except (ValueError, TypeError): page_num = 0 - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) - repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: content = self.m_heading("Not Found", 1) + "\n\nThe requested repository does not exist or you do not have access to it.\n" @@ -720,9 +691,6 @@ class NomadNetworkNode(): render = True if render else False raw = True if raw else False - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) - repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: content = self.m_heading("Not Found", 1) + "\n\nThe requested repository does not exist or you do not have access to it.\n" @@ -741,10 +709,9 @@ class NomadNetworkNode(): content = self.m_heading("Invalid Path", 1) + "\n\nNo file path specified.\n" return self.render_template(content, st=st) - file_path = file_path.removeprefix("./").replace("/./", "/") + file_path = file_path.lstrip("./").replace("/./", "/") file_ext = os.path.splitext(file_path)[1].lower() renderable = file_ext in self.RENDERABLE_EXTS - convertable = file_ext in self.CONVERTABLE_EXTS if not renderable: raw = True; render = False else: if raw: render = False @@ -771,16 +738,14 @@ class NomadNetworkNode(): nav_parts.append(">>\n" + breadcrumb + "\n") sep = self.icon("sep") - dl_link = self.m_link("Download", self.FILE_DOWNLOAD, g=group_name, r=repo_name, ref=ref, path=file_path) + dl_link = self.m_link("Download", self.FILE_DOWNLOAD, g=group_name, r=repo_name, ref=ref, path=file_path) if not renderable: nav_parts.append(f"\nDisplaying Raw {sep} {dl_link}\n") else: rnd_link = self.m_link("View rendered", self.PATH_BLOB, g=group_name, r=repo_name, ref=ref, path=file_path, render="y") raw_link = self.m_link("View raw", self.PATH_BLOB, g=group_name, r=repo_name, ref=ref, path=file_path, raw="y") - mu_link = self.m_link("as micron", self.FILE_DOWNLOAD, g=group_name, r=repo_name, ref=ref, path=file_path, fmt="mu") if render: render_controls = f"Displaying Rendered {sep} {raw_link}" else: render_controls = f"Displaying Raw {sep} {rnd_link}" - if convertable: nav_parts.append(f"\n{render_controls} {sep} {dl_link} {self.CLR_DIM}{mu_link}`f\n") - else: nav_parts.append(f"\n{render_controls} {sep} {dl_link}\n") + nav_parts.append(f"\n{render_controls} {sep} {dl_link}\n") # Get blob info blob_info = self.get_blob_info(repo_path, resolved_ref, file_path) @@ -806,9 +771,8 @@ class NomadNetworkNode(): content_parts.append(f"`*{self.m_escape(symlink_target or 'unknown')}`*\n") elif is_binary: - if file_ext in self.IMAGE_EXTS: - content_parts.append(f"`(Image file`w=n`a=c`:/media/{group_name}/{repo_name}/{ref}/{urllib.parse.quote_plus(file_path)})\n") - else: content_parts.append("This file appears to be binary and cannot be displayed as text.\n") + content_parts.append("This file appears to be binary and cannot be displayed as text.\n") + # TODO: Implement raw file downloads elif size > self.BLOB_SIZE_LIMIT: content_parts.append(f"This file is {RNS.prettysize(size)}, which exceeds the display limit of {RNS.prettysize(self.BLOB_SIZE_LIMIT)}.\n") @@ -859,9 +823,6 @@ class NomadNetworkNode(): except (ValueError, TypeError): page_num = 0 - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) - repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: content = self.m_heading("Not Found", 1) + "\n\nThe requested repository does not exist or you do not have access to it.\n" @@ -941,9 +902,6 @@ class NomadNetworkNode(): content = self.m_heading("Error", 2) + "\nInvalid request\n" return self.render_template(content, st=st) - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) - repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: content = self.m_heading("Error", 2) + "\nThe requested repository was not found.\n" @@ -1099,9 +1057,6 @@ class NomadNetworkNode(): nav_parts.append(">>\n" + breadcrumb + "\n") nav_content = "".join(nav_parts) - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) - if not group_name or not repo_name: content = self.m_heading("Error", 2) + "\nInvalid request\n" return self.render_template(content, st=st) @@ -1200,9 +1155,6 @@ class NomadNetworkNode(): if not group_name or not repo_name: content = self.m_heading("Error", 2) + "\nInvalid request\n" return self.render_template(content, st=st) - - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) content_parts = [] nav_parts = [] @@ -1304,9 +1256,6 @@ class NomadNetworkNode(): content = self.m_heading("Error", 2) + "\nInvalid request\n" return self.render_template(content, st=st) - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) - repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: content = self.m_heading("Error", 2) + "\nThe requested repository was not found.\n" @@ -1371,22 +1320,11 @@ class NomadNetworkNode(): content = self.m_heading("Error", 2) + "\nInvalid request\n" return self.render_template(content, st=st) - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) - repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: content = self.m_heading("Error", 2) + "\nThe requested repository was not found.\n" return self.render_template(content, st=st) - content_parts = [] - nav_parts = [] - - # Breadcrumb navigation - breadcrumb = f">>\n{self.m_link('Node', self.PATH_INDEX)} / {self.m_link(group_name, self.PATH_GROUP, g=group_name)} / {self.m_link(repo_name, self.PATH_REPO, g=group_name, r=repo_name)} / {self.m_link('releases', self.PATH_RELEASES, g=group_name, r=repo_name)} / {tag}" - nav_parts.append(breadcrumb + "\n") - nav_content = "".join(nav_parts) - releases_path = f"{repo['path']}.releases" if tag == "latest": releases, latest_release = self.owner.releases_list_data(releases_path) @@ -1400,6 +1338,14 @@ class NomadNetworkNode(): else: tag = latest_release + content_parts = [] + nav_parts = [] + + # Breadcrumb navigation + breadcrumb = f">>\n{self.m_link('Node', self.PATH_INDEX)} / {self.m_link(group_name, self.PATH_GROUP, g=group_name)} / {self.m_link(repo_name, self.PATH_REPO, g=group_name, r=repo_name)} / {self.m_link('releases', self.PATH_RELEASES, g=group_name, r=repo_name)} / {tag}" + nav_parts.append(breadcrumb + "\n") + nav_content = "".join(nav_parts) + release_dir = os.path.join(releases_path, tag) if not os.path.isdir(release_dir): @@ -1476,17 +1422,11 @@ class NomadNetworkNode(): content = self.m_heading("Error", 2) + "\nInvalid request\n" return self.render_template(content, st=st) - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) - repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: content = self.m_heading("Error", 2) + "\nThe requested repository was not found.\n" return self.render_template(content, st=st) - work_path = f"{repo['path']}.work" - scopes_to_show = ["active", "completed", "proposed"] if scope == "all" else [scope] - content_parts = [] nav_parts = [] @@ -1501,46 +1441,17 @@ class NomadNetworkNode(): cmplt_s = "`_" if scope == "completed" else "" prpsd_s = "`_" if scope == "proposed" else "" all_s = "`_" if scope == "all" else "" - - # Count work documents - scope_counts = {"active": 0, "completed": 0, "proposed": 0} - for s in ["active", "completed", "proposed"]: - try: - folder_path = os.path.join(work_path, s) - if os.path.isdir(folder_path): - for entry in os.listdir(folder_path): - try: - doc_dir = os.path.join(folder_path, entry) - if not os.path.isdir(doc_dir): continue - - doc_id = int(entry) - read_access = self.resolve_doc_permission(remote_identity, group_name, repo_name, doc_id, self.owner.PERM_READ) - if not read_access: continue - - root_path = os.path.join(doc_dir, "root") - if not os.path.isfile(root_path): continue - scope_counts[s] += 1 - except Exception as e: RNS.trace_exception(e) - except Exception as e: RNS.trace_exception(e) - - force_counts = True - adc = scope_counts['active'] - cdc = scope_counts['completed'] - pdc = scope_counts['proposed'] - tdc = adc+cdc+pdc - adc_str = f" ({adc})" if force_counts or (not scope == "active" and adc) else "" - cdc_str = f" ({cdc})" if force_counts or (not scope == "completed" and cdc) else "" - pdc_str = f" ({pdc})" if force_counts or (not scope == "proposed" and pdc) else "" - tdc_str = f" ({tdc})" if force_counts or (not scope == "all" and tdc) else "" - filter_links = [] - filter_links.append(active_s+self.m_link("Active", self.PATH_WORK, g=group_name, r=repo_name, scope="active")+active_s+adc_str) - filter_links.append(cmplt_s+self.m_link("Completed", self.PATH_WORK, g=group_name, r=repo_name, scope="completed")+cmplt_s+cdc_str) - filter_links.append(prpsd_s+self.m_link("Proposed", self.PATH_WORK, g=group_name, r=repo_name, scope="proposed")+prpsd_s+pdc_str) - filter_links.append(all_s+self.m_link("All", self.PATH_WORK, g=group_name, r=repo_name, scope="all")+all_s+tdc_str) + filter_links.append(active_s+self.m_link("Active", self.PATH_WORK, g=group_name, r=repo_name, scope="active")+active_s) + filter_links.append(cmplt_s+self.m_link("Completed", self.PATH_WORK, g=group_name, r=repo_name, scope="completed")+cmplt_s) + filter_links.append(prpsd_s+self.m_link("Proposed", self.PATH_WORK, g=group_name, r=repo_name, scope="proposed")+prpsd_s) + filter_links.append(all_s+self.m_link("All", self.PATH_WORK, g=group_name, r=repo_name, scope="all")+all_s) content_parts.append(f" {sep} ".join(filter_links) + "\n\n") # Load work documents + work_path = f"{repo['path']}.work" + scopes_to_show = ["active", "completed", "proposed"] if scope == "all" else [scope] + for s in scopes_to_show: folder_path = os.path.join(work_path, s) @@ -1572,11 +1483,11 @@ class NomadNetworkNode(): docs.sort(key=lambda x: max(x["created"], x["edited"]), reverse=True) if not docs: - content_parts.append(self.m_heading(f"{s.capitalize()}", 2)+f"\n`*No {s} work documents`*\n") + content_parts.append(self.m_heading(f"{s.capitalize()} ({len(docs)})", 2)+f"\n`*No {s} work documents`*\n") content_parts.append("\n") else: - content_parts.append(self.m_heading(f"{s.capitalize()}", 2)) + content_parts.append(self.m_heading(f"{s.capitalize()} ({len(docs)})", 2)) content_parts.append("\n") for doc in docs: @@ -1611,9 +1522,6 @@ class NomadNetworkNode(): content = self.m_heading("Error", 2) + "\nInvalid request\n" return self.render_template(content, st=st) - if not remote_identity and self.null_ident.hash in self.owner.blocked_identities: - return self.render_template("", template="no_ident", st=st) - try: doc_id = int(doc_id) except: content = self.m_heading("Error", 2) + "\nInvalid document ID\n" @@ -1763,7 +1671,7 @@ class NomadNetworkNode(): repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: - RNS.log(f"Repository not found or no access for artifact request {group_name}/{repo_name}/{tag}/{artifact}", RNS.LOG_DEBUG) + RNS.log(f"Repository not found or no access for artifact request {group_name}/{repo_name}/{tag}/{artifact}", RNS.LOG_WARNING) return None releases_path = f"{repo['path']}.releases" @@ -1807,69 +1715,6 @@ class NomadNetworkNode(): self.owner.release_download_succeeded(group_name, repo_name, remote_identity) return [open(artifact_path, "rb"), {"name": artifact.encode("utf-8")}] - def serve_media(self, path, data, request_id, link_id, remote_identity, requested_at): - st = time.time() - RNS.log(f"Media request from {remote_identity}", RNS.LOG_DEBUG) - - if not data or not type(data) == dict: data = {} - if not "key" in data: - RNS.log(f"Missing request key in media request", RNS.LOG_DEBUG) - return False - - media_path = data.get("path", None) - if not media_path: - RNS.log(f"Missing path in media request", RNS.LOG_DEBUG) - return False - - comps = media_path.removeprefix("/media").lstrip("/").split("/") - if len(comps) < 4: - RNS.log(f"Insufficient path components in media request", RNS.LOG_DEBUG) - return False - - group_name = comps[0] - repo_name = comps[1] - ref = comps[2] - file_path = "/".join(comps[3:]) - file_path = urllib.parse.unquote_plus(file_path) - file_name = os.path.basename(file_path) - - repo = self.get_accessible_repository(remote_identity, group_name, repo_name) - if not repo: - RNS.log(f"Repository not found or no access for media request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_DEBUG) - return False - - repo_path = repo["path"] - - resolved_ref = self.resolve_ref(repo_path, ref) - if not resolved_ref: - RNS.log(f"Ref not found for media request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_WARNING) - return False - - if not file_path: - RNS.log(f"No file path for media request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_WARNING) - return False - - blob_info = self.get_blob_info(repo_path, resolved_ref, file_path) - if blob_info is None: - RNS.log(f"File not found at ref for media request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_WARNING) - return False - - else: - stream = None - response_name = file_name - file_ext = os.path.splitext(file_path)[1].lower() - if self.media_conversion and file_ext in self.IMAGE_EXTS and file_ext != ".webp": - converted = self.get_webp_stream(repo_path, resolved_ref, file_path, link_id) - if converted: stream, response_name = converted - - if not stream: stream = self.get_blob_stream(repo_path, resolved_ref, file_path) - if stream: return [stream, {"name": response_name.encode("utf-8")}] - else: - RNS.log(f"Could not resolve blob stream for media request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_WARNING) - return None - - return None - def serve_download(self, path, data, request_id, link_id, remote_identity, requested_at): st = time.time() RNS.log(f"File download request from {remote_identity}", RNS.LOG_DEBUG) @@ -1878,19 +1723,13 @@ class NomadNetworkNode(): group_name = data.get("var_g", "") if data else "" repo_name = data.get("var_r", "") if data else "" ref = data.get("var_ref", "HEAD") if data else "HEAD" - file_fmt = data.get("var_fmt", "") if data else "" file_path = data.get("var_path", "") if data else "" file_path = urllib.parse.unquote_plus(file_path) file_name = os.path.basename(file_path) - file_ext = os.path.splitext(file_path)[1].lower() - convertable = file_ext in self.CONVERTABLE_EXTS - if file_fmt and not convertable: - RNS.log(f"Download conversion request for non-convertable file", RNS.LOG_DEBUG) - return None repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: - RNS.log(f"Repository not found or no access for download request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_DEBUG) + RNS.log(f"Repository not found or no access for download request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_WARNING) return None repo_path = repo["path"] @@ -1913,47 +1752,7 @@ class NomadNetworkNode(): stream = self.get_blob_stream(repo_path, resolved_ref, file_path) if stream is not None: self.owner.download_succeeded(group_name, repo_name, remote_identity) - if not file_fmt: return [stream, {"name": file_name.encode("utf-8")}] - elif file_fmt == "mu": - link = self.active_links.get(link_id) - if not link: - RNS.log(f"Could not resolve link for file conversion of {file_path}", RNS.LOG_WARNING) - return None - - if not hasattr(link, "temporary_directories"): link.temporary_directories = [] - tmpdir = tempfile.TemporaryDirectory() - link.temporary_directories.append(tmpdir) - - stem = os.path.splitext(os.path.basename(file_path))[0] - response_name = stem + ".mu" - - try: - path_components = file_path.strip("/").split("/") - path = "/".join(path_components[:-1])+"/" if len(path_components) > 1 else "" - url_scope = f":/page/blob.mu`g={group_name}|r={repo_name}|ref={ref}|path={path}" - mdc = MarkdownToMicron(max_width=self.MAX_RENDER_WIDTH, syntax_highlighter=self.highlighter, url_scope=url_scope) - mu = mdc.format_block(stream.read().decode("utf-8")).rstrip().encode("utf-8") - - if not mu: - if tmpdir in link.temporary_directories: link.temporary_directories.remove(tmpdir) - tmpdir.cleanup() - return None - - fd, spool_path = tempfile.mkstemp(prefix=stem+".", suffix=".mu", dir=tmpdir.name) - os.close(fd) - with open(spool_path, "wb") as f: f.write(mu) - - spool = open(spool_path, "rb") - return [spool, {"name": f"{os.path.splitext(file_name)[0]}.mu".encode("utf-8")}] - - except Exception as e: - RNS.log(f"Error during file conversion of {file_path} for {link}: {e}", RNS.LOG_WARNING) - if tmpdir in link.temporary_directories: link.temporary_directories.remove(tmpdir) - try: tmpdir.cleanup() - except Exception: pass - return None - - else: return None + return [stream, {"name": file_name.encode("utf-8")}] else: RNS.log(f"Could not resolve blob stream for download request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_WARNING) @@ -1984,7 +1783,7 @@ class NomadNetworkNode(): repo = self.get_accessible_repository(remote_identity, group_name, repo_name) if not repo: - RNS.log(f"Repository not found or no access for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id}", RNS.LOG_DEBUG) + RNS.log(f"Repository not found or no access for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id}", RNS.LOG_WARNING) return None doc_access = self.resolve_doc_permission(remote_identity, group_name, repo_name, doc_id, self.owner.PERM_READ) @@ -2285,40 +2084,6 @@ class NomadNetworkNode(): return None - def get_webp_stream(self, repo_path, ref, file_path, link_id): - file_path = file_path.strip("/") - link = self.active_links.get(link_id) - if not link: - RNS.log(f"Could not resolve link for media conversion of {file_path}", RNS.LOG_WARNING) - return None - - if not hasattr(link, "temporary_directories"): link.temporary_directories = [] - tmpdir = tempfile.TemporaryDirectory() - link.temporary_directories.append(tmpdir) - - stem = os.path.splitext(os.path.basename(file_path))[0] - response_name = stem + ".webp" - - try: - fd, spool_path = tempfile.mkstemp(prefix=stem+".", suffix=".webp", dir=tmpdir.name) - os.close(fd) - - converted = media.convert_to_webp(["git", "show", f"{ref}:{file_path}"], spool_path, cwd=repo_path) - if not converted: - if tmpdir in link.temporary_directories: link.temporary_directories.remove(tmpdir) - tmpdir.cleanup() - return None - - spool = open(spool_path, "rb") - return spool, response_name - - except Exception as e: - RNS.log(f"Error during media conversion of {file_path} for {link}: {e}", RNS.LOG_WARNING) - if tmpdir in link.temporary_directories: link.temporary_directories.remove(tmpdir) - try: tmpdir.cleanup() - except Exception: pass - return None - def get_refs_info(self, repo_path, default_branch=None): refs = {"heads": [], "tags": []} @@ -2930,38 +2695,11 @@ class NomadNetworkNode(): def remote_connected(self, link): RNS.log(f"Peer connected to {self.destination}", RNS.LOG_DEBUG) - self.active_links[link.link_id] = link link.set_remote_identified_callback(self.remote_identified) link.set_link_closed_callback(self.remote_disconnected) def remote_disconnected(self, link): RNS.log(f"Peer disconnected from {self.destination}", RNS.LOG_DEBUG) - if link.link_id in self.active_links: self.active_links.pop(link.link_id) - self.cleanup_link_temporary_resources(link) - - def clean_links(self): - stale_links = [] - for link_id, link in self.active_links.items(): - if not link.status == RNS.Link.ACTIVE: stale_links.append(link_id) - - cleaned_links = 0 - for link_id in stale_links: - link = self.active_links.pop(link_id, None) - if link: - self.cleanup_link_temporary_resources(link) - cleaned_links += 1 - - if cleaned_links > 0: RNS.log(f"Cleaned {cleaned_links} stale link{'s' if cleaned_links != 1 else ''}", RNS.LOG_DEBUG) - - def cleanup_link_temporary_resources(self, link): - if hasattr(link, "temporary_directories"): - for tmpdir in link.temporary_directories: - try: - tmpdir.cleanup() - RNS.log(f"Cleaned up {tmpdir.name}", RNS.LOG_DEBUG) - except Exception as e: RNS.log(f"Error while cleaning temporary directory: {e}", RNS.LOG_ERROR) - - link.temporary_directories = [] def remote_identified(self, link, identity): RNS.log(f"Peer identified as {link.get_remote_identity()} on {link}", RNS.LOG_DEBUG) @@ -3057,8 +2795,5 @@ DEFAULT_WORK_TEMPLATE = """{PAGE_CONTENT}""" DEFAULT_WORK_DOC_TEMPLATE = """{PAGE_CONTENT}""" -# Error state templates -DEFAULT_NO_IDENT_TEMPLATE = """>>No Identity\n\nThis page requires identification, and none was received.\n""" - # Fallback template FALLBACK_TEMPLATE = """{PAGE_CONTENT}""" \ No newline at end of file diff --git a/RNS/Utilities/rngit/server.py b/RNS/Utilities/rngit/server.py index 4b50dc55..e1567d15 100644 --- a/RNS/Utilities/rngit/server.py +++ b/RNS/Utilities/rngit/server.py @@ -46,7 +46,7 @@ from datetime import datetime, timezone from RNS._version import __version__ from RNS.Utilities.rngit import APP_NAME from RNS.Utilities.rngit.pages import NomadNetworkNode -from RNS.Utilities.rngit.util import san_ref, san_refs, san_sha, medium_path_timeout +from RNS.Utilities.rngit.util import san_ref, san_refs, san_sha from RNS.vendor.configobj import ConfigObj from RNS.vendor import umsgpack as mp from RNS.Utilities.rnid import create_rsg, validate_rsg, get_rsg_hash @@ -465,7 +465,7 @@ class ReticulumGitClient(): def connect_remote(self, remote): destination_hash = self.parse_remote_destination_url(remote) print(f"Requesting path... ", end="") - if not RNS.Transport.await_path(destination_hash, timeout=medium_path_timeout(self.path_timeout)): + if not RNS.Transport.await_path(destination_hash, timeout=self.path_timeout): print(f"\n", end="") self.abort(f"Could not resolve path to {RNS.prettyhexrep(destination_hash)}") @@ -479,7 +479,6 @@ class ReticulumGitClient(): self.link = RNS.Link(self.destination) self.link.set_link_established_callback(self.link_established) self.link.set_link_closed_callback(self.link_closed) - self.link_timeout = max(self.link_timeout, self.link.establishment_timeout) def link_established(self, link): print(f"\rLink established ", end="") @@ -1999,7 +1998,6 @@ class ReticulumGitNode(): self.active_links_lock = Lock() self.stats_lock = Lock() self.sync_lock = Lock() - self.perms_lock = Lock() self.stats_ignored = {} self.stats_push_ignored = {} self.node_name = "Anonymous Git Node" @@ -2508,22 +2506,6 @@ class ReticulumGitNode(): return permissions - def load_allowed_permissions(self, allowed_path): - allowed_input = "" - dynamic_perms = False - if os.path.isfile(allowed_path): - if os.access(allowed_path, os.X_OK): - allowed_result = subprocess.run([allowed_path], stdout=subprocess.PIPE) - allowed_input = allowed_result.stdout.decode("utf-8") - dynamic_perms = True - - else: - fh = open(allowed_path, "rb") - allowed_input = fh.read().decode("utf-8") - fh.close() - - return self.permissions_from_allowed_input(allowed_input), dynamic_perms - def load_repository_group(self, group_name, group_path): if not group_name in self.groups: self.groups[group_name] = { "path": group_path, "name": group_name, "repositories": {}, "dynamic_perms": False, "read": [], "write": [], "create": [], "stats": [], "release": [], @@ -2549,8 +2531,8 @@ class ReticulumGitNode(): RNS.log(f"Attempt to set group permissions for non-existing group {group_name}, aborting", RNS.LOG_WARNING) return - new_permissions = {} - for perm in self.ALL_PERMS: new_permissions[perm] = [] + # Clear permissions before update + for perm in self.ALL_PERMS: self.groups[group_name][perm] = [] # Apply permissions from allowed file if present group_path = self.groups[group_name]["path"] @@ -2558,14 +2540,22 @@ class ReticulumGitNode(): if os.path.isfile(allowed_path): RNS.log(f"Applying group permissions for {group_name} from {allowed_path}", RNS.LOG_DEBUG) try: - group_permissions, dynamic_perms = self.load_allowed_permissions(allowed_path) - for perm in self.ALL_PERMS: new_permissions[perm] = group_permissions[perm] - self.groups[group_name]["dynamic_perms"] = dynamic_perms + allowed_input = "" + if os.access(allowed_path, os.X_OK): + allowed_result = subprocess.run([allowed_path], stdout=subprocess.PIPE) + allowed_input = allowed_result.stdout.decode("utf-8") + self.groups[group_name]["dynamic_perms"] = True + + else: + fh = open(allowed_path, "rb") + allowed_input = fh.read().decode("utf-8") + fh.close() + + group_permissions = self.permissions_from_allowed_input(allowed_input) + for perm in group_permissions: self.groups[group_name][perm] = group_permissions[perm] except Exception as e: RNS.log(f"Could not load group permissions from {allowed_path}: {e}", RNS.LOG_ERROR) - else: self.groups[group_name]["dynamic_perms"] = False - # Apply permissions from config file if present if "access" in self.config: section = self.config["access"] @@ -2589,33 +2579,14 @@ class ReticulumGitNode(): if perm == self.PERM_PROPOSE: propose = True if perm == self.PERM_ADMIN: admin = True - if read and not target in new_permissions["read"]: new_permissions["read"].append(target) - if write and not target in new_permissions["write"]: new_permissions["write"].append(target) - if create and not target in new_permissions["create"]: new_permissions["create"].append(target) - if stats and not target in new_permissions["stats"]: new_permissions["stats"].append(target) - if release and not target in new_permissions["release"]: new_permissions["release"].append(target) - if interact and not target in new_permissions["interact"]: new_permissions["interact"].append(target) - if propose and not target in new_permissions["propose"]: new_permissions["propose"].append(target) - if admin and not target in new_permissions["admin"]: new_permissions["admin"].append(target) - - for perm in self.ALL_PERMS: self.groups[group_name][perm] = new_permissions[perm] - - def update_repository_permissions(self, group_name, repository_name): - if not group_name in self.groups: - RNS.log(f"Attempt to update permissions for non-existing group {group_name}, aborting", RNS.LOG_WARNING) - return - - if not repository_name in self.groups[group_name]["repositories"]: - RNS.log(f"Attempt to update permissions for non-existing repository {group_name}/{repository_name}, aborting", RNS.LOG_WARNING) - return - - repo = self.groups[group_name]["repositories"][repository_name] - allowed_path = repo["path"]+".allowed" - - try: - repo_permissions, dynamic_perms = self.load_allowed_permissions(allowed_path) - for perm in self.ALL_PERMS: repo[perm] = repo_permissions[perm] if perm in repo_permissions else [] - except Exception as e: RNS.log(f"Could not update repository permissions for {group_name}/{repository_name} from {allowed_path}: {e}", RNS.LOG_ERROR) + if read and not target in self.groups[group_name]["read"]: self.groups[group_name]["read"].append(target) + if write and not target in self.groups[group_name]["write"]: self.groups[group_name]["write"].append(target) + if create and not target in self.groups[group_name]["create"]: self.groups[group_name]["create"].append(target) + if stats and not target in self.groups[group_name]["stats"]: self.groups[group_name]["stats"].append(target) + if release and not target in self.groups[group_name]["release"]: self.groups[group_name]["release"].append(target) + if interact and not target in self.groups[group_name]["interact"]: self.groups[group_name]["interact"].append(target) + if propose and not target in self.groups[group_name]["propose"]: self.groups[group_name]["propose"].append(target) + if admin and not target in self.groups[group_name]["admin"]: self.groups[group_name]["admin"].append(target) def load_repository(self, group, path): if not group or not path: return False @@ -2629,15 +2600,27 @@ class ReticulumGitNode(): RNS.log(f"You can change it to a bare repository using \"git config --bool core.bare true\".", RNS.LOG_WARNING) else: - repository_name = os.path.basename(path) - allowed_path = f"{path}.allowed" - repo_permissions, dynamic_perms = self.load_allowed_permissions(allowed_path) + repository_name = os.path.basename(path) + allowed_path = f"{path}.allowed" + allowed_input = "" + dynamic_perms = False + if os.path.isfile(allowed_path): + if os.access(allowed_path, os.X_OK): + allowed_result = subprocess.run([allowed_path], stdout=subprocess.PIPE) + allowed_input = allowed_result.stdout.decode("utf-8") + dynamic_perms = True + + else: + fh = open(allowed_path, "rb") + allowed_input = fh.read().decode("utf-8") + fh.close() fork = self.__is_fork(path) mirror = self.__is_mirror(path) + p = self.permissions_from_allowed_input(allowed_input) group["repositories"][repository_name] = { "name": repository_name, "group": group_name, "path": path, "fork": fork, "mirror": mirror } - for perm in self.ALL_PERMS: group["repositories"][repository_name][perm] = repo_permissions[perm] if perm in repo_permissions else [] + for perm in self.ALL_PERMS: group["repositories"][repository_name][perm] = p[perm] if perm in p else [] return True @@ -4247,7 +4230,6 @@ class ReticulumGitNode(): return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error" def _work_complete(self, work_path, data, remote_identity): - group_name, repository_name = self.parse_request_repository_path(data[self.IDX_REPOSITORY]) doc_id = data.get("doc_id") if doc_id is None: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No document ID specified" @@ -4263,9 +4245,7 @@ class ReticulumGitNode(): doc = self._work_load_document(root_path) if not doc: return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Error loading document" - is_author = doc.get("meta", {}).get("author") == remote_identity.hash - admin_access = self.resolve_doc_permission(remote_identity, group_name, repository_name, doc_id, self.PERM_ADMIN) - if not is_author and not admin_access: return self.RES_DISALLOWED.to_bytes(1, "big") + b"Not allowed" + if doc.get("meta", {}).get("author") != remote_identity.hash: return self.RES_DISALLOWED.to_bytes(1, "big") + b"No access, not author" try: completed_dir = os.path.join(completed_base, str(doc_id)) @@ -4279,35 +4259,26 @@ class ReticulumGitNode(): return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error" def _work_activate(self, work_path, data, remote_identity): - group_name, repository_name = self.parse_request_repository_path(data[self.IDX_REPOSITORY]) doc_id = data.get("doc_id") if doc_id is None: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No document ID specified" try: doc_id = int(doc_id) except: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid document ID" + completed_dir = os.path.join(work_path, "completed", str(doc_id)) active_base = os.path.join(work_path, "active") - doc_dir = None - for scope in ["completed", "proposed"]: - d = os.path.join(work_path, scope, str(doc_id)) - if os.path.isdir(d): - doc_dir = d - break - - if not doc_dir: return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Document not found" + if not os.path.isdir(completed_dir): return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Document not found" - root_path = os.path.join(doc_dir, "root") + root_path = os.path.join(completed_dir, "root") doc = self._work_load_document(root_path) if not doc: return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Error loading document" - is_author = doc.get("meta", {}).get("author") == remote_identity.hash - admin_access = self.resolve_doc_permission(remote_identity, group_name, repository_name, doc_id, self.PERM_ADMIN) - if not is_author and not admin_access: return self.RES_DISALLOWED.to_bytes(1, "big") + b"Not allowed" + if doc.get("meta", {}).get("author") != remote_identity.hash: return self.RES_DISALLOWED.to_bytes(1, "big") + b"No access, not author" try: active_dir = os.path.join(active_base, str(doc_id)) - shutil.move(doc_dir, active_dir) + shutil.move(completed_dir, active_dir) RNS.log(f"Activated work document {doc_id} by {RNS.prettyhexrep(remote_identity.hash)}", RNS.LOG_VERBOSE) return b"\x00" + mp.packb({"id": doc_id, "scope": "active"}) @@ -4533,17 +4504,10 @@ class ReticulumGitNode(): try: group_path = self.groups[group_name]["path"] allowed_path = group_path + ".allowed" - - if os.access(allowed_path, os.X_OK): return self.RES_DISALLOWED.to_bytes(1, "big") + b"Executable permission resolvers can only be modified node-side" - tmp_path = allowed_path + ".tmp" with open(tmp_path, "w", encoding="utf-8") as f: f.write(content) os.rename(tmp_path, allowed_path) - with self.perms_lock: - try: self.update_group_permissions(group_name) - except Exception as e: RNS.log(f"Error while refreshing permissions for group {group_name}: {e}", RNS.LOG_ERROR) - RNS.log(f"Permissions for group {group_name} updated by {RNS.prettyhexrep(remote_identity.hash)}", RNS.LOG_VERBOSE) return b"\x00" @@ -4615,17 +4579,10 @@ class ReticulumGitNode(): try: repo_path = self.groups[group_name]["repositories"][repository_name]["path"] allowed_path = repo_path + ".allowed" - - if os.access(allowed_path, os.X_OK): return self.RES_DISALLOWED.to_bytes(1, "big") + b"Executable permission resolvers can only be modified node-side" - tmp_path = allowed_path + ".tmp" with open(tmp_path, "w", encoding="utf-8") as f: f.write(content) os.rename(tmp_path, allowed_path) - with self.perms_lock: - try: self.update_repository_permissions(group_name, repository_name) - except Exception as e: RNS.log(f"Error while refreshing permissions for repository {group_name}/{repository_name}: {e}", RNS.LOG_ERROR) - RNS.log(f"Permissions for repository {group_name}/{repository_name} updated by {RNS.prettyhexrep(remote_identity.hash)}", RNS.LOG_VERBOSE) return b"\x00" @@ -4939,11 +4896,6 @@ announce_interval = 360 # blocked_identities = d31aeea49873006f13b3415520666a4e -# To make it easier to handle scrapers, crawlers, slopware -# and other annoyances, you can block unidentified peers by -# adding the null_ident hash to to blocked identities. - -# blocked_identities = d7db22f63b453c23bb0688dde565b7c1 [repositories] @@ -5043,26 +4995,14 @@ internal = rw:9710b86ba12c42d1d8f30f74fe509286 # serve_nomadnet = no -# It is possible to disable Nerd Font icons and instead +# It is possible to disable Nerd Font icons and instead # use simpler (but more compatible) unicode icons. # unicode_icons = yes -# You can configure whether the page server should try -# to convert media files to WebP on the fly, for serving -# to nomadnet clients. Enabled by default, but will -# require an available encoding backend installed on -# your system. Supported backends utilities are "magick", -# "convert", "gm", "ffmpeg" and "avconv". If any one is -# installed, rngit will auto-detect and use it, but you -# can force a specific backend with the environment -# variable RNGIT_MEDIA_BACKEND. - -# media_conversion = yes - [logging] -# Valid log levels are 0 through 8: +# Valid log levels are 0 through 7: # 0: Log only critical information # 1: Log errors and lower log levels # 2: Log warnings and lower log levels @@ -5070,8 +5010,7 @@ internal = rw:9710b86ba12c42d1d8f30f74fe509286 # 4: Log info and lower (this is the default) # 5: Verbose logging # 6: Debug logging -# 7: Pathing logging -# 8: Extreme logging +# 7: Extreme logging loglevel = 4 diff --git a/RNS/Utilities/rngit/util.py b/RNS/Utilities/rngit/util.py index 64a121f7..0b3e053f 100644 --- a/RNS/Utilities/rngit/util.py +++ b/RNS/Utilities/rngit/util.py @@ -814,6 +814,3 @@ class MarkdownToMicron: def convert_markdown_to_micron(text): converter = MarkdownToMicron() return converter.format_block(text) - -def medium_path_timeout(default_timeout): - return max(default_timeout, RNS.Reticulum.get_instance().get_medium_path_timeout()) \ No newline at end of file diff --git a/RNS/Utilities/rnir.py b/RNS/Utilities/rnir.py index 2d1002fb..265fa1fb 100644 --- a/RNS/Utilities/rnir.py +++ b/RNS/Utilities/rnir.py @@ -55,12 +55,19 @@ def main(): parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) parser.add_argument('-v', '--verbose', action='count', default=0) parser.add_argument('-q', '--quiet', action='count', default=0) + parser.add_argument("--exampleconfig", action='store_true', default=False, help="print verbose configuration example to stdout and exit") parser.add_argument("--version", action="version", version="rnir {version}".format(version=__version__)) args = parser.parse_args() - if args.config: configarg = args.config - else: configarg = None + if args.exampleconfig: + print(__example_rns_config__) + exit() + + if args.config: + configarg = args.config + else: + configarg = None program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet) diff --git a/RNS/Utilities/rnodeconf.py b/RNS/Utilities/rnodeconf.py index c799a38f..881d8d8c 100755 --- a/RNS/Utilities/rnodeconf.py +++ b/RNS/Utilities/rnodeconf.py @@ -3766,8 +3766,8 @@ def main(): print("\nDevice configuration:") if ec_bt == 0x73: print(f" Bluetooth : Enabled") else: print(f" Bluetooth : Disabled") - if ec_wifi == 0x01: print(f" WiFi : Enabled (Station)") - elif ec_wifi == 0x02: print(f" WiFi : Enabled (AP)") + if ec_wifi == 0x01: print(f" WiFi : Enabled (Station)") + if ec_wifi == 0x02: print(f" WiFi : Enabled (AP)") else: print(f" WiFi : Disabled") if ec_wifi == 0x01 or ec_wifi == 0x02: if not ec_wchn: print(f" Channel : Unknown") diff --git a/RNS/Utilities/rnpath.py b/RNS/Utilities/rnpath.py index 3f65a691..5b0a30db 100644 --- a/RNS/Utilities/rnpath.py +++ b/RNS/Utilities/rnpath.py @@ -42,7 +42,6 @@ remote_link = None output_rst_str = "\r \r" def connect_remote(destination_hash, auth_identity, timeout, no_output = False, purpose="management"): global remote_link, reticulum - timeout = max(timeout, reticulum.get_medium_path_timeout()) if not RNS.Transport.has_path(destination_hash): if not no_output: print("Path to "+RNS.prettyhexrep(destination_hash)+" requested", end=" ") @@ -452,7 +451,7 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, i = 0 syms = "⢄⢂⢁⡁⡈⡐⡠" - limit = time.time()+max(timeout, reticulum.get_medium_path_timeout()) + limit = time.time()+timeout while not RNS.Transport.has_path(destination_hash) and time.time() _timeout: @@ -131,7 +131,7 @@ def program_setup(configdir, destination_hexhash, size=None, full_name = None, v print("\rSent probe "+str(sent)+" ("+str(size)+" bytes) to "+RNS.prettyhexrep(destination_hash)+more+" ", end=" ") - _timeout = time.time() + (timeout or max(DEFAULT_TIMEOUT+reticulum.get_first_hop_timeout(destination_hash), reticulum.get_medium_path_timeout())) + _timeout = time.time() + (timeout or DEFAULT_TIMEOUT+reticulum.get_first_hop_timeout(destination_hash)) i = 0 while receipt.status == RNS.PacketReceipt.SENT and not time.time() > _timeout: time.sleep(0.1) diff --git a/RNS/Utilities/rnsd.py b/RNS/Utilities/rnsd.py index 4e4e5ea3..515fffba 100755 --- a/RNS/Utilities/rnsd.py +++ b/RNS/Utilities/rnsd.py @@ -215,14 +215,6 @@ instance_name = default # required_discovery_value = 14 -# You can disable allowing interfaces to be attached, de- -# tached or reloaded via rnstatus, for example if running -# on a shared system where users with normal RPC access -# should not be allowed to control interface states. - -# enable_interface_management = no - - # You can configure Reticulum to panic and forcibly close # if an unrecoverable interface error occurs, such as the # hardware device for an interface disappearing. This is diff --git a/RNS/Utilities/rnsh/__init__.py b/RNS/Utilities/rnsh/__init__.py index a528c782..fbf23102 100644 --- a/RNS/Utilities/rnsh/__init__.py +++ b/RNS/Utilities/rnsh/__init__.py @@ -41,9 +41,7 @@ module_dir = os.path.dirname(module_abs_filename) py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") -so_modules = glob.glob(os.path.dirname(__file__)+"/*.so") -pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd") -modules = py_modules+pyc_modules+so_modules+pyd_modules -__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"])) +modules = py_modules+pyc_modules +__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) def _get_version(): return __version__ diff --git a/RNS/Utilities/rnsh/initiator.py b/RNS/Utilities/rnsh/initiator.py index 25cccfef..6310afde 100644 --- a/RNS/Utilities/rnsh/initiator.py +++ b/RNS/Utilities/rnsh/initiator.py @@ -405,7 +405,7 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, logfile: chunk_segment = None chunk_segment = None - max_data_len = channel.mdu - protocol.StreamDataMessage.HEADER_LEN + max_data_len = channel.mdu - protocol.StreamDataMessage.OVERHEAD while chunk_len > 32 and comp_try < comp_tries: chunk_segment_length = int(chunk_len/comp_try) compressed_chunk = bz2.compress(buf[:chunk_segment_length]) diff --git a/RNS/Utilities/rnsh/retry.py b/RNS/Utilities/rnsh/retry.py index 202fc042..ef0910a6 100644 --- a/RNS/Utilities/rnsh/retry.py +++ b/RNS/Utilities/rnsh/retry.py @@ -32,7 +32,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import RNS import asyncio import threading import time diff --git a/RNS/Utilities/rnsh/session.py b/RNS/Utilities/rnsh/session.py index 9a2167a4..b0d3455e 100644 --- a/RNS/Utilities/rnsh/session.py +++ b/RNS/Utilities/rnsh/session.py @@ -235,7 +235,7 @@ class ListenerSession: chunk_segment = None chunk_segment = None - max_data_len = self.channel.mdu - protocol.StreamDataMessage.HEADER_LEN + max_data_len = self.channel.mdu - protocol.StreamDataMessage.OVERHEAD while chunk_len > 32 and comp_try < comp_tries: chunk_segment_length = int(chunk_len/comp_try) compressed_chunk = bz2.compress(buf[:chunk_segment_length]) diff --git a/RNS/Utilities/rnstatus.py b/RNS/Utilities/rnstatus.py index c69b62bb..60795d13 100644 --- a/RNS/Utilities/rnstatus.py +++ b/RNS/Utilities/rnstatus.py @@ -63,10 +63,9 @@ request_concluded = False first_remote_req = True remote_destination = None remote_link = None -def get_remote_status(destination_hash, include_lstats, include_profiling, identity, no_output=False, timeout=RNS.Transport.PATH_REQUEST_TIMEOUT): +def get_remote_status(destination_hash, include_lstats, identity, no_output=False, timeout=RNS.Transport.PATH_REQUEST_TIMEOUT): global request_result, request_concluded, first_remote_req, remote_destination, remote_link link_count = None - profiling_results = None if not RNS.Transport.has_path(destination_hash): if not no_output: @@ -115,10 +114,7 @@ def get_remote_status(destination_hash, include_lstats, include_profiling, ident if len(response) > 1: link_count = response[1] else: link_count = None - if len(response) > 2: profiling_results = response[2] - else: profiling_results = None - - request_result = (status, link_count, profiling_results) + request_result = (status, link_count) request_concluded = True @@ -129,7 +125,7 @@ def get_remote_status(destination_hash, include_lstats, include_profiling, ident print("Sending request...", end=" ") sys.stdout.flush() link.identify(identity) - link.request("/status", data = [include_lstats, include_profiling], response_callback = got_response, failed_callback = request_failed) + link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed) first_remote_req = False if not remote_link and not no_output: @@ -156,11 +152,9 @@ def get_remote_status(destination_hash, include_lstats, include_profiling, ident return request_result -def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, pstats=False, lstats=False, - sorting=None, sort_reverse=False, remote=None, management_identity=None, must_exit=True, rns_instance=None, - traffic_totals=False, discovered_interfaces=False, config_entries=False, burst_filter=False, blocked_ips=False, - queue_stats=False, pps=False, profiling=False, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, - attach=None, detach=None, reload=None, show_stale=False, show_unknown=False): +def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, pstats=False, lstats=False, sorting=None, + sort_reverse=False, remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True, + rns_instance=None, traffic_totals=False, discovered_interfaces=False, config_entries=False, burst_filter=False): if remote: require_shared = False else: require_shared = True @@ -177,41 +171,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= if must_exit: exit(1) else: return - if attach: - result = reticulum.attach_interface(attach) - if result == True: print(f"Interface {attach} was attached") - elif result == False: print(f"Could not attach interface {attach}") - elif result == None: print(f"The interface {attach} does not exist") - else: print(f"Unknown error while attaching interface {attach}") - - if result == True: exit(0) - else: exit(1) - - if detach: - result = reticulum.detach_interface(detach) - if result == True: print(f"Interface {detach} was detached") - elif result == False: print(f"Could not detach interface {detach}") - elif result == None: print(f"The interface {detach} does not exist") - else: print(f"Unknown error while detaching interface {detach}") - - if result == True: exit(0) - else: exit(1) - - if reload: - result = reticulum.reload_interface(reload) - if result == True: print(f"Interface {reload} was reloaded") - elif result == False: print(f"Could not reload interface {reload}") - elif result == None: print(f"The interface {reload} does not exist") - else: print(f"Unknown error while reloading interface {reload}") - - if result == True: exit(0) - else: exit(1) - - link_count = None - active_link_count = None stats = None - profiling_results = None details = False if config_entries: @@ -243,17 +204,12 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= name = i["name"] if_type = i["type"] status = i["status"] - has_impl_info = "impl_name" in i and "version" in i and i["impl_name"] and i["version"] - impl_str = f"{i['impl_name']} {i['version']}" if has_impl_info else "Unknown" if status == "available": status_display = "Available" elif status == "unknown": status_display = "Unknown" elif status == "stale": status_display = "Stale" else: status_display = status - if status == "stale" and not show_stale: continue - if not has_impl_info and not show_unknown: continue - now = time.time() dago = now-i["discovered"] hago = now-i["last_heard"] @@ -269,22 +225,18 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= location = f"{lat}, {lon}{height}" else: location = "Unknown" - lxmf_addr = None transport_id = None - network = None - if "operator_lxmf_address" in i: lxmf_addr = i['operator_lxmf_address'] + network = None if "transport_id" in i: transport_id = i["transport_id"] if "transport_id" in i and "network_id" in i and i["transport_id"] != i["network_id"]: network = i["network_id"] - if idx > 0: print("\n"+"="*47+"\n") + if idx > 0: print("\n"+"="*32+"\n") if network: print(f"Network ID : {network}") if transport_id: print(f"Transport ID : {transport_id}") - if lxmf_addr: print(f"LXMF address : {lxmf_addr}") print(f"Name : {name}") print(f"Type : {if_type}") - print(f"Stack : {impl_str}") print(f"Status : {status_display}") print(f"Transport : {transport_str}") print(f"Distance : {i['hops']} hop{'' if i['hops'] == 1 else 's'}") @@ -306,18 +258,16 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= config_lines = i["config_entry"].split('\n') for line in config_lines: print(f" {line}") - except Exception as e: pass + except Exception as e: + pass else: - print(f"{'Name':<25} {'Type':<10} {'Status':<10} {'Last Heard':<12} {'Value':<7} {'Running':<16} {'Location':<15}") - print("-" * 110) + print(f"{'Name':<25} {'Type':<12} {'Status':<12} {'Last Heard':<12} {'Value':<8} {'Location':<15}") + print("-" * 89) for i in filtered_ifs: try: name = i["name"][:24] + "…" if len(i["name"]) > 24 else i["name"] - has_impl_info = "impl_name" in i and "version" in i and i["impl_name"] and i["version"] - impl_str = f"{i['impl_name']} {i['version']}" if has_impl_info else "Unknown" - if len(impl_str) > 16: impl_str = impl_str[:15]+"…" if_type = i["type"].replace("Interface", "") @@ -326,9 +276,6 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= elif status == "unknown": status_display = "? Unknown" elif status == "stale": status_display = "× Stale" else: status_display = status - - if status == "stale" and not show_stale: continue - if not has_impl_info and not show_unknown: continue now = time.time() last_heard = i["last_heard"] @@ -353,7 +300,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= location = f"{lat}, {lon}" else: location = "N/A" - print(f"{name:<25} {if_type:<10} {status_display:<12} {last_heard_display:<12} {value:<7} {impl_str:<16} {location:<15}") + print(f"{name:<25} {if_type:<12} {status_display:<12} {last_heard_display:<12} {value:<8} {location:<15}") except Exception as e: pass @@ -376,8 +323,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= if identity == None: raise ValueError("Could not load management identity from "+str(management_identity)) try: - remote_status = get_remote_status(destination_hash, lstats, profiling, identity, no_output=json, timeout=remote_timeout) - if remote_status != None: stats, link_count, profiling_results = remote_status + remote_status = get_remote_status(destination_hash, lstats, identity, no_output=json, timeout=remote_timeout) + if remote_status != None: stats, link_count = remote_status except Exception as e: raise e except Exception as e: @@ -389,12 +336,6 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= if lstats: try: link_count = reticulum.get_link_count() except Exception as e: pass - try: active_link_count = reticulum.get_active_link_count() - except Exception as e: pass - - if profiling: - try: profiling_results = reticulum.get_profiling_results() - except Exception as e: pass try: stats = reticulum.get_interface_stats() except Exception as e: pass @@ -420,31 +361,34 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= interfaces = stats["interfaces"] if sorting != None and isinstance(sorting, str): sorting = sorting.lower() - if sorting == "rate" or sorting == "bitrate": interfaces.sort(key=lambda i: i["bitrate"], reverse=not sort_reverse) - if sorting == "rx": interfaces.sort(key=lambda i: i["rxb"], reverse=not sort_reverse) - if sorting == "tx": interfaces.sort(key=lambda i: i["txb"], reverse=not sort_reverse) - if sorting == "rxs": interfaces.sort(key=lambda i: i["rxs"], reverse=not sort_reverse) - if sorting == "txs": interfaces.sort(key=lambda i: i["txs"], reverse=not sort_reverse) - if sorting == "traffic": interfaces.sort(key=lambda i: i["rxb"]+i["txb"], reverse=not sort_reverse) - if sorting == "anns" or sorting == "announces": interfaces.sort(key=lambda i: i["incoming_announce_frequency"]+i["outgoing_announce_frequency"], reverse=not sort_reverse) - if sorting == "arx": interfaces.sort(key=lambda i: i["incoming_announce_frequency"], reverse=not sort_reverse) - if sorting == "atx": interfaces.sort(key=lambda i: i["outgoing_announce_frequency"], reverse=not sort_reverse) - if sorting == "arxc": interfaces.sort(key=lambda i: i["arxc"], reverse=not sort_reverse) - if sorting == "atxc": interfaces.sort(key=lambda i: i["atxc"], reverse=not sort_reverse) - if sorting == "prx": interfaces.sort(key=lambda i: i["incoming_pr_frequency"], reverse=not sort_reverse) - if sorting == "ptx": interfaces.sort(key=lambda i: i["outgoing_pr_frequency"], reverse=not sort_reverse) - if sorting == "prxc": interfaces.sort(key=lambda i: i["prxc"], reverse=not sort_reverse) - if sorting == "ptxc": interfaces.sort(key=lambda i: i["ptxc"], reverse=not sort_reverse) - if sorting == "held": interfaces.sort(key=lambda i: i["held_announces"], reverse=not sort_reverse) - if sorting == "pvs": interfaces.sort(key=lambda i: i["protocol_violations"], reverse=not sort_reverse) - if sorting == "ivs": interfaces.sort(key=lambda i: i["ifac_violations"], reverse=not sort_reverse) - if sorting == "flt": interfaces.sort(key=lambda i: i["packet_filter_hits"], reverse=not sort_reverse) - if sorting == "gravity" or sorting == "g": interfaces.sort(key=lambda i: i["gravity"], reverse=not sort_reverse) - if sorting == "txdrp": interfaces.sort(key=lambda i: i["txdrp"], reverse=not sort_reverse) - if sorting == "txdrb": interfaces.sort(key=lambda i: i["txdrb"], reverse=not sort_reverse) - if sorting == "txbuf": interfaces.sort(key=lambda i: i["txbuffered"], reverse=not sort_reverse) - + if sorting == "rate" or sorting == "bitrate": + interfaces.sort(key=lambda i: i["bitrate"], reverse=not sort_reverse) + if sorting == "rx": + interfaces.sort(key=lambda i: i["rxb"], reverse=not sort_reverse) + if sorting == "tx": + interfaces.sort(key=lambda i: i["txb"], reverse=not sort_reverse) + if sorting == "rxs": + interfaces.sort(key=lambda i: i["rxs"], reverse=not sort_reverse) + if sorting == "txs": + interfaces.sort(key=lambda i: i["txs"], reverse=not sort_reverse) + if sorting == "traffic": + interfaces.sort(key=lambda i: i["rxb"]+i["txb"], reverse=not sort_reverse) + if sorting == "announces" or sorting == "announce": + interfaces.sort(key=lambda i: i["incoming_announce_frequency"]+i["outgoing_announce_frequency"], reverse=not sort_reverse) + if sorting == "arx": + interfaces.sort(key=lambda i: i["incoming_announce_frequency"], reverse=not sort_reverse) + if sorting == "atx": + interfaces.sort(key=lambda i: i["outgoing_announce_frequency"], reverse=not sort_reverse) + if sorting == "prx": + interfaces.sort(key=lambda i: i["incoming_pr_frequency"], reverse=not sort_reverse) + if sorting == "ptx": + interfaces.sort(key=lambda i: i["outgoing_pr_frequency"], reverse=not sort_reverse) + if sorting == "held": + interfaces.sort(key=lambda i: i["held_announces"], reverse=not sort_reverse) + if sorting == "gravity" or sorting == "g": + interfaces.sort(key=lambda i: i["gravity"], reverse=not sort_reverse) + for ifstat in interfaces: name = ifstat["name"] @@ -514,9 +458,6 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= p = ifstat["blocked_ips"] > 0 if p: clients_string += "\n Blocked : "+str(ifstat["blocked_ips"])+" IP"+"s" if p else "" - if blocked_ips and "blocked_ip_list" in ifstat and len(ifstat["blocked_ip_list"]) > 0: - for bip in ifstat["blocked_ip_list"]: clients_string += f"\n {bip}" - else: clients = None @@ -536,11 +477,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= if not (name.startswith("Shared Instance[") or name.startswith("TCPInterface[Client") or name.startswith("LocalInterface[")): print(f" Mode : {modestr}") - if ifstat["txdrp"]: - print(f" TX Drops : {ifstat['txdrp']} ({RNS.prettysize(ifstat['txdrb'])})") - if "bitrate" in ifstat and ifstat["bitrate"] != None: - print(f" Rate : {speed_str(ifstat['bitrate'])}, MTU {ifstat['mtu']}") + print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"]))) if "noise_floor" in ifstat: if not "interference" in ifstat: nstr = "" @@ -634,17 +572,13 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= burst_str = "" if "burst_active" in ifstat and ifstat["burst_active"]: - if "burst_count" in ifstat and ifstat["burst_count"]: bcstr = f"on {ifstat['burst_count']} " - else: bcstr = "" for_str = RNS.prettytime(time.time()-ifstat["burst_activated"]) - burst_str = f" burst {bcstr}for {for_str}" + burst_str = f" burst for {for_str}" pburst_str = "" if "pr_burst_active" in ifstat and ifstat["pr_burst_active"]: - if "pr_burst_count" in ifstat and ifstat["pr_burst_count"]: prbcstr = f"on {ifstat['pr_burst_count']} " - else: prbcstr = "" for_str = RNS.prettytime(time.time()-ifstat["pr_burst_activated"]) - pburst_str = f"burst {prbcstr}for {for_str}" + pburst_str = f"burst for {for_str}" rxb_str = "↓"+RNS.prettysize(ifstat["rxb"]) txb_str = "↑"+RNS.prettysize(ifstat["txb"]) @@ -659,16 +593,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= cspec = "c" if clients == None and "peers" in ifstat and ifstat["peers"]: clients = ifstat["peers"]; cspec = "p" - if clients != None and clients > 0: pc_str = f" {RNS.prettyfrequency(ifstat['outgoing_announce_frequency']/clients, d=1, lpf=True)}/{cspec}" + if clients != None and clients > 0: pc_str = f"{RNS.prettyfrequency(ifstat['outgoing_announce_frequency']/clients, d=1, lpf=True)}/{cspec}" else: pc_str = "" - - if "prxs" in ifstat and "rxs" in ifstat and "ptxs" in ifstat and "txs" in ifstat: - arxspct = min(100.0, (ifstat["arxs"] / ifstat["rxs"])*100.0) if ifstat["rxs"] and ifstat["arxs"] else 0.0 - atxspct = min(100.0, (ifstat["atxs"] / ifstat["txs"])*100.0) if ifstat["txs"] and ifstat["atxs"] else 0.0 - apctstr = f"(↓{int(arxspct)}% / ↑{int(atxspct)}% of flow)" - if pc_str: pc_str = f"{pc_str} {apctstr}" - else: pc_str = f"{apctstr}" - asr = True psr = False @@ -684,16 +610,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= ipf = RNS.prettyfrequency(ipn,d=1, lpf=True)+"↓" cspec = "c" if clients == None and "peers" in ifstat and ifstat["peers"]: clients = ifstat["peers"]; cspec = "p" - if clients != None and clients > 0: rpc_str = f" {RNS.prettyfrequency(ifstat['outgoing_pr_frequency']/clients, d=1, lpf=True)}/{cspec}" + if clients != None and clients > 0: rpc_str = f"{RNS.prettyfrequency(ifstat['outgoing_pr_frequency']/clients, d=1, lpf=True)}/{cspec}" else: rpc_str = "" - - if "prxs" in ifstat and "rxs" in ifstat and "ptxs" in ifstat and "txs" in ifstat: - prxspct = min(100.0, (ifstat["prxs"] / ifstat["rxs"])*100.0) if ifstat["rxs"] and ifstat["prxs"] else 0.0 - ptxspct = min(100.0, (ifstat["ptxs"] / ifstat["txs"])*100.0) if ifstat["txs"] and ifstat["ptxs"] else 0.0 - ppctstr = f"(↓{int(prxspct)}% / ↑{int(ptxspct)}% of flow)" - if rpc_str: rpc_str = f"{rpc_str} {ppctstr}" - else: rpc_str = f"{ppctstr}" - psr = True if not asr: iaf = ""; oaf = "" @@ -709,30 +627,12 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= rxb_str += (mlen-len(rxb_str))*" " txb_str += (mlen-len(txb_str))*" " - if "protocol_violations" in ifstat and "ifac_violations" in ifstat: - pv = ifstat["protocol_violations"]; iv = ifstat["ifac_violations"] - if pv or iv: - pvstr = f"{pv} protocol{', ' if iv else ''}" - ivstr = f"{iv} IFAC" if iv else "" - print(f" Violatns. : {pvstr}{ivstr}") - - if "packet_filter_hits" in ifstat and ifstat["packet_filter_hits"]: - print(f" Flt. Hits : {ifstat['packet_filter_hits']}") - if psr: - if ifstat["prxc"] and ifstat["ptxc"]: - print(f" Path Rqs. : {ifstat['prxc']}↓ {ifstat['ptxc']}↑ total") - print(f" {opf} {rpc_str}") - else: - print(f" Path Rqs. : {opf} {rpc_str}") + print(f" Path Rqs. : {opf} {rpc_str}") print(f" {ipf} {pburst_str}") if asr: - if ifstat["arxc"] and ifstat["atxc"]: - print(f" Announces : {ifstat['arxc']}↓ {ifstat['atxc']}↑ total") - print(f" {oaf} {pc_str}") - else: - print(f" Announces : {oaf} {pc_str}") + print(f" Announces : {oaf} {pc_str}") print(f" {iaf} {art_str}{burst_str}") rxstat = rxb_str @@ -740,11 +640,6 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= if "rxs" in ifstat and "txs" in ifstat: rxstat += " "+RNS.prettyspeed(ifstat["rxs"]) txstat += " "+RNS.prettyspeed(ifstat["txs"]) - - if ifstat["txbuffered"]: - if ifstat["txstalled"]: stlstr = ", stalled" - else: stlstr = "" - txstat += f" ({RNS.prettysize(ifstat['txbuffered'])} waiting{stlstr})" print(f" Traffic : {txstat}\n {rxstat}") @@ -756,96 +651,19 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= else: lstr = f" {link_count} entr{ms} in link table" - if active_link_count: lstr = f"{lstr} ({active_link_count} active)" - if traffic_totals: rxb_str = "↓"+RNS.prettysize(stats["rxb"]) txb_str = "↑"+RNS.prettysize(stats["txb"]) strdiff = len(rxb_str)-len(txb_str) - if strdiff > 0: txb_str += " "*strdiff - elif strdiff < 0: rxb_str += " "*-strdiff + if strdiff > 0: + txb_str += " "*strdiff + elif strdiff < 0: + rxb_str += " "*-strdiff rxstat = rxb_str+" "+RNS.prettyspeed(stats["rxs"]) txstat = txb_str+" "+RNS.prettyspeed(stats["txs"]) - - if pstats or astats: - if "prxs" in stats and "ptxs" in stats and "arxs" in stats and "atxs" in stats: - parxs = stats["prxs"]+stats["arxs"] - patxs = stats["ptxs"]+stats["atxs"] - drxpct = 100.0-min(100.0, (parxs / stats["rxs"])*100.0) if stats["rxs"] else 100.0 - dtxpct = 100.0-min(100.0, (patxs / stats["txs"])*100.0) if stats["txs"] else 100.0 - drxs = stats["rxs"]*(drxpct/100.0) - dtxs = stats["txs"]*(dtxpct/100.0) - - if stats["rxs"] > 0: rxstat = f"{rxstat}, {int(drxpct)}% data ({RNS.prettyspeed(drxs)})" - if stats["txs"] > 0: txstat = f"{txstat}, {int(dtxpct)}% data ({RNS.prettyspeed(dtxs)})" - - if pps: - rpps = RNS.prettysize(stats['rxpps'], suffix="pps") - tpps = RNS.prettysize(stats['txpps'], suffix="pps") - rxstat = f"{rxstat}, {rpps}" - txstat = f"{txstat}, {tpps}" - print(f"\n Totals : {txstat}\n {rxstat}") - if pstats: - if "prxb" in stats and "ptxb" in stats and "prxs" in stats and "ptxs" in stats: - prxb_str = "↓"+RNS.prettysize(stats["prxb"]) - ptxb_str = "↑"+RNS.prettysize(stats["ptxb"]) - strdiff = len(prxb_str)-len(ptxb_str) - if strdiff > 0: ptxb_str += " "*strdiff - elif strdiff < 0: prxb_str += " "*-strdiff - - prxspct = min(100.0, (stats["prxs"] / stats["rxs"])*100.0) if stats["rxs"] and stats["prxs"] else 0.0 - ptxspct = min(100.0, (stats["ptxs"] / stats["txs"])*100.0) if stats["txs"] and stats["ptxs"] else 0.0 - - prxfstr = RNS.prettyfrequency(stats["prxf"]) - ptxfstr = RNS.prettyfrequency(stats["ptxf"]) - - prxstat = prxb_str+" "+RNS.prettyspeed(stats["prxs"])+f", {int(prxspct)}% of flow, {prxfstr}" - ptxstat = ptxb_str+" "+RNS.prettyspeed(stats["ptxs"])+f", {int(ptxspct)}% of flow, {ptxfstr}" - print(f"\n Path Rqs. : {ptxstat}\n {prxstat}") - - if astats: - if "arxb" in stats and "atxb" in stats and "arxs" in stats and "atxs" in stats: - arxb_str = "↓"+RNS.prettysize(stats["arxb"]) - atxb_str = "↑"+RNS.prettysize(stats["atxb"]) - strdiff = len(arxb_str)-len(atxb_str) - if strdiff > 0: atxb_str += " "*strdiff - elif strdiff < 0: arxb_str += " "*-strdiff - - arxspct = min(100.0, (stats["arxs"] / stats["rxs"])*100.0) if stats["rxs"] and stats["arxs"] else 0.0 - atxspct = min(100.0, (stats["atxs"] / stats["txs"])*100.0) if stats["txs"] and stats["atxs"] else 0.0 - - arxfstr = RNS.prettyfrequency(stats["arxf"]) - atxfstr = RNS.prettyfrequency(stats["atxf"]) - - arxstat = arxb_str+" "+RNS.prettyspeed(stats["arxs"])+f", {int(arxspct)}% of flow, {arxfstr}" - atxstat = atxb_str+" "+RNS.prettyspeed(stats["atxs"])+f", {int(atxspct)}% of flow, {atxfstr}" - print(f"\n Announces : {atxstat}\n {arxstat}") - - - if queue_stats: - tqdp = f", {stats['rxqtd']} dropped" if stats['rxqtd'] else "" - dqdp = f", {stats['rxqdd']} dropped" if stats['rxqdd'] else "" - aqdp = f", {stats['rxqad']} dropped" if stats['rxqad'] else "" - pqdp = f", {stats['rxqpd']} dropped" if stats['rxqpd'] else "" - ilqdp = f", {stats['rxqild']} dropped" if stats['rxqild'] else "" - tqpress = f"{round(stats['tqpressure']*100.0, 1)}% total, {stats['rxqt']} pkts{tqdp}" - dqpress = f"{round(stats['dqpressure']*100.0, 1)}% data, {stats['rxqd']} pkts{dqdp}" - aqpress = f"{round(stats['aqpressure']*100.0, 1)}% announce, {stats['rxqa']} pkts{aqdp}" - pqpress = f"{round(stats['pqpressure']*100.0, 1)}% path request, {stats['rxqp']} pkts{pqdp}" - ilpress = f"{round(stats['ilqpressure']*100.0, 1)}% ingress limiter, {stats['rxqil']} pkts{ilqdp}" - - print(f"\n Qu. Pressure : {tqpress}") - print(f" {dqpress}") - print(f" {aqpress}") - print(f" {pqpress}") - print(f" {ilpress}") - - if profiling_results: - print(f"\n Profiling :\n{RNS.Profiler.format_results(profiling_results)}") - if "transport_id" in stats and stats["transport_id"] != None: print("\n Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" running") if "network_id" in stats and stats["network_id"] != None: @@ -876,21 +694,13 @@ def main(must_exit=True, rns_instance=None): parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) parser.add_argument("--version", action="version", version="rnstatus {version}".format(version=__version__)) - parser.add_argument("--attach", action="store", metavar="name", help="Attach interface by name", default=None, type=str) - parser.add_argument("--detach", action="store", metavar="name", help="Detach interface by name", default=None, type=str) - parser.add_argument("--reload", action="store", metavar="name", help="Reload interface by name", default=None, type=str) - parser.add_argument("-a", "--all", action="store_true", help="show all interfaces", default=False) parser.add_argument("-A", "--announce-stats", action="store_true", help="show announce stats", default=False) parser.add_argument("-P", "--pr-stats", action="store_true", help="show path request stats", default=False) parser.add_argument("-l", "--link-stats", action="store_true", help="show link stats", default=False) parser.add_argument("-B", "--burst", action="store_true", help="only show interfaces with active bursts", default=False) - parser.add_argument("-b", "--blocked-ips", action="store_true", help="show blocked IPs per interface", default=False) parser.add_argument("-t", "--totals", action="store_true", help="display traffic totals", default=False) - parser.add_argument("-p", "--pps", action="store_true", help="display packets per second in totals", default=False) - parser.add_argument("-q", "--queues", action="store_true", help="display queue stats", default=False) - parser.add_argument("-z", "--profiling", action="store_true", help="display live profiling results", default=False) - parser.add_argument("-s", "--sort", action="store", help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, anns, arx, atx, arxc, atxc, held, prx, ptx, prxc, ptxc, pvs, ivs, flt, txdrp, txdrb, txbuf]", default=None, type=str) + parser.add_argument("-s", "--sort", action="store", help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, prx, ptx, held]", default=None, type=str) parser.add_argument("-r", "--reverse", action="store_true", help="reverse sorting", default=False) parser.add_argument("-j", "--json", action="store_true", help="output in JSON format", default=False) parser.add_argument("-R", action="store", metavar="hash", help="transport identity hash of remote instance to get status from", default=None, type=str) @@ -898,8 +708,6 @@ def main(must_exit=True, rns_instance=None): parser.add_argument("-w", action="store", metavar="seconds", type=float, help="timeout before giving up on remote queries", default=RNS.Transport.PATH_REQUEST_TIMEOUT) parser.add_argument("-d", "--discovered", action="store_true", help="list discovered interfaces", default=False) parser.add_argument("-D", action="store_true", help="show details and config entries for discovered interfaces", default=False) - parser.add_argument("--show-stale", action="store_true", help="show stale discovery entries", default=False) - parser.add_argument("--show-unknown", action="store_true", help="show discovery entries without version info", default=False) parser.add_argument("-m", "--monitor", action="store_true", help="continuously monitor status", default=False) parser.add_argument("-I", "--monitor-interval", action="store", metavar="seconds", type=float, help="refresh interval for monitor mode (default: 1)", default=1.0) parser.add_argument('-v', '--verbose', action='count', default=0) @@ -929,9 +737,7 @@ def main(must_exit=True, rns_instance=None): program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json, astats=args.announce_stats, pstats=args.pr_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R, management_identity=args.i, remote_timeout=args.w, must_exit=False, rns_instance=reticulum, - traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst, - blocked_ips=args.blocked_ips, queue_stats=args.queues, pps=args.pps, profiling=args.profiling, - show_stale=args.show_stale, show_unknown=args.show_unknown) + traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst) finally: sys.stdout = old_stdout @@ -948,9 +754,7 @@ def main(must_exit=True, rns_instance=None): program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json, astats=args.announce_stats, pstats=args.pr_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R, management_identity=args.i, remote_timeout=args.w, must_exit=must_exit, rns_instance=rns_instance, - traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst, - blocked_ips=args.blocked_ips, queue_stats=args.queues, pps=args.pps, profiling=args.profiling, - attach=args.attach, detach=args.detach, reload=args.reload, show_stale=args.show_stale, show_unknown=args.show_unknown) + traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst) except KeyboardInterrupt: print("") diff --git a/RNS/Utilities/rnx.py b/RNS/Utilities/rnx.py index 4bfcf83b..0be8219b 100644 --- a/RNS/Utilities/rnx.py +++ b/RNS/Utilities/rnx.py @@ -347,7 +347,7 @@ def execute(configdir, identitypath = None, verbosity = 0, quietness = 0, detail if not RNS.Transport.has_path(destination_hash): RNS.Transport.request_path(destination_hash) - if not spin(until=lambda: RNS.Transport.has_path(destination_hash), msg="Path to "+RNS.prettyhexrep(destination_hash)+" requested", timeout=max(timeout, reticulum.get_medium_path_timeout())): + if not spin(until=lambda: RNS.Transport.has_path(destination_hash), msg="Path to "+RNS.prettyhexrep(destination_hash)+" requested", timeout=timeout): print("Path not found") exit(242) @@ -365,7 +365,7 @@ def execute(configdir, identitypath = None, verbosity = 0, quietness = 0, detail link = RNS.Link(listener_destination) link.did_identify = False - if not spin(until=lambda: link.status == RNS.Link.ACTIVE, msg="Establishing link with "+RNS.prettyhexrep(destination_hash), timeout=max(timeout, link.establishment_timeout)): + if not spin(until=lambda: link.status == RNS.Link.ACTIVE, msg="Establishing link with "+RNS.prettyhexrep(destination_hash), timeout=timeout): print("Could not establish link with "+RNS.prettyhexrep(destination_hash)) exit(243) diff --git a/RNS/__init__.py b/RNS/__init__.py index 81f9b75d..e38684cd 100755 --- a/RNS/__init__.py +++ b/RNS/__init__.py @@ -35,25 +35,32 @@ import time import datetime import random import threading -import math -import bisect from ._version import __version__ -from collections import deque -from functools import partial, wraps + +from .Reticulum import Reticulum +from .Identity import Identity +from .Link import Link, RequestReceipt +from .Channel import MessageBase +from .Buffer import Buffer, RawChannelReader, RawChannelWriter +from .Transport import Transport +from .Discovery import InterfaceAnnouncer +from .Destination import Destination +from .Packet import Packet +from .Packet import PacketReceipt +from .Resolver import Resolver +from .Resource import Resource, ResourceAdvertisement +from .Cryptography import HKDF +from .Cryptography import Hashes py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") -so_modules = glob.glob(os.path.dirname(__file__)+"/*.so") -pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd") -modules = py_modules+pyc_modules+so_modules+pyd_modules -__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"])) +modules = py_modules+pyc_modules +__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) import importlib.util -try: from ._buildinfo import compiled -except ImportError: - if importlib.util.find_spec("cython"): import cython; compiled = cython.compiled - else: compiled = False +if importlib.util.find_spec("cython"): import cython; compiled = cython.compiled +else: compiled = False LOG_NONE = -1 LOG_CRITICAL = 0 @@ -154,6 +161,7 @@ def log(msg, level=3, _override_destination = False, pt=False): log("Exception occurred while calling external log handler: "+str(e), LOG_CRITICAL) log("Dumping future log events to console!", LOG_CRITICAL) log(msg, level) + def rand(): result = instance_random.random() @@ -283,7 +291,7 @@ def prettytime(time, verbose=False, compact=False): if not neg: return tstr else: return f"-{tstr}" -def prettyshorttime(time, verbose=False, compact=False, tight=False): +def prettyshorttime(time, verbose=False, compact=False): neg = False time = time*1e6 if time < 0: @@ -319,8 +327,8 @@ def prettyshorttime(time, verbose=False, compact=False, tight=False): for c in components: i += 1 if i == 1: pass - elif i < len(components): tstr += ", " if not tight else " " - elif i == len(components): tstr += " and " if not tight else " " + elif i < len(components): tstr += ", " + elif i == len(components): tstr += " and " tstr += c @@ -357,50 +365,20 @@ class Profiler: profilers = {} tags = {} - # Samples per tag - MAX_CAPTURES = 10000 - @staticmethod - def get_profiler(tag=None, super_tag=None, max_captures=None): + def get_profiler(tag=None, super_tag=None): if tag in Profiler.profilers: return Profiler.profilers[tag] else: - if max_captures is None: max_captures = Profiler.MAX_CAPTURES - profiler = Profiler(tag, super_tag, max_captures) + profiler = Profiler(tag, super_tag) Profiler.profilers[tag] = profiler return profiler - @staticmethod - def profile(func=None, *, tag=None, max_captures=None): - """ - Decorator to profile a function. With no arguments, it can be - used as `@RNS.Profiler.profile`, in which case it will use the - qualified name of the function as the profiler tag. It can - also be used as `@RNS.Profiler.profile(tag="prof_tag")` to use - a specific profiler tag. - - :param tag: Either a profiler tag as passed to `Profiler.get_profiler(...)` or a `Profiler` object. - :param max_captures: Maximum samples to capture for the specified profiler tag. - """ - if func is None: - return partial(Profiler.profile, tag=tag, max_captures=max_captures) - - tag = func.__qualname__ if tag is None else tag - if isinstance(tag, Profiler): profiler = tag - else: profiler = Profiler.get_profiler(tag=tag, max_captures=max_captures) - - @wraps(func) - def wrapper(*args, **kwargs): - with profiler: - return func(*args, **kwargs) - return wrapper - - def __init__(self, tag=None, super_tag=None, max_captures=None): + def __init__(self, tag=None, super_tag=None): self.paused = False self.pause_time = 0 self.pause_started = None self.tag = tag self.super_tag = super_tag - self.max_captures = max_captures if max_captures is not None else Profiler.MAX_CAPTURES if self.super_tag in Profiler.profilers: self.super_profiler = Profiler.profilers[self.super_tag] @@ -418,18 +396,11 @@ class Profiler: tag = self.tag super_tag = self.super_tag thread_ident = threading.get_ident() - if not tag in Profiler.tags: Profiler.tags[tag] = {"threads": {}, "super": super_tag, "captures": deque(maxlen=self.max_captures)} + if not tag in Profiler.tags: Profiler.tags[tag] = {"threads": {}, "super": super_tag} if not thread_ident in Profiler.tags[tag]["threads"]: - Profiler.tags[tag]["threads"][thread_ident] = {"running": deque()} + Profiler.tags[tag]["threads"][thread_ident] = {"current_start": None, "captures": []} - # The tag deque stores a shared reference to the capture, and the end - # time isn't updated until the context manager exits. This leaves the - # deque sorted by start time, except for cases where the thread is - # preemted between getting the current time and appending. We also store - # a stack of start times per thread to support reentrancy. - capture = {"start": time.perf_counter(), "end": None, "thread_ident": thread_ident} - Profiler.tags[tag]["captures"].append(capture) - Profiler.tags[tag]["threads"][thread_ident]["running"].append(capture) + Profiler.tags[tag]["threads"][thread_ident]["current_start"] = time.perf_counter() self.resume_super() def __exit__(self, exc_type, exc_value, traceback): @@ -439,15 +410,14 @@ class Profiler: end = time.perf_counter() - self.pause_time self.pause_time = 0 thread_ident = threading.get_ident() - try: - if tag in Profiler.tags and thread_ident in Profiler.tags[tag]["threads"]: - try: capture = Profiler.tags[tag]["threads"][thread_ident]["running"].pop() - except IndexError as e: return - capture["end"] = end + if tag in Profiler.tags and thread_ident in Profiler.tags[tag]["threads"]: + if Profiler.tags[tag]["threads"][thread_ident]["current_start"] != None: + begin = Profiler.tags[tag]["threads"][thread_ident]["current_start"] + Profiler.tags[tag]["threads"][thread_ident]["current_start"] = None + Profiler.tags[tag]["threads"][thread_ident]["captures"].append(end-begin) if not Profiler._ran: Profiler._ran = True - finally: - self.resume_super() + self.resume_super() def pause(self, pause_started=None): if not self.paused: @@ -466,153 +436,78 @@ class Profiler: @staticmethod def results(): - def find_window_start(captures, start_time, hi=None): - hi = len(captures) if hi is None else hi - idx = bisect.bisect_left(captures, start_time, hi=hi, key=lambda c: c["start"]) - return idx if len(captures) - idx > 1 else None - - # Fast one-pass calculation of summary statistics - def calc_stats(captures, start=0, end=None, key=lambda c: c, threads_key=None): - if end is None: end = len(captures) - while start > 0 and start < len(captures) and start < end and key(captures[start]) is None: start += 1 - while end > 0 and end <= len(captures) and start < end and key(captures[end-1]) is None: end -= 1 - count = end - start - - if count <= 0: return None - elif count == 1: - capture = key(captures[start]) - return { "count": 1, - "mean": capture, - "median": capture, - "min": capture, - "max": capture, - "stdev": None, - "sum": capture, - "threads": 1 if threads_key else None} - - # Median is approximate if there are any incomplete captures in the - # middle of the range. - med_even = count % 2 == 0 - med_idx = start + (count // 2 if med_even else (count - 1) // 2) - while key(captures[med_idx]) is None and med_idx < end: med_idx += 1 - if med_idx == end: c_median = None - elif med_even: c_median = key(captures[med_idx]) - else: - med_idx2 = med_idx + 1 - while key(captures[med_idx2]) is None and med_idx2 < end: med_idx2 += 1 - if med_idx2 == end: c_median = None - else: - med_cap1 = key(captures[med_idx]) - med_cap2 = key(captures[med_idx+1]) - if med_cap1 is None or med_cap2 is None: c_median = None - else: c_median = (med_cap1 + med_cap2) / 2 - - c_count = 0; c_sum = 0; c_min = key(captures[start]); c_max = key(captures[start]); ck = 0; ck2 = 0 - uniq_threads = set() - for idx in range(start, end): - c = key(captures[idx]) - if c is None: continue - else: c_count += 1 - c_sum += c - if c < c_min: c_min = c - if c > c_max: c_max = c - ck += c - c_median - ck2 += (c - c_median) ** 2 - if threads_key: uniq_threads.add(threads_key(captures[idx])) - c_mean = c_sum / c_count if c_count > 0 else None - c_std = math.sqrt((ck2 - (ck ** 2)/c_count) / (c_count - 1)) if c_count > 1 else None - - return { "count": c_count, - "mean": c_mean, - "median": c_median, - "min": c_min, - "max": c_max, - "stdev": c_std, - "sum": c_sum, - "threads": len(uniq_threads) if threads_key else None } - - def stats_key(capture): - end = capture["end"] - if end is None: return None - else: return end - capture["start"] - + from statistics import mean, median, stdev results = {} - now = time.perf_counter() - for tag in sorted(Profiler.tags): + + for tag in Profiler.tags: + tag_captures = [] tag_entry = Profiler.tags[tag] - tag_captures = sorted(tag_entry["captures"], key=lambda c: c["start"]) + + for thread_ident in tag_entry["threads"]: + thread_entry = tag_entry["threads"][thread_ident] + thread_captures = thread_entry["captures"] + sample_count = len(thread_captures) + + if sample_count > 1: + thread_results = { "count": sample_count, + "mean": mean(thread_captures), + "median": median(thread_captures), + "stdev": stdev(thread_captures) } + + elif sample_count == 1: + thread_results = { "count": sample_count, + "mean": mean(thread_captures), + "median": median(thread_captures), + "stdev": None } - if len(tag_captures): - captures_1m = None; captures_5m = None; captures_30m = None; captures_60m = None - stats_1m = None; stats_5m = None; stats_30m = None; stats_60m = None + tag_captures.extend(thread_captures) - captures_1m = find_window_start(tag_captures, now - 1*60) - if captures_1m: captures_5m = find_window_start(tag_captures, now - 5*60, hi=captures_1m) - if captures_5m: captures_30m = find_window_start(tag_captures, now - 30*60, hi=captures_5m) - if captures_30m: captures_60m = find_window_start(tag_captures, now - 60*60, hi=captures_30m) + sample_count = len(tag_captures) + if sample_count > 1: + tag_results = { "name": tag, + "super": tag_entry["super"], + "count": len(tag_captures), + "mean": mean(tag_captures), + "median": median(tag_captures), + "stdev": stdev(tag_captures) } + + elif sample_count == 1: + tag_results = { "name": tag, + "super": tag_entry["super"], + "count": len(tag_captures), + "mean": mean(tag_captures), + "median": median(tag_captures), + "stdev": None } - stats_all = calc_stats(tag_captures, 0, None, key=stats_key, threads_key=lambda c: c["thread_ident"]) - if captures_1m: stats_1m = calc_stats(tag_captures, captures_1m, None, key=stats_key) - if captures_5m: stats_5m = calc_stats(tag_captures, captures_5m, captures_1m, key=stats_key) - if captures_30m: stats_30m = calc_stats(tag_captures, captures_30m, captures_5m, key=stats_key) - if captures_60m: stats_60m = calc_stats(tag_captures, captures_60m, captures_30m, key=stats_key) - - if stats_all["count"]: - results[tag] = { "name": tag, - "super": tag_entry["super"], - "stats_all": stats_all, - "stats_1m": stats_1m, - "stats_5m": stats_5m, - "stats_30m": stats_30m, - "stats_60m": stats_60m } - - # Yield to avoid bogging down the instance - time.sleep(0.001) - - return results - - @staticmethod - def format_results(results): - def pst(time): - if time is not None: return prettyshorttime(time, tight=True) - else: return "-----" + results[tag] = tag_results def print_results_recursive(tag, results, level=0): - results_str = print_tag_results(tag, level+1) + "\n" + print_tag_results(tag, level+1) for tag_name in results: sub_tag = results[tag_name] if sub_tag["super"] == tag["name"]: - results_str += print_results_recursive(sub_tag, results, level=level+1) + print_results_recursive(sub_tag, results, level=level+1) - return results_str def print_tag_results(tag, level): ind = " "*level - name = tag["name"] - stats_all = tag["stats_all"]; stats_1m = tag["stats_1m"]; stats_5m = tag["stats_5m"]; stats_30m = tag["stats_30m"]; stats_60m = tag["stats_60m"] - results_str = f" {ind}{name}\n" - results_str += f" {ind} Samples : {stats_all['count']} from {stats_all['threads']} thread{'s' if stats_all['threads'] > 1 else ''}\n" - if stats_all != None: - results_str += f" {ind} {'Mean':^15} | {'Median':^15} | {'Min':^15} | {'Max':^15} | {'St. dev':^15} | {'Total':^15}\n" - results_str += f" {ind} Stats : ({pst(stats_all['mean']):^15} | {pst(stats_all['median']):^15} | {pst(stats_all['min']):^15} | {pst(stats_all['max']):^15} | {pst(stats_all['stdev']):^15} | {pst(stats_all['sum']):^15})\n" - if stats_1m != None: - results_str += f" {ind} 0-1m : ({pst(stats_1m['mean']):^15} | {pst(stats_1m['median']):^15} | {pst(stats_1m['min']):^15} | {pst(stats_1m['max']):^15} | {pst(stats_1m['stdev']):^15} | {pst(stats_1m['sum']):^15})\n" - if stats_5m != None: - results_str += f" {ind} 1-5m : ({pst(stats_5m['mean']):^15} | {pst(stats_5m['median']):^15} | {pst(stats_5m['min']):^15} | {pst(stats_5m['max']):^15} | {pst(stats_5m['stdev']):^15} | {pst(stats_5m['sum']):^15})\n" - if stats_30m != None: - results_str += f" {ind} 5-30m : ({pst(stats_30m['mean']):^15} | {pst(stats_30m['median']):^15} | {pst(stats_30m['min']):^15} | {pst(stats_30m['max']):^15} | {pst(stats_30m['stdev']):^15} | {pst(stats_30m['sum']):^15})\n" - if stats_60m != None: - results_str += f" {ind} 30-60m : ({pst(stats_60m['mean']):^15} | {pst(stats_60m['median']):^15} | {pst(stats_60m['min']):^15} | {pst(stats_60m['max']):^15} | {pst(stats_60m['stdev']):^15} | {pst(stats_60m['sum']):^15})\n" - return results_str + name = tag["name"]; count = tag["count"] + mean = tag["mean"]; median = tag["median"]; stdev = tag["stdev"] + print( f"{ind}{name}") + print( f"{ind} Samples : {count}") + if stdev != None: + print(f"{ind} Mean : {prettyshorttime(mean)}") + print(f"{ind} Median : {prettyshorttime(median)}") + print(f"{ind} St.dev. : {prettyshorttime(stdev)}") + print( f"{ind} Total : {prettyshorttime(mean*count)}") + print("") - results_str = "" + print("\nProfiler results:\n") for tag_name in results: tag = results[tag_name] if tag["super"] == None: - results_str += print_results_recursive(tag, results) - - return results_str + print_results_recursive(tag, results) profile = Profiler.get_profiler @@ -662,19 +557,3 @@ def bytes_to_b256(data): if not type(data) == bytes: raise TypeError("Invalid input data for base256 encode") try: return [byte_to_b256(c) for c in data] except Exception as e: raise TypeError(f"Could not encode to base256: {e}") - - -from .Reticulum import Reticulum -from .Identity import Identity -from .Link import Link, RequestReceipt -from .Channel import MessageBase -from .Buffer import Buffer, RawChannelReader, RawChannelWriter -from .Transport import Transport -from .Discovery import InterfaceAnnouncer -from .Destination import Destination -from .Packet import Packet -from .Packet import PacketReceipt -from .Resolver import Resolver -from .Resource import Resource, ResourceAdvertisement -from .Cryptography import HKDF -from .Cryptography import Hashes \ No newline at end of file diff --git a/RNS/_version.py b/RNS/_version.py index 8c118e61..daa50c7c 100644 --- a/RNS/_version.py +++ b/RNS/_version.py @@ -1 +1 @@ -__version__ = "1.5.5" +__version__ = "1.4.2" diff --git a/RNS/vendor/__init__.py b/RNS/vendor/__init__.py index 7ec88c11..ccfb449d 100755 --- a/RNS/vendor/__init__.py +++ b/RNS/vendor/__init__.py @@ -3,7 +3,5 @@ import glob py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") -so_modules = glob.glob(os.path.dirname(__file__)+"/*.so") -pyd_modules = glob.glob(os.path.dirname(__file__)+"/*.pyd") -modules = py_modules+pyc_modules+so_modules+pyd_modules -__all__ = list(set([os.path.basename(f).split(".")[0] for f in modules if not os.path.basename(f).split(".")[0] == "__init__"])) \ No newline at end of file +modules = py_modules+pyc_modules +__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) \ No newline at end of file diff --git a/RNS/vendor/umsgpack.py b/RNS/vendor/umsgpack.py index 56fc3709..c6fa1e7d 100644 --- a/RNS/vendor/umsgpack.py +++ b/RNS/vendor/umsgpack.py @@ -2,7 +2,7 @@ # https://github.com/vsergeev/u-msgpack-python # # u-msgpack-python is a lightweight MessagePack serializer and deserializer -# module, compatible with Python 3, as well CPython and PyPy +# module, compatible with both Python 2 and 3, as well CPython and PyPy # implementations of Python. u-msgpack-python is fully compliant with the # latest MessagePack specification.com/msgpack/msgpack/blob/master/spec.md). In # particular, it supports the new binary, UTF-8 string, and application ext @@ -35,7 +35,7 @@ u-msgpack-python v2.7.1 - v at sergeev.io https://github.com/vsergeev/u-msgpack-python u-msgpack-python is a lightweight MessagePack serializer and deserializer -module, compatible with Python 3, as well CPython and PyPy +module, compatible with both Python 2 and 3, as well CPython and PyPy implementations of Python. u-msgpack-python is fully compliant with the latest MessagePack specification.com/msgpack/msgpack/blob/master/spec.md). In particular, it supports the new binary, UTF-8 string, and application ext @@ -49,7 +49,10 @@ import datetime import sys import io -from collections.abc import Hashable +if sys.version_info[0:2] >= (3, 3): + from collections.abc import Hashable +else: + from collections import Hashable __version__ = "2.7.1" "Module version string" @@ -101,6 +104,8 @@ class Ext(object): # Check data is type bytes or str elif sys.version_info[0] == 3 and not isinstance(data, bytes): raise TypeError("ext data is not type \'bytes\'") + elif sys.version_info[0] == 2 and not isinstance(data, str): + raise TypeError("ext data is not type \'str\'") self.type = type self.data = data @@ -124,7 +129,7 @@ class Ext(object): """ s = "Ext Object (Type: {:d}, Data: ".format(self.type) s += " ".join(["0x{:02}".format(ord(self.data[i:i + 1])) - for i in range(min(len(self.data), 8))]) + for i in xrange(min(len(self.data), 8))]) if len(self.data) > 8: s += " ..." s += ")" @@ -447,6 +452,94 @@ def _pack_map(obj, fp, options): ######################################## + +# Pack for Python 2, with 'unicode' type, 'str' type, and 'long' type +def _pack2(obj, fp, **options): + """ + Serialize a Python object into MessagePack bytes. + + Args: + obj: a Python object + fp: a .write()-supporting file-like object + + Kwargs: + ext_handlers (dict): dictionary of Ext handlers, mapping a custom type + to a callable that packs an instance of the type + into an Ext object + force_float_precision (str): "single" to force packing floats as + IEEE-754 single-precision floats, + "double" to force packing floats as + IEEE-754 double-precision floats. + + Returns: + None. + + Raises: + UnsupportedType(PackException): + Object type not supported for packing. + + Example: + >>> f = open('test.bin', 'wb') + >>> umsgpack.pack({u"compact": True, u"schema": 0}, f) + >>> + """ + global compatibility + + ext_handlers = options.get("ext_handlers") + + if obj is None: + _pack_nil(obj, fp, options) + elif ext_handlers and obj.__class__ in ext_handlers: + _pack_ext(ext_handlers[obj.__class__](obj), fp, options) + elif obj.__class__ in _ext_class_to_type: + try: + _pack_ext(Ext(_ext_class_to_type[obj.__class__], obj.packb()), fp, options) + except AttributeError: + raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(obj.__class__))) + elif isinstance(obj, bool): + _pack_boolean(obj, fp, options) + elif isinstance(obj, (int, long)): + _pack_integer(obj, fp, options) + elif isinstance(obj, float): + _pack_float(obj, fp, options) + elif compatibility and isinstance(obj, unicode): + _pack_oldspec_raw(bytes(obj), fp, options) + elif compatibility and isinstance(obj, bytes): + _pack_oldspec_raw(obj, fp, options) + elif isinstance(obj, unicode): + _pack_string(obj, fp, options) + elif isinstance(obj, str): + _pack_binary(obj, fp, options) + elif isinstance(obj, (list, tuple)): + _pack_array(obj, fp, options) + elif isinstance(obj, dict): + _pack_map(obj, fp, options) + elif isinstance(obj, datetime.datetime): + _pack_ext_timestamp(obj, fp, options) + elif isinstance(obj, Ext): + _pack_ext(obj, fp, options) + elif ext_handlers: + # Linear search for superclass + t = next((t for t in ext_handlers.keys() if isinstance(obj, t)), None) + if t: + _pack_ext(ext_handlers[t](obj), fp, options) + else: + raise UnsupportedTypeException( + "unsupported type: {:s}".format(str(type(obj)))) + elif _ext_class_to_type: + # Linear search for superclass + t = next((t for t in _ext_class_to_type if isinstance(obj, t)), None) + if t: + try: + _pack_ext(Ext(_ext_class_to_type[t], obj.packb()), fp, options) + except AttributeError: + raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(t))) + else: + raise UnsupportedTypeException("unsupported type: {:s}".format(str(type(obj)))) + else: + raise UnsupportedTypeException("unsupported type: {:s}".format(str(type(obj)))) + + # Pack for Python 3, with unicode 'str' type, 'bytes' type, and no 'long' type def _pack3(obj, fp, **options): """ @@ -534,6 +627,40 @@ def _pack3(obj, fp, **options): raise UnsupportedTypeException( "unsupported type: {:s}".format(str(type(obj)))) + +def _packb2(obj, **options): + """ + Serialize a Python object into MessagePack bytes. + + Args: + obj: a Python object + + Kwargs: + ext_handlers (dict): dictionary of Ext handlers, mapping a custom type + to a callable that packs an instance of the type + into an Ext object + force_float_precision (str): "single" to force packing floats as + IEEE-754 single-precision floats, + "double" to force packing floats as + IEEE-754 double-precision floats. + + Returns: + A 'str' containing serialized MessagePack bytes. + + Raises: + UnsupportedType(PackException): + Object type not supported for packing. + + Example: + >>> umsgpack.packb({u"compact": True, u"schema": 0}) + '\x82\xa7compact\xc3\xa6schema\x00' + >>> + """ + fp = io.BytesIO() + _pack2(obj, fp, **options) + return fp.getvalue() + + def _packb3(obj, **options): """ Serialize a Python object into MessagePack bytes. @@ -758,9 +885,9 @@ def _unpack_array(code, fp, options): raise Exception("logic error, not array: 0x{:02x}".format(ord(code))) if options.get('use_tuple'): - return tuple((_unpack(fp, options) for i in range(length))) + return tuple((_unpack(fp, options) for i in xrange(length))) - return [_unpack(fp, options) for i in range(length)] + return [_unpack(fp, options) for i in xrange(length)] def _deep_list_to_tuple(obj): @@ -780,7 +907,7 @@ def _unpack_map(code, fp, options): raise Exception("logic error, not map: 0x{:02x}".format(ord(code))) d = {} if not options.get('use_ordered_dict') else collections.OrderedDict() - for _ in range(length): + for _ in xrange(length): # Unpack key k = _unpack(fp, options) @@ -811,6 +938,53 @@ def _unpack(fp, options): ######################################## + +def _unpack2(fp, **options): + """ + Deserialize MessagePack bytes into a Python object. + + Args: + fp: a .read()-supporting file-like object + + Kwargs: + ext_handlers (dict): dictionary of Ext handlers, mapping integer Ext + type to a callable that unpacks an instance of + Ext into an object + use_ordered_dict (bool): unpack maps into OrderedDict, instead of + unordered dict (default False) + use_tuple (bool): unpacks arrays into tuples, instead of lists (default + False) + allow_invalid_utf8 (bool): unpack invalid strings into instances of + InvalidString, for access to the bytes + (default False) + + Returns: + A Python object. + + Raises: + InsufficientDataException(UnpackException): + Insufficient data to unpack the serialized object. + InvalidStringException(UnpackException): + Invalid UTF-8 string encountered during unpacking. + UnsupportedTimestampException(UnpackException): + Unsupported timestamp format encountered during unpacking. + ReservedCodeException(UnpackException): + Reserved code encountered during unpacking. + UnhashableKeyException(UnpackException): + Unhashable key encountered during map unpacking. + The serialized map cannot be deserialized into a Python dictionary. + DuplicateKeyException(UnpackException): + Duplicate key encountered during map unpacking. + + Example: + >>> f = open('test.bin', 'rb') + >>> umsgpack.unpackb(f) + {u'compact': True, u'schema': 0} + >>> + """ + return _unpack(fp, options) + + def _unpack3(fp, **options): """ Deserialize MessagePack bytes into a Python object. @@ -975,11 +1149,27 @@ def __init(): global _utc_tzinfo global _float_precision global _unpack_dispatch_table + global xrange # Compatibility mode for handling strings/bytes with the old specification compatibility = False - _utc_tzinfo = datetime.timezone.utc + if sys.version_info[0] == 3: + _utc_tzinfo = datetime.timezone.utc + else: + class UTC(datetime.tzinfo): + ZERO = datetime.timedelta(0) + + def utcoffset(self, dt): + return UTC.ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return UTC.ZERO + + _utc_tzinfo = UTC() # Calculate an aware epoch datetime _epoch = datetime.datetime(1970, 1, 1, tzinfo=_utc_tzinfo) @@ -991,14 +1181,25 @@ def __init(): _float_precision = "single" # Map packb and unpackb to the appropriate version - pack = _pack3 - packb = _packb3 - dump = _pack3 - dumps = _packb3 - unpack = _unpack3 - unpackb = _unpackb3 - load = _unpack3 - loads = _unpackb3 + if sys.version_info[0] == 3: + pack = _pack3 + packb = _packb3 + dump = _pack3 + dumps = _packb3 + unpack = _unpack3 + unpackb = _unpackb3 + load = _unpack3 + loads = _unpackb3 + xrange = range + else: + pack = _pack2 + packb = _packb2 + dump = _pack2 + dumps = _packb2 + unpack = _unpack2 + unpackb = _unpackb2 + load = _unpack2 + loads = _unpackb2 # Build a dispatch table for fast lookup of unpacking function diff --git a/Roadmap.md b/Roadmap.md new file mode 100644 index 00000000..5689e309 --- /dev/null +++ b/Roadmap.md @@ -0,0 +1,94 @@ +# Reticulum Development Roadmap +This document outlines the currently established development roadmap for Reticulum. + +1. [Currently Active Work Areas](#currently-active-work-areas) +2. [Primary Efforts](#primary-efforts) + - [Comprehensibility](#comprehensibility) + - [Universality](#universality) + - [Functionality](#functionality) + - [Usability & Utility](#usability--utility) + - [Interfaceability](#interfaceability) +3. [Auxillary Efforts](#auxillary-efforts) +4. [Release History](#release-history) + +## Currently Active Work Areas +For each release cycle of Reticulum, improvements and additions from the five [Primary Efforts](#primary-efforts) are selected as active work areas, and can be expected to be included in the upcoming releases within that cycle. While not entirely set in stone for each release cycle, they serve as a pointer of what to expect in the near future. + +## Primary Efforts +The development path for Reticulum is currently laid out in five distinct areas: *Comprehensibility*, *Universality*, *Functionality*, *Usability & Utility* and *Interfaceability*. Conceptualising the development of Reticulum into these areas serves to advance the implementation and work towards the Foundational Goals & Values of Reticulum. + +### Comprehensibility +These efforts are aimed at improving the ease of which Reticulum is understood, and lowering the barrier to entry for people who wish to start building systems on Reticulum. + +- Improving [the manual](https://markqvist.github.io/Reticulum/manual/) with tutorials specifically for beginners +- Updating the documentation to reflect recent changes and improvements + - Update descriptions of protocol mechanics + - Update announce description + - Add in-depth explanation of the IFAC system + - Software + - Update software descriptions and screenshots + - Communications hardware section + - Add information about RNode external displays. + - Possibly add other relevant types here as well. + - Setup *Best Practices For...* / *Installation Examples* section. + - Home or office (example) + - Vehicles (example) + - No-grid/solar/remote sites (example) + +### Universality +These efforts seek to broaden the universality of the Reticulum software and hardware ecosystem by continously diversifying platform support, and by improving the overall availability and ease of deployment of the Reticulum stack. + +- Create a standalone RNS Daemon app for Android +- A lightweight and portable C implementation for microcontrollers, µRNS +- A portable, high-performance Reticulum implementation in C/C++, see [#21](https://github.com/markqvist/Reticulum/discussions/21) +- Bindings for other programming languages + +### Functionality +These efforts aim to expand and improve the core functionality and reliability of Reticulum. + +- Add interface hot-plug and live up/down control to running instances +- Add automatic retries to all use cases of the `Request` API +- Network-wide path balancing +- Distributed Destination Naming System +- Globally routable multicast +- Destination proxying +- [Metric-based path selection and multiple paths](https://github.com/markqvist/Reticulum/discussions/86) + +### Usability & Utility +These efforts seek to make Reticulum easier to use and operate, and to expand the utility of the stack on deployed systems. + +- Easy way to share interface configurations, see [#19](https://github.com/markqvist/Reticulum/discussions/19) +- Transit traffic display in `rnstatus` +- `rnsconfig` utility + +### Interfaceability +These efforts aim to expand the types of physical and virtual interfaces that Reticulum can natively use to transport data. + +- Plain ESP32 devices (ESP-Now, WiFi, Bluetooth, etc.) +- More LoRa transceivers +- AT-compatible modems +- Filesystem interface +- Direct SDR Support +- Optical mediums +- IR Transceivers +- AWDL / OWL +- HF Modems +- GNU Radio +- CAN-bus +- Raw SPI +- Raw i²c +- MQTT +- XBee +- Tor + +## Auxillary Efforts +The Reticulum ecosystem is enriched by several other software and hardware projects, and the support and improvement of these, in symbiosis with the core Reticulum project helps expand the reach and utility of Reticulum itself. + +This section lists, in no particular order, various important efforts that would be beneficial to the goals of Reticulum. + +- The [RNode](https://unsigned.io/rnode/) project + - [x] Create a WebUSB-based bootstrapping utility, and integrate this directly into the [RNode Bootstrap Console](#), both on-device, and on an Internet-reachable copy. This will make it much easier to create new RNodes for average users. + +## Release History + +Please see the [Changelog](./Changelog.md) for a complete release history and changelog of Reticulum. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index dae1b7f3..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,5 +0,0 @@ -# Reporting Security Issues - -It goes without saying: If you have discovered a security issue in Reticulum or any related project, please report it responsibly to the maintainer over LXMF at `<8dd57a738226809646089335a6b03695>`. If you wish to remain anonymous, please create a disposable identity to send from, and notify us of any such wishes. - -If you are not able to use LXMF, you can send an email to the address currently posted on the maintainer's profile on this site. diff --git a/This Is Not a Teahouse.md b/This Is Not a Teahouse.md index d56a00b6..754769be 100644 --- a/This Is Not a Teahouse.md +++ b/This Is Not a Teahouse.md @@ -2,7 +2,7 @@ Recently, and mostly from people who I've never seen before, the opinions about - The decision to no longer mirror release notes on GitHub. - Some people feeling there were too many "barriers to entry" to joining RNS development. -- The project not really being "[open source](https://reticulum.network/manual/brandolinis.html#open-source-means-open-source)" because random strangers couldn't just "contribute". +- The project not really being "open source" because random strangers couldn't just "contribute". Joakim posted some very relevant observations about how Reticulum operates, along with the following quote: @@ -10,7 +10,7 @@ Joakim posted some very relevant observations about how Reticulum operates, alon That E. F. Schumacher quote perfectly illustrates the ontological schism that makes it so tiresome to deal with stuff like this. -There is, in this day and age, between different people, widely different base conceptual integrations of what "open source" means. For many people, "open source" has become synonymous **not** with skilled people working together in a coordinated and careful way on complex engineering challenges, but a sort of growth- and attention-focused "free-for-all" *behavioral* codex that must be followed above all else; a *social* modus operandi of fake inclusivity where everyone "should have their voice heard", and adherence to that specific *process* is weighed much higher than the final results. +There is, in this day and age, between different people, widely different base conceptual integrations of what "open source" means. For many people, "open source" has become synonymous **not** with skilled people working together in a coordinated and careful way on complex engineering challenges, but a sort of growth- and attention-focused "free-for-all" *behavioral* codex that must be followed above all else; a *social* modus operandi of fake inclusivity where everyone "should have their voice heard", and adherence to that specific process is weighed much higher than the final results. I do not subscribe to, and consequently do not operate the Reticulum project under *any* versions of that idea. @@ -28,7 +28,7 @@ I do not subscribe to, and consequently do not operate the Reticulum project und Can you imagine how much time that wasted? Can you imagine what we could have accomplished with that time instead? -The only thing that this creates is *noise* and confusion. Clogging up the mental and physical workspaces, of people who are actually investing time and effort on the project, with stuff like that is objectively just taking time that could have been used on development, and replacing it with *nothing*. +The only thing that this creates is *noise* and confusion. Clogging up the mental and physical workspaces, of people who are actually investing time and effort on the project with stuff like that is objectively just taking time that could have been used on development, and replacing it with *nothing*. I was receiving *actual* bug reports, pull requests, proper technical investigations and patches via methods outside GitHub and "public" internet-based channels *way* before GitHub interaction and similar was closed down. That was were almost *all* of the real contributions were coming from, anyway. Apparently, and not unsurprisingly, the people who has invested the time and effort to understand Reticulum also prefer to collaborate in this way. Since leaving the GitHub madhouse behind, the signal-to-noise ratio has **significantly** increased. @@ -46,7 +46,7 @@ When this pattern repeats, over and over, the only sensible, measured and constr So, now it's your turn. Go look at the diffs for the last six months. What does it look like I have been doing? -But I will be damned straight with you all, and say that part of that solution is **absolutely** to erect barriers to entry. You can fucking bet your arse on that. I don't want opinionated man-babies running around in my living-room at 3am. I don't want to clean up the floor after a wannabe "dev-ops star" with LLMs and a peripheral case of influencitis has puked all over the office. +But I will be damned straight with you all, and say that part of that solution is **absolutely** to erect barriers to entry. You can fucking bet your arse on that. I don't want opinionated man-babies running around in my living-room at 3am. I don't want to clean up the floor after a wannabe "dev-ops stars" with LLMs and a peripheral case of influencitis has puked all over the office. - If you want to join the fun of changing core networking code that thousands of people rely on for communication daily, you better know what the fuck you're doing. diff --git a/docs/history/2026_09_18_Yes_I_am_Angry.md b/docs/history/2026_09_18_Yes_I_am_Angry.md deleted file mode 100644 index 699ac182..00000000 --- a/docs/history/2026_09_18_Yes_I_am_Angry.md +++ /dev/null @@ -1,339 +0,0 @@ -# Yes, I am Angry - -Polite, but deeply malicious bullshit is a much worse evil than the coarse variant, so you will have to excuse my bluntness here. - -Unfortunately, the only way to respond, as a normal, healthy human being, to something so astronomically tasteless and disgraceful as this, is not in kind, but in honest outrage, modulated to some degree with sarcasm and humor (the dark variant, yes). - -Because *this* shit, this *is* outrageous. - -The man that produced the "article" in question here is either so disposessed of intelligence that his head is teetering on the brink of collapsing into a singularity, or he is deeply malicious. - -If any rational explanation can produce an alternative, I am all ears. - -**To the footsoldiers of the "cause" the article represents** - -Let's get this show on. Let's see the barrage of new smear pieces you can prompt forth from the fuel I provide you here. It'll fit straight into your theater production, so: Be my fucking guest, and dig your own hole of delusion, mistrust and ridicule even deeper than it already is. I can then hurl it right back at you, and we can all have a crap-fight till the end of days. *Or* we could take responsibility and ownership for our words and actions, and actually build something together. Ball's in your court. - -**To anyone else** - -Consider this a paranthetical, sarcastic (and for me, cathartic) aside in the whole of the corpus of writings I have published, and weigh it in the context of the rest of those 200,000 words or so, together with the frustration and exhaustion that inevitably entails from dealing with this same fabricated campaign and its regurgitated nonsense for *years* on end, while trying to do constructive and helpful work; and not expecting anything else than basic decency in return. - -I am no angel, not perfect, flawed in many ways, but I certainly ain't afraid to show that I am a real human being, with the capacity for anger, and that in this case, the anger is fed by the most bitter form of disappointment and sadness in what people will stoop to for influence and attention. - -[This is the article in question](https://gaggl.com/blogs/2026-09-05-enclosure-by-good-intentions/) - -[This is the polite, measured analysis of the *generalized* phenomenon, all 18,000 words of it](https://reticulum.network/manual/brandolinis.html) - -And here is a very short, but central point from that analysis: - -> In whole, this is the description of three fronts of the same operation: -> -> * The license attack and other urgency-engineered narratives are the *cover*: They supply -> the ideological justification and battering ram for treating the reference as illegitimate, -> and needing replacement. -> * The ports are the *inventory*: they supply the “replacement” material that the narrative -> claims the ecosystem needs. -> * The takeover narrative is the *objective function*: the repeated, persistent, escalating claim that -> the reference should be replaced, and that the speakers themself are the natural heirs. - ---- - -*This*, right here, is the real, human and honest response. And the only sane one there could ever be, lest madness be allowed to descend wholly. - -While this is *written* as a response to you, Leo Gaggl, you absolute degenerate piece of fecal matter, it is really a response to the phenomenon *as a whole*. - -I chose your article because it is such a complete and rich example of the madness. What I attack here is *conduct*. - -Leo, according to his own evident preference, is the actor on the theatre stage, and I will treat him as such, in the play that follows. - ---- - -> So when Mark Qvist, the person behind the [Reticulum](https://gaggl.com/blogs/2026-02-25-lpwan-meshes-reticulum-deep-dive/) mesh networking stack - -Color me surprised, the link to "Reticulum" doesn't even point to the repo, but to *another* one of your sloporated "articles". - -Don't wanna accidentially lead users to anywhere they could discover the truth about things now, would we? Like commit histories, the *actual* Reticulum community, that kind of stuff. Nah, nah... - -> ... replaced its MIT licence with a custom one built to keep the project out of corporate AI training sets and out of weapons systems - -Which happened 18 months ago. *Fuuuunny* timing bringing it up again now, right where it's [mighty fucking convenient](https://reticulum.network/manual/brandolinis.html#a-persistent-but-fragile-triad) for your slop-coding license-violating friends, don't you think? - -Interesting you don't bring up *any* of the extensive argumentation made against the same old points you recycle here, but act as they simply never existed. Or maybe you just chose to completely ignore them in the service of your God-given mission? - -> Reticulum is now marked non-free by Debian - -Which is [excellent](https://reticulum.network/manual/brandolinis.html#the-distribution-argument), since debian users will not have to use old, and potentially outdated versions of a package that updates *fast* everytime it is required. - -And "now"? Weasel wording. - -> kept out of the main F-Droid repository - -RNS? In the F-Droid repository? How would that even work? People install the base stack as an *app* and run it... how? Are we going to bundle a command line or `systemd` with that? Do you have any idea about how software actually works? - -If people want to do that, open Termux and type `pip install rns`. Done. Has been possible and documented since the dawn of the project. - -All actual *apps* that use Reticulum are distributed as APKs, ready to install and download, distribute offline, fetch from their websites, over Reticulum itself, or whatever platform they prefer. Commercially or for free. That's the point. - -> It cannot be combined with GPL-licensed code - -[Cry me a river](https://reticulum.network/manual/brandolinis.html#on-freedom). Not my fault that people choose to use viral licenses that absolutely must subsume everything and their aunt under their own restrictive terms. - -> It is not eligible for most open-source grant funding. - -Exactly, and I specifically rejected that, several times, to keep the license right and avoid political takeover. That's 50.000€ I said no to, right there, while I barely had enough to make it three months into the future. Because I value integrity and *real* sustainability a lot higher. - -Do *you* have anything to show for it, in that regard? Who's footing your pay-check? A varied community of people from all walks of life who use the things you created? No? Didn't think so. I wonder who, then. - -> And the clause that triggered all of this is very likely unenforceable. - -"Very likely" is very likely a lot of handwaving, Leo. It's certainly not an argument. [This is](https://reticulum.network/manual/brandolinis.html#what-the-reticulum-license-is). - -> As one Debian developer put it in the project’s own discussion thread, people who build harmful systems are not going to be stopped by a term they were always going to ignore. - -It turns out [that most do](https://reticulum.network/manual/brandolinis.html#the-prediction-and-what-happened), because of a thing called "the law". - -Or are you proposing we live in a state of complete and utter anarchy already? Or maybe that your cult is simply above the law? - -> The moment you add a “not for harm” or “not for AI” condition, you have left open source behind - -Strike up the organ, start the hymns. - -> The Open Source Definition rules out licences that discriminate against people or groups, or against fields of endeavour - -[Erratum](https://reticulum.network/manual/brandolinis.html#open-source-means-open-source): *Your* "open source" religion, not "The Open Source Definition" (Capitals Of Authority, bravo). - -Yes, let's not discriminate against people, **BY FUCKING MURDERING THEM WITH DRONES**. Time for a party Leo! - -Beauuuutiful way to hide [the true nonsense](https://reticulum.network/manual/brandolinis.html#what-the-argument-is-actually-doing) of the "fields of endeavour" by slyly insinuating *I* am being discriminatory. - -Side-note: The overall patronizing, better-than-thy and fakely knowledgable tone is almost hilarious, if it wasn't so malicious. - -> The tool was already on the shelf - -Oh for Christ's sake, this one [again](https://reticulum.network/manual/brandolinis.html#recycled-wine-fermenting)... - -> Kaonic tactical mesh radios are sold for military command and control, drone and autonomous-systems networking, and operation in “austere, contested environments”, and which has since been selected for NATO’s DIANA defence-innovation accelerator. It is MIT-licensed, it is an independent implementation of a protocol that is itself public domain, and its repository predates Qvist’s harm clause by nine months. - -Those fuckers came to me and presented themselves as wanting to create a rust port for the good of the ecosystem, the world and the community. That was a blatant lie and front for their actual goal of acquiring my goodwill and expertise so they could quickly create a shallow clone, hardfork it with their own sloppy "Post-Quantum" crypto, and get big money from the military. They deliberately lied and manipulated to extract from a whole community and someone they believed to be a naive, vulnerable developer. - -Why do you think a non-harm clause is needed? Their implementation is still not *working*, by the way, and has provided **nothing** to the community or world at large. - -I cut ties with those bastards when I found out what their end-game was, but not until I had spent every ounce of energy I had slowing them down as much as humanly possible, while *I was still working on the actual Reticulum implementation and providing updates to users, managing a community, and living on next to nothing in a camper*. - -Holy fuck I hate people like you Leo, you willfully ignorant, destructive, spoiled piece of shit. - -> His fix trades a door nobody has worked out how to close for the loss of every distribution channel - -Every distribution channel that [we never used, and never needed](https://reticulum.network/manual/brandolinis.html#the-distribution-argument)? I haven't seen PyPi acting up about our apparently infuriating conduct yet. - -Moot point, even if they do, since we can distribute over Reticulum just fine. With full cryptographic authentication, and no centralized infrastructure or PKI required. Also, no governance required. *That's* resilience. - -> and every downstream collaborator who needs a known-good licence. - -We are definitely not short of downstream collaborators currently, so no thanks, you don't have much to sell me there. - -> You pay the full price in distribution and collaboration - -The quality of collaboration has gone up *significantly* since that decision. Most of the wannabes stay away and sob in their Stallman handkerchiefs, while we keep working, dilligently as always. Great success. - -(Sorry Stallman, I love you) - -Reticulum is available everywhere. It is one of the leanest, most dependency free software packages there is, and it ships an entire networking stack, a plethora of management utilities, a device flashing utility, a full GitHub replacement running over Reticulum, remote shell utilities, and supports a whole ecosystem of application-layer protocols and user-facing applications, including messaging (LXMF), voice (LXST), chat (RRC), content hosting and browsing (nomadnet), and many others. - -*In a cross-platform, portable wheel of 537 kilobytes* that installs or embeds on every platform known to man. Drop it on a floppy, baby. - -The delusions run so deep here I cannot even begin to fathom it. - -> and you get an AI-training restriction a determined actor routes around in an afternoon. - -In reality, of the 10 sampled language models (both proprietary and open-weights) that were tested for traces of Reticulum source material, *all* stopped training on the Reticulum source after the license change. - -Turns out, *it worked*, Leo. - -Did *you* check? No you didn't, handwaving and inventions again. - -> A written answer to “what happens when the maintainer walks away”, because for Reticulum that stopped being hypothetical in December 2025. - -Did you check the [commit logs](https://github.com/markqvist/Reticulum/commits/) you dimwit? No, you just randomly rolled around in the personal puddle of phantasmogoria you constructed from wishful thinking. - -Have you seen what I have produced in those ten months? I'd wager it's a lot more than you, you lazy fuck, not even doing the most basic fact-checking of your smear-job propaganda pieces. - -Try that on for a "written answer". - -> There is now a Reticulum_CE, a “community edition” fork that exists mainly to carry a redistributable licence. - -What psychosis are you getting these from? [Reticulum_CE](https://github.com/Reticulum-Community/Reticulum_CE) is published under the Reticulum License. Did you read their repo? No, you did not: - -*"This is a community maintained fork of the Reticulum Network Stack and is intended as a public forum for discussion and issue reporting, as well as a repository to collect community enhancements to RNS."* - -> There is RetiNet, an AGPL fork of the Python stack. - -Which has been [defunct and abandoned for 8 months](https://codeberg.org/SkyGuy/RetiNet). Are you not equipped with visual perception or basic literacy? - -> There are side-loaded F-Droid repositories so people can install the apps at all. - -What nonsense is this? You are literally so horny to paint a picture of a dead system that you have to assume people cannot think for two seconds straight? - -There has never been a point in time where people could not install Reticulum-based apps on Android. These have always, intentionally, been distributed as *re-distributable* APK files so people could share them offline. Sideband can host and share it's own APK. This is the point, Archimedes. - -The F-droid repository was made as service to users by a volunteer in the *real* Reticulum community (not your abhorrent confabulation of what it is) who *has never once objected to the license and project directions*, and who just wanted to help make things easier! - -Don't use them as ammunition in your perverse fantasies! - -> Every one of those is real maintenance work, done by volunteers, spent entirely on the problem of being allowed to share the software. - -A copy of my repo, then pasting my (Reticulum Licensed) work into four files, then abandoning the project is not "real maintenance work". - -What I have been doing, for over a decade, is real maintenance work. - -> four independent teams have rebuilt Reticulum from scratch in Rust. - -Those [have not "rebuild"](https://reticulum.network/manual/brandolinis.html#copyright-the-two-legal-layers) anything ["from scratch"](https://reticulum.network/manual/brandolinis.html#the-clean-room-delusion). - -They are feeding the source code of RNS directly into an LLM, running automated software loops, and copying it, blatantly, bit for bit, without any modicum of creative input, work or understanding. - -Such a connoisseur of the commons you are. - -> The people rebuilding the project to keep it alive - -The project is already alive, and has been for a decade. You are lying so hard that it's fucking raining. - -Those people, are in their own words, "Killing Reticulum" (quote, Lew Palm, "author" of "Leviculum"), so they can appropriate it for their own profit and control. - -> The two that put enclosure resistance first, Leviculum and the rsReticulum stack behind Ratspeak, both chose AGPLv3. - -And all of those are [void and null](https://reticulum.network/manual/brandolinis.html#void-grants-legal-foundations-of-machine-generated-code), because what they *really* chose is **copyright violation**, and complete disrespect ([as is now becoming tradition](https://reticulum.network/manual/brandolinis.html#licensing-analysis)) for open source development practices, attribution and basic decency. - -> Underneath the licence choice sits a harder problem, and it is the one this site keeps coming back to: a system can be decentralised in its architecture and completely centralised in who decides things. - -Oooooh... Tension. Drama. Tell me more, let's have the juicy details, huh? Outrage incoming, I can feel it! Ramp up your audience like a good boy, Leo! - -The only thing that keeps coming back here is nonsense. - -The decisions follow those responsible, over years, and the actual work. Not cry-babies like you. Get used to it. - -> Relicensing a codebase other people contributed to, under MIT terms, with a single unilateral commit, is itself a small act of enclosure. - -I am responsible for over 98% of commits in that repository. Not a single one of the other contributors disagreed to the license decision. - -But your ideology runs so deep now, *that we cannot ourselves decide how we license our own software*? Yes, let's consider for a moment **who the fucking dictator is here**... - -It's *almost* like you've decided you already personally own Reticulum. - -> The pattern shows up off the codebase too. In late August 2026, after a badly-behaved crawler hammered his NomadNet git node with tens of thousands of requests - -Ah yes, so I got personally mad because it hit *my* node, and had a tantrum? - -The scraper was hitting *every node across the entire fucking intermesh*, from a high-bandwidth VPS, with no throttling, no regards for people's radio links, airtime concerns, metered connections or limited compute resources. - -For what reason? To gouge tens of **millions** of requests to a broken piece of shit software that an idiot deployed and left running without oversight, trying to infintely download **everything that everyone had ever put on their nodes several times over** on a recursive loop. - -Who made that stop? I did, because I [contacted](https://github.com/roogle-dev/reticulum-phantom/issues/4) the idiot and talked him out of it. - -Let's hear more of your brilliant ideas about how to "protect the commons" you fucking ape. - -> Qvist removed the volunteer moderator of the Reticulum subreddit, deleted roughly three years of posts, and closed it with a notice that ended “Well done, vibecoders. You ruined it for everyone.” - -The moderator was not moderating anything, and hadn't been for months. The precise original agreement with him for allowing him moderator rights was exactly the arrangement, that if the space was not moderated, it would be closed, and that I would maintain the final decision on that. - -This is now being spun into yet another [bottle of piss](https://reticulum.network/manual/brandolinis.html#recycled-wine-fermenting) by the dipshits like you. Well done, moron. - -> Around the same window the project’s Matrix room shut down and the GitHub issue tracker was hidden. - -Does time not exist in your alternate universe? The events you allude to are separated by month or years, and the rationales were clearly stated. You deliberately ignored all of that. - -Further, I did not manage or administrate the Matrix room. It was a community room, run by volunteers, flooded with spam and bad-faith actors. I had *zero* admin rights on it. The moderators of the room made that decision themselves, long after I had left - *to focus on actual work*. - -The development was *never* centered on the GitHub issue tracker, it ran over Reticulum long before the preposterous madhouse of GitHub was left behind for good, as it continues to do today, as is documented and stated several places (including the front-page of the repository you refuse to link to). - -Signal to noise ratio has gone up *significantly* because of that. The community is *more* alive, and more productive. - -You are literally making things up as you go. - -> One community member’s summary: for a supposedly decentralised network, the founder holds and uses a lot of centralised levers. - -Yes, let's just [invent](https://reticulum.network/manual/brandolinis.html#the-movement-of-a-dozen) another concerned "community member". Who? Some random nutjob on reddit? - -A random stranger of your choosing is not a "community member" that gets to decide how a highly complex engineering project is run. But damn man, you wish it worked that way. You wish it badly. - -> None of this is meant as a pile-on. - -Oh, *thanks*, you're SO *considerate* and full of empathy Leo! - -> The Reticulum Licence is presented as a shield for the commons - -**Because that is what it actually is.** - -How about trying to actually *prove* otherwise, with *a coherent argument*, instead of getting an LLM to spin a load of high-strung nonsense to further your deranged religion? - -> If your users are European public bodies - -Oh, those poor administrative organizations! Do you think I dedicated my life to empowering European Public Bodies, or actual people? - -> Then spend the effort you saved on the part that is actually hard: governance that survives the founder losing interest. - -[Ze Governance](https://reticulum.network/manual/brandolinis.html#a-history-of-what-works), again, I see. - -Maybe when you've worked 12 hours a day on a project for ten years, which is persistently flooded with this kind of profound idiocy, and kept going, can you begin to consider expounding your finer points on "losing interest". - -> The commons is not protected by a clause forbidding bad uses. - -It sure as hell ain't protected by pretending the world is made of candy-floss either, like you seem to prefer. - -> It is protected by how many people can fork it, ship it, and keep it running without asking anyone’s permission. - -Oh isn't that convenient now? How many can do that today? 99.999999% of the worlds population. What's the equation if we change to your beloved AGPL again, you moron? - -> Reticulum’s licence made that number smaller. - -[Mathematical failure](https://reticulum.network/manual/brandolinis.html#on-freedom). - -> The intention was to guard the commons. The effect was to fence off a piece of it. - -On the intent, [correct](https://reticulum.network/manual/brandolinis.html#what-the-reticulum-license-is). - -On the effect, no: The effect apparently was to induce the uncomfortable feeling of grinding against your own inconsistent dogmas in dishonest, propagandistic nutjobs like you, because *you* now feel *fenced out*. - -There's an important difference there. - -The self-referential circle-jerk you've got going on here is *exactly* the definition of a fence; one made of hysteria and propaganda with the ulterior motive of usurping power away from a community of thousands of people, and putting it, squarely, into your own, trembling, wanting and lazy little hands. - -> Reticulum is also not a toy. People run it over LoRa and packet radio for emergency and off-grid communication, so “someone degrades the network” is a safety question, not an inconvenience. - -**Bingo**, Leo. - -Which is why you need a *real* [responsible, accountable and competent person](https://reticulum.network/manual/brandolinis.html#a-history-of-what-works), with a decade-long track-record to run the project, not a bunch of kids with a fixation on Claude, and their sugardaddies like you. - -> Here is the part that frustrates me. - -Yes, you must *surely* be frustrated Leo, how very frustrating this must be for you and your ChatGPT subscription to have to go through all of this. - -Tell me about frustration once you have actually done something that required more than pillow-fucking your own sense of grandiosity. - -All your github repos are minor personal projects with up to... count them... coooount them... five stars. No users. You must surely know a lot about software development, distribution, and these commons you are protecting so heroically. - -I can see you have a lot of skin in the game here, and a lot of work and experience on your back to bolster your authority. - -Fuck off, you fraud. - -> Every few years a piece of infrastructure I care about gets a new licence - -You don't care one fucking bit about infrastructure Leo. *You don't even know what it is*, least of all what it takes to create it. - -I do. Because I did it. - -Go home, you're drunk. - -Thanks for the material, though. You've provided a textbook example of the incessant bullshit, appropriation, deliberate enclosure and intentionally weaponized manipulation that [this project is up against](https://reticulum.network/manual/brandolinis.html#intent-consequence). - -*In ideology and war, self-justification wears the same coat, and feeds the same destruction, perpetually.* - -Yours truly, - -Mark Qvist - -September 18th, 2026 - ---- - -That it is, I am no longer angry. Back to work. \ No newline at end of file diff --git a/docs/history/2026_09_21_Void_Grants.md b/docs/history/2026_09_21_Void_Grants.md deleted file mode 100644 index 90bbe21a..00000000 --- a/docs/history/2026_09_21_Void_Grants.md +++ /dev/null @@ -1,253 +0,0 @@ -## Void Grants: Legal Foundations of Machine-Generated Code -*From the [Brandolini's Reference](https://reticulum.network/manual/brandolinis.html) chapter of the Reticulum Manual.* - -A grant of rights is only as real as the rights behind it. A license conveys what its issuer holds, and nothing more. A derivative may be distributed only under whatever license covers what it took. And over a work that a machine originated, no human holds anything at all. - -Each of these statements is a matter of settled statute and case law, and this document sets out the authorities. The conclusions they support are: - -1. A software implementation generated from a licensed work by a machine, published under its operator's claimed copyright and license, **conveys rights the operator never possessed**. -2. **Its grant is void**. The projects, products, and promises built upon such grants stand on nothing. A field of void grants is a minefield for everyone who crosses it. - -This document is a general examination of copyright law as it applies to machine-generated code. It is not legal advice. - -### I. The Foundational Statute: Translation Is Copying - -The starting point is the statutory definition of the class of works at issue. Title 17, United States Code, section 101 provides the definition of a derivative work: - -> A "derivative work" is a work based upon one or more preexisting works, such as a *translation*, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or *any other form in which a work may be recast, transformed, or adapted*. - -Section 101 further provides that *"the terms 'including' and 'such as' are illustrative and not limitative"*. As such, the list is a list of *examples*, and the very first example is translation. The definition also reaches, in its own words, *"any other form in which a work may be recast, transformed, or adapted"*. - -The exclusive right at issue is defined in section 106: - -> Subject to sections 107 through 122, the owner of copyright under this title has the exclusive rights to do and to authorize any of the following: -> (1) to reproduce the copyrighted work in copies or phonorecords; -> (2) to prepare derivative works based upon the copyrighted work; -> (3) to distribute copies or phonorecords of the copyrighted work to the public by sale or other transfer of ownership, or by rental, lease, or lending; - -Violation of any of these rights is infringement. 17 U.S.C. § 501(a). - -Taken together, the provisions yield a construction that is difficult to improve upon and impossible to escape: - -1. **A port is a translation.** A work that takes a program written in one language and from it produces a program written in another language, implementing the same behavior, is a translation of the original - whatever mechanism performed it, and however the surface presentation differs. -2. **A translation is a derivative work.** Section 101 says so, explicitly. -3. **Preparing a derivative work is the copyright owner's exclusive right.** Section 106(2) says so explicitly. A derivative work prepared without authorization, or outside the scope of any authorization, infringes that right. - -The common objection - "the code is in a different language, so it is completely different" - is not a legal argument; it is the definition of what the statute already classifies. - -Translation *is the paradigm* of derivative authorship, not an exception to it. If translating a work without permission was a defense, the derivative-work right would be meaningless; every infringing adaptation would simply need to be "different enough" to count as a translation of its source. - -Nor does the identity of the translator change the analysis. The statute asks whether the work is "based upon" a preexisting work. It does not ask who or what performed the translation. A machine that ingests a licensed work and reproduces a translated transformation of it has performed mechanical translation; the output is a derivative of the ingested work, and the law governing derivative works applies to it exactly as it applies to a translation made by hand. - -### II. Ideas Are Free, Expressions Protected - -The second foundation is the distinction between what a work *is* - its ideas, its processes, its systems - and the *expression* in which those ideas are embodied. The distinction is codified in section 102(b): - -> In no case does copyright protection for an original work of authorship extend to any idea, procedure, process, system, method of operation, concept, principle, or discovery, regardless of the form in which it is described, explained, illustrated, or embodied in such work. - -The principle is older than the statute, and its application to software is settled. An author may own the particular code in which a method is expressed; the method itself, the process, the algorithm, belongs to no one - because *"the sine qua non of copyright is originality"* (*Feist Publications, Inc. v. Rural Telephone Service Co.*, 499 U.S. 340, 345. 1991). And ideas are not original to anyone in particular, *Baker v. Selden*, 101 U.S. 99 (1879), established the line: Copyright in a book describing a system does not prevent others from practicing the system. - -Applied to a communications protocol, the structure is clean: - ---- - -**An implementation of a public-domain protocol, honestly reasoned and written by humans, is welcome and unencumbered, and owes nothing to any license. An implementation generated from a licensed implementation is a derivative of licensed work, subject to its conditions and to the ordinary copyright of its source.** - ---- - -The public-domain status of the protocol's ideas changes nothing about the copyright in an implementation's expression. A person who copies an implementation - by machine or by hand - cannot defend the copying by pointing to the protocol, because the protocol covers the ideas, not the symbols in which someone expressed them. - -A further misconception can be dismissed here; the recently popularized "clean-room" claim about the absence of copying. A [clean-room](https://reticulum.network/manual/brandolinis.html#the-clean-room-delusion) is a not a magic label achievable by LLM, but a specific, institutionalized procedure for proving to a court that copying did not occur: Physically separated teams, written functional specifications, attestable non-exposure, and no access to the original by the implementers (*Altai*, 982 F.2d at 709–11). - -*"I created a clean-room implementation by feeding the original source code into a machine"* attempts to *claim* the certified, legally established procedure for proving the absence of copying by describing, in the same breath, a process **composed entirely of copying**. The word is deployed because the procedure it names is what the claimant would *need to have happened*. - -### III. Machine Output Has No Author - -The third foundation is the human authorship requirement. It is settled at both levels of the federal courts and in the Copyright Office's administering practice. - -In *Thaler v. Perlmutter*, the district court held that *"copyright law protects only works of human creation,"* and that *"human authorship is a bedrock requirement of copyright"* (687 F. Supp. 3d 140, 146, D.D.C. 2023). On appeal, the United States Court of Appeals for the District of Columbia Circuit affirmed, holding that the Copyright Act *"requires all eligible works to be authored in the first instance by a human being"* (No. 23-5233, D.C. Cir. Mar. 18, 2025). - -The requirement traces to the ordinary meaning of the statutory term. In *Burrow-Giles Lithographic Co. v. Sarony*, the Supreme Court defined an "author" as *"he to whom anything owes its origin; originator; maker; one who completes a work of science or literature"* (111 U.S. 53, 57–58, 1884), and more recently described the author as *"the person who translates an idea into a fixed, tangible expression entitled to copyright protection"* (*Community for Creative Non-Violence v. Reid*, 490 U.S. 730, 737, 1989). - ---- - -**A machine does not owe expression its origin; it reproduces it. No court has recognized copyright in material created by non-humans, and the courts that have addressed the question have rejected it. If you create by machine generation, you do not own the output.** - ---- - -The Copyright Office's registration guidance applies the requirement to generative systems in terms that are directly on point. The Office asks: - -> whether the "work" is basically one of human authorship, with the computer [or other device] merely being an assisting instrument, or whether the traditional elements of authorship in the work (literary, artistic, or musical expression or elements of selection, arrangement, etc.) were actually conceived and executed not by man but by a machine. - -- *Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence*, 88 Fed. Reg. 16190, 16192 (Mar. 16, 2023) - -And it states the consequence: *"If a work's traditional elements of authorship were produced by a machine, the work lacks human authorship and the Office will not register it"* (*Id.*). - -On the role of prompts, the Office is equally explicit: when a system receives a prompt and produces a work in response, *"these prompts function more like instructions to a commissioned artist - they identify what the prompter wishes to have depicted, but the machine determines how those instructions are implemented in its output"* (*Id.*). - -The agency applied this standard and excluded machine-originated material from registration in the leading administrative case. In *Zarya of the Dawn* (Cancellation Decision, Feb. 21, 2023, Registration No. VAu001480196), the Office cancelled registration of images generated by the Midjourney service, holding that *"it was Midjourney-not [the author]-that originated the 'traditional elements of authorship' in the images"*, while preserving protection for the human-authored text and the human selection and arrangement of the work. - -The Copyright Office's 2025 report on AI copyrightability reaches the same conclusion: - -> Based on the functioning of current generally available technology, prompts do not alone provide sufficient control. - -- U.S. Copyright Office, *Copyright and Artificial Intelligence, Part 2: Copyrightability*, at iii (Jan. 29, 2025) - -The report's analysis is explicit: *"The Office concludes that, given current generally available technology, prompts alone do not provide sufficient human control to make users of an AI system the authors of the output. Prompts essentially function as instructions that convey unprotectible ideas"* (*Id.* at 18). - -Its guiding distinction is stated plainly: *"The use of AI tools to assist rather than stand in for human creativity does not affect the availability of copyright protection for the output"* while *"Copyright does not extend to purely AI-generated material, or material where there is insufficient human control over the expressive elements"* (*Id.* at iii). - -The boundary is therefore not "AI or no AI". It is *assistance or substitution*: Whether a human exercised sufficient creative control over the expressive elements and can answer for them, or whether the machine originated them and the human merely supplied instructions and a signature. That is a question of fact in each case, determined from the record of how the work was made. - -Two facts follow, and stated together, they form the base legal reality: - -1. **A work whose expressive elements were originated by a machine contains no valid new copyright: It has no human author. To the extent that the same work reproduces a preexisting copyrighted work without explicit license, it is at the same time an unauthorized derivative. It is unowned for anything new it contains, and unlicensed for everything taken.** -2. **Its operator therefore holds no copyright in it and no grant over it - and a person who holds neither copyright nor grant can convey neither.** - -The first fact makes the work **unownable** by its operator. The second makes it infringing against its source. Neither replaces the other: - -- The absence of copyright in machine output does not make the output free for anyone to take; it makes it unowned. -- This is a different thing entirely, especially when what it contains is someone else's expression. - -### IV. License Conditions Bound the Grant - -The fourth foundation governs what a license *is*. A copyright owner may grant permission to use the work on conditions. The conditions define the scope of the permission. Use outside the scope is use that was never authorized. - -In *Jacobsen v. Katzer*, 535 F.3d 1373 (Fed. Cir. 2008), the Federal Circuit held that the conditions of an open-source license are *conditions of the copyright license itself*, not mere contractual promises, and that a licensee operating outside them is an infringer: - -> Copyright holders who engage in open source licensing have the right to control the modification and distribution of copyrighted material. - -> The choice to exact consideration in the form of compliance with the open source requirements of disclosure and explanation of changes, rather than as a dollar-denominated fee, is entitled to no less legal recognition. - -> It is outside the scope of the [license] to modify and distribute the copyrighted materials without copyright notices and a tracking of modifications from the original computer files. - -- *Id.* at 1381–82 - -The doctrinal mechanism is settled: *"A copyright owner who grants a nonexclusive license to use his copyrighted material waives his right to sue the licensee for copyright infringement"* within the scope of the license. If, however *"the licensee acts outside the scope, the licensor can bring an action for copyright infringement"*: An act outside the granted scope **is infringement** (*Sun Microsystems, Inc. v. Microsoft Corp.*, 188 F.3d 1115, 1121, 9th Cir. 1999; *Graham v. James*, 144 F.3d 229, 236, 2d Cir. 1998; *S.O.S., Inc. v. Payday, Inc.*, 886 F.2d 1081, 1087, 9th Cir. 1989). - -Whether a given license term is a condition of the grant or a mere covenant is determined from the license's own language; conditions that limit what may be copied, modified, and distributed - the attribution terms, the notice terms, the modification-tracking terms, the field-of-use terms - are the boundaries of the copyright grant (*MDY Industries, LLC v. Blizzard Entertainment, Inc.*, 629 F.3d 928, 940, 9th Cir. 2010). - -Three consequences follow for the analysis at hand. They are not hypothetical. - -**First, the conditions are measured at the moment of copying** - -A work is licensed under whatever conditions applied when the reproduction, adaptation, and distribution occurred. There is **no** period in which copying a licensed work is "free" because the copy was made by a machine; the machine is the *licensee's tool*, and the licensee is bound by the conditions regardless. A derivative generated without satisfying the conditions - without the attribution, without authorization for the manner of use - was never within the scope of the license, from the first instant of its existence. - -**Second, breach does not restore rights that never attached** - -The district court in *Artifex Software, Inc. v. Hancom, Inc.*, No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017), analyzing the GPL's termination provisions, observed that *"the language of the GPL suggests that [the licensee's] obligations persisted beyond termination of its rights to propagate software using Ghostscript,"* because the source-code obligation recurs each time a covered work is conveyed. The same structure operates a fortiori where a derivative was produced *outside* the license entirely: No grant ever attached, and no subsequent event creates one. - -**Third, the settled conditions are the enforceable core** - -The attribution and notice conditions rest on the settled doctrine above. Novel conditions (restrictions on field of use, restrictions on how the work may be consumed) are enforceable as conditions of the grant when clearly drafted, but their enforcement history is thinner. The argument that a derivative is unlicensed does *not* depend on the novel conditions being vindicated; it needs **only** the settled ones, and the ordinary law of derivative works, to stand. - -### V. The Void Grant - -The preceding sections assemble into a single structure. Let us apply it to an example situation: - -- A project presented as an independent implementation of a protocol; "ground-up", "from scratch" or "clean-room" work. -- The actual history shows continuous, machine-mediated ingestion of a licensed reference implementation. -- The output is generated at a scale no human could produce or review, with the provenance of the process obscured. - -**Step One: The derivative** - -The project's output is a translation of the licensed implementation: A derivative work, produced by a copy process. 17 U.S.C. §§ 101, 106(2). Preparing it without authorization infringed from the first moment of its existence. *Id.* § 501(a). - -**Step Two: The license** - -The derivative was produced outside the conditions of the license; without the notices, without the attribution. The license never attached (*Jacobsen*, 535 F.3d at 1381–82). - -**Step Three: Authorship** - -The expression was originated by a machine. No human can account for its creative decisions, and no human authored it. It contains no valid new copyright (*Thaler*, 687 F. Supp. 3d at 146; No. 23-5233, D.C. Cir. 2025; 88 Fed. Reg. at 16192). - -**Consequence: Void Grant** - -The statute supplies the consequence. Section 103(a) provides: - -> The subject matter of copyright as specified by section 102 includes compilations and derivative works, but protection for a work employing preexisting material in which copyright subsists does not extend to any part of the work in which such material has been used unlawfully. - -And section 103(b) provides that copyright in a derivative work *"extends only to the material contributed by the author of such work."* The legislative history states the point without ambiguity: *"An unauthorized translation of a novel could not be copyrighted at all"* (H.R. Rep. No. 94-1476, at 57, 1976). - -In our example, the operator of the derivative's copy process decides to assert copyright over the output, and licenses it to the world under terms of the operator's choosing, for example the Apache License. - -But the operator holds no copyright in machine-originated content (section III) and no license over the source from which it was derived (sections I and IV). - -For the derived material, the operator's copyright claim **is an assertion of a right that never existed**. The material was used unlawfully, and section 103(a) withholds protection from *every* part of the work in which it appears. For the machine-originated remainder, the operator holds no copyright either, for lack of any human author. - -The operator's license to the world is a promise the operator had no power to make. Every person who receives the work believing they received a valid grant has received **a void grant**, and stands, as a matter of settled law, in the position of a downstream user of an unauthorized derivative, whatever they were told at the time. - -This is a cascading consequence, and it is why the pattern is a hazard. No combination of self-granted licenses - "MIT," "Apache," "public domain," anything at all - can transfer rights the operator never held. Every downstream project, product, or deployment built on the derivative is built on unlicensed reproduction of a third party's work, whether or not it knew, whether or not it was told, and whether or not the marketing was plausible. - -The longer the derivative circulates unexamined, the more it becomes embedded, forked, vendored, depended upon, and the more expensive its correction becomes for everyone who trusted it. A field of superficially real projects, repositories, and licenses that **in reality confer nothing on anyone who relies on them is a minefield**, in the most precise sense the law allows: The rights they purport to grant *were never possessed*. - -The law defines what may be asserted, not what will be. The copyright owner may choose whether and how to assert these rights, and at what point in time. The exposure exists regardless, and for *every* downstream that trusted the void grant. This is the scope of the hazard that the conduct exemplified here produces. - -### VI. Fair Use Does Not Rescue This Pattern - -Every defendant raises fair use. It is an essential and important limitation in the service of free speech, accountability, criticism, education and other important domains. Therefore, it must be considered, and in the case exemplified here, it fails. - -Fair use is a limitation on the exclusive rights, evaluated case by case under four factors: The purpose and character of the use; the nature of the copyrighted work; the amount and substantiality of the portion used; and the effect of the use on the potential market for or value of the work (17 U.S.C. § 107). - -On the first factor, the governing standard is *Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith*, 598 U.S. 508 (2023), in which the Supreme Court held that: - -> In sum, the first fair use factor considers whether the use of a copyrighted work has a further purpose or different character, which is a matter of degree, and the degree of difference must be balanced against the commercial nature of the use - -The question is whether the use serves a further purpose, not whether the surface changed. A derivative that exists to serve as the same thing as the original, marketed to the people who would otherwise use the original, has not acquired a further purpose by being translated; the surface changed, the function did not, and the use is commercial. - -The remaining factors resolve the same way. Source code is expressive creative work, not bare fact. The pattern under examination copies the whole; a complete implementation, ingested and translated in its entirety. And the market effect is the decisive factor, because it is engineered: A derivative marketed as a drop-in replacement for the work it was copied from is not a complement to the original's market, it is a substitute for it. The Supreme Court has long held that use of a work to satisfy the market demand for the original is the paradigm of an *unfair* use (*Harper & Row, Publishers, Inc. v. Nation Enterprises*, 471 U.S. 539, 566–67, 1985). - -The one case that is always raised in response, *Google LLC v. Oracle America, Inc.*, 593 U.S. 1 (2021), holds less than it is believed to. The Court there assumed *"for argument's sake, that the material was copyrightable"* and held *"that the copying here at issue nonetheless constituted a fair use"* (*Id.* at 11). - -The material in question was a thin slice of *declaring code*, which the Court found sits, *"if copyrightable at all, further than are most computer programs (such as the implementing code) from the core of copyright"* (*id.*), in a use that built a new platform for a new environment, with the copier **having written the vast majority of its own code**. By comparison: A wholesale translation of an entire implementation, marketed as a substitute for it, is **not** that case; it is the other end of every factor. - -The valuable zones of fair use remain, and they are important. **Commentary and criticism**, quoting from a work to review, discuss or criticize it. **Teaching and scholarship**, explaining concepts and quoting excerpts, is its traditional beneficiary. - -### Conclusion - -The law is neither obscure nor new. The conclusion it yields can now be stated: - -- A machine translation of a protected implementation **is a derivative work**, and an unauthorized derivative **is an infringement**, whatever language it speaks and whatever machinery produced it. -- A work whose expression a machine originated has no human author, no valid new copyright, and without valid copyright **no license can be granted on it**. -- § 103 withholds protection from every part of a derivative in which protected material was used unlawfully. A license is a grant with conditions, and a derivative made outside the conditions **never received a grant**. -- A person who holds no copyright and no grant **can convey neither**, and *every* downstream consumer who received from them received a **void grant**. - -This is a *catastrophic* failure state for all who want to create things good and useful: The projects that inherit only void grants and liability believing they have inherited foundations, the users who depend on code that no one on earth can answer for, the honest builders whose work is buried under a field of plausible-looking impostors. - -### Sources and Authorities - -Statutory references are to Title 17, United States Code. Case citations are to the official or generally cited reporters. Online links are for reference only. Section numbers refer to the sections of this document in which each authority is principally relied upon. - -**Statutes** - -- [17 U.S.C. § 101 (definitions: "derivative work," "including," "publication") - I, II](https://www.law.cornell.edu/uscode/text/17/101) -- [17 U.S.C. § 102(b) (idea/expression) - II](https://www.law.cornell.edu/uscode/text/17/102) -- [17 U.S.C. § 103(a)–(b) (derivative works employing unlawfully used preexisting material) - V](https://www.law.cornell.edu/uscode/text/17/103) -- [17 U.S.C. § 106(1)–(3) (exclusive rights: reproduction, derivative works, distribution) - I](https://www.law.cornell.edu/uscode/text/17/106) -- [17 U.S.C. § 107 (fair use) - VI](https://www.law.cornell.edu/uscode/text/17/107) -- [17 U.S.C. § 501(a) (infringement) - I, V](https://www.law.cornell.edu/uscode/text/17/501) - -**Judicial decisions** - -- [*Jacobsen v. Katzer*, 535 F.3d 1373 (Fed. Cir. 2008) - IV, V](https://www.casemine.com/judgement/us/5914b290add7b04934760a54) -- [*Sun Microsystems, Inc. v. Microsoft Corp.*, 188 F.3d 1115 (9th Cir. 1999) - IV](https://law.justia.com/cases/federal/appellate-courts/F3/188/1115/629501/) -- [*Graham v. James*, 144 F.3d 229 (2d Cir. 1998) - IV](https://caselaw.findlaw.com/court/us-2nd-circuit/1011522.html) -- [*S.O.S., Inc. v. Payday, Inc.*, 886 F.2d 1081 (9th Cir. 1989) - IV](https://law.justia.com/cases/federal/appellate-courts/F2/886/1081/19091/) -- [*MDY Industries, LLC v. Blizzard Entertainment, Inc.*, 629 F.3d 928 (9th Cir. 2010) - IV](https://law.justia.com/cases/federal/appellate-courts/ca9/09-15932/09-15932-2011-02-25.html) -- [*Artifex Software, Inc. v. Hancom, Inc.*, No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017) (settled; dismissed with prejudice Jan. 2018) - IV](https://storage.courtlistener.com/recap/gov.uscourts.cand.305835/gov.uscourts.cand.305835.54.0_1.pdf) -- [*Thaler v. Perlmutter*, 687 F. Supp. 3d 140 (D.D.C. 2023), aff'd, No. 23-5233 (D.C. Cir. Mar. 18, 2025) - III, V](https://www.bitlaw.com/source/cases/copyright/Thaler-Perlmutter-Dct.html) -- [*Burrow-Giles Lithographic Co. v. Sarony*, 111 U.S. 53 (1884) - III](https://supreme.justia.com/cases/federal/us/111/53/) -- [*Community for Creative Non-Violence v. Reid*, 490 U.S. 730 (1989) - III](https://supreme.justia.com/cases/federal/us/490/730/) -- [*Feist Publications, Inc. v. Rural Telephone Service Co.*, 499 U.S. 340 (1991) - II](https://supreme.justia.com/cases/federal/us/499/340/) -- [*Baker v. Selden*, 101 U.S. 99 (1879) - II](https://supreme.justia.com/cases/federal/us/101/99/) -- [*Computer Associates International, Inc. v. Altai, Inc.*, 982 F.2d 693 (2d Cir. 1992) - II](https://law.justia.com/cases/federal/appellate-courts/F2/982/693/137252/) -- [*Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith*, 598 U.S. 508 (2023) - VI](https://www.supremecourt.gov/opinions/22pdf/21-869_87ad.pdf) -- [*Google LLC v. Oracle America, Inc.*, 593 U.S. 1 (2021) - VI](https://supreme.justia.com/cases/federal/us/593/18-956/) -- [*Harper & Row, Publishers, Inc. v. Nation Enterprises*, 471 U.S. 539 (1985) - VI](https://supreme.justia.com/cases/federal/us/471/539/) - -**Administrative and policy authorities** - -- [*Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence*, 88 Fed. Reg. 16190 (Mar. 16, 2023) - III](https://www.federalregister.gov/documents/2023/03/16/2023-05321/copyright-registration-guidance-works-containing-material-generated-by-artificial-intelligence) -- [*Zarya of the Dawn*, U.S. Copyright Office (Cancellation Decision, Feb. 21, 2023), Registration No. VAu001480196 - III](https://www.copyright.gov/docs/zarya-of-the-dawn.pdf) -- [U.S. Copyright Office, *Copyright and Artificial Intelligence, Part 2: Copyrightability* (Jan. 29, 2025) - III](https://www.copyright.gov/ai/Copyright-and-Artificial-Intelligence-Part-2-Copyrightability-Report.pdf) -- [H.R. Rep. No. 94-1476 (1976) - V](https://law.resource.org/pub/us/works/aba/ibr/H.Rep.94-1476.pdf) diff --git a/docs/manual/.buildinfo b/docs/manual/.buildinfo index 85787315..28f590fa 100644 --- a/docs/manual/.buildinfo +++ b/docs/manual/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file records the configuration used when building these files. When it is not found, a full rebuild will be done. -config: a83095d108892439ad6027acd44f7c0a +config: 4e2f0f7390586c35b5c4b57cd1fc8b5b tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/manual/_images/columba.webp b/docs/manual/_images/columba.webp deleted file mode 100644 index 07ea65ed..00000000 Binary files a/docs/manual/_images/columba.webp and /dev/null differ diff --git a/docs/manual/_images/meshchatx.webp b/docs/manual/_images/meshchatx.webp index 94edb72a..24fd7ba0 100644 Binary files a/docs/manual/_images/meshchatx.webp and b/docs/manual/_images/meshchatx.webp differ diff --git a/docs/manual/_images/modem73.webp b/docs/manual/_images/modem73.webp deleted file mode 100644 index 5adadc76..00000000 Binary files a/docs/manual/_images/modem73.webp and /dev/null differ diff --git a/docs/manual/_images/nomadnet_3.png b/docs/manual/_images/nomadnet_3.png new file mode 100644 index 00000000..4a8c0def Binary files /dev/null and b/docs/manual/_images/nomadnet_3.png differ diff --git a/docs/manual/_images/nomadnet_6.webp b/docs/manual/_images/nomadnet_6.webp deleted file mode 100644 index 3f973658..00000000 Binary files a/docs/manual/_images/nomadnet_6.webp and /dev/null differ diff --git a/docs/manual/_images/partyline.webp b/docs/manual/_images/partyline.webp deleted file mode 100644 index ac7d96c1..00000000 Binary files a/docs/manual/_images/partyline.webp and /dev/null differ diff --git a/docs/manual/_images/rbrowser.webp b/docs/manual/_images/rbrowser.webp new file mode 100644 index 00000000..e717d2b4 Binary files /dev/null and b/docs/manual/_images/rbrowser.webp differ diff --git a/docs/manual/_images/ren_browser.webp b/docs/manual/_images/ren_browser.webp deleted file mode 100644 index 6a6223ec..00000000 Binary files a/docs/manual/_images/ren_browser.webp and /dev/null differ diff --git a/docs/manual/_images/rentui.webp b/docs/manual/_images/rentui.webp deleted file mode 100644 index 0c40fad6..00000000 Binary files a/docs/manual/_images/rentui.webp and /dev/null differ diff --git a/docs/manual/_images/reticulated.webp b/docs/manual/_images/reticulated.webp deleted file mode 100644 index ebd2a67e..00000000 Binary files a/docs/manual/_images/reticulated.webp and /dev/null differ diff --git a/docs/manual/_images/retipedia.webp b/docs/manual/_images/retipedia.webp deleted file mode 100644 index 8ffe1a0f..00000000 Binary files a/docs/manual/_images/retipedia.webp and /dev/null differ diff --git a/docs/manual/_images/rngit.webp b/docs/manual/_images/rngit.webp deleted file mode 100644 index 5e1f54e4..00000000 Binary files a/docs/manual/_images/rngit.webp and /dev/null differ diff --git a/docs/manual/_images/rrc.webp b/docs/manual/_images/rrc.webp deleted file mode 100644 index 56889713..00000000 Binary files a/docs/manual/_images/rrc.webp and /dev/null differ diff --git a/docs/manual/_sources/brandolinis.rst.txt b/docs/manual/_sources/brandolinis.rst.txt deleted file mode 100644 index 334dc662..00000000 --- a/docs/manual/_sources/brandolinis.rst.txt +++ /dev/null @@ -1,1625 +0,0 @@ -.. _brandolinis-main: - -Brandolini's Reference -====================== - - | *A little learning is a dang’rous thing;* - | *Drink deep, or taste not the Pierian spring.* - | *There shallow draughts intoxicate the brain,* - | *And drinking largely sobers us again.* - | - | - Alexander Pope, 1711 - -This chapter serves as a helpful reference for developers, users and community members living in an Age of Bullshit Manufacture equivalent to the Cambrian Explosion. - -As we are surely only at the on-ramp of this trajectory, the chapter will continue to evolve, expand and be updated based on community feedback as events occur. - -.. _brandolinis-what-the-license-is: - -What The Reticulum License Is ------------------------------ - -*To understand the legal reality relating to this section, see* :ref:`Void Grants`. - -The :ref:`Reticulum License` is the license under which RNS, the Reticulum reference implementation, has been published since **April 15, 2025**\ , in versioning effective from **0.9.4** and every release since. In practical terms, it covers all of "modern Reticulum": The current wire format, the cryptographic machinery as it exists today (including the AES-256-based link encryption, ratchets, et cetera), and all protocol additions that distinguish the modern implementation from the earliest pre-1.0.0 beta formats. If an implementation speaks today's Reticulum protocol, it is, in all relevant respects, built on Reticulum-licensed work. - -The license is a permissive license. It grants the full range of rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, free of charge; subject to three conditions, which are reproduced here verbatim: - -.. - - * - The Software shall not be used in any kind of system which includes amongst its - functions the ability to purposefully do harm to human beings. - - * - The Software shall not be used, directly or indirectly, in the creation of an artificial - intelligence, machine learning or language model training dataset, including but not - limited to any use that contributes to the training or development of such a model or - algorithm. - - * - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - -.. _brandolinis-why-the-license-exists: - -Why This License Exists -^^^^^^^^^^^^^^^^^^^^^^^ - -Reticulum was not moved to a custom license casually or quickly. It was licensed under the MIT license for the greater part of its existence, and that served well for a long time. The change came when it became clear that no existing license expressed what the project needed, and that the two most commonly proposed alternatives would either say the wrong thing or say nothing useful at all. Three factors had primary influence on the situation: - -**The harm problem** - -Reticulum is a tool that was built, deliberately and at great cost, to empower people; to make communication something you own rather than something you are temporarily granted. It has also been, at various times, exactly what certain kinds of organisations would like to use to control people. This is the duality of any powerful tool. A permissive license could not, and cannot, say anything about that; MIT is silent on the question of whether the software may be used to build systems designed to harm human beings. The Reticulum License is not silent. It does not pretend that a license can stop an individual madman; it does assert, however, plainly and in enforceable terms, that corporations and organisations integrated into the legal system cannot consume this software for harm and then claim they never knew. The line is drawn by the creators of the tool, not by an external authority, and it is drawn deliberately. This is covered in more depth in the discussion of the ethics of the tool in the *Zen of Reticulum*. - -**The appropriation problem** - -The second factor was the machine-learning appropriation of the open-source commons. By 2025 that was no longer hypothetical: The commons was already being systematically ingested by corporate training pipelines, and the code, words, and creativity of the people who built it were being converted into synthetic systems designed to replace them. The license responds to this with an explicit condition: The software shall not be used in the creation of an AI, machine-learning, or language-model training dataset, nor in any use that contributes to the training or development of such a model or algorithm. - -It is important to understand what this condition does and does not do, which matters for what follows in this chapter. **The AI-training condition is not the legal basis for protecting the code from machine copying**. That protection *already exists*\ , in the oldest and most well-tested form of copyright law, and it applies regardless of any license (see :ref:`Copyright & The Two Legal Layers`). - -The AI-training condition addresses something *additional*\ : the appropriation of the code *into the models themselves*\ ; the ingestion of human-made work as raw material for a synthetic replacement, and the use of that ingestion to fabricate "prior work" minefields, patent-troll material, and a wall of slop meant to bury and devalue the actual ecosystem. That danger was foreseeable; the licensing decision was made with full awareness of it. The events of 2026 have since demonstrated, in public and in detail, that the concern was not theoretical. - -**The sustainability problem** - -Reticulum has been developed and maintained for over a decade by a single person, supported by a small group of contributors and network-building users, without corporate backing, without VC funding, through several serious attempts at commercialization and takeover from both the corporate and the ideological side. The license is part of what makes that sustainable, and possible at all. - -It ensures that the specific, painstaking labor of the reference implementation cannot be hijacked to undermine the intent of the project, while leaving the protocol itself, the mathematical rules of how Reticulum works, dedicated to the public domain, where it belongs to humanity and can never be owned. Anyone can implement the protocol, provided they themselves invest the *actual* work and effort. What the license protects is the work, and the intent embodied in it. - -.. _brandolinis-on-freedom: - -On Freedom -^^^^^^^^^^ - -The most persistent mischaracterisation of the Reticulum License is that it is "more restrictive" than the licenses it replaced or than the GPL family. This is backwards, and I'll state the comparison plainly, because the confusion is the foundation of a great deal of deliberately manufactured controversy examined later in this chapter. - -The GPL and AGPL are constructed around a single, self-referential axiom: that "free" is *defined by the terms of the license itself*\ , and that any use falling outside those terms is not free. In practice, this means the GPL restricts the freedom of **everyone** - you may not write, distribute, link, or (in some cases) even communicate with GPL-covered software without the entire arrangement being pulled under the same terms, which the licensor then controls. The restriction applies to 100% of users, including the ones building purely constructive things. - -The Reticulum License restricts the freedom of almost nobody. Its restrictions apply only to: - -* Those who wish to use the software in systems that purposefully harm human beings, and -* Those who wish to ingest the software into AI training pipelines. - -That is a restriction on a vanishingly small fraction of would-be users - the 0.0000001% - and it is a restriction they can characterise however they like, forever. The bickering is itself a signal that the licensing decision hit exactly the right target. - -Everyone else retains the full breadth of use, modification, distribution, and sale, with none of the viral encumbrance of copyleft. - -Both licenses contain moral axioms. The difference is that the Reticulum License states its axioms out loud: - -* *Intentionally restricting the way that millions of people use and build upon a foundational - communications technology* - **The mechanism of copyleft** -* *Intentionally restricting a small number of organisations from using the technology to - violently harm human beings* - **The mechanism of the Reticulum License** - -Which of these is more free? The answer is not a matter of committee approval. It is a matter of what the word "free" means to the people *whose freedom is at stake*. The question of whether "open source" requires the blessing of a particular licensing orthodoxy is examined in :ref:`Open Source Means Open Source`. - -.. _brandolinis-prediction: - -The Prediction, And What Happened -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When the license change was made and publicly discussed in 2025, the following was said, in public, about the machine-learning threat: - -.. - - A generative system could be trained on the Reticulum codebase, and for extremely low cost - be set to work spewing out millions of source repositories for all kinds of ridiculous - "Reticulum-based programs". This is already happening. You can find several examples of this - on GitHub already. … the point of this kind of trolling is to establish as wide a base for - "prior work" as possible. By doing that, a patent troll or IP abuser can create an immense - minefield for real developers. - - -This was written when the phenomenon was just beginning. It was not a guess; it was a description of a mechanism already in motion, in the specific context of Reticulum, by people who had already begun to appear around the project. - -Less than a year and a half later, the prediction has now been demonstrated, in public, exhaustively and to the letter. There are now roughly a dozen machine-generated "Reticulum implementations" of the same shape: fluent, marketing-heavy, some without attribution, and - in several cases - actively harmful to the network. The :ref:`Prns case` is an example documented in this chapter. The pattern that produced it (and the people, mechanisms, and the recycling of falsehoods behind the *wider* phenomenon) is examined in :ref:`A Movement Of A Dozen`. - -The license was written, in part, so that when this happened, the community would not be defenceless. - -.. _brandolinis-means-for-other-implementations: - -What This Means For Other Implementations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Two simple things must be kept separate: - -#. - **The protocol is public domain.** The mathematical rules of Reticulum - addresses, - signatures, routing, the wire format as an idea - belong to humanity. A *genuine* clean-room - implementation of the protocol, written by human beings who reasoned it out themselves, is - free and welcome, and owes nothing to the Reticulum License. Nothing in this chapter is an - argument against implementing the protocol. On the contrary: More good implementations, - honestly made, are good for the network. - -#. - **The implementation is licensed.** Copying the reference implementation - by machine or - by silk-screen press - and modifying and redistributing the result, is - exercising rights granted by the license, and is therefore subject to its conditions: The attribution - notice, the harm condition, the AI-training condition. A copy that does not follow those - conditions is **not** licensed, and it is not "open source in spirit"; it is an unauthorized - derivative work, and therefore a copyright violation. - -The distinction is the same one the license itself makes, and it must be held firmly: An implementation that *speaks* Reticulum is exercising the public domain protocol, **if** it was truly created independently. An implementation that was *generated from* RNS, reproduced by a copy process, is a derivative of licensed work. - -.. _brandolinis-copyright-and-layers: - -Copyright & The Two Legal Layers -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The exact legal structure is frequently muddied in public discussion; and in several cases deliberately. - -**Layer A - Copyright (license-independent)** - -Copyright does not depend on any license. A primarily machine-generated copy of a copyrighted work - a copy not accompanied by demonstrable, substantial, new human creative input - is a derivative work over which the copier holds no copyright whatsoever, and the reproduction of which, without the original author's permission, is an infringement of the original author's copyright. This is standard, well-tested law, and it applies with equal force under the MIT license, the GPL, or any other license, because it is not about the license at all; it is about who created what. A process that copies the reference implementation into a machine, produces a transformed derivative, and then claims "Copyright, [name of the process operator]" - with no human able to account for the creative decisions in the result - **is not** asserting a valid copyright over the result. They are openly asserting the absence of one, and reproducing someone else's work without authorisation. This is probably the cleanest cut copyright violation one could imagine. - -**Layer B - License Conditions (Reticulum License specific)** - -Where a copy *is* a licensed derivative, the conditions of the Reticulum License apply: The copyright notice and permission notice must be included in all copies and substantial portions; the harm condition must be honoured; the AI-training condition must be honoured. A derivative that follows none of these is unlicensed *a second way over*. - -The two layers are often intentionally confused in public discussion - the AI-training condition is waved around as if the entire question of machine copying rested on it, when in fact machine copying of copyrighted work is a copyright violation that would exist even under the most permissive license ever written. The purpose of keeping the layers separate is not rhetorical flourish; it is that Layer A is old, settled law, and does not rely on any novel legal theory, while Layer B is specific to this license. Both matter. Neither is a substitute for the other. - -Layer B is the required mechanism for closing the loophole where the work is *first* ingested during model training into the weights of a model itself, which would make any resulting violations almost impossible to detect and enforce. - -.. _brandolinis-cleanroom-delusion: - -The "Clean-Room" Delusion -^^^^^^^^^^^^^^^^^^^^^^^^^ - -I regularly receive mail or LXMF messages from people who proudly and happily proclaim that they have created a "clean-room" implementation of one of the software systems I maintain; RNS, Sideband, LXMF or others. The declaration is delivered with the flat certainty of someone who believes they have found the legal lock-pick that settles everything at once: Their production is independent, uncontaminated, and therefore free of any obligation to the work they copied, its license, or its author. The proclamation usually concludes with a request for blessing, or at least acknowledgment. The proclamation is, usually, also generated by an LLM. - -This is a delusion, and I'll cut through it surgically. The word they here invoke as magic is attempting to do a great deal of work it was never able to bear. - -**What a clean room actually was** - -The legal "clean room" is not folklore, incantation, witchcraft or a claim a person gets to make about their own output. It is a very specific, institutionalized procedure that grew out of the hardware and software litigations of the 1980s, and it existed for exactly one purpose: **Convincingly proving the absence of copying in a court of law**. - -The canonical case was Phoenix Technologies and its 1984 reimplementation of the IBM PC BIOS. IBM's BIOS was copyrighted, which meant that the entire PC-compatible industry depended on being able to show that a compatible BIOS had been written independently, and not from IBM's code. Phoenix's answer was a physical and procedural firewall: - -* One team, the "dirty room", read the original code and produced written *functional specifications*\ : descriptions of what the BIOS did, not how IBM's code did it. -* A second team, the "clean room", consisted of engineers who had *never seen the original code at all*\ , and they wrote the new BIOS from those specifications alone. - -This obsessively documented, legally attested separation was not a preference or a willy-nilly show for a kindergarten; it was the entire legal point, and it had to be *solid*. If even one clean-room engineer had *glanced* at IBM's code, the clone would have been contaminated, and the defense would shatter. - -The other landmark case was *Computer Associates v. Altai* (1992), in which Altai had copied CA's scheduler code into its own product, and (upon being caught) hired outside counsel to run a genuine clean-room procedure: A programmer with no exposure to the original was brought in to rewrite the infringing modules from functional specifications prepared by the team that had seen the code. The court credited the good-faith effort precisely because it was done, as the carefully documented record showed, to the letter of the procedure. - -Bear in mind what this procedure entailed in practice, because this is the part the modern claimants never seem to have even *heard* of: Physical separation of teams, legal counsel managing the process, signed and notarised declarations of non-exposure, controlled specification documents that described *behaviour* and never *code*, months of work, and testing against the specification - not against the original, which the implementers were forbidden to consult. - -The people on the clean-room side of the wall did not just *claim* they had not read the code. Their non-exposure was institutionally guaranteed, obsessively documented, and legally attestable through every fart and sneeze of the process, because the entire point was that a court - hostile, adversarial, and entitled to discovery - would believe it. - -*That* is the process. "Clean-room implementation" is the name of a *certified procedure for proving independent authorship*. It is not a mood, a marketing label, or a property a person gets to declare about their own work. - -**The moronic inversion** - -Now consider what is actually being described when someone says: *"I created a clean-room implementation of Reticulum by feeding the source code into a machine."* - -That sentence is the opposite of a clean room. Feeding a copyrighted work into a machine so that it reproduces and transforms the work is **the definition of copying**\ , and a clean room is the one procedure whose entire purpose is the demonstrated *absence* of copying. This process cannot demonstrate the absence of copying, because the process is *composed of* copying. The cleanest possible room has been replaced by the dirtiest possible room, and the dirt has been installed as the base feature. What these claimants have actually performed is a photocopy with extra steps, and the extra steps, far from cleaning anything, are simply steps that forge a certificate they believe the output has earned. - -**Why the word is deployed** - -The word "clean-room" is deployed because it sounds like what the speaker wishes were true. It carries the accumulated legal prestige of the 1980s and 1990s - the BIOS clones, the Altai credit - and it sounds like a certificate already issued: *Independent, original, unencumbered*\ , the work of a person who owes nothing. The claimants want that halo without the procedure: No law firm, no physical separation, no written specifications, no signed declarations, no months of process - just a laptop, a loop, and the word "clean-room" typed into a README. The word must do the work of the process it replaces, and the delusional replacement is the entire point. - -**The irony runs deep** - -The Reticulum protocol is public domain. Any human being can implement it - from the mathematics, from the documentation, from their own reasoning - and owe nothing whatsoever to the license, to the reference implementation, or to me. - -An honestly written implementation needs no incantations, no legal theater, and no special vocabulary; it simply *is* what it claims. It's plain for everyone to see. **It's also plain for everyone to see when you're faking it**. The people who reach for the word "clean-room" are, without exception, the people who need it to create a fantasy - and the people who need that are precisely the people who cannot have it. If what you wrote were genuinely independent work on the public-domain protocol, you would not need to call it anything. The word is not a seal of independence. In this context it is a confession of copying, filed by the copier, in the confident belief that it says the opposite. - -.. _brandolinis-opensource: - -Open Source Means Open Source ------------------------------ - -An objection regarding the "open source status" of RNS appears occasionally in variations, but always reduces to the same syllogism: - - -#. "Open source" means *licensed under one of the licenses approved by the Open Source - Initiative (or, in other variants, compatible with GPL orthodoxy)*. -#. The Reticulum License is not on that list. -#. Therefore Reticulum is not open source, and anything built on it is illegitimate. - -The entire argument rests on the first premise, and the first premise is false. - -Open source is not a certification conferred by a committee. It is a description of a fact: The source code is available, and the rights to use, copy, modify, merge, publish, distribute, sublicense, and sell it are granted to everyone, free of charge. - -The Reticulum License does all of those things. It is, in every substantive respect that concerns the person who wants to use, study, modify, or build upon the software, **an open license**. What it adds - and what disqualifies it in the eyes of the orthodoxy - is two conditions on *who may do what with the original work*\ , not on what the *user* is allowed to do with it. - -The definition being wielded against Reticulum is not even a young one. It is, in essence, the Debian Free Software Guidelines written in 1997, adopted by the OSI in 1998, and treated ever since by certain people as if it had descended from a mountain on tablets. It is an approach that is close to three decades old. Being old is not itself a flaw - but being old is also not a proof of correctness, and the suggestion that the meaning of a word like "open" was settled for all time by a committee in the 1990s, and may never be revisited, is not a position rooted in reason. It is rooted in institutional inertia dressed as principle, maintained by those whose ideologies it serves. - -.. _brandolinis-what-the-argument-is-doing: - -What The Argument Is Actually Doing -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -I would like to go into some detail about the rhetorical mechanism, because it is roughly the same mechanism that powers the rest of the campaign described later in this chapter. - -The orthodoxy's argument is self-referential: *"open source means what we say it means, because we are the ones who define it."* When the Reticulum License is measured against that definition and found wanting, the conclusion is presented as a property of Reticulum - when in fact it is *a property of the definition*. The definition is treated as neutral and authoritative - as some base fact of raw cosmology - and the license is treated as deviant. But the definition is no more neutral than the license is. It encodes a moral position, and that moral position is exactly the one that this project declines to take. - -Consider what the orthodoxy's definition requires, in the case at hand. The OSI-based definition contains, in its ancestry, the Debian Free Software Guideline of "No discrimination against fields of endeavor": The software must not restrict who may use it or for what purpose. Taken to its logical end, this means that open source, properly "certified", must be equally available to those who build hospitals and those who build weapons, those who connect people and those who control them, those who create and those who ingest and replace. - -The orthodoxy presents this as neutrality. It is not neutrality; it is a moral position - one that says, in effect, that *harming human beings is a field of endeavor like any other, and software must not discriminate against it*. - -The Reticulum License takes a different position, and it has the honesty to state it in the license text: this software is for life, not for death; it is for people, not for the systems that would replace them. That is not a restriction on openness, nor on freedom. It is a restriction on *who may consume the work*\ , and it is a restriction the orthodoxy's definition cannot express - which is why the orthodoxy must always attack the license as "not open source" rather than engage with what it actually does. - -.. _brandolinis-who-is-restricted: - -Who Is Restricted By Which License? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The arithmetic is simple, so let's do it out loud, because the orthodoxy's framing inverts it every time. - -The GPL family imposes its conditions on **everyone**\ : You may not distribute, link, or - in the AGPL case - even *communicate with* GPL-covered software without your own work being pulled under the same terms. "Free" in this system is defined (self-referentially) *by the license's own terms*\ ; everything outside those terms is, by definition, unfree. The restriction applies to 100% of users. It applies to the hospital, the mesh community, the school, and the hobbyist with the same force it applies to anyone else. That is not freedom for everyone; it is a *specific moral architecture applied to everyone*\ , presented *as if it were the absence of one*. - -The Reticulum License imposes its conditions on a fraction of would-be users so small as to be practically unmeasurable: - -* Those whose intended use is to build systems that purposefully harm human beings, and -* Those whose intended use is to ingest the work into AI training pipelines. - -That is the entire list. Everyone else - the overwhelming majority, the community, the developers, the users, the downstream projects - receives the full, unrestricted rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, with none of the viral encumbrance of copyleft. - -So when the claim is made that the Reticulum License "is more restrictive" than the GPL, the claim is not merely wrong; it is the exact inverse of the truth. The GPL restricts the freedom of everyone, in the name of a specific ideology. - -The Reticulum License restricts the freedom of the harm-doer and the appropriator, and leaves everyone else free. The two approaches do not differ in whether they contain moral axioms - **all licenses do**. They differ in whether the axioms are stated openly, and in *whose* freedom they restrict. - -.. _brandolinis-consequences-not-open: - -The Consequences Of The "Not Open Source" Claim -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This would be a footnote debate if the mischaracterisation had no practical consequences. It has very large ones, and therefore this section exists. - -The claim that "Reticulum is not open source" is the foundation of the takeover narrative it so often accompanies. The sequence is always the same: - -* First, declare the reference implementation illegitimate ("not really open source", "owned by one man", "not a real community project") -* Second, conclude that it is therefore acceptable - even necessary - to replace it. -* Third, present the replacement, which is (conveniently) often the speaker's own machine-generated clone, as the legitimate heir. - -This is not speculation about motives; it is the documented structure of the campaign examined in :ref:`A Movement Of A Dozen`. - -The "not open source" claim is the legal-ideological battering ram of that campaign. It is recycled, in article after article, in forum after forum, because it works on people who assume that a committee's definition is the same thing as the truth. - -It is also worth stating the other obvious fact: None of these critics have ever shown that the Reticulum License restricts *them*. They do not build weapons. They are not training models on the codebase. They are free, in every practical sense, to use Reticulum exactly as they would under MIT or GPL. Their objection is not to a restriction on themselves; it is to the *mere existence* of a license that states a moral position they disagree with, or which they can use as leverage for a different goal entirely, and to the institutional fact that it does so outside *their* system of approval and control. The complaint is not that the license makes them unfree. The complaint is that the license is *free of them*. - -.. _brandolinis-distribution-argument: - -The Distribution Argument -^^^^^^^^^^^^^^^^^^^^^^^^^ - -A recurring companion claim is that RNS is not properly "distributed", usually deployed as "it's not packaged as a .deb" or "it's not on package manager X", with the implication that it is somehow unavailable or inaccessible, or hard to install on a given system. - -This is simply false, and it has been false for years. The reference implementation is available on practically every platform and architecture with a single command (\ ``pip install rns``\ ). It is packaged in the Arch User Repository, in NixOS, and in a range of other distribution channels, community-maintained and official. It can be installed, updated, and kept current *over Reticulum itself* - the network transports its own software updates. It is used on more than a hundred thousand devices worldwide, is embedded in over a hundred other packages and applications, and carries terabytes of traffic every day. - -The claim that Reticulum needs a ``.deb`` to be "really distributed" mistakes the mechanism for the goal. The goal is that people can obtain and update the software easily, everywhere, **on their own terms**, and distribute it themselves, both offline, online and over Reticulum with full cryptographic verification of the install packages, without any central infrastructure. - -That goal is met to an excellence that few software packages can speak of, and met in a way that does not pin the project's release cadence to the review cycles of distribution maintainers, which is an achievement, not a bug. - -A project like RNS that updates continuously, sometimes daily, and has a track-record of shipping security updates mere hours after issues are discovered, would be crippled by packaging that makes every change wait months for approval. The distribution complaint is a strawman generated for the occasion. It has been answered, in public, many times; it is repeated anyway, because the people repeating it are not counting on facts - they are counting on the reader never having heard the answer. - -.. _brandolinis-on-institutions: - -On The Institutions, In Fairness -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -None of the above is hostility toward the Open Source Initiative or the Free Software Foundation as institutions, nor toward the individuals who have dedicated decades of work to them. Their contribution to the world of software is **enormous**\ , and this document does not dispute it. What is disputed is a specific claim of authority: That "open source" is a term whose meaning is reserved to a committee's approval, and that a license which does not submit itself to that approval is therefore not open. - -That claim of authority is not in Reticulum's interest, and it is not in the interest of the people the movement claims to serve. The current legal and technological landscape - the machine-appropriation of the commons, the weaponisation of "prior work" generation, the erosion of human agency by synthetic systems - was not anticipated by the DFSG of 1997, and the institutions have not, as of this writing, produced *any* meaningful answer to it. The choice, as it was put at the time of the license change, is whether developers should sit back and wait for a solution from a committee that "may or may not ever materialize, before the entire foundation we stand on has been eaten away". - -**Reticulum's answer was to act**. If we had waited, it would have been too late. - -If an OSI-approved license existed that expressed the same constraints - and did not impose the far more severe restrictions of copyleft - the project would have used it. None such exists. That is not Reticulum's failure. It is the measure of what the definition is missing. - -.. _brandolinis-what-open-means: - -What "Open" Means Positively -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For anyone who wants the positive statement rather than the rebuttal, Open Source, in this project's understanding, means: - -* The source code is available, to everyone, without charge -* Everyone may use, copy, modify, merge, publish, distribute, sublicense, and sell the - software -* No one is discriminated against as a person - developer, user, community, or project -* The protocol itself is dedicated to the public domain, where no one can own it -* And the single, plainly stated condition of the license is that the work shall not be - consumed to harm human beings, or ingested to replace the humans who made it - -If that is not "open" by a definition written in the 1990s, then the definition is the thing -that has failed - not the software, and not the people who use it. - -**Open source means open source**. It was true before the committees existed to certify it, and it will remain true after they are gone. - -.. _brandolinis-assistance-versus-substitution: - -Assistance Versus Machine-Substitution --------------------------------------- - -.. _brandolinis-false-dichotomy: - -False Dichotomy -^^^^^^^^^^^^^^^ - -The public discussion around machine learning and software development has been derailed, by the loudest voices on both sides, into a farcical fight between "AI enthusiasts" and "AI haters". - -.. - - **Both positions are absolute caricatures.** - - **The fight is beside the point in the ways that matters most.** - -It is not an accident, but a very conscious choice that the initial analysis of the Prns case examined herein, and this very chapter, were themself produced with the assistance of a machine-learning based generative system. - -Specifically, it was generated using ``lc`` managing a ``Q8_K_XL`` quant of ``DeepSeek v4 Flash`` running in the ``llama.cpp`` inference engine. The main context base used to stabilize, ground and inform the generative environment consisted of approximately 100,000 tokens of text material that I personally thought out and wrote (by hand), then archived and organized, over the last eight years. - -If you know my writing, and have a hard time discerning where this chapter is me directly, typing manually, and where it is the machine output, it is because of two things: Almost every line in this chapter was either directly edited by my hands on the keyboard or typed by me, and the text corpus that informed the process *is* genuinely me - my thoughts, written by me, over a period of years. - -The chapter was produced this way *deliberately and openly*, under a software framework I designed and wrote, with the human directing the inquiry points, flow of investigation, structure of the outputs, contents of every section, making the editorial decisions, reviewing the conclusions, painstakingly adjusting every line, and taking responsibility for the final claims presented, with the goal of reaching a higher level of synthesis and symbolic density than I could have achieved without that assistance. - -The tool was *used as tool*, both to structure and produce a complex chapter, and to inspect a repository so preposterous in size, commit velocity and nonsensical wording that it barred any purely human inquiry. - -As such, the position is hereby, by personal example and demonstration, stated clearly: - -* **Technological assistance is not the problem** -* **It was never the problem** -* **It is the opposite of the problem** - -The problems are *substitution* and *usurpation*\ : The perverse process in which the machine becomes the author of choices, and the human becomes the facade of software, while expecting "reciprocity" or recognition for deeds they never performed. - -.. _brandolinis-the-line: - -The Line -^^^^^^^^ - -The exact distinction can be reduced neither to how many lines of code a machine produced, nor who pressed the keys. The final distinction has to rest on **who, or what, the real agent of the work is**. - - -* **Assistance** is when the human retains the full seat of agency: The human decides what to build, judges - what is correct, rejects what is wrong, understands what is shipped, and can answer for every single line of it. - The machine writes, suggests, drafts, accelerates *meaningfully* and within reason - but every choice that survives into the - final work is a choice the human made *consciously*\ , and understood - or grew to understand - the consequences of. -* **Substitution** is when the machine *becomes* the agency: the machine decides what to build, - decides what is correct, iterates on its own errors, generates at a scale and speed no single - human can follow - and the human's role is reduced to providing the name, the account, the - signature, and the marketing launch. - -.. - - The Line can be stated as a single test, and this is the same test that has always separated authorship from couriership: **Can the person whose name is on the work answer for it fully?** Not "can they describe what it does" - can they account for why it is the way it is, line by line and decision by decision, and the history of how it came to be that way, under questioning, without machine assistance, including by people who know more than they do? - -If they cannot, they did not author the work. They wore it, or in more severe cases, they stole it. And a courier who claims authorship is not a writer; they are a fraud. - -.. _brandolinis-what-assistance-looks-like: - -What assistance looks like -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Assisted work is recognisable, and recognisability is itself part of its value. It has the following observable properties: - -#. **The human can explain it.** Under examination, the author can say why this choice was - made, what was rejected, and what they verified. The ability to explain is not a luxury; - it is the very demonstration of authorship. On a large project with many contributors this is partial in - practice - no one can explain every line of a large codebase - but the *full capacity* must - exist between the authors and the *habit* must be visible. -#. **The human can, and does, reject machine output.** An author who has never rejected the machine's output is not - an author, but an open drain. Rejection is not a stylistic flourish; it is the - mechanism by which any work attains Quality. A record of decisions, including reversals and rejections, - is a sign of agency. A record without rejection is a record of assent-by-default. -#. **The human's judgment is directional.** The work contains the human's actual choices; - things the machine would not have decided on its own, which the human can defend on their - merits. This directionality has many names in engineering: Design, architecture, taste, - judgment. Where it is absent, the work has the uniform, self-consistent texture of - something that was never doubted, and doubt is exactly what anything important requires. -#. **Provenance is not concealed.** Disclosure of how the work was made is not a legal - requirement in every jurisdiction; it is not even always a practical requirement. But it - is a *statement* that matters. A project that discloses its process - - including its use of machines - is telling the truth about the relationship between the - human and the work, and that is a property users *are* entitled to know. Concealment of - provenance is not a stylistic choice; it is deception, made by someone who knows what the - record would otherwise show. -#. **The human stands behind the failures.** When assisted work fails, the human can say: Yes, I - chose this, under these assumptions, here is why, here is what I got wrong, here is the fix. - That sentence is the entire contract between an author and the people who depend on their work. - A human who cannot say it - because the choices were never theirs - *has nothing to offer anyone*\ , and - is by definition entirely replaceable; anyone else could have pressed the same button. - -.. _brandolinis-what-substitution-looks-like: - -What substitution looks like -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Substitution is equally recognisable, once the pattern is known: - -#. **Scope that no human could have authored.** Output that - structurally cannot have been the product of human authorship, review, and judgment, for - example when the volume exceeds any reasonable human capacity within the span of the project's life. - When the review capacity and the output volume are incommensurable, the output was not - reviewed. It was generated, then forgotten, and the record was signed blindly. -#. **Uniform generation.** The work carries no trace of a human's changing mind. No style - drift, no contested decisions, no "we tried X and it was wrong, so we did Y". That is, *no - signs of actual development, growth and learning*. Instead, endless, self-referential - refactoring, renamed abstractions, and the same fluent register in every commit, - every document, every reply. A human's history shows the wear of judgment. - Pure generation shows the superficial polish of its absence. -#. **Discovery by recursive regeneration.** When errors are found, they are found by the machine's own - subsequent runs, not by human scrutiny - and the findings are recorded as planned - "reviews" and "alignments" rather than as the corrections they are. This is the tell: A - machine editing its own previous output, while the edit is recorded as if it were a - scheduled milestone. That's not review. That's a continuous generation loop revising itself, - with the attempt of an illusion-preserving voice-over. -#. **Laundered provenance.** Theatrical concealment machinery: Commit hooks that refuse - attribution of machine generation, contribution policies that declare machine output - "yours", processes that scrub personal traces from the history, fake identities in the - contributor list. All of these have one function: To make the record say what the "author" - *wished* had happened, rather than what happened. A project that builds such machinery is - not making a record of its work; it is using automated software to manufacture one. -#. **Authorship by assignment.** The human's name is on the work, but the human cannot - answer for it - because the human did not choose any of it, and knows it. The name is not a - signature of authorship; it is a *fraudulent claim of ownership* over something the claimant did not - make and cannot explain. In every field that has ever dealt with such claims - art, - literature, invention, journalism - *this has a name*. That **forgery** is now cheap in software - makes it no more forgivable. - -.. _brandolinis-infrastructure-matters: - -Infrastructure Matters -^^^^^^^^^^^^^^^^^^^^^^ - -For a hobby project, the distinction between assistance and substitution is a personal matter. For infrastructure - code that other people's communications, safety, and freedom run on - it is a matter of life and safety, and any statement about this must be entirely unapologetic. - -Infrastructure has one property that distinguishes it from almost all other software: While most of the people who depend on it cannot verify it themselves, **it must be verifiable, auditable and open to human inspection**\ , and be engineered in such a way that this is as frictionless and straight-forward as possible. - -The reference implementation of Reticulum was designed this way, and continues to make that goal a primary one. While it *is* a complex piece of software, with an intricate array of interlocking and moving parts, the core parts of RNS, the entire Reticulum protocol implementation, the transport logic, the cryptographic primitive interfaces, **is less than 15,000 lines of code**. That line count even includes the rather extensive comments, incredibly detailed logging facilities and other choices that makes the code easy to read and understand for humans, and easy to operate, verify and debug. It took real dedication, and *a lot* of hard work to achieve that. - -Exactly because not *everyone* can do this, it is a fundamental necessity for infrastructure software that it is designed and authored so that *as many as possible can*\ , and that the *chain of accountability* - who made it, who reviewed it, who answers for it - is held intact at every link. That chain is not decoration. It is the entire mechanism by which normal users decide to trust a system they cannot fully verify. The transparent foundations and the chain combined *is* the verification for most users. - -Substitution breaks the chain both at the foundations, and at its load-bearing link: The human at the top can no longer answer, because they were never the maker. Unfortunately, the chain does not always fail visibly; often it fails silently, at the exact moment a question goes deep, or when routing fails subtly at a critical moment. - -For the user, the result is not a project with a normal and humanly flawed author - it is a project *with no author at all*\ ; a signature with no substance behind it, a name that cannot be held to account, and a body of work that nobody on earth can (or wants to) explain, defend, or fix in a crisis. - -.. _brandolinis-the-fluency-trap: - -The Fluency Trap -^^^^^^^^^^^^^^^^ - -It is worth understanding, briefly, why substitution is so attractive and so dangerous at once. Machine-generated output is fluent. It is confident, well-structured, and terminologically *self-consistent*. At the surface, and from the perspective of someone outside a given craft, it is indistinguishable from the output of a knowledgeable human. Fluency of this kind can be a useful property: It makes assisted work fast. But fluency is *not* understanding. It is the *minimal conformal surface* of understanding. - -The danger is that the surface is exclusively where the substituter invests; for the simple reason that the surface is where most of the desired audience looks, and what yields attention to the project. - -A human who can tell the difference uses the tool well. A human who cannot is being deceived through means they cannot detect from inside, namely through their own inability to account for the limits imposed by what they do not know. - -This is why the most important quality for anyone working with systems like Reticulum is not speed, and not vocabulary, but the oldest engineering virtue there is: The honest, uncomfortable, terrorizing knowledge of **what you do not know**. The substitute *never* has it. The substitute's operator, *if* they ever had it, loses it precisely the moment they accept the fantasy of the substitution process. - -The tool is not the hazard, plain old human nature is. The tool is a mirror, and mirrors - as has been observed for rather longer than software has existed - show the truth only to those willing and yearning to see their own shortcomings. - -.. _brandolinis-disclosed-assistance: - -Disclosed Assistance Or Laundered Generation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The contrast at the centre of this entire discussion can be made clearer with two examples, both of which are publicly accessible, both produced in the same year, by people using the same class of tools. - -The first is this chapter. It was produced with machine assistance, openly: the structure was directed by the human, the material was assembled with the tool's help, every conclusion, every line, every slight nuance of meaning and semantics was examined, edited, and accepted or rejected by the human, and the process described publicly - including the precise characteristics of the technological assistance framework used. The human can answer for every claim in these documents, because the human made the decisions the claims rest on. - -The second is the project examined in :ref:`the Prns case`. It was produced by a machine, at a volume no human could review, and the authorship record was engineered to erase the machine's role: A commit hook that refuses attribution of machine generation, a contribution policy declaring machine output the human's own, a fictitious contributor identity, 637,000 lines of unreviewed code. The human cannot answer for the code, because the choices were never made by a human - only the conformal surface of the presentation was. - -The same tool class. The same technology. Two diametrically opposed relationships to truth and responsibility. **The difference is not the tool**. The difference is the presence or absence of the human as the agent of the work - and the honesty or concealment with which that relationship is recorded. - -Assistance leaves the human as the agent and the commons as the source from which the human works and can grow. Substitution removes the human from the loop and converts the commons into fuel for the thing that seeks to replace them, or usurp their work for the profit of the few, human atrophying in the process. - -.. _brandolinis-license-in-context: - -The License In This Context -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The Reticulum License, examined in :ref:`What The Reticulum License Is`, draws the same line, at the level of the law. Its AI condition does not forbid the use of machine-learning technology to *assist* development - no reading of the license could support that, and no one associated with this project has ever claimed so. What the condition forbids is the *appropriation of the work into the machine itself*\ : The ingestion of the commons as raw material for synthetic replacement, and the concealed fabrication of "prior work" that would bury the real thing. - -As also noted in :ref:`Copyright & The Two Legal Layers`, plain old copyright law already protects against the case where a process copies the reference implementation into a machine, produces a transformed derivative, and then claims "Copyright, [name of the process operator]", with no human able to account for the creative decisions in the result. That is simple, petty theft, as it always has been. - -.. _brandolinis-general-principle: - -The General Principle -^^^^^^^^^^^^^^^^^^^^^ - -In these regards, the position of this project can be codensed to something relatively concise: - -* Machine assistance in human learning, development, and creation is accepted and useful. - It is not a shameful secret; but it *is* a tool that needs knowledge, training, understanding, - and great care to use. It should not make anything easier or more fun, it should make the - results better, more carefully considered, and arrive at *higher levels of synthesis*\ , not - produce endless streams of code churn. -* The line is crossed when the human stops being the agent of the work. When *any* output is - accepted that the human cannot explain, produced at a scale the human cannot review, or filed under a record - where the human has arranged to conceal the fact. -* The line is crossed, when a human claims authorship of work they cannot - answer for or has usurped. Credit follows the ability to answer. It has always worked that way, and - machines do not change the rule - they only make it cheaper and more tempting to break. -* The people who depend on infrastructure have the right to know which side of the line - the code they run was made on. That knowledge is the only verification most of - them will ever be able to perform. - -Use assistance, by all means, *and learn*\ , but don't substitute. And never sign what you cannot answer for. - -Somewhere, within the general shape of the conceptual framework outlined in this section, or realistically, a more refined variant of it, I do believe there is the *potential* of the *beginning* of a constructive outcome to the immense challenge we are faced with as a species in regards to these problems. - -At the same time, I reserve my right to revert that position to Butlerian Jihad without prior notice. We are on a knife's edge, and you know it just as well as I do. - -.. _brandolinis-void-grants: - -Void Grants: Legal Foundations of Machine-Generated Code --------------------------------------------------------- - -A grant of rights is only as real as the rights behind it. A license conveys what its issuer holds, and nothing more. A derivative may be distributed only under whatever license covers what it took. And over a work that a machine originated, no human holds anything at all. - -Each of these statements is a matter of settled statute and case law, and this section sets out the authorities. The conclusions they support are: - -#. A software implementation generated from a licensed work by a machine, published under its operator's claimed copyright and license, **conveys rights the operator never possessed**. -#. **Its grant is void**. The projects, products, and promises built upon such grants stand on nothing. A field of void grants is a minefield for everyone who crosses it. - -This section is a general examination of copyright law as it applies to machine-generated code. It is not legal advice. - -I. The Foundational Statute: Translation Is Copying -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The starting point is the statutory definition of the class of works at issue. Title 17, United States Code, section 101 provides the definition of a derivative work: - -.. - - A "derivative work" is a work based upon one or more preexisting works, such as a *translation*\ , musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or *any other form in which a work may be recast, transformed, or adapted*. - - -Section 101 further provides that *"the terms 'including' and 'such as' are illustrative and not limitative"*. As such, the list is a list of *examples*\ , and the very first example is translation. The definition also reaches, in its own words, *"any other form in which a work may be recast, transformed, or adapted"*. - -The exclusive right at issue is defined in section 106: - -.. - - Subject to sections 107 through 122, the owner of copyright under this title has the exclusive rights to do and to authorize any of the following: - (1) to reproduce the copyrighted work in copies or phonorecords; - (2) to prepare derivative works based upon the copyrighted work; - (3) to distribute copies or phonorecords of the copyrighted work to the public by sale or other transfer of ownership, or by rental, lease, or lending; - - -Violation of any of these rights is infringement. 17 U.S.C. § 501(a). - -Taken together, the provisions yield a construction that is difficult to improve upon and impossible to escape: - -#. **A port is a translation.** A work that takes a program written in one language and from it produces a program written in another language, implementing the same behavior, is a translation of the original - whatever mechanism performed it, and however the surface presentation differs. -#. **A translation is a derivative work.** Section 101 says so, explicitly. -#. **Preparing a derivative work is the copyright owner's exclusive right.** Section 106(2) says so explicitly. A derivative work prepared without authorization, or outside the scope of any authorization, infringes that right. - -The common objection - "the code is in a different language, so it is completely different" - is not a legal argument; it is the definition of what the statute already classifies. - -Translation *is the paradigm* of derivative authorship, not an exception to it. If translating a work without permission was a defense, the derivative-work right would be meaningless; every infringing adaptation would simply need to be "different enough" to count as a translation of its source. - -Nor does the identity of the translator change the analysis. The statute asks whether the work is "based upon" a preexisting work. It does not ask who or what performed the translation. A machine that ingests a licensed work and reproduces a translated transformation of it has performed mechanical translation; the output is a derivative of the ingested work, and the law governing derivative works applies to it exactly as it applies to a translation made by hand. - -II. Ideas Are Free, Expressions Protected -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The second foundation is the distinction between what a work *is* - its ideas, its processes, its systems - and the *expression* in which those ideas are embodied. The distinction is codified in section 102(b): - -.. - - In no case does copyright protection for an original work of authorship extend to any idea, procedure, process, system, method of operation, concept, principle, or discovery, regardless of the form in which it is described, explained, illustrated, or embodied in such work. - - -The principle is older than the statute, and its application to software is settled. An author may own the particular code in which a method is expressed; the method itself, the process, the algorithm, belongs to no one - because *"the sine qua non of copyright is originality"* (\ *Feist Publications, Inc. v. Rural Telephone Service Co.*\ , 499 U.S. 340, 345. 1991). And ideas are not original to anyone in particular, *Baker v. Selden*\ , 101 U.S. 99 (1879), established the line: Copyright in a book describing a system does not prevent others from practicing the system. - -Applied to a communications protocol, the structure is clean: - ----- - -**An implementation of a public-domain protocol, honestly reasoned and written by humans, is welcome and unencumbered, and owes nothing to any license. An implementation generated from a licensed implementation is a derivative of licensed work, subject to its conditions and to the ordinary copyright of its source.** - ----- - -The public-domain status of the protocol's ideas changes nothing about the copyright in an implementation's expression. A person who copies an implementation - by machine or by hand - cannot defend the copying by pointing to the protocol, because the protocol covers the ideas, not the symbols in which someone expressed them. - -A further misconception can be dismissed here; the recently popularized "clean-room" claim about the absence of copying. A :ref:`clean-room` is a not a magic label achievable by LLM, but a specific, institutionalized procedure for proving to a court that copying did not occur: Physically separated teams, written functional specifications, attestable non-exposure, and no access to the original by the implementers (\ *Altai*\ , 982 F.2d at 709–11). - -*"I created a clean-room implementation by feeding the original source code into a machine"* attempts to *claim* the certified, legally established procedure for proving the absence of copying by describing, in the same breath, a process **composed entirely of copying**. The word is deployed because the procedure it names is what the claimant would *need to have happened*. - -III. Machine Output Has No Author -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The third foundation is the human authorship requirement. It is settled at both levels of the federal courts and in the Copyright Office's administering practice. - -In *Thaler v. Perlmutter*\ , the district court held that *"copyright law protects only works of human creation,"* and that *"human authorship is a bedrock requirement of copyright"* (687 F. Supp. 3d 140, 146, D.D.C. 2023). On appeal, the United States Court of Appeals for the District of Columbia Circuit affirmed, holding that the Copyright Act *"requires all eligible works to be authored in the first instance by a human being"* (No. 23-5233, D.C. Cir. Mar. 18, 2025). - -The requirement traces to the ordinary meaning of the statutory term. In *Burrow-Giles Lithographic Co. v. Sarony*\ , the Supreme Court defined an "author" as *"he to whom anything owes its origin; originator; maker; one who completes a work of science or literature"* (111 U.S. 53, 57–58, 1884), and more recently described the author as *"the person who translates an idea into a fixed, tangible expression entitled to copyright protection"* (\ *Community for Creative Non-Violence v. Reid*\ , 490 U.S. 730, 737, 1989). - ----- - -**A machine does not owe expression its origin; it reproduces it. No court has recognized copyright in material created by non-humans, and the courts that have addressed the question have rejected it. If you create by machine generation, you do not own the output.** - ----- - -The Copyright Office's registration guidance applies the requirement to generative systems in terms that are directly on point. The Office asks: - -.. - - whether the "work" is basically one of human authorship, with the computer [or other device] merely being an assisting instrument, or whether the traditional elements of authorship in the work (literary, artistic, or musical expression or elements of selection, arrangement, etc.) were actually conceived and executed not by man but by a machine. - - - -* *Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence*\ , 88 Fed. Reg. 16190, 16192 (Mar. 16, 2023) - -And it states the consequence: *"If a work's traditional elements of authorship were produced by a machine, the work lacks human authorship and the Office will not register it"* (\ *Id.*\ ). - -On the role of prompts, the Office is equally explicit: when a system receives a prompt and produces a work in response, *"these prompts function more like instructions to a commissioned artist - they identify what the prompter wishes to have depicted, but the machine determines how those instructions are implemented in its output"* (\ *Id.*\ ). - -The agency applied this standard and excluded machine-originated material from registration in the leading administrative case. In *Zarya of the Dawn* (Cancellation Decision, Feb. 21, 2023, Registration No. VAu001480196), the Office cancelled registration of images generated by the Midjourney service, holding that *"it was Midjourney-not [the author]-that originated the 'traditional elements of authorship' in the images"*\ , while preserving protection for the human-authored text and the human selection and arrangement of the work. - -The Copyright Office's 2025 report on AI copyrightability reaches the same conclusion: - -.. - - Based on the functioning of current generally available technology, prompts do not alone provide sufficient control. - - - -* U.S. Copyright Office, *Copyright and Artificial Intelligence, Part 2: Copyrightability*\ , at iii (Jan. 29, 2025) - -The report's analysis is explicit: *"The Office concludes that, given current generally available technology, prompts alone do not provide sufficient human control to make users of an AI system the authors of the output. Prompts essentially function as instructions that convey unprotectible ideas"* (\ *Id.* at 18). - -Its guiding distinction is stated plainly: *"The use of AI tools to assist rather than stand in for human creativity does not affect the availability of copyright protection for the output"* while *"Copyright does not extend to purely AI-generated material, or material where there is insufficient human control over the expressive elements"* (\ *Id.* at iii). - -The boundary is therefore not "AI or no AI". It is *assistance or substitution*\ : Whether a human exercised sufficient creative control over the expressive elements and can answer for them, or whether the machine originated them and the human merely supplied instructions and a signature. That is a question of fact in each case, determined from the record of how the work was made. - -Two facts follow, and stated together, they form the base legal reality: - - -#. **A work whose expressive elements were originated by a machine contains no valid new copyright: It has no human author. To the extent that the same work reproduces a preexisting copyrighted work without explicit license, it is at the same time an unauthorized derivative. It is unowned for anything new it contains, and unlicensed for everything taken.** -#. **Its operator therefore holds no copyright in it and no grant over it - and a person who holds neither copyright nor grant can convey neither.** - -The first fact makes the work **unownable** by its operator. The second makes it infringing against its source. Neither replaces the other: - - -* The absence of copyright in machine output does not make the output free for anyone to take; it makes it unowned. -* This is a different thing entirely, especially when what it contains is someone else's expression. - -IV. License Conditions Bound the Grant -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The fourth foundation governs what a license *is*. A copyright owner may grant permission to use the work on conditions. The conditions define the scope of the permission. Use outside the scope is use that was never authorized. - -In *Jacobsen v. Katzer*\ , 535 F.3d 1373 (Fed. Cir. 2008), the Federal Circuit held that the conditions of an open-source license are *conditions of the copyright license itself*\ , not mere contractual promises, and that a licensee operating outside them is an infringer: - -.. - - Copyright holders who engage in open source licensing have the right to control the modification and distribution of copyrighted material. - - The choice to exact consideration in the form of compliance with the open source requirements of disclosure and explanation of changes, rather than as a dollar-denominated fee, is entitled to no less legal recognition. - - It is outside the scope of the [license] to modify and distribute the copyrighted materials without copyright notices and a tracking of modifications from the original computer files. - - - -* *Id.* at 1381–82 - -The doctrinal mechanism is settled: *"A copyright owner who grants a nonexclusive license to use his copyrighted material waives his right to sue the licensee for copyright infringement"* within the scope of the license. If, however *"the licensee acts outside the scope, the licensor can bring an action for copyright infringement"*\ : An act outside the granted scope **is infringement** (\ *Sun Microsystems, Inc. v. Microsoft Corp.*\ , 188 F.3d 1115, 1121, 9th Cir. 1999; *Graham v. James*\ , 144 F.3d 229, 236, 2d Cir. 1998; *S.O.S., Inc. v. Payday, Inc.*\ , 886 F.2d 1081, 1087, 9th Cir. 1989). - -Whether a given license term is a condition of the grant or a mere covenant is determined from the license's own language; conditions that limit what may be copied, modified, and distributed - the attribution terms, the notice terms, the modification-tracking terms, the field-of-use terms - are the boundaries of the copyright grant (\ *MDY Industries, LLC v. Blizzard Entertainment, Inc.*\ , 629 F.3d 928, 940, 9th Cir. 2010). - -Three consequences follow for the analysis at hand. They are not hypothetical. - -**First, the conditions are measured at the moment of copying** - -A work is licensed under whatever conditions applied when the reproduction, adaptation, and distribution occurred. There is **no** period in which copying a licensed work is "free" because the copy was made by a machine; the machine is the *licensee's tool*\ , and the licensee is bound by the conditions regardless. A derivative generated without satisfying the conditions - without the attribution, without authorization for the manner of use - was never within the scope of the license, from the first instant of its existence. - -**Second, breach does not restore rights that never attached** - -The district court in *Artifex Software, Inc. v. Hancom, Inc.*\ , No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017), analyzing the GPL's termination provisions, observed that *"the language of the GPL suggests that [the licensee's] obligations persisted beyond termination of its rights to propagate software using Ghostscript,"* because the source-code obligation recurs each time a covered work is conveyed. The same structure operates a fortiori where a derivative was produced *outside* the license entirely: No grant ever attached, and no subsequent event creates one. - -**Third, the settled conditions are the enforceable core** - -The attribution and notice conditions rest on the settled doctrine above. Novel conditions (restrictions on field of use, restrictions on how the work may be consumed) are enforceable as conditions of the grant when clearly drafted, but their enforcement history is thinner. The argument that a derivative is unlicensed does *not* depend on the novel conditions being vindicated; it needs **only** the settled ones, and the ordinary law of derivative works, to stand. - -V. The Void Grant -^^^^^^^^^^^^^^^^^ - -The preceding sections assemble into a single structure. Let us apply it to an example situation: - - -* A project presented as an independent implementation of a protocol; "ground-up", "from scratch" or "clean-room" work. -* The actual history shows continuous, machine-mediated ingestion of a licensed reference implementation. -* The output is generated at a scale no human could produce or review, with the provenance of the process obscured. - -**Step One: The derivative** - -The project's output is a translation of the licensed implementation: A derivative work, produced by a copy process. 17 U.S.C. §§ 101, 106(2). Preparing it without authorization infringed from the first moment of its existence. *Id.* § 501(a). - -**Step Two: The license** - -The derivative was produced outside the conditions of the license; without the notices, without the attribution. The license never attached (\ *Jacobsen*\ , 535 F.3d at 1381–82). - -**Step Three: Authorship** - -The expression was originated by a machine. No human can account for its creative decisions, and no human authored it. It contains no valid new copyright (\ *Thaler*\ , 687 F. Supp. 3d at 146; No. 23-5233, D.C. Cir. 2025; 88 Fed. Reg. at 16192). - -**Consequence: Void Grant** - -The statute supplies the consequence. Section 103(a) provides: - -.. - - The subject matter of copyright as specified by section 102 includes compilations and derivative works, but protection for a work employing preexisting material in which copyright subsists does not extend to any part of the work in which such material has been used unlawfully. - - -And section 103(b) provides that copyright in a derivative work *"extends only to the material contributed by the author of such work."* The legislative history states the point without ambiguity: *"An unauthorized translation of a novel could not be copyrighted at all"* (H.R. Rep. No. 94-1476, at 57, 1976). - -In our example, the operator of the derivative's copy process decides to assert copyright over the output, and licenses it to the world under terms of the operator's choosing, for example the Apache License. - -But the operator holds no copyright in machine-originated content (section III) and no license over the source from which it was derived (sections I and IV). - -For the derived material, the operator's copyright claim **is an assertion of a right that never existed**. The material was used unlawfully, and section 103(a) withholds protection from *every* part of the work in which it appears. For the machine-originated remainder, the operator holds no copyright either, for lack of any human author. - -The operator's license to the world is a promise the operator had no power to make. Every person who receives the work believing they received a valid grant has received **a void grant**\ , and stands, as a matter of settled law, in the position of a downstream user of an unauthorized derivative, whatever they were told at the time. - -This is a cascading consequence, and it is why the pattern is a hazard. No combination of self-granted licenses - "MIT," "Apache," "public domain," anything at all - can transfer rights the operator never held. Every downstream project, product, or deployment built on the derivative is built on unlicensed reproduction of a third party's work, whether or not it knew, whether or not it was told, and whether or not the marketing was plausible. - -The longer the derivative circulates unexamined, the more it becomes embedded, forked, vendored, depended upon, and the more expensive its correction becomes for everyone who trusted it. A field of superficially real projects, repositories, and licenses that **in reality confer nothing on anyone who relies on them is a minefield**\ , in the most precise sense the law allows: The rights they purport to grant *were never possessed*. - -The law defines what may be asserted, not what will be. The copyright owner may choose whether and how to assert these rights, and at what point in time. The exposure exists regardless, and for *every* downstream that trusted the void grant. This is the scope of the hazard that the conduct exemplified here produces. - -VI. Fair Use Does Not Rescue This Pattern -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Every defendant raises fair use. It is an essential and important limitation in the service of free speech, accountability, criticism, education and other important domains. Therefore, it must be considered, and in the case exemplified here, it fails. - -Fair use is a limitation on the exclusive rights, evaluated case by case under four factors: The purpose and character of the use; the nature of the copyrighted work; the amount and substantiality of the portion used; and the effect of the use on the potential market for or value of the work (17 U.S.C. § 107). - -On the first factor, the governing standard is *Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith*\ , 598 U.S. 508 (2023), in which the Supreme Court held that: - -.. - - In sum, the first fair use factor considers whether the use of a copyrighted work has a further purpose or different character, which is a matter of degree, and the degree of difference must be balanced against the commercial nature of the use - - -The question is whether the use serves a further purpose, not whether the surface changed. A derivative that exists to serve as the same thing as the original, marketed to the people who would otherwise use the original, has not acquired a further purpose by being translated; the surface changed, the function did not, and the use is commercial. - -The remaining factors resolve the same way. Source code is expressive creative work, not bare fact. The pattern under examination copies the whole; a complete implementation, ingested and translated in its entirety. And the market effect is the decisive factor, because it is engineered: A derivative marketed as a drop-in replacement for the work it was copied from is not a complement to the original's market, it is a substitute for it. The Supreme Court has long held that use of a work to satisfy the market demand for the original is the paradigm of an *unfair* use (\ *Harper & Row, Publishers, Inc. v. Nation Enterprises*\ , 471 U.S. 539, 566–67, 1985). - -The one case that is always raised in response, *Google LLC v. Oracle America, Inc.*\ , 593 U.S. 1 (2021), holds less than it is believed to. The Court there assumed *"for argument's sake, that the material was copyrightable"* and held *"that the copying here at issue nonetheless constituted a fair use"* (\ *Id.* at 11). - -The material in question was a thin slice of *declaring code*\ , which the Court found sits, *"if copyrightable at all, further than are most computer programs (such as the implementing code) from the core of copyright"* (\ *id.*\ ), in a use that built a new platform for a new environment, with the copier **having written the vast majority of its own code**. By comparison: A wholesale translation of an entire implementation, marketed as a substitute for it, is **not** that case; it is the other end of every factor. - -The valuable zones of fair use remain, and they are important. **Commentary and criticism**\ , quoting from a work to review, discuss or criticize it. **Teaching and scholarship**\ , explaining concepts and quoting excerpts, is its traditional beneficiary. - -Conclusion -^^^^^^^^^^ - -The law is neither obscure nor new. The conclusion it yields can now be stated: - - -* A machine translation of a protected implementation **is a derivative work**, and an unauthorized derivative **is an infringement**, whatever language it speaks and whatever machinery produced it. -* A work whose expression a machine originated has no human author, no valid new copyright, and without valid copyright **no license can be granted on it**. -* § 103 withholds protection from every part of a derivative in which protected material was used unlawfully. A license is a grant with conditions, and a derivative made outside the conditions **never received a grant**. -* A person who holds no copyright and no grant **can convey neither**, and *every* downstream consumer who received from them received a **void grant**. - -This is a *catastrophic* failure state for all who want to create things good and useful: The projects that inherit only void grants and liability believing they have inherited foundations, the users who depend on code that no one on earth can answer for, the honest builders whose work is buried under a field of plausible-looking impostors. - -Sources and Authorities -^^^^^^^^^^^^^^^^^^^^^^^ - -Statutory references are to Title 17, United States Code. Case citations are to the official or generally cited reporters. Online links are for reference only. Section numbers refer to the sections in which each authority is principally relied upon. - -**Statutes** - - -* `17 U.S.C. § 101 (definitions: "derivative work," "including," "publication") - I, II `_ -* `17 U.S.C. § 102(b) (idea/expression) - II `_ -* `17 U.S.C. § 103(a)–(b) (derivative works employing unlawfully used preexisting material) - V `_ -* `17 U.S.C. § 106(1)–(3) (exclusive rights: reproduction, derivative works, distribution) - I `_ -* `17 U.S.C. § 107 (fair use) - VI `_ -* `17 U.S.C. § 501(a) (infringement) - I, V `_ - -**Judicial decisions** - - -* `\ *Jacobsen v. Katzer*\ , 535 F.3d 1373 (Fed. Cir. 2008) - IV, V `_ -* `\ *Sun Microsystems, Inc. v. Microsoft Corp.*\ , 188 F.3d 1115 (9th Cir. 1999) - IV `_ -* `\ *Graham v. James*\ , 144 F.3d 229 (2d Cir. 1998) - IV `_ -* `\ *S.O.S., Inc. v. Payday, Inc.*\ , 886 F.2d 1081 (9th Cir. 1989) - IV `_ -* `\ *MDY Industries, LLC v. Blizzard Entertainment, Inc.*\ , 629 F.3d 928 (9th Cir. 2010) - IV `_ -* `\ *Artifex Software, Inc. v. Hancom, Inc.*\ , No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017) (settled; dismissed with prejudice Jan. 2018) - IV `_ -* `\ *Thaler v. Perlmutter*\ , 687 F. Supp. 3d 140 (D.D.C. 2023), aff'd, No. 23-5233 (D.C. Cir. Mar. 18, 2025) - III, V `_ -* `\ *Burrow-Giles Lithographic Co. v. Sarony*\ , 111 U.S. 53 (1884) - III `_ -* `\ *Community for Creative Non-Violence v. Reid*\ , 490 U.S. 730 (1989) - III `_ -* `\ *Feist Publications, Inc. v. Rural Telephone Service Co.*\ , 499 U.S. 340 (1991) - II `_ -* `\ *Baker v. Selden*\ , 101 U.S. 99 (1879) - II `_ -* `\ *Computer Associates International, Inc. v. Altai, Inc.*\ , 982 F.2d 693 (2d Cir. 1992) - II `_ -* `\ *Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith*\ , 598 U.S. 508 (2023) - VI `_ -* `\ *Google LLC v. Oracle America, Inc.*\ , 593 U.S. 1 (2021) - VI `_ -* `\ *Harper & Row, Publishers, Inc. v. Nation Enterprises*\ , 471 U.S. 539 (1985) - VI `_ - -**Administrative and policy authorities** - - -* `\ *Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence*\ , 88 Fed. Reg. 16190 (Mar. 16, 2023) - III `_ -* `\ *Zarya of the Dawn*\ , U.S. Copyright Office (Cancellation Decision, Feb. 21, 2023), Registration No. VAu001480196 - III `_ -* `U.S. Copyright Office, *Copyright and Artificial Intelligence, Part 2: Copyrightability* (Jan. 29, 2025) - III `_ -* `H.R. Rep. No. 94-1476 (1976) - V `_ - -.. _brandolinis-evaluating: - -Evaluating a Reticulum Implementation -------------------------------------- - -.. _brandolinis-intention-of-guide: - -The Intention Of This Guide -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Very few user of network infrastructure can verify the software they run. This is true of the reference implementation as well, and it is precisely why it was engineered the way it was: the entire Reticulum protocol implementation - the transport logic, the cryptographic interfaces, the routing - is less than 15,000 lines of code, including its extensive comments and detailed logging. The reference is *designed* to be readable by a human in a reasonable sitting. That is a deliberate decision, and it is the standard against which every other implementation should be measured: An implementation that asks for trust while being structurally unreadable is asking for the absurd. - -Because most users cannot verify the code directly, the evaluation of the *project* is the evaluation of the software. The framework below exists so that this evaluation can be performed in minutes, by anyone, without expertise - and so that each new "implementation" does not require the kind of full investigation that would otherwise be if not impossible, prohibitevly expensive. - -.. _brandolinis-two-questions: - -Two Questions That Resolve Most Cases -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Other considerations in this guide is further corroboration. These two questions allow you to decide the case quickly in most instances, and they should be asked first. - -**Question 1: Does this speak the protocol, or was it just generated from the reference?** - -The copyright and license realities, described in :ref:`What The Reticulum License Is`, is the legal foundation of this distinction. The protocol is public domain: A *genuine* :ref:`clean-room` implementation, reasoned out and written by humans, is free and welcome, and owes nothing to any license. But an implementation that was *directly generated from* the reference - reproduced by a copy process, whether manually or through a machine or software - is a derivative of the licensed work, and is subject to both the license's conditions, and to the ordinary copyright law examined in :ref:`Copyright & The Two Legal Layers`. A primarily machine-generated copy without substantial human creative input is not merely unlicensed; it is a copyright violation that would exist under any license at all. - -**Ask**: Does the project say where its protocol knowledge came from? Does it credit the reference implementation, or claim "ground-up" status while its own history shows continuous ingestion of the reference's source? Does its license carry the original copyright notice? The answers tell you, within minutes, which of two very different kinds of project you are dealing with. - -**Question 2: Who, or what, is the agent of the work?** - -This distinction, established in :ref:`Assistance Versus Machine Substitution`, is the trust foundation of everything else. Is there a human who decided what to build, judged what was correct, rejected what was wrong, understands what was shipped - and can answer for it, line by line and decision by decision, *without machine assistance*\ ? Or is the machine the agent, with the human's role reduced to the name, the account, and the signature? - -.. - - This test has a sharper form: **would the project be meaningfully different if someone else had pressed the button?** If the answer is no - if any person could have been the signature and nothing would change - then the "author" is not the agent of the work. They are its facade, and the project is, in the most literal sense, worthless and unowned. - -A project with no owner is a project no one can answer for, and infrastructure with no one to answer for it has no place carrying other people's traffic. - -.. _brandolinis-other-signals: - -Other Signals To Look For -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Once the first two questions are answered, the following signals determine the verdict. None is alone decisive; together they form a pattern that is difficult to fake in the directions that matter. - -.. _brandolinis-provenance: - -Provenance and attribution -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* **Credit to the reference.** Does the project name Reticulum's reference implementation - in its README, its website, its licenses, and the code? Does it present itself as a port - or as a "ground-up" origin story? Absence of credit, in a field where every implementation - owes its existence to the reference, is not an oversight; it is a decision. -* **License compliance.** For a derivative, the license conditions apply: the copyright - notice and permission notice in all copies and substantial portions, the harm condition, - the AI-training condition. A project whose license file claims a fresh copyright in the - operator's name, over work that was generated from the reference, has failed the minimum - bar of honesty before any code is read. -* **Disclosed provenance.** Is the use of machine assistance stated openly, or does the - project maintain machinery to conceal it - commit hooks that refuse attribution of - generation, contribution policies that declare machine output the human's own, scrubbers - for personal traces, fictitious contributor identities? The first is the mark of a human - using a tool. The second is the mark of a record being manufactured. -* **The author's history.** Is there a prior record - projects, contributions, a body of - work that a person can look at and reasonably assess? A sudden, fully-formed "decade of experience" - appearing from nowhere, attached to a project of unprecedented scope, is a story that - contradicts itself. - -.. _brandolinis-agency: - -Agency and answerability -~~~~~~~~~~~~~~~~~~~~~~~~ - -* **Who answers the questions?** When technical questions are asked, does a human answer - with substance - going deeper, explaining trade-offs, admitting uncertainty - or do - answers deflect to process ("the CI verifies it", "the proofs prove it", "the audit - covered it")? Are the answers themself machine generated? Deflection to process is - the substitute's answer pattern: It names the machinery instead of the reasoning. -* **Velocity commensurate with authorship.** Is the output volume compatible with human - authorship, review, and judgment? An implementation ten (or 42!) times the size of the reference, - produced in a fraction of the time by a single person, is not evidence of brilliance; it - is evidence of unchecked generation. Churn is part of the same signal: A project that - deletes half of everything it adds is not building; it is looping. -* **Signs of development, growth and learning.** Do reversals exist? Contested decisions? - "We tried X and it was wrong, so we did Y"? Humans change their minds, admit mistakes, and - leave the wear of judgment in their history. Pure generation shows "the polish of its - absence": uniform register, endless renaming, and errors discovered by the generator's own - later runs, recorded as if they were planned milestones. -* **Standing behind failures.** When something is wrong, does the human say "I chose this, - here is what I got wrong, here is the fix"? Or do corrections arrive as process language, - with nobody accepting that a decision was even made? A project whose history contains no - first-person acceptance of error contains no author. - -.. _brandolinis-engineering: - -Engineering reality -~~~~~~~~~~~~~~~~~~~ - -* **Honest benchmarks, or none.** If performance is claimed, is the baseline real - stock, - current, interpreted RNS, on the modern interface, at equal policy, with methodology - published in advance and reproducible by anyone? -* **Interop with reality.** Is compatibility verified against the *current* reference - implementation - on real hardware, including the radio and serial interfaces this - protocol family lives on - in both directions, over long transfers, uptimes and edge cases? -* **The readability standard.** Can a competent human read the core of this implementation - in a reasonable sitting, the way the 15,000-line reference can be read? Was human - readability a stated design goal, or an afterthought? A project that cannot be inspected - is a project asking for trust it has not earned and will never attend to. -* **Nonstandard inventions.** Does the project "improve" on the protocol with its own - modes - new channel behaviour on shared spectrum, new rendezvous mechanisms, new crypto, - "post-quantum" replacements, "turbo" variants? Inventions that change behaviour on a - shared network are not features; they are forks of the airwaves, imposed on every other - node that shares the medium. A project that invents on top of a protocol it has not yet - correctly implemented is not advanced; it is dangerous to its neighbours. -* **The public record.** The commit history is public. Read it. - -.. _brandolinis-network-citizenship: - -Network citizenship -~~~~~~~~~~~~~~~~~~~ - -* **Honest presence.** Does the project identify itself honestly on the network, or does - it masquerade as something it is not? -* **Coexistence.** Does it respect the shared medium and network conventions? A node that - is a bad neighbour on the spectrum is a hazard regardless of how well its code works in isolation. -* **Community participation.** Does the author participate in the community as a member - - answering questions, contributing, learning - or only appear to advertise? The second is - not participation; it is exploiting the community as a broadcasting target. - -.. _brandolinis-signals: - -Monetary and influence signals -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* **Money under the claim.** Donations, funding, or commercial offers premised on claims - like "89× faster" or "formally proven" convert false marketing into a different category - of problem. When money changes hands on the strength of fabricated measurements, the - stakes are no longer theoretical. -* **Tokens and "investment" vehicles.** Any implementation that surrounds itself with a - crypto token, a "coin", or an investment pitch should be treated as what it is: A project - whose primary purpose is to earn the controller a profit, not open networking. -* **Marketing-first ordering.** A polished website on day three, a twelve-language marketing - presence, a "benchmarks" page and a "get started" funnel existing before the protocol works - is a massive red flag. Real builders ship, then talk. The inverse order is the signature of - a project whose goal is attention. - -.. _brandolinis-conformal-surface: - -The Conformal Surface -^^^^^^^^^^^^^^^^^^^^^ - -One warning before the synthesis. When you evaluate any project, assume - as a working hypothesis - that its surface is an investment. The marketing, the design, the badge collection, the "proof records", the fluent and confident prose: This is exactly where substitution invests, for the simple reason that it is where the audience looks first. Fluency is the *minimal conformal surface* of understanding, and the surface is not evidence of anything except that the author *wanted to look convincing*. If you find yourself impressed by the surface before you have asked Question 1 and Question 2, you are doing the evaluation in the order the substitute operator hoped you would. - -.. _brandolinis-possible-verdicts: - -Possible Verdicts -^^^^^^^^^^^^^^^^^ - -The framework proposed here can yield three different verdicts: - -#. - **Meets the standard.** A named human who can answer for the work and demonstrably does; - provenance disclosed; attribution given; honest benchmarks; interop - demonstrated against current, real RNS on real media; readable code; a maintenance - history that shows growth and learning. This standard is not high. It is what the - reference implementation has met, in public, for over a decade, and it is what any - project that wants other people's traffic should be willing to meet in turn. - -#. - **Experimental, but honest.** A project that clearly labels itself as in-progress and - experimental ("do not use this yet", in plain words, as the first thing a user sees) - discloses its provenance, credits the reference, and is being developed in the open by - humans who answer questions. Such projects should be encouraged. They are how the - ecosystem grows, new ideas are tested, and the framework above is designed to make an - honest experimental project *easy* to recognize, not hard. - -#. - **Does not meet the standard.** Unanswerable authorship, concealed provenance, fabricated - measurements, invented protocol behaviour, absent interop, marketing ahead of substance. - Such projects should be treated as unproven regardless of their surface, and as hazards - if they invite users onto shared networks. The community owes them exactly the - skepticism they have earned, and not a moment of engagement beyond that; the network's - time is better spent elsewhere. - -.. _brandolinis-standards-reasonable: - -These Standards Are Reasonable -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -I'll close this guide by stating what these standards are *not*\ : They are neither a demand for perfection, nor a ban on machine assistance. Assistance is useful, when it is used to arrive at *higher levels of synthesis* than unaided work. It is not a requirement that an implementation be a clone of the reference; neither is it a requirement that it invents everything from scratch or aspires to be a :ref:`"clean-room"` implementation. Good implementations of the protocol are very welcome, *however they come*, but working on one requires *serious* work, considerable skill, and **deep competence**. - -The reference implementation has met this standard for years, and its users know what that looks like. - -.. _brandolinis-movement-of-a-dozen: - -The Movement Of A Dozen ------------------------ - -.. _brandolinis-presentation-elongated: - -Presentation, Elongated -^^^^^^^^^^^^^^^^^^^^^^^ - -Whenever Reticulum defends itself, its license, or its community - or simply continues to exist - a familiar chorus appears. The register is always similar: *concern*. Concern about the direction of the project. Concern about its license. Concern about its governance. Concern about its "bullying" of "good-faith contributors". Concern about the future of the ecosystem at the hands of a single maintainer. The chorus speaks in the plural, presents itself as a body of independent voices, and insists, with some frequency, that it is... *not alone*. - -The implicit claim under examination in this section is that there exists a *movement* of concerned individuals, spread across the ecosystem, all worried about Reticulum, and therefore (for some reason) deserving of a place at the table of decisions. - -A resolution of this claim requires one step: Count them. - -.. _brandolinis-headcount: - -Headcount -^^^^^^^^^ - -This "movement" does have a headcount. It is not in the hundreds, and it is not in the dozens. It is a small number of people who appear, in various combinations and under various names, in the following roles: - -* Creators of the machine-generated "Reticulum implementations" examined in this chapter. -* Creators of articles praising those implementations and lamenting the state of the - reference, its moral shortcomings, its unavailability or lack of something it already has. -* Commenters "defending" the implementations when they are evaluated reasonably and critically, - most often by attempting simply to derail the discussion. -* Critics of the Reticulum License on "open source" grounds. -* "Concerned community members" objecting to the maintainer's conduct. -* Campaigners on online platforms; and -* In some cases, a single individual performing *several* of these roles at once, under - different names, in the same week. - -The overlap is well documented at this point, across time and different platforms. The same phrasing, the same arguments, the same links, the same reactive reflexes, the same handful of accounts appearing in the same threads at the same moments. When the sockpuppets are unmasked (and they frequently unmask themselves) the cast does not grow, but rapidly shrinks. - -This is the most interesting single fact about the "movement": **It isn't one. It is a synthetic cast**, held together mostly by LLM-glue, reddit handles and hysteria. A cast is not a constituency. It performs; it does not represent. The difference matters quite a bit, because a great deal of this "movement's" imagined power comes from the assumption that the *plural form* is actual evidence of plurality. If that sentence seemed opaque, notice how many times a statement or article uses "we" instead of "I", and it will be less so. - -.. _brandolinis-recycled-wine: - -Recycled Wine, Fermenting -^^^^^^^^^^^^^^^^^^^^^^^^^ - -The cast is not only small, it is also, by any evidentiary standard, *forgetful*. Every major claim this "movement" advances has been advanced before, answered in public, with evidence, and often years ago - and is then advanced again, unchanged and completely ignoring any rationale or evidence presented previously (to the same people), as if the answer had never been written. - -* **The license argument.** Reticulum is "not really open source" because its license is not - approved by a particular orthodoxy. This is answered (now in a new and updated version, for the - fifth time) in full in :ref:`Open Source Means Open Source`. It was also - answered in public, at length, at the time of the license change - including the - arithmetic of who is actually restricted by which license. It is repeated *ad infinitum* anyway. - Quite a conversation starter, apparently. -* **The distribution argument.** Reticulum is "not properly distributed" because it is not - packaged as a ``.deb`` or on some favoured catalogue. Reality: It is available on practically every - platform with a single command, packaged by the community in a wide range of channels, and - capable of updating itself over its own network. The argument has been answered; it is repeated. -* **The "stepped back" narrative.** When the primary mirror of the project moved away from - a public internet platform, it was spun as the founder leaving for good, or now taking on dictatorial - powers, or abandoning his creation, or any variation or nonsensical combination thereof. - Those spins always center around justifying that *a vaccuum now needs to be filled*. - There was no such vaccuum. The narrative is repeated in various forms whenever convenient. -* **The enclosure narrative.** The license is reframed as a "power grab" or an "enclosure" - of the commons - as if a license whose protocol is public domain and whose conditions - restrict only harm and appropriation were a form of enclosure. The `most recent `_ (and mostly incoherent) - expression of this narrative is, once again, fluent, polite, and factually hollow. The - bizarre arguments supporting the "article" include: Defunct and abandoned forks presented - as living, community forks bearing the very Reticulum License mislabeled as AGPL license vehicles - (created by "concerned users"), companion app forks correctly retaining the Creative Commons - licensing used as further evidence of the same and completely abandoned projects propped up - as being "the ecosystem". Another case of LLM generated nonsense that the "author" didn't bother checking. - The intent is clear here, simply produce more *mass* of the bullshit, so it can be further - referenced, regurgitated and linked to. -* **The "bullying" narrative.** Whenever the community declines to treat manufactured - controversies as legitimate, or calmly documents the falsity of claims, or (Lord forbid) - moves a forum post for violating clearly stated forum rules, - the response is the same: The documentation is "bullying", the community is "excluding" people, the - maintainer is "unprofessional". In the most recent example, those accusations were made by someone who, in the - same week, publically called for the Reticulum maintainer's "removal from the equation", forged a moderator's - username, attempted to drag unrelated individuals into the campaign, and presented a new - round of outright falsehoods. Contrary to popular belief, the accusation of incivility - is not the last resort of the wronged; it is the standard reflex of the exposed. - -None of the "arguments" engages the answers given repeatedly, and extensively. That is the point, and the strategy. The arguments are not produced for the purpose of being engaged; they are produced for the purpose of *being repeated* - the same wine, poured into new bottles, on a cycle. Each cycle is short, cheap to produce, and (thanks to freely available fluent text generation) now nearly effortless. The intended audience is not the community, which has heard it all before. The intended audience is the newcomer, who has not. - -.. _brandolinis-the-method: - -The Method of Madness -^^^^^^^^^^^^^^^^^^^^^ - -The method behind the pattern is consistent and simple enough to be described precisely: - -**Bullshit asymmetry** - -Producing a fluent, plausibly-sourced smear is now the work of minutes. Refuting it to the standard this manual demands is the work of days, and the refuted smear is replaced by the next one within the week. This asymmetry isn't just a fine point of the campaign; **it is the campaign**. The goal is exhaustion - the slow conversion of the community's time and attention from building to defending. - -**Weaponized concern** - -The register of the campaign is almost always the register of solicitude: "we are worried about the ecosystem", "we just want what is best for the community", "this is about the future of the project". The concern is directed, without exception, at the things the campaign wants changed, and the change it wants is always the same: **The replacement of the reference's authority with its own**. Concern is the *costume*\ ; capture is the choreography. It is the oldest form of cheap power grab in the book. - -**Event seizure** - -Any event - a license change, a platform move, a forum moderation decision, a subreddit closure, a security notice, even the most banal messages from maintainers or known contributors - are seized, stripped of their actual context, and re-narrated as evidence of the same pre-written story. The subreddit that was closed after its moderator vanished and ceased moderating (according to *prior agreement with said moderator*) becomes "the maintainer shutting down dissent". Moving away from a highly centralized online platform becomes "the founder retreating". The license becomes "the enclosure". The pattern is so reliable that the story could be pre-written by a lightly mechanized quill; only the date changes. The actual real-world context of these events is documented, in public, in each case; the re-narration proceeds regardless. - -**The legitimacy circuit** - -The campaign invests heavily in the places where legitimacy is minted: Conference talks, blog platforms, social threads, and forums - presented as if these appearances constituted some sort of ecosystem-wide groundswell rather than the same dozen people moving through the same circuit. The LLM-generated articles cite the LLM-generated ports; the ports are praised in the articles; the managers of the ports spread the articles, and echo them on the forums and internet platforms; the conference presentations cite the forums. Random bypassers with the need for a cause watch the conference presentations and take on the holy duty of Internet Soldier. The circuit is closed, self-referential, and (to a reader encountering it fresh) convincing. It is also, entirely, **a loop of bullshit**. - -**The prior-work minefield** - -The machine-generated implementations have a consequence beyond attention: They flood the space with fake "prior work". The effect, as the license discussion predicted in the beginning of 2025, is the creation of a minefield of sloppy repositories that can later be matched against any genuine project, and attempt to claim prior art, or even worse: Dissuade actual, honest developers or builders from taking up a real project they might have succeeded with, because they now believe it has already been done. Just as importantly, it also buries real efforts under machine-generated noise. The machine-ports are not failed attempts to contribute. They are (disregarding intentions, but effectively) the campaign's long game, deployed as inventory. - -.. _brandolinis-fragile-triad: - -A Persistent, But Fragile Triad -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This chapter as a whole has documented three fronts: The legal-ideological attack on the license (:ref:`Open Source Means Open Source`), the substitution and usurpation pattern in machine-generated implementations (:ref:`Assistance Versus Machine Substitution`), and the takeover narratives examined in this section. They are three fronts of the same operation, run by the same cast. - -* The license attack and other urgency-engineered narratives are the *cover*: They supply - the ideological justification for treating the reference as illegitimate, and needing replacement. -* The ports are the *inventory*: they supply the "replacements" that the narrative claims - the ecosystem needs. -* The takeover narrative is the *objective function*: the repeated, persistent, escalating claim that - the reference should be replaced, and that the speakers themself are the natural heirs. - -Each front depends on the others, and all three depend on a single, fragile assumption: That their audience cannot count. Restore the count, and the charade collapses into what it is: - -**A small group of people, obsessively orbiting a technology they did not build, synthetically producing the -appearance of a constituency they do not have, in service of a takeover they cannot justify.** - -.. _brandolinis-pattern-is-not: - -What The Pattern Is Not -^^^^^^^^^^^^^^^^^^^^^^^ - -It is important, to also say what this pattern is *not*, exactly because the campaign depends on being mistaken for such things. - -**It is not criticism** - -The Reticulum community has always had lively, rigorous criticism - of decisions, of priorities, of direction - and it has always engaged that criticism on the merits, and changed course and development priorities many times. The license discussions at the time of the change were extensive, public, and answered in full, including with the critics themselves. Core protocol decisions were always discussed and evaluated in the community. The complex coordination and execution of several protocol upgrades between a loosely organised group of developers, node operators and users have succeeded surprisingly fluidly. All of these are success stories that the "critics" **always** ignore. Real criticism engages the content and accepts evidence. The campaign engages nothing. - -**It is not a community** - -Communities *build things*, maintain them, and have skin in the game; long-term users, operators of nodes, authors of real applications, people whose traffic flows through the network every day. A community has a stake. The "movement" only has an agenda. - -**It is not a debate** - -A debate has parties who evaluate arguments and evidence, and as a function of that, the debate moves forward, perpetually. The campaign's arguments, as shown above, are not designed to survive contact with answers, but to be repeated past them. You cannot debate a loop. You can decline to perform for it, though. - -.. _brandolinis-intent-and-consequence: - -Intent & Consequence -^^^^^^^^^^^^^^^^^^^^ - -While it would provide for interesting speculation, we do not need to know what the individuals in the cast *intend*, and this document will not speculate about their inner lives. People may believe their own narratives entirely, or partly, or not at all; **the pattern is the same in any case**, and the pattern is what matters here, since it is the exact thing that has been so insidiously destructive and resource wasting for the actual Reticulum project and community, ever since Reticulum was more widely discovered, popularized, and became a prime target for all sorts of grift known to man. - -The consequences are very real: - -* **Time**. The community's scarcest resource is the attention of skilled people who actually build - and maintain the network. The campaign converts that attention into reactive defence, on a cycle - with no end. -* **Harm**. Hastily machine-generated implementations, marketed as drop-in replacements and carrying - traffic onto shared networks, mislead users and can damage the network and its neighbours. - We, the actual reticulum community, all rely on these networks every day, and it is my belief - that we need to defend them. The fabricated measurements and laundered authorship examined in this chapter are not - editorial embellishments; they are the mechanism by which real users are induced to run - unverified, and often destructive software. -* **Erosion of the currency of trust**. Every manufactured scandal, every fluently polite smear, every - fabricated "movement" debases the currency of trust and degrades it for **everyone**; - including, most of all, the honest projects that come after. This is deep harm, and - it is the primary reason this chapter exists. - -Whatever the intent - profit, control, attention, or a simple, obsessive inability to stop - the mechanism is the same, and the response should be *calibrated to the mechanism as a whole*, not to the individual person acting as the current avatar for it. - -.. _brandolinis-recognition-and-response: - -Recognition & Response -^^^^^^^^^^^^^^^^^^^^^^ - -For anyone who encounters the pattern fresh, here is a proposal on how to recognise it, and what to do about it. - -**Recognition:** The pattern is recognisable in minutes, once the signs are known: - -* Claims that have been answered before, repeated as if never answered. -* The register of solemn concern applied, without exception, in one direction. -* A severe allergic reaction to answering any questions posed. -* A cast that is small, overlapping, and frequently anonymous, with the *plural form* doing the - work of plurality. -* Sockpuppet theater: The same person or footsoldiers thereof appearing as several "supporters" - (and occasionally slipping up mid-performance). -* Articles, ports, and forum threads that cite and reference each other in a closed loop. -* The immediate, reflexive turn to "bullying", "exclusion" and "toxicity" whenever a claim is refuted. -* A complete absence of engagement with the substance of the answers given, and a complete - absence of stake in the network itself. - -**Response:** A constructive response is one that is proportionate, and does not cost the -community its resources, sanity and dignity: - -* Engage the content once, on its merits (if any), with evidence, in public, and then refer to the - existing records. The answers already exist; they do not need to be re-derived for every new - bottle of piss-sour burgundy served from a man behind the curtains. I will do my best to keep this - chapter up to date with such answers and records, for easy reference, but other great resources - exist. -* Do not become part of the theater performance. The "movement" feeds on engagement in *any* form; - attention is its fuel, and outrage is as good as endorsement for them, if not better. -* Do the headcount. When the audience is reminded that a "movement" is a dozen people - recycling answered falsehoods, its projected power dissipates. The power was never real; - only the audience's own assumption of scale can make the illusion effective. A polished-looking - "ground up" implementation might look like the work of many people, but is most likely - just the result of an LLM looping on a laptop for a couple of months (this is weird, but it is - the reality we now live in). Four different users suddenly singing in unison may look like the - representatives of a broader sentiment, but is most likely the same person hiding behind the same keyboard. -* Then, spend the saved time on the networks: On building real infrastructure, putting up nodes - and antennas, on real development, real maintenance, real coexistence, and on the people - *who actually use Reticulum*. The things that we can only do *together*, as humans. That is - what the fake "movement" cannot do, no matter how fluent and smooth its productions become. - -.. _brandolinis-health-and-coexistence: - -Network Health & Coexistence ----------------------------- - -.. _brandolinis-substance: - -Substance Of The Networks -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Before examining health, we should consider what is being protected. The networks are not a repository. They are not a website, a social presence, or a set of marketing pages. They are a habitat: A living commons of communication, carried by, from a catious estimate, an intermesh now consisting of over a hundred thousand physical devices, embedded in a plethora of applications and packages, transporting terabytes of traffic every single day - real messages, real coordination, real people - across radios, serial lines, phones, laptops, and backbone servers, in cities, places and ways that no other network reaches. - -The protocol belongs to humanity; no one owns it. The network belongs to everyone on it, and its membership is defined by nothing other than participation: A node on a mast, a phone in a pocket, a server routing for its neighbourhood, a developer building on the commons. The network has no gatekeepers because it needs none; it is held together by the same physics and mathematics everywhere, and by a shared expectation of how members behave. That expectation - unwritten, unenforced by any central authority, and just as important as any code - is the subject of this section. The *Zen of Reticulum* calls the software a habitat. A habitat is healthy or unhealthy as a whole, and the health of this one is decided, every day, by the people who live in it. - -.. _brandolinis-property-of-the-whole: - -Health Is A Property Of The Whole -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A network is not made healthy by the excellence of any single part. It is made healthy by the behaviour of every part on the shared medium, and how they interact. And the shared medium is the point. When your node transmits, it transmits into the same spectrum, the same line, the same time, the same mathematical address space, the same bandwidth that its neighbours depend on. - -The network cannot be certified by any single project's claims. A node that is a bad neighbour, that invents its own channel behaviour on shared spectrum, that announces without discipline, that spawns tens of thousands of links to scrape the network, that floods and retries without mercy, is a hazard to every node that shares its medium, regardless of how well its "creator" imagines its own code to work in isolation. - -On a shared medium, "my software works" is not a complete sentence. The complete sentence is "my software works *and coexists*". A network's health deteriorates with its worst-behaved member, and every member has a responsibility to not be *that* member. There is no final, technical solution for this; the solutions are human. - -.. _brandolinis-history-of-what-works: - -A History Of What Works -^^^^^^^^^^^^^^^^^^^^^^^ - -Let's recall, because it is the ground truth this entire thing rests on, that the coexistence model has *worked* - is working, right now, **at scale**. Reticulum's protocol decisions have been discussed and evaluated in the community, openly, for years. Protocol upgrades have been designed, coordinated, and executed between a loosely organised group of independent developers, node operators, and users - and have succeeded, repeatedly, without any corporate board, without any central command, and without the collapse that the skeptics predict every second week (while screaming for *GOVERNANCE*). The reference implementation has been developed in the open for over a decade, by a maintainer who answers for it, fixes bugs or issues within hours of their discovery, and is set down in code deliberately kept small enough (the entire protocol implementation under 15,000 lines, including its comments) for a human to read, verify, *and challenge*. - -This is the empirical baseline that every narrative examined in :ref:`A Movement Of A Dozen` conveniently ignores, in order to justify: - -* The claim that the project is a one-man dictatorship -* The claim that the community is excluded -* The claim that the networks need saving from their own founders - -The **reality**, documented and reproducible, is that a loose community of humans coordinates complex technical evolution *without* the apparatus the narratives insist is missing. The health of the networks is not a promise for the future; it is a demonstrated property of the past and present, under the leadership and community model that brought it into existence and navigated it through difficult decisions and situations over a decade. **My job is to keep it that way**, and you can be damned certain that I take it seriously. - -.. _brandolinis-role-of-the-individual: - -The Role Of The Individual -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The networks are built by physical acts and human coordination. You cannot prompt an antenna into existence. No model can string up a dipole, or stand in the rain aligning a radio, or coordinate with a neighbour three valleys away about who covers which repeater frequency, or sit down with a newcomer and explain why their first node didn't announce. These are the acts that make a network *organic* - present in the physical world, resilient because it is *real*, valuable because it is *human*. The machines and technology we use are tools aimed at a well-defined, openly described goal. But the networks themself are composed of intentional human actions, and every node operator is infrastructure for other humans. - -This has a corollary for the individual: You do not need permission, and you do not need to be an expert, to participate. Run a node. Help a neighbour. Ask questions in the community, and answer the questions you can. Build something small at first. The network grows by these acts, and they are the most meaningful ones - the things that can only be done together, as humans, in the physical world. The parasitic "movement" that haunts the peripheries of this ecosystem cannot do any of this. It cannot build a node because it does not exist, in any real connected and human sense; only people can, and people are precisely what a self-recurrent loop lacks. - -.. _brandolinis-place-of-new: - -The Place Of New Implementations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The future of the network includes more implementations - and they are welcome. This chapter is not an argument against implementing Reticulum. The protocol is public domain, by design, and the network is made stronger by genuine diversity: More *good* implementations, honestly made, mean more resilience, more experimentation, more people who understand the protocol deeply *because they have built it themselves*. That is the goal: **For people to understand and own the technology they rely on**. Not to spew out 600,000 line rust ports that fills no meaningful gap or purpose. - -What the ecosystem needs, from every implementation, is the ordinary floor that :ref:`Evaluating A Reticulum Implementation` establishes, and that the reference has met for over a decade: A human who made it and answers for it; provenance disclosed; attribution given; benchmarks honest or absent; interop demonstrated against the real network; code a human can read. And it needs the one thing that costs nothing and changes everything: **Honest labeling**. - -An experimental implementation that says, plainly, as the first thing a user sees: "This is in-progress, experimental, do not depend on it yet, but help if you can", that is a gift to the ecosystem. It contributes, it invites participation, and it protects users. - -The opposite, the polished "drop-in replacement", does damage far beyond its own users: It dissuades the honest developers who might have built *the real thing*, because the polished slop persuades them the work is already done. That dissuasion is one of the quietest and most expensive harms in this entire affair, and the antidote is honesty at every project's door. - -.. _brandolinis-expanding-commons: - -Expanding The Commons -^^^^^^^^^^^^^^^^^^^^^ - -Coexistence, reduced to its essentials, is a short formula; the duties of membership in a network no one owns: - -* **Be honest.** Say what you are, what you built, how you built it, and what you used to - build it. Label experimental work as experimental. Credit the work you build on, including - the reference implementation, in the licenses and in the prose. Concealment is not a - defence; it is a very clear diagnosis marker. -* **Be owned.** Have a human who made the work and can answer for it. Use assistance, don't - substitute. Never sign what you cannot answer for. The chain of accountability is the - community's only verification at scale, and every link in it is a *person*, not a facade. -* **Be a good neighbour.** Respect the shared mediums. Do not invent new behaviour in our - shared space and call it progress. The network belongs to its other members as much as - to you. -* **Verify against the network.** Compatibility is a property demonstrated with actual nodes, - on actual media, against the actual current protocol, over a period of *years*, not 48 hours. - And not a claim pinned to an old snapshot over loopback. -* **Participate as a member.** Ask, answer, help, build. *Advertising at* the community is not - the same as being part of it, and it is **not** the first thing you should do. - -None of this is a high bar. It is the bar the reference implementation has met, in public, for over a decade, and the bar the ecosystem has adopted organically itself, successfully, through every protocol upgrade it has ever coordinated, and everything running on Reticulum today, that people actually use. It is not a new requirement; it is the existing, working culture of the networks, now written down so that newcomers can find it and so that those who would exploit us cannot claim they never saw it. - -.. _brandolinis-defence-of-commons: - -Defence Of The Commons -^^^^^^^^^^^^^^^^^^^^^^ - -The networks need defending - not because they are under severe physical attacks (yet), but because attention, talent and resources is being stolen from them, trust is being mined against them, and its newcomers are being harvested by the machinery documented in this chapter. Defence, for a commons, has specific shapes, and it is not what the theater would like it to be. - -* Defence is **documentation**: Records kept public, of what was claimed, what was - answered, and what turned out true. -* Defence is **a headcount**: Calm, unexcited reminders that a "movement" might just be a dozen - people having a tantrum. -* Defence is **honesty in action**: The visible, ordinary practice of the good behaviour - by the people who actually run the networks. -* And defence is **building**: Every node raised, every page of *real* documentation, every - honest implementation, every new user helped into the community with the knowledge to know - what they are doing is more durable than any rebuttal, because it adds to the thing being - defended. - ----- - -**The theatre cannot keep up with a network.** - -**It can only orbit it, while it recycles its own sour piss.** - ----- - -A lot of us rely on these networks every day. They carry our messages, our coordination, our friendships, and in some places, our safety. That is exactly why they are a target: - -They are valuable, free, and very hard to shut down. The response is not to become aggressive. It is to be *more* of what the network already is: Real, open, owned, and shared. - -*Mark* - -.. _brandolinis-prns: - -Addendum A: The Prns Case -------------------------- - -.. _brandolinis-purpose-and-scope: - -Purpose & Scope -^^^^^^^^^^^^^^^ - -This section provides a concrete example, which future evaluations can use as a template. Its purpose is not to be the last word on this particular subject, but to be an example of how the ideas in this chapter can be applied, and to make the next such examination easier. - -The subject of this case study is **Prns**, a project published at the repository ``github.com/KenAKAFrosty/Prns``, presented as a "ground-up implementation of Reticulum, written in Rust", marketed as faster, safer, and more strictly tested than the reference, widely promoted with the claim of "up to 89× the throughput" of the reference implementation, and an "author" actively engaged in the tactics described in :ref:`A Movement Of A Dozen` on online platforms. The record described below is the project's own, and sourced from its GitHub repository, analyzed locally. - -.. _brandolinis-project-in-brief: - -The Project In Brief -^^^^^^^^^^^^^^^^^^^^ - -* **Time span.** 3,374 commits between 2026-05-26 and 2026-09-12: approximately 110 days, - at a mean rate of ~31 commits per day. Monthly: 132 / 1,137 / 1,097 / 805 / 203. -* **Authorship.** 3,252 of 3,374 commits (96.4%) are authored by a single account. A - second identity, "Prns Tests tests@example.test" - a fictitious identity on a reserved - domain - contributed 35 commits, every one of them website and marketing content. -* **Size.** 3,519 tracked files; 1,983 Rust files; 636,226 lines of Rust; approximately - 22.7 MB of Rust source; 69.5 MB of total repository content. -* **Scope claimed.** A daemon ("drop-in replacement" for the reference daemon), embedded - firmware for three microcontroller families (including six ESP32-S3 board variants and - nRF52840), SDKs and bindings for nine languages, a browser-based node and web flasher, a - twelve-language website, a benchmark apparatus with its own publication and "proof" - pipeline, and a validation hub with oracles, fuzzing, mutation testing, and formal-proof - claims. -* **Marketing claims.** "Ground-up implementation of Reticulum"; "up to 89× the - throughput"; "48× smaller peak-memory footprint"; "33× the energy efficiency"; - "Measured, not just claimed"; "Enforced, then audited"; "byte-for-byte wire parity with - the reference implementation"; "works with Sideband, NomadNet, MeshChat, etc."; - "no_std (no alloc required either)". -* **Status at the time of writing.** Version 0.3.7; published version history begins at - 0.3.0. The repository continues to receive commits. - -.. _brandolinis-prns-question-1: - -Question 1: Is this a clean implementation, or was it directly generated from the reference? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The first of the two deciding questions from :ref:`Evaluating A Reticulum Implementation` is settled by the project's own record. An actual :ref:`clean-room` implementation, reasoned out and written by humans, would owe nothing to any license. The data available shows clearly that Prns is not that. - -* **The founding state.** The first commit, "Scaffold Personal Reticulum suite", contains - the scaffolding of not one but two ports - ``personal-rns`` and ``personal-lxmf``\ , the latter - being the reference's companion messaging protocol - together with a daemon scaffold and - a 233-line founding document, ``docs/build-ethos.md``. That document declares the project's - goal as a "performance-focused drop-in-replacement for rnsd" and its governing principle - as: "Port the contract, not the implementation." It further states directly to the - machine-loop, that **"You owe the reference fidelity at exactly two boundaries: - The wire... The behavior. ... Between those two boundaries, the model is yours"**. - At this point, the very first commit, effectively the whole of the implementation design - and any relevant decisions are handed over to the LLM. -* **Continuous ingestion from the reference.** 506 commits reference RNS or Reticulum - source behaviour; the word "parity" appears in 49 commit messages. Commits cite the reference - implementation's own source locations (for example ``Transport.py:1367``) when correcting - their own divergences and mistakes. -* **Byte-level fidelity as an explicit goal.** Within 48 hours of the first commit, the - record shows a "crypto adapter - vetted primitives, byte-exact vs RNS 1.3.1", and a wire - implementation matching the reference's packet header layout. The project's own - validation apparatus decodes wire vectors using the reference implementation's own - packet parser, and the interop suite consists of real, stock-RNS peer nodes - that is, - the reference source itself, used as the ground truth against which this LLM output is - checked, and committed as accepted and verified after 48 hours of the project's recorded start. -* **The marketing mismatch.** The same record that documents all of the above describes the - project, publicly, as "a ground-up implementation of Reticulum" - while declining, on its - website, to link to the reference implementation at all: the entire tree mentions the - reference's author in six files, and the website in none. - -Conclusion: the project's own history and commit record establishes that it was generated directly from the reference implementation's source code, not reasoned out from the public-domain protocol. It is a derivative of licensed work within the meaning of :ref:`the Reticulum License` and :ref:`general copyright law`. The "ground-up" claim, which carries its entire legal and marketing posture, is disproven by the project's own history. - -.. _brandolinis-prns-question-2: - -Question 2: Who, or what, is the agent of the work? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The second deciding question is answered by the application of :ref:`Assistance Versus Machine Substitution` criteria to the project. - -**Scope that no human could have authored** - -636,226 lines of Rust in 110 days is approximately 5,800 lines per day, every day, including weekends - with 149 commits recorded between midnight and 06:00, and Saturday the single busiest weekday. No human review or decision capacity is commensurable with this volume; the review capacity and the output volume are structurally incommensurable, which is the definition of unauthored scope. - -**Uniform generation** - -The record shows no sign of human development, growth, or learning: No contested decisions, no style drift. Instead: endless renaming (in the first 48 hours: ``State → EngineState → TickInput → TickOutput``\ ), uniform fluent register in every commit, and the exact same, synthetic voice in code, documentation, and website copy. - -**Discovery by regeneration** - -Corrections arrived by the machine's own subsequent runs, recorded as planned milestones. In early July 2026, a series of "core review" commits documented, in the project's own words: - -* announce-ID history "was unbounded per-slot growth" (\ ``54cc8d601``\ ); -* interface announce limiting could "leak a sustained flood" (\ ``9d27af75c``\ ); -* a "phantom-airtime leak" where frames "used to vanish silently" (\ ``4ed75ffdd``\ ); -* Group-packet deduplication "which RNS does not do" - a behaviour the reference never had, - implemented and later removed (\ ``edd8e72be``\ ); -* "both ``unwrap_or(&[])`` arms were unreachable dead defaults" (\ ``74bea355e``\ ); -* a benchmark crate "hasn't compiled since" a prior rename, undetected for weeks because it - was outside the CI's workspace (\ ``86240b027``\ ). - -Earlier still: the record shows a ">1 MiB RNS-to-Prns interop stall" closed three weeks in -(\ ``cc4d524a3``\ ), and a known send-path hang shipped with the note "typed fast-fail a -candidate for later" (\ ``a010e6613``\ ). - -**Laundered provenance** - -The repository contains a committed commit-message hook (\ ``.githooks/commit-msg``\ ) that *blocks* any commit containing attribution for machine generation ("generated with claude/codex/copilot/cursor/gemini", "co-authored-by"), with the explanation "The committer owns the commit". The contributing guide is explicit: "AI tools ... are welcome ... what you submit is *yours*", and describes the contributing population as "both human and automated contributors". The fictitious "Prns Tests" identity completes the picture: A manufactured, unknowable contributor, used for marketing content. - -**Authorship by assignment** - -The human whose name is on the work cannot answer for it, because the choices were never made by a human, and only the general shape and constraints of the outward presentation was. Nothing in the project's history shows an individual explaining a design decision, accepting a mistake in the first person, or going deeper under technical questioning; the record shows process language and marketing instead. Per the earlier mentioned test: any other person could have pressed the same button, and nothing would be different. The project is effectively unowned, and void of value. - -By the criteria of :ref:`Assistance Versus Machine Substitution`, this is substitution. The machine was the agent of the work; the "author" was a facade. - -.. _brandolinis-prns-performance: - -The performance claims -^^^^^^^^^^^^^^^^^^^^^^ - -The project's central marketing claim - "up to 89× the throughput" of the reference - is also its most completely documented failure, because the project published the evidence of its own measurement. The apparatus, the results, and the problems are all on the record. - -**The claim** - -The README states "up to 89× the throughput"; the on-network marketing page repeats "up to 89× the throughput, 48× smaller peak-memory footprint, and 33× the energy efficiency of stock RNS 1.4.2"; the website's front page carries the tagline "Measured, not just claimed". - -**The apparatus** - -The reference is loaded through ``benchmarks/reference/compiled_reference.py``\ , which installs ``pyximport`` and then asserts two things: that ``RNS.compiled == True``\ , and that at least *one* RNS module was loaded from a native extension. The project's own published "proof" records show exactly what those checks captured, on every platform: - -.. code-block:: - - aarch64-apple-darwin: .../RNS.cpython-313-darwin.so, Cython 3.2.8, Python 3.13.13 - x86_64-unknown-linux-gnu: .../RNS.cpython-313-x86_64-linux-gnu.so, gcc 11.4.0 - x86_64-pc-windows-msvc: ~/.prns-oc/lib.win-amd64-cpython-313/RNS.cp313-win_amd64.pyd - -In every case the recorded native module is the **top-level package only** - ``RNS.so``\ , nothing below it. Nothing named ``RNS.Transport``\ , ``RNS.Destination``\ , ``RNS.Packet``\ , ``RNS.Link``\ , or ``RNS.Resource`` ever appears, because *nothing below the top level was ever compiled*. Nobody bothered to check or verify *anything* here. - -The machine output was simply taken for truth, and a full marketing strategy was spun and produced on it. "RNS 1.4.2 (compiled)" - the label used in every table, chart, and marketing page - *is not a mode of RNS that exists*, but the hallucinations of a machine, which the humans created a marketing strategy on. In reality, it is the old ``CRNS`` development shim, whose top-level-only, debug-oriented "compilation" is slower than plain interpreted RNS, and which was *defunct for over a year* before these benchmarks were published. - -**Interface choices** - -The harness writes the reference's configuration itself (\ ``benchmarks/reference/participant_node.py``\ ): ``TCPClientInterface`` for the initiator, ``TCPServerInterface`` for the responder and both relay sides, with ``UDPInterface`` as the abstract wire. The reference was never given its modern ``BackboneInterface`` on any platform, forcing the slowest possible interrfaces. The interface selection is never mentioned in any methodology text. - -**The policy** - -The published default-policy tables record the asymmetry directly: Prns at 500 Mbps / 128 KiB TCP policy against the reference at 10 Mbps / 8 KiB - slyly footnoted as "preserves each implementation's normal TCP policy" (in the case of RNS, a conservative default policy that was selected to prioritize coexistence on slower, shared links, requiring the user to dial it up to full speed if they knew what they were doing). Raw transport "throughput" ratios of 20.65× (Linux) and 38.84× (Windows) are therefore mostly policy, not engine. - -**The cross-host evidence** - -The published results show the same scenario producing wildly different ratios by host, and the variance is entirely in the reference artifact: - -.. list-table:: - :header-rows: 1 - - * - Host - - Prns, single-packet - - "Reference", single-packet - - Ratio - * - macOS (Apple M4) - - 37.4k/s - - 420/s - - 89.09× - * - Linux (i7-1260P) - - 25.3k/s - - 3.5k/s - - 7.19× - * - Windows (Ryzen 5 5600X) - - 30.8k/s - - 3.9k/s - - 8.00× - -Prns is consistent (25–37k/s). The reference artifact collapses to different depths on different hosts, and the single worst collapse - 420 packets per second on macOS - is the cell selected as the global headline. The "89×" is not a measure of Prns; it is a measure of how badly the broken baseline happened to fail on one host. - -For reality calibration: Stock, interpreted RNS 1.5.4 on an ordinary mid-range laptop, single core, sustains roughly 30,000 single-packet deliveries per second (including ephemeral-key decryption), ~175,000–280,000 packets per second in transit relay, and on the order of 10 Gbps at 16 KiB payloads. The project's "compiled" artifact measured 420–3,900 packets per second for its single-packet scenario: An order of magnitude or more *below* stock interpreted RNS. The comparison being marketed was not "Rust vs Python"; **it was "an implementation running under optimal conditions versus a mislabeled, broken shim on a slow legacy interface at a severe policy disadvantage"**. - -**The equalized rows** - -The project's own "1 Gbps policy" rows, in which both sides are explicitly configured identically, publish ratios of roughly 2.4–8.4× across all hosts and scenarios - still against the broken baseline, but nowhere near 89×, and consistent with the ordinary, uncontroversial observation that a native implementation tends to outperform an interpreted one in raw throughput. That point needed no fudging to make, and is banal. - -Interestingly, though, it seems that Prns is really only *marginally* faster than a Python implementation, running in interpreted mode, on a single CPU core. - -**Assessment** - -The front page says "Measured, not just claimed." The record shows the opposite ordering: claimed, and arranged to look measured. No methodology section anywhere states what "compiled" means, that it is a deprecated development shim, that it builds debug targets, or which interface the reference was given; the one statement of the policy asymmetry appears in a footnote of the results tables themselves. If they did not need to fudge it, they would have published the method clearly. - -.. _brandolinis-prns-overall: - -Overall Evaluation -^^^^^^^^^^^^^^^^^^ - -Evaluated against the conditions of :ref:`Evaluating A Reticulum Implementation`: - -* **Provenance and attribution.** Fails: The fake "ground-up" origin story; zero reference links - on the website; a single well-hidden link in the README; a license file reading - ``Copyright (c) 2026 The Prns Authors`` over work directly generated from the licensed reference; - no original copyright or permission notice anywhere. -* **Agency and answerability.** Fails on every criterion of :ref:`Assistance Versus Machine Substitution`: - Unauthored scope, uniform generation, discovery by regeneration, laundered provenance, - authorship by assignment. -* **Engineering.** Fails: The benchmark apparatus above; interop demonstrated only - against a pinned, outdated reference version over loopback while the marketing claims current - parity, interop failures with stock RNS; vendored prebuilt binaries and a republished fork - of an upstream crate (``nrf-softdevice 0.1.0-prns.1``) with no verifiable provenance; - committed WASM bundles. -* **Network behaviour.** Fails: Invented spectrum behaviour and compatibility claims - about third-party applications of the reference ecosystem (Sideband, NomadNet, MeshChat) - that are not established by any evidence. -* **Monetary and influence signals.** The marketing apparatus - website before protocol, - day-three twelve-language marketing, false benchmark headlines, "audited" and "formal proof" - claims - the conformal surface described in :ref:`Evaluating A Reticulum Implementation`: - is where this project invested. - -.. _brandolinis-prns-licensing: - -Licensing Analysis -^^^^^^^^^^^^^^^^^^ - -Applying the two legal layers established in :ref:`Copyright & The Two Legal Layers`: - -**Layer A - copyright infringement (license-independent)** - -The record shows a direct copy roundtrip: the licensed reference implementation fed into a machine, transformed, at a scale and cadence no human could review, with a license file asserting a fresh copyright in the operator's name and no acknowledgment of the work the project was generated from. No substantial, new human creative input is demonstrated anywhere in the record - the founding document explicitly frames the design as an instructed port of the reference's contract. A primarily machine-generated copy of a copyrighted work, not accompanied by demonstrable substantial human creative input, is a derivative work over which the copier holds no copyright, and a reproduction of which without the original author's permission infringes the original author's copyright. This holds under any license, MIT included. The operator's copyright claim over the result is, on this evidence, not merely unsubstantiated; it is the absence of one, directly asserted in the project's own writing. - -**Layer B - license conditions (Reticulum License specific)** - -Modern Reticulum - the current wire format, the AES-256-based link encryption, ratchets, the cryptographic machinery as it exists today - has been published under the Reticulum License since April 15, 2025, effective from release 0.9.4 and every release since. As a derivative of that work, the license's conditions attach: The copyright and permission notice must be included in all copies and substantial portions; the AI-training condition must be honoured; the harm condition must be honoured. The record shows none of the notice requirements met, and a production process that consists of the licensed work being ingested into a machine and regurgitated, at industrial scale and cadence. - -**The cascading consequence** - -Prns is distributed claiming dual MIT/Apache-2.0 licensing. It is, however, a direct derivative of Reticulum-licensed work, and the claimed grant is therefore void. Every downstream project that builds on Prns believing it is MIT/Apache-licensed is building on a grant that does not exist. The users of Prns, and all software developers using it, have in effect, been placed under conditions they were never told about. The "ground-up" claim is not merely false; it is the sole load-bearing element of an entire licensing posture, and it fails spectacularly. - -.. _brandolinis-prns-consequences: - -Consequences -^^^^^^^^^^^^ - -The consequences of this pattern are examined in the following sections: The harm to users and to the network in :ref:`A Movement Of A Dozen`, and the human dimension in :ref:`Network Health & Coexistence`. The contribution of this addendum is narrower and intended as an instructive example: This is what a substitution project looks like, in full, on its own record. It is a reference example for :ref:`Evaluating A Reticulum Implementation`, and it demonstrates, in one case, most of the elements this chapter describes. - -.. _brandolinis-prns-verdict: - -Verdict -^^^^^^^ - -Applying the three verdicts of :ref:`Evaluating A Reticulum Implementation`, this project does not meet the standard. It is not "experimental but honest" - it is not labeled as experimental anywhere; the first thing users saw was a polished marketing presence promising a finished, tested, drop-in replacement. It is a project whose central claims are fabricated (the performance apparatus), whose provenance is laundered (the authorship machinery), whose foundation is misrepresented (the "ground-up" story), whose core semantics were learned late and by regeneration, and whose operator cannot possibly answer for any of it. It is also a blatant copyright violation of the reference implementation. - -The proposed conclusion, for users and for the network: Treat the Prns project as hostile regardless of its polished surface, and as a hazard insofar as it invites users onto shared networks. Nothing in this section is a judgment about the people involved; it is a reading of the results produced, their history and how they came to be, which in the regrettable lack of any human authorship is the only thing that can be expected to be held to account. - -.. _brandolinis-prns-dating: - -Dating -^^^^^^ - -This examination reflects the publicly available material as of September 2026; the repository in question continued to receive commits at the time of writing, so readers should treat citations to commits and files as point-in-time evidence. A `forensics snapshot `_ is publicly available on GitHub. diff --git a/docs/manual/_sources/git.rst.txt b/docs/manual/_sources/git.rst.txt index 3699be39..6cedd1b0 100644 --- a/docs/manual/_sources/git.rst.txt +++ b/docs/manual/_sources/git.rst.txt @@ -8,16 +8,6 @@ This chapter of the manual serves as the technical reference for the distributed A set of utilities for distributed collaborative software development and publishing are included in RNS. -.. only:: html - - .. image:: screenshots/rngit.webp - :align: center - -.. only:: latex - - .. image:: screenshots/rngit.png - :align: center - The system consists of two parts: The ``rngit`` node that hosts repositories, and the ``git-remote-rns`` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: ``rns://DESTINATION_HASH/group/repo``. If you set a branch to track a Reticulum remote as the default upstream, you can simply use ``git`` as you normally would; all commands work transparently and as expected. @@ -143,9 +133,6 @@ To create a new empty repository on a remote node: This creates a bare Git repository at the specified path. You must have ``create`` permission for the target group. When a repository is created, the creator automatically receives ``adm`` (admin) permissions on the repository through an auto-generated ``.allowed`` file. -.. tip:: - If you want to use a different primary branch name than ``master``, you can change it inside the bare git repository directory with ``git symbolic-ref HEAD refs/heads/name_of_other_branch``. - **All Command-Line Options (rngit create)** .. code:: text @@ -696,7 +683,6 @@ The following template files are supported: - ``commit.mu`` - Individual commit detail page - ``refs.mu`` - Branches and tags listing - ``stats.mu`` - Statistics page -- ``no_ident.mu`` - Displayed to non-identified initiators if blocked Templates can include the following variables: diff --git a/docs/manual/_sources/index.rst.txt b/docs/manual/_sources/index.rst.txt index fd305241..d00efa5c 100644 --- a/docs/manual/_sources/index.rst.txt +++ b/docs/manual/_sources/index.rst.txt @@ -32,7 +32,6 @@ to participate in the development of Reticulum itself. support examples license - brandolinis .. toctree:: :maxdepth: 2 diff --git a/docs/manual/_sources/interfaces.rst.txt b/docs/manual/_sources/interfaces.rst.txt index 9a9374a8..61625592 100644 --- a/docs/manual/_sources/interfaces.rst.txt +++ b/docs/manual/_sources/interfaces.rst.txt @@ -998,9 +998,6 @@ When ``discoverable`` is enabled, a variety of additional options become availab ``discovery_name`` A human-readable name for the interface. This name will be displayed to users on remote systems when they list discovered interfaces. If not specified, the interface name (the section header) will be used. -``discovery_lxmf_address`` - An optional LXMF address for contacting the operator of this interface. The address will be displayed to users on remote systems when they view details for discovered interfaces. Useful for getting in touch with other operators to interconnect networks with and coordinating network coverage. - ``announce_interval`` The interval in minutes between successive discovery announces for this interface. Default is 360 minutes (6 hours). For stable, long-running infrastructure, higher intervals (12 to 22 hours) are usually sufficient and reduce network load. Minimum allowed value is 5 minutes (but expect to have your announces throttled if using intervals below one hour). @@ -1570,34 +1567,4 @@ but all the parameters are exposed for configuration if needed. All of the above settings can be configured both as instance-wide defaults under the ``[reticulum]`` section of the configuration file, or on a per- -interface basis under the relevant interface configuration section. - - -Tuning Ingress Queues -===================== - -Reticulum uses separate, prioritized queues for processing inbound packets. The default four queues are drained in the following order: - - 1. Data and general traffic - - 2. Announces - - 3. Path requests - - 4. Ingress-limited traffic - -You can tune the queue sizes from the defaults, for example if you're running a setup where you want to simply drop most traffic from ingress-limited interfaces. The queue sizes are configured in the ``[reticulum]`` section of the config - - * | The ``qlen_in_data`` option sets the maximum size of the data - queue. Defaults to ``1024``. - - * | The ``qlen_in_announce`` option sets the maximum size of the announce - queue. Defaults to ``128``. - - * | The ``qlen_in_pr`` option sets the maximum size of the path request - queue. Defaults to ``128``. - - * | The ``qlen_in_il`` option sets the maximum size of the ingress limiter - queue. Defaults to ``8``. - -If an inbound packet arrives and its target queue is full, the packet is dropped. Reticulum will always completely drain higher-priority queues before starting to drain a lower-priority one. As an example, the ingress limiter queue will only start draining once all other queues are empty. \ No newline at end of file +interface basis under the relevant interface configuration section. \ No newline at end of file diff --git a/docs/manual/_sources/software.rst.txt b/docs/manual/_sources/software.rst.txt index 66315fea..c7314aa1 100644 --- a/docs/manual/_sources/software.rst.txt +++ b/docs/manual/_sources/software.rst.txt @@ -4,225 +4,146 @@ Programs Using Reticulum ************************ -This chapter provides a non-exhaustive list of notable programs, systems and application-layer protocols that have been built using Reticulum. +This chapter provides a non-exhaustive list of notable programs, systems and application-layer +protocols that have been built using Reticulum. -Many different applications using Reticulum already exist, serving a wide variety of purposes from day-to-day communication and information sharing to systems administration and tackling advanced networking and communications challenges. +These programs will let you get a feel for how Reticulum works. Most of them have been designed +to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast +links, such as local WiFi, wired Ethernet, the Internet, or any combination. -.. tip:: - For ease of discovery while reading this manual over the web or as an e-book, links provided in this chapter primarily point to repositories and pages located on internet servers. In many cases, these are public web mirrors, with the upstream repositories residing on Reticulum nodes. For the latest source, updates and information regarding any project, be sure to check their Nomad Network or ``rngit`` nodes. +As such, it is easy to get started experimenting, without having to set up any radio +transceivers or infrastructure just to try it out. Launching the programs on separate +devices connected to the same WiFi network is enough to get started, and physical +radio interfaces can then be added later. -These programs will let you get a feel for how Reticulum works. Most of them have been designed to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast links, such as local WiFi, wired Ethernet, the Internet, or any combination. +Programs & Utilities +==================== -As such, it is easy to get started experimenting, without having to set up any radio transceivers or infrastructure just to try it out. Launching the programs on separate devices connected to the same WiFi network is enough to get started, and physical radio interfaces can then be added later. +Many different applications using Reticulum already exist, serving a wide variety of purposes +from day-to-day communication and information sharing to systems administration and tackling +advanced networking and communications challenges. -Development of Reticulum-based applications and systems is ongoing, so consider this list a non-exhaustive starting point of *some* of the options available. With a bit of searching, primarily over Reticulum itself, you will find many more interesting things. +Development of Reticulum-based applications and systems is ongoing, so consider this list +a non-exhaustive starting point of *some* of the options available. With a bit of searching, +primarily over Reticulum itself, you will find many more interesting things. -Browsing & Publishing -===================== +Remote Shell +^^^^^^^^^^^^ -In addition to the programs listed in this section, other Nomad Network clients include: +The `rnsh `_ program lets you establish fully interactive +remote shell sessions over Reticulum. It also allows you to pipe any program to or from a +remote system, and is similar to how ``ssh`` works. The ``rnsh`` program is very efficient, and +can facilitate fully interactive shell sessions, even over extremely low-bandwidth links, +such as LoRa or packet radio. -- `MeshChatX `_ -- `Columba `_ +In addition to the default, fully interactive terminal mode, +for extremely limited links, ``rnsh`` offers line-interactive mode, allowing you to interact +with remote systems, even when link throughput is counted in a few hundreds of bits per second. + +.. raw:: latex + + \newpage Nomad Network ^^^^^^^^^^^^^ -The terminal-based program `Nomad Network `_ provides a complete encrypted communications suite built with Reticulum. +The terminal-based program `Nomad Network `_ +provides a complete encrypted communications suite built with Reticulum. It features +encrypted messaging (both direct and delayed-delivery for offline users), file sharing, +and has a built-in text-browser and page server with support for dynamically rendered pages, +user authentication and more. -.. only:: html +.. image:: screenshots/nomadnet_3.png + :target: https://github.com/markqvist/nomadnet - .. image:: screenshots/nomadnet_6.webp - :align: center - :target: https://github.com/markqvist/nomadnet - -.. only:: latex - - .. image:: screenshots/nomadnet_6.png - :align: center - :target: https://github.com/markqvist/nomadnet - -It features encrypted LXMF messaging (both direct and delayed-delivery for offline users), file sharing, and has a built-in text-browser and page server with support for dynamically rendered pages, user authentication and more. It also includes an RRC client and a suite of network utilities. - - -Ren Browser -^^^^^^^^^^^ - -`Ren Browser `_ is a modern, standalone browser for Nomad Network using the `Reticulum-Go `_ implementation. The project is actively being developed, and should be considered alpha level software, but much of the functionality is already working. If you want to help expanding the ecosystem, this is an excellent project to take a look at. - -.. only:: html - - .. image:: screenshots/ren_browser.webp - :align: center - :target: https://github.com/fr33n0w/rBrowser - -.. only:: latex - - .. image:: screenshots/ren_browser.png - :align: center - :target: https://github.com/fr33n0w/rBrowser - -Ren Browser is great example of an application aiming for true, seamless interoperability with the existing software ecosystem. It can operate fully standalone using it's own embedded `Go implementation `_ of Reticulum, or seamlessly utilise a shared RNS instance already running on the system. +`Nomad Network `_ is a user-facing client +for the messaging and information-sharing protocol LXMF. RNS Page Node ^^^^^^^^^^^^^ -`RNS Page Node `_ is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files. +`RNS Page Node `_ is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files. Retipedia ^^^^^^^^^ -You can host the entirity of Wikipedia, StackExchange or any other ``.zim`` file, to Nomad Network clients using `Retipedia `_. - -.. only:: html - - .. image:: screenshots/retipedia.webp - :align: center - :target: https://github.com/RFnexus/Retipedia - -.. only:: latex - - .. image:: screenshots/retipedia.png - :align: center - :target: https://github.com/RFnexus/Retipedia - - -Git Over Reticulum -^^^^^^^^^^^^^^^^^^ - -The included :ref:`rngit` system provides a fully distributed solution for managing, hosting, collaborating on and interacting with Git repositories over Reticulum. - -.. only:: html - - .. image:: screenshots/rngit.webp - :align: center - -.. only:: latex - - .. image:: screenshots/rngit.png - :align: center +You can host the entirity of Wikipedia (or any ``.zim``) file to other Nomad Network clients using `Retipedia `_. .. raw:: latex \newpage -Messaging & Voice -================= - Sideband ^^^^^^^^ -`Sideband `_ is a technical LXMF and LXST client, for Android, Linux, macOS and Windows. It also serves as a multi-purpose Reticulum utility, with features and functionality targeted at advanced users. +If you would rather use an LXMF client with a graphical user interface, you can take +a look at `Sideband `_, which is available for Android, +Linux, macOS and Windows. Sideband is an advanced LXMF and LXST client, and a multi-purpose Reticulum +utility, with features and functionality targeted at advanced users. .. only:: html .. image:: screenshots/sideband_devices.webp :align: center - :target: https://github.com/markqvist/Sideband + :target: https://unsigned.io/sideband .. only:: latex .. image:: screenshots/sideband_devices.png :align: center - :target: https://github.com/markqvist/Sideband + :target: https://unsigned.io/sideband -Sideband allows you to communicate with other people or LXMF-compatible systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR Paper Messages, or anything else Reticulum supports. +Sideband allows you to communicate with other people or LXMF-compatible +systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR +Paper Messages, or anything else Reticulum supports. -It interoperates with all other LXMF clients, and provides advanced features such as PTT and voice messaging, real-time voice calls, file attachments, private telemetry sharing, and a full plugin system for expandability. +It also interoperates with all other LXMF clients, and provides advanced features such as voice messaging, +real-time voice calls, file attachments, private telemetry sharing, and a full +plugin system for expandability. .. raw:: latex \newpage MeshChatX -^^^^^^^^^ +^^^^^^^^ -`MeshChatX `_ is an all-in-one Reticulum client with messaging, calling, and a NomadNet browser, providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. +A `Reticulum MeshChat fork from the future `_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original `Reticulum MeshChat `_ project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork. .. only:: html .. image:: screenshots/meshchatx.webp :align: center - :target: https://meshchatx.com/ + :target: https://git.quad4.io/RNS-Things/MeshChatX .. only:: latex .. image:: screenshots/meshchatx.png :align: center - :target: https://meshchatx.com/ + :target: https://git.quad4.io/RNS-Things/MeshChatX -Features include full LXST support, custom voicemail, phonebook, contact sharing, ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security. +Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security. -Columba -^^^^^^^ +.. raw:: latex -`Columba `_ is a simple and familiar LXMF messaging app Android, built with a native Android interface and Material Design 3. It also includes LXST voice call functionality, and a mobile Nomad Network page browser. - -.. only:: html - - .. image:: screenshots/columba.webp - :align: center - :target: https://github.com/torlando-tech/columba/ - -.. only:: latex - - .. image:: screenshots/columba.png - :align: center - :target: https://github.com/torlando-tech/columba/ - - -Ren TUI -^^^^^^^ - -`Ren TUI `_ is an early prototype terminal LXMF client for Reticulum built with Odin on librns (Reticulum-Go). It aims for NomadNet-like messaging/browsing without urwid or ncurses. - -.. only:: html - - .. image:: screenshots/rentui.webp - :align: center - :target: https://github.com/Quad4-Software/Ren-TUI - -.. only:: latex - - .. image:: screenshots/rentui.png - :align: center - :target: https://github.com/Quad4-Software/Ren-TUI - -Chat -==== + \newpage Reticulum Relay Chat ^^^^^^^^^^^^^^^^^^^^ `Reticulum Relay Chat `_ is a live chat system built on top of the Reticulum Network Stack. It exists to let people talk to each other in real time over Reticulum without dragging in message databases, synchronization engines, or architectural commitments they did not ask for. -.. only:: html - - .. image:: screenshots/rrc.webp - :align: center - -.. only:: latex - - .. image:: screenshots/rrc.png - :align: center +The `rrcd `_ program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include `rrc-gui `_ and `rrc-web `_. RRC is closer in spirit to IRC than to modern “everything platforms.” You connect, you join a room, you talk, and then you leave. If you were present, you saw the conversation. If you were not, the conversation did not wait for you. This is not an accident. This is the entire design. -The `rrcd `_ program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include: - -- `Nomad Network `_ -- `MeshChatX `_ -- `Eridanus `_ -- `rrc-gui `_ -- `rrc-web `_ - RetiBBS ^^^^^^^ -`RetiBBS `_ is an old-school bulletin board system implementation for Reticulum networks. +`RetiBBS `_ is a bulletin board system implementation for Reticulum networks. .. only:: html @@ -242,14 +163,29 @@ RetiBBS allows users to communicate through message boards in a secure manner. \newpage -Voice & Telephony -================= +RBrowser +^^^^^^^^ -In addition to the programs listed in this section, other LXST voice clients include: +The `rBrowser `_ program is a cross-platform, standalone, web-based browser for exploring NomadNetwork Nodes over Reticulum Network. It automatically discovers NomadNet nodes through network announces and provides a user-friendly interface for browsing distributed content with Micron markup support. -- `Sideband `_ -- `MeshChatX `_ -- `Columba `_ +.. only:: html + + .. image:: screenshots/rbrowser.webp + :align: center + :target: https://github.com/fr33n0w/rBrowser + +.. only:: latex + + .. image:: screenshots/rbrowser.png + :align: center + :target: https://github.com/fr33n0w/rBrowser + +Includes useful features like automatic listening for announce, adding nodes to favorites, browsing and rendering any kind of NomadNet links, downloading files from remote nodes, a unique local NomadNet Search Engine and more. + + +.. raw:: latex + + \newpage Reticulum Network Telephone ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -270,31 +206,14 @@ The ``rnphone`` program, included as part of the `LXST `_ is a group voice application for Reticulum, built on `LXST `_, inspired by `Mumble `_. - -.. only:: html - - .. image:: screenshots/partyline.webp - :align: center - :target: https://github.com/markqvist/LXST - -.. only:: latex - - .. image:: screenshots/partyline.png - :align: center - :target: https://github.com/markqvist/LXST - -It provides both realtime group voice over any Reticulum transport capable of >6 kilobits per second, LXST dial-in support, listen-only music/broadcasting rooms, and many other useful functions. - + \newpage LXST Phone ^^^^^^^^^^ -The `LXST Phone `_ program is a cross-platform desktop application for performing LXST voice calls over Reticulum. It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management. +The `LXST Phone `_ program is a cross-platform desktop application for performing LXST voice calls over Reticulum. .. only:: html @@ -308,60 +227,28 @@ The `LXST Phone `_ program is a cross-plat :align: center :target: https://github.com/kc1awv/lxst_phone +It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management. + .. raw:: latex \newpage -System Administration -===================== - -Remote Shell -^^^^^^^^^^^^ - -The included :ref:`rnsh` program lets you establish fully interactive -remote shell sessions over Reticulum. It also allows you to pipe any program to or from a -remote system, and is similar to how ``ssh`` works. The ``rnsh`` program is very efficient, and -can facilitate fully interactive shell sessions, even over extremely low-bandwidth links, -such as LoRa or packet radio. - -In addition to the default, fully interactive terminal mode, for extremely limited links, -``rnsh`` offers line-interactive mode, allowing you to interact with remote systems, even -when link throughput is counted in a few hundreds of bits per second. - -RNS FileSync -^^^^^^^^^^^^ - -The `RNS FileSync `_ program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing. - -.. raw:: latex - - \newpage - -Utilities & Components -====================== - -Modem73 -^^^^^^^ - -`MODEM73 `_ and its companion `Modem73Interface `_ is an open source software modem that works with any HF, VHF, or UHF radio capable of 2400 Hz of bandwidth. All you need is a sound card and audio cable for your radio. - -.. only:: html - - .. image:: screenshots/modem73.webp - :align: center - :target: https://github.com/RFnexus/modem73 - -.. only:: latex - - .. image:: screenshots/modem73.jpg - :align: center - :target: https://github.com/RFnexus/modem73 - LXMFy ^^^^^ -`LXMFy `_ is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. `Bot implementations exist `_ for Home Assistant control, LLM integrations, and various other purposes. +`LXMFy `_ is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. `Bot implementations exist `_ for Home Assistant control, LLM integrations, and various other purposes. + + +LXMF Interactive Client +^^^^^^^^^^^^^^^^^^^^^^^ + +`LXMF Interactive Client `_ is a feature-rich, terminal-based LXMF messaging client with many advanced features and an extensible plugin architecture. + +RNS FileSync +^^^^^^^^^^^^ + +The `RNS FileSync `_ program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing. Micron Parser JS @@ -369,25 +256,6 @@ Micron Parser JS `Micron Parser JS `_ is the JavaScript-based parser for the Micron markup language, that most web-based Nomad Network browsers use. If you want to make utilities or tools that display Micron pages, this library is essential. -Reticulated -^^^^^^^^^^^ - -`Reticulated `_ is a Reticulum Network simulator that spawns live, interactive Reticulum instances controllable from a node-based Web UI. - -.. only:: html - - .. image:: screenshots/reticulated.webp - :align: center - :target: https://github.com/RFnexus/Reticulated - -.. only:: latex - - .. image:: screenshots/reticulated.jpg - :align: center - :target: https://github.com/RFnexus/Reticulated - -You can create networks of any topology and set bitrate, MTU, and configure path loss and propagation behavior. It provides a top-down view to a live network. You can connect any Reticulum application to the simulated network. It also has built in utilities like a basic LXMF chat function and Resource transfer included. - RNMon ^^^^^ @@ -399,16 +267,6 @@ RNMon \newpage -Device Flashers -=============== - -In addition to the included command-line ``rnodeconf`` flasher, several community-provided RNode device flashers exist: - -- `RNS Moscow `_ -- `Aetherlab `_ - -Several programs, such as `Sideband `_ and `MeshChatX `_ also include built-in device flashers that can be used fully offline. - Protocols ========= @@ -440,8 +298,8 @@ Interface Modules & Connectivity Resources This section provides a list of various community-provided interface modules, guides and resources for creating Reticulum networks over special or challenging mediums. -* `Modem73 `_ is an efficient software modem that can be used with Reticulum -* Custom interface module for running `RNS over HTTP `_ +* Custom interface module for running `RNS over HTTP `_ * Guide for running `Reticulum over ICMP `_ using ``PipeInterface`` * Guide for running `Reticulum over DNS `_ with Iodine -* Guide for running `Reticulum over HF radio `_ \ No newline at end of file +* Guide for running `Reticulum over HF radio `_ +* `Modem73 `_ is a KISS TNC OFDM modem frontend that can be used with Reticulum \ No newline at end of file diff --git a/docs/manual/_sources/support.rst.txt b/docs/manual/_sources/support.rst.txt index 011f1231..39bcf8f3 100644 --- a/docs/manual/_sources/support.rst.txt +++ b/docs/manual/_sources/support.rst.txt @@ -28,6 +28,9 @@ Donations are gratefully accepted via the following channels: Ko-Fi: https://ko-fi.com/markqvist +Are certain features in the development roadmap are important to you or your +organisation? Make them a reality quickly by sponsoring their implementation. + .. raw:: latex \newpage diff --git a/docs/manual/_sources/using.rst.txt b/docs/manual/_sources/using.rst.txt index c31ce7dd..d87eefaa 100644 --- a/docs/manual/_sources/using.rst.txt +++ b/docs/manual/_sources/using.rst.txt @@ -274,7 +274,8 @@ You can easily add ``rnsd`` as an always-on service by :ref:`configuring a servi The rnstatus Utility ==================== -Using the ``rnstatus`` utility, you can view the status of and control configured Reticulum interfaces, similar to the ``ifconfig`` program. +Using the ``rnstatus`` utility, you can view the status of configured Reticulum +interfaces, similar to the ``ifconfig`` program. **Usage Examples** @@ -336,8 +337,8 @@ Filter output to only show some interfaces: .. code:: text - usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A] [-P] [-l] - [-B] [-b] [-t] [-q] [-s SORT] [-r] [-j] [-R hash] [-i path] + usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A] + [-l] [-t] [-s SORT] [-r] [-j] [-R hash] [-i path] [-w seconds] [-d] [-D] [-m] [-I seconds] [-v] [filter] Reticulum Network Stack Status @@ -349,19 +350,12 @@ Filter output to only show some interfaces: -h, --help show this help message and exit --config CONFIG path to alternative Reticulum config directory --version show program's version number and exit - --attach name Attach interface by name - --detach name Detach interface by name - --reload name Reload interface by name -a, --all show all interfaces -A, --announce-stats show announce stats - -P, --pr-stats show path request stats -l, --link-stats show link stats - -B, --burst only show interfaces with active bursts - -b, --blocked-ips show blocked IPs per interface -t, --totals display traffic totals - -q, --queues display queue stats -s, --sort SORT sort interfaces by [rate, traffic, rx, tx, rxs, txs, - announces, arx, atx, prx, ptx, held] + announces, arx, atx, held] -r, --reverse reverse sorting -j, --json output in JSON format -R hash transport identity hash of remote instance to get status from @@ -369,8 +363,6 @@ Filter output to only show some interfaces: -w seconds timeout before giving up on remote queries -d, --discovered list discovered interfaces -D show details and config entries for discovered interfaces - --show-stale show stale discovery entries - --show-unknown show discovery entries without version info -m, --monitor continuously monitor status -I, --monitor-interval seconds refresh interval for monitor mode (default: 1) @@ -709,7 +701,7 @@ The rnx Utility The ``rnx`` utility is a basic remote command execution program. It allows you to execute commands on remote systems over Reticulum, and to view returned command output. For a fully interactive remote shell solution, be sure to also take a look -at the :ref:`rnsh` program. +at the `rnsh `_ program. **Usage Examples** @@ -775,8 +767,6 @@ another one, which will be created if it does not already exist --version show program's version number and exit -.. _using-rnsh: - The rnsh Utility ================ @@ -1236,8 +1226,6 @@ For more fine-grained control over how discovered interfaces are auto-connected, * The ``autoconnect_announces_to_internal`` option allows you to specify that auto-connected interfaces should propagate announces to ``internal`` mode interfaces, even if the auto-connected interface's mode would normally not allow for this. -* The ``autoconnect_unverified_implementations`` option will allow the interface discovery system to auto-connect to *any* announced interface, even those coming from unverified, experimental or potentially broken implementations. Be very careful with this option. - Remote Management ----------------- diff --git a/docs/manual/_static/documentation_options.js b/docs/manual/_static/documentation_options.js index 1e03c972..6d656b6d 100644 --- a/docs/manual/_static/documentation_options.js +++ b/docs/manual/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '1.5.5', + VERSION: '1.4.2', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/manual/brandolinis.html b/docs/manual/brandolinis.html deleted file mode 100644 index 319841f9..00000000 --- a/docs/manual/brandolinis.html +++ /dev/null @@ -1,1611 +0,0 @@ - - - - - - - - - - Brandolini’s Reference - Reticulum Network Stack 1.5.5 documentation - - - - - - - - - - - - - - - - - - Contents - - - - - - Menu - - - - - - - - Expand - - - - - - Light mode - - - - - - - - - - - - - - Dark mode - - - - - - - Auto light/dark, in light mode - - - - - - - - - - - - - - - Auto light/dark, in dark mode - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Skip to content - - - -
-
-
- -
- -
-
- -
- -
-
- -
-
-
- - - - - Back to top - -
-
- -
- -
-
-
-

Brandolini’s Reference

-
-
-
A little learning is a dang’rous thing;
-
Drink deep, or taste not the Pierian spring.
-
There shallow draughts intoxicate the brain,
-
And drinking largely sobers us again.
-

-
- Alexander Pope, 1711
-
-
-

This chapter serves as a helpful reference for developers, users and community members living in an Age of Bullshit Manufacture equivalent to the Cambrian Explosion.

-

As we are surely only at the on-ramp of this trajectory, the chapter will continue to evolve, expand and be updated based on community feedback as events occur.

-
-

What The Reticulum License Is

-

To understand the legal reality relating to this section, see Void Grants.

-

The Reticulum License is the license under which RNS, the Reticulum reference implementation, has been published since April 15, 2025, in versioning effective from 0.9.4 and every release since. In practical terms, it covers all of “modern Reticulum”: The current wire format, the cryptographic machinery as it exists today (including the AES-256-based link encryption, ratchets, et cetera), and all protocol additions that distinguish the modern implementation from the earliest pre-1.0.0 beta formats. If an implementation speaks today’s Reticulum protocol, it is, in all relevant respects, built on Reticulum-licensed work.

-

The license is a permissive license. It grants the full range of rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, free of charge; subject to three conditions, which are reproduced here verbatim:

-
-
    -
  • The Software shall not be used in any kind of system which includes amongst its -functions the ability to purposefully do harm to human beings.

  • -
  • The Software shall not be used, directly or indirectly, in the creation of an artificial -intelligence, machine learning or language model training dataset, including but not -limited to any use that contributes to the training or development of such a model or -algorithm.

  • -
  • The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software.

  • -
-
-
-

Why This License Exists

-

Reticulum was not moved to a custom license casually or quickly. It was licensed under the MIT license for the greater part of its existence, and that served well for a long time. The change came when it became clear that no existing license expressed what the project needed, and that the two most commonly proposed alternatives would either say the wrong thing or say nothing useful at all. Three factors had primary influence on the situation:

-

The harm problem

-

Reticulum is a tool that was built, deliberately and at great cost, to empower people; to make communication something you own rather than something you are temporarily granted. It has also been, at various times, exactly what certain kinds of organisations would like to use to control people. This is the duality of any powerful tool. A permissive license could not, and cannot, say anything about that; MIT is silent on the question of whether the software may be used to build systems designed to harm human beings. The Reticulum License is not silent. It does not pretend that a license can stop an individual madman; it does assert, however, plainly and in enforceable terms, that corporations and organisations integrated into the legal system cannot consume this software for harm and then claim they never knew. The line is drawn by the creators of the tool, not by an external authority, and it is drawn deliberately. This is covered in more depth in the discussion of the ethics of the tool in the Zen of Reticulum.

-

The appropriation problem

-

The second factor was the machine-learning appropriation of the open-source commons. By 2025 that was no longer hypothetical: The commons was already being systematically ingested by corporate training pipelines, and the code, words, and creativity of the people who built it were being converted into synthetic systems designed to replace them. The license responds to this with an explicit condition: The software shall not be used in the creation of an AI, machine-learning, or language-model training dataset, nor in any use that contributes to the training or development of such a model or algorithm.

-

It is important to understand what this condition does and does not do, which matters for what follows in this chapter. The AI-training condition is not the legal basis for protecting the code from machine copying. That protection already exists, in the oldest and most well-tested form of copyright law, and it applies regardless of any license (see Copyright & The Two Legal Layers).

-

The AI-training condition addresses something additional: the appropriation of the code into the models themselves; the ingestion of human-made work as raw material for a synthetic replacement, and the use of that ingestion to fabricate “prior work” minefields, patent-troll material, and a wall of slop meant to bury and devalue the actual ecosystem. That danger was foreseeable; the licensing decision was made with full awareness of it. The events of 2026 have since demonstrated, in public and in detail, that the concern was not theoretical.

-

The sustainability problem

-

Reticulum has been developed and maintained for over a decade by a single person, supported by a small group of contributors and network-building users, without corporate backing, without VC funding, through several serious attempts at commercialization and takeover from both the corporate and the ideological side. The license is part of what makes that sustainable, and possible at all.

-

It ensures that the specific, painstaking labor of the reference implementation cannot be hijacked to undermine the intent of the project, while leaving the protocol itself, the mathematical rules of how Reticulum works, dedicated to the public domain, where it belongs to humanity and can never be owned. Anyone can implement the protocol, provided they themselves invest the actual work and effort. What the license protects is the work, and the intent embodied in it.

-
-
-

On Freedom

-

The most persistent mischaracterisation of the Reticulum License is that it is “more restrictive” than the licenses it replaced or than the GPL family. This is backwards, and I’ll state the comparison plainly, because the confusion is the foundation of a great deal of deliberately manufactured controversy examined later in this chapter.

-

The GPL and AGPL are constructed around a single, self-referential axiom: that “free” is defined by the terms of the license itself, and that any use falling outside those terms is not free. In practice, this means the GPL restricts the freedom of everyone - you may not write, distribute, link, or (in some cases) even communicate with GPL-covered software without the entire arrangement being pulled under the same terms, which the licensor then controls. The restriction applies to 100% of users, including the ones building purely constructive things.

-

The Reticulum License restricts the freedom of almost nobody. Its restrictions apply only to:

-
    -
  • Those who wish to use the software in systems that purposefully harm human beings, and

  • -
  • Those who wish to ingest the software into AI training pipelines.

  • -
-

That is a restriction on a vanishingly small fraction of would-be users - the 0.0000001% - and it is a restriction they can characterise however they like, forever. The bickering is itself a signal that the licensing decision hit exactly the right target.

-

Everyone else retains the full breadth of use, modification, distribution, and sale, with none of the viral encumbrance of copyleft.

-

Both licenses contain moral axioms. The difference is that the Reticulum License states its axioms out loud:

-
    -
  • Intentionally restricting the way that millions of people use and build upon a foundational -communications technology - The mechanism of copyleft

  • -
  • Intentionally restricting a small number of organisations from using the technology to -violently harm human beings - The mechanism of the Reticulum License

  • -
-

Which of these is more free? The answer is not a matter of committee approval. It is a matter of what the word “free” means to the people whose freedom is at stake. The question of whether “open source” requires the blessing of a particular licensing orthodoxy is examined in Open Source Means Open Source.

-
-
-

The Prediction, And What Happened

-

When the license change was made and publicly discussed in 2025, the following was said, in public, about the machine-learning threat:

-
-

A generative system could be trained on the Reticulum codebase, and for extremely low cost -be set to work spewing out millions of source repositories for all kinds of ridiculous -“Reticulum-based programs”. This is already happening. You can find several examples of this -on GitHub already. … the point of this kind of trolling is to establish as wide a base for -“prior work” as possible. By doing that, a patent troll or IP abuser can create an immense -minefield for real developers.

-
-

This was written when the phenomenon was just beginning. It was not a guess; it was a description of a mechanism already in motion, in the specific context of Reticulum, by people who had already begun to appear around the project.

-

Less than a year and a half later, the prediction has now been demonstrated, in public, exhaustively and to the letter. There are now roughly a dozen machine-generated “Reticulum implementations” of the same shape: fluent, marketing-heavy, some without attribution, and - in several cases - actively harmful to the network. The Prns case is an example documented in this chapter. The pattern that produced it (and the people, mechanisms, and the recycling of falsehoods behind the wider phenomenon) is examined in A Movement Of A Dozen.

-

The license was written, in part, so that when this happened, the community would not be defenceless.

-
-
-

What This Means For Other Implementations

-

Two simple things must be kept separate:

-
    -
  1. The protocol is public domain. The mathematical rules of Reticulum - addresses, -signatures, routing, the wire format as an idea - belong to humanity. A genuine clean-room -implementation of the protocol, written by human beings who reasoned it out themselves, is -free and welcome, and owes nothing to the Reticulum License. Nothing in this chapter is an -argument against implementing the protocol. On the contrary: More good implementations, -honestly made, are good for the network.

  2. -
  3. The implementation is licensed. Copying the reference implementation - by machine or -by silk-screen press - and modifying and redistributing the result, is -exercising rights granted by the license, and is therefore subject to its conditions: The attribution -notice, the harm condition, the AI-training condition. A copy that does not follow those -conditions is not licensed, and it is not “open source in spirit”; it is an unauthorized -derivative work, and therefore a copyright violation.

  4. -
-

The distinction is the same one the license itself makes, and it must be held firmly: An implementation that speaks Reticulum is exercising the public domain protocol, if it was truly created independently. An implementation that was generated from RNS, reproduced by a copy process, is a derivative of licensed work.

-
- -
-

The “Clean-Room” Delusion

-

I regularly receive mail or LXMF messages from people who proudly and happily proclaim that they have created a “clean-room” implementation of one of the software systems I maintain; RNS, Sideband, LXMF or others. The declaration is delivered with the flat certainty of someone who believes they have found the legal lock-pick that settles everything at once: Their production is independent, uncontaminated, and therefore free of any obligation to the work they copied, its license, or its author. The proclamation usually concludes with a request for blessing, or at least acknowledgment. The proclamation is, usually, also generated by an LLM.

-

This is a delusion, and I’ll cut through it surgically. The word they here invoke as magic is attempting to do a great deal of work it was never able to bear.

-

What a clean room actually was

-

The legal “clean room” is not folklore, incantation, witchcraft or a claim a person gets to make about their own output. It is a very specific, institutionalized procedure that grew out of the hardware and software litigations of the 1980s, and it existed for exactly one purpose: Convincingly proving the absence of copying in a court of law.

-

The canonical case was Phoenix Technologies and its 1984 reimplementation of the IBM PC BIOS. IBM’s BIOS was copyrighted, which meant that the entire PC-compatible industry depended on being able to show that a compatible BIOS had been written independently, and not from IBM’s code. Phoenix’s answer was a physical and procedural firewall:

-
    -
  • One team, the “dirty room”, read the original code and produced written functional specifications: descriptions of what the BIOS did, not how IBM’s code did it.

  • -
  • A second team, the “clean room”, consisted of engineers who had never seen the original code at all, and they wrote the new BIOS from those specifications alone.

  • -
-

This obsessively documented, legally attested separation was not a preference or a willy-nilly show for a kindergarten; it was the entire legal point, and it had to be solid. If even one clean-room engineer had glanced at IBM’s code, the clone would have been contaminated, and the defense would shatter.

-

The other landmark case was Computer Associates v. Altai (1992), in which Altai had copied CA’s scheduler code into its own product, and (upon being caught) hired outside counsel to run a genuine clean-room procedure: A programmer with no exposure to the original was brought in to rewrite the infringing modules from functional specifications prepared by the team that had seen the code. The court credited the good-faith effort precisely because it was done, as the carefully documented record showed, to the letter of the procedure.

-

Bear in mind what this procedure entailed in practice, because this is the part the modern claimants never seem to have even heard of: Physical separation of teams, legal counsel managing the process, signed and notarised declarations of non-exposure, controlled specification documents that described behaviour and never code, months of work, and testing against the specification - not against the original, which the implementers were forbidden to consult.

-

The people on the clean-room side of the wall did not just claim they had not read the code. Their non-exposure was institutionally guaranteed, obsessively documented, and legally attestable through every fart and sneeze of the process, because the entire point was that a court - hostile, adversarial, and entitled to discovery - would believe it.

-

That is the process. “Clean-room implementation” is the name of a certified procedure for proving independent authorship. It is not a mood, a marketing label, or a property a person gets to declare about their own work.

-

The moronic inversion

-

Now consider what is actually being described when someone says: “I created a clean-room implementation of Reticulum by feeding the source code into a machine.”

-

That sentence is the opposite of a clean room. Feeding a copyrighted work into a machine so that it reproduces and transforms the work is the definition of copying, and a clean room is the one procedure whose entire purpose is the demonstrated absence of copying. This process cannot demonstrate the absence of copying, because the process is composed of copying. The cleanest possible room has been replaced by the dirtiest possible room, and the dirt has been installed as the base feature. What these claimants have actually performed is a photocopy with extra steps, and the extra steps, far from cleaning anything, are simply steps that forge a certificate they believe the output has earned.

-

Why the word is deployed

-

The word “clean-room” is deployed because it sounds like what the speaker wishes were true. It carries the accumulated legal prestige of the 1980s and 1990s - the BIOS clones, the Altai credit - and it sounds like a certificate already issued: Independent, original, unencumbered, the work of a person who owes nothing. The claimants want that halo without the procedure: No law firm, no physical separation, no written specifications, no signed declarations, no months of process - just a laptop, a loop, and the word “clean-room” typed into a README. The word must do the work of the process it replaces, and the delusional replacement is the entire point.

-

The irony runs deep

-

The Reticulum protocol is public domain. Any human being can implement it - from the mathematics, from the documentation, from their own reasoning - and owe nothing whatsoever to the license, to the reference implementation, or to me.

-

An honestly written implementation needs no incantations, no legal theater, and no special vocabulary; it simply is what it claims. It’s plain for everyone to see. It’s also plain for everyone to see when you’re faking it. The people who reach for the word “clean-room” are, without exception, the people who need it to create a fantasy - and the people who need that are precisely the people who cannot have it. If what you wrote were genuinely independent work on the public-domain protocol, you would not need to call it anything. The word is not a seal of independence. In this context it is a confession of copying, filed by the copier, in the confident belief that it says the opposite.

-
-
-
-

Open Source Means Open Source

-

An objection regarding the “open source status” of RNS appears occasionally in variations, but always reduces to the same syllogism:

-
    -
  1. “Open source” means licensed under one of the licenses approved by the Open Source -Initiative (or, in other variants, compatible with GPL orthodoxy).

  2. -
  3. The Reticulum License is not on that list.

  4. -
  5. Therefore Reticulum is not open source, and anything built on it is illegitimate.

  6. -
-

The entire argument rests on the first premise, and the first premise is false.

-

Open source is not a certification conferred by a committee. It is a description of a fact: The source code is available, and the rights to use, copy, modify, merge, publish, distribute, sublicense, and sell it are granted to everyone, free of charge.

-

The Reticulum License does all of those things. It is, in every substantive respect that concerns the person who wants to use, study, modify, or build upon the software, an open license. What it adds - and what disqualifies it in the eyes of the orthodoxy - is two conditions on who may do what with the original work, not on what the user is allowed to do with it.

-

The definition being wielded against Reticulum is not even a young one. It is, in essence, the Debian Free Software Guidelines written in 1997, adopted by the OSI in 1998, and treated ever since by certain people as if it had descended from a mountain on tablets. It is an approach that is close to three decades old. Being old is not itself a flaw - but being old is also not a proof of correctness, and the suggestion that the meaning of a word like “open” was settled for all time by a committee in the 1990s, and may never be revisited, is not a position rooted in reason. It is rooted in institutional inertia dressed as principle, maintained by those whose ideologies it serves.

-
-

What The Argument Is Actually Doing

-

I would like to go into some detail about the rhetorical mechanism, because it is roughly the same mechanism that powers the rest of the campaign described later in this chapter.

-

The orthodoxy’s argument is self-referential: “open source means what we say it means, because we are the ones who define it.” When the Reticulum License is measured against that definition and found wanting, the conclusion is presented as a property of Reticulum - when in fact it is a property of the definition. The definition is treated as neutral and authoritative - as some base fact of raw cosmology - and the license is treated as deviant. But the definition is no more neutral than the license is. It encodes a moral position, and that moral position is exactly the one that this project declines to take.

-

Consider what the orthodoxy’s definition requires, in the case at hand. The OSI-based definition contains, in its ancestry, the Debian Free Software Guideline of “No discrimination against fields of endeavor”: The software must not restrict who may use it or for what purpose. Taken to its logical end, this means that open source, properly “certified”, must be equally available to those who build hospitals and those who build weapons, those who connect people and those who control them, those who create and those who ingest and replace.

-

The orthodoxy presents this as neutrality. It is not neutrality; it is a moral position - one that says, in effect, that harming human beings is a field of endeavor like any other, and software must not discriminate against it.

-

The Reticulum License takes a different position, and it has the honesty to state it in the license text: this software is for life, not for death; it is for people, not for the systems that would replace them. That is not a restriction on openness, nor on freedom. It is a restriction on who may consume the work, and it is a restriction the orthodoxy’s definition cannot express - which is why the orthodoxy must always attack the license as “not open source” rather than engage with what it actually does.

-
-
-

Who Is Restricted By Which License?

-

The arithmetic is simple, so let’s do it out loud, because the orthodoxy’s framing inverts it every time.

-

The GPL family imposes its conditions on everyone: You may not distribute, link, or - in the AGPL case - even communicate with GPL-covered software without your own work being pulled under the same terms. “Free” in this system is defined (self-referentially) by the license’s own terms; everything outside those terms is, by definition, unfree. The restriction applies to 100% of users. It applies to the hospital, the mesh community, the school, and the hobbyist with the same force it applies to anyone else. That is not freedom for everyone; it is a specific moral architecture applied to everyone, presented as if it were the absence of one.

-

The Reticulum License imposes its conditions on a fraction of would-be users so small as to be practically unmeasurable:

-
    -
  • Those whose intended use is to build systems that purposefully harm human beings, and

  • -
  • Those whose intended use is to ingest the work into AI training pipelines.

  • -
-

That is the entire list. Everyone else - the overwhelming majority, the community, the developers, the users, the downstream projects - receives the full, unrestricted rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, with none of the viral encumbrance of copyleft.

-

So when the claim is made that the Reticulum License “is more restrictive” than the GPL, the claim is not merely wrong; it is the exact inverse of the truth. The GPL restricts the freedom of everyone, in the name of a specific ideology.

-

The Reticulum License restricts the freedom of the harm-doer and the appropriator, and leaves everyone else free. The two approaches do not differ in whether they contain moral axioms - all licenses do. They differ in whether the axioms are stated openly, and in whose freedom they restrict.

-
-
-

The Consequences Of The “Not Open Source” Claim

-

This would be a footnote debate if the mischaracterisation had no practical consequences. It has very large ones, and therefore this section exists.

-

The claim that “Reticulum is not open source” is the foundation of the takeover narrative it so often accompanies. The sequence is always the same:

-
    -
  • First, declare the reference implementation illegitimate (“not really open source”, “owned by one man”, “not a real community project”)

  • -
  • Second, conclude that it is therefore acceptable - even necessary - to replace it.

  • -
  • Third, present the replacement, which is (conveniently) often the speaker’s own machine-generated clone, as the legitimate heir.

  • -
-

This is not speculation about motives; it is the documented structure of the campaign examined in A Movement Of A Dozen.

-

The “not open source” claim is the legal-ideological battering ram of that campaign. It is recycled, in article after article, in forum after forum, because it works on people who assume that a committee’s definition is the same thing as the truth.

-

It is also worth stating the other obvious fact: None of these critics have ever shown that the Reticulum License restricts them. They do not build weapons. They are not training models on the codebase. They are free, in every practical sense, to use Reticulum exactly as they would under MIT or GPL. Their objection is not to a restriction on themselves; it is to the mere existence of a license that states a moral position they disagree with, or which they can use as leverage for a different goal entirely, and to the institutional fact that it does so outside their system of approval and control. The complaint is not that the license makes them unfree. The complaint is that the license is free of them.

-
-
-

The Distribution Argument

-

A recurring companion claim is that RNS is not properly “distributed”, usually deployed as “it’s not packaged as a .deb” or “it’s not on package manager X”, with the implication that it is somehow unavailable or inaccessible, or hard to install on a given system.

-

This is simply false, and it has been false for years. The reference implementation is available on practically every platform and architecture with a single command (pip install rns). It is packaged in the Arch User Repository, in NixOS, and in a range of other distribution channels, community-maintained and official. It can be installed, updated, and kept current over Reticulum itself - the network transports its own software updates. It is used on more than a hundred thousand devices worldwide, is embedded in over a hundred other packages and applications, and carries terabytes of traffic every day.

-

The claim that Reticulum needs a .deb to be “really distributed” mistakes the mechanism for the goal. The goal is that people can obtain and update the software easily, everywhere, on their own terms, and distribute it themselves, both offline, online and over Reticulum with full cryptographic verification of the install packages, without any central infrastructure.

-

That goal is met to an excellence that few software packages can speak of, and met in a way that does not pin the project’s release cadence to the review cycles of distribution maintainers, which is an achievement, not a bug.

-

A project like RNS that updates continuously, sometimes daily, and has a track-record of shipping security updates mere hours after issues are discovered, would be crippled by packaging that makes every change wait months for approval. The distribution complaint is a strawman generated for the occasion. It has been answered, in public, many times; it is repeated anyway, because the people repeating it are not counting on facts - they are counting on the reader never having heard the answer.

-
-
-

On The Institutions, In Fairness

-

None of the above is hostility toward the Open Source Initiative or the Free Software Foundation as institutions, nor toward the individuals who have dedicated decades of work to them. Their contribution to the world of software is enormous, and this document does not dispute it. What is disputed is a specific claim of authority: That “open source” is a term whose meaning is reserved to a committee’s approval, and that a license which does not submit itself to that approval is therefore not open.

-

That claim of authority is not in Reticulum’s interest, and it is not in the interest of the people the movement claims to serve. The current legal and technological landscape - the machine-appropriation of the commons, the weaponisation of “prior work” generation, the erosion of human agency by synthetic systems - was not anticipated by the DFSG of 1997, and the institutions have not, as of this writing, produced any meaningful answer to it. The choice, as it was put at the time of the license change, is whether developers should sit back and wait for a solution from a committee that “may or may not ever materialize, before the entire foundation we stand on has been eaten away”.

-

Reticulum’s answer was to act. If we had waited, it would have been too late.

-

If an OSI-approved license existed that expressed the same constraints - and did not impose the far more severe restrictions of copyleft - the project would have used it. None such exists. That is not Reticulum’s failure. It is the measure of what the definition is missing.

-
-
-

What “Open” Means Positively

-

For anyone who wants the positive statement rather than the rebuttal, Open Source, in this project’s understanding, means:

-
    -
  • The source code is available, to everyone, without charge

  • -
  • Everyone may use, copy, modify, merge, publish, distribute, sublicense, and sell the -software

  • -
  • No one is discriminated against as a person - developer, user, community, or project

  • -
  • The protocol itself is dedicated to the public domain, where no one can own it

  • -
  • And the single, plainly stated condition of the license is that the work shall not be -consumed to harm human beings, or ingested to replace the humans who made it

  • -
-

If that is not “open” by a definition written in the 1990s, then the definition is the thing -that has failed - not the software, and not the people who use it.

-

Open source means open source. It was true before the committees existed to certify it, and it will remain true after they are gone.

-
-
-
-

Assistance Versus Machine-Substitution

-
-

False Dichotomy

-

The public discussion around machine learning and software development has been derailed, by the loudest voices on both sides, into a farcical fight between “AI enthusiasts” and “AI haters”.

-
-

Both positions are absolute caricatures.

-

The fight is beside the point in the ways that matters most.

-
-

It is not an accident, but a very conscious choice that the initial analysis of the Prns case examined herein, and this very chapter, were themself produced with the assistance of a machine-learning based generative system.

-

Specifically, it was generated using lc managing a Q8_K_XL quant of DeepSeek v4 Flash running in the llama.cpp inference engine. The main context base used to stabilize, ground and inform the generative environment consisted of approximately 100,000 tokens of text material that I personally thought out and wrote (by hand), then archived and organized, over the last eight years.

-

If you know my writing, and have a hard time discerning where this chapter is me directly, typing manually, and where it is the machine output, it is because of two things: Almost every line in this chapter was either directly edited by my hands on the keyboard or typed by me, and the text corpus that informed the process is genuinely me - my thoughts, written by me, over a period of years.

-

The chapter was produced this way deliberately and openly, under a software framework I designed and wrote, with the human directing the inquiry points, flow of investigation, structure of the outputs, contents of every section, making the editorial decisions, reviewing the conclusions, painstakingly adjusting every line, and taking responsibility for the final claims presented, with the goal of reaching a higher level of synthesis and symbolic density than I could have achieved without that assistance.

-

The tool was used as tool, both to structure and produce a complex chapter, and to inspect a repository so preposterous in size, commit velocity and nonsensical wording that it barred any purely human inquiry.

-

As such, the position is hereby, by personal example and demonstration, stated clearly:

-
    -
  • Technological assistance is not the problem

  • -
  • It was never the problem

  • -
  • It is the opposite of the problem

  • -
-

The problems are substitution and usurpation: The perverse process in which the machine becomes the author of choices, and the human becomes the facade of software, while expecting “reciprocity” or recognition for deeds they never performed.

-
-
-

The Line

-

The exact distinction can be reduced neither to how many lines of code a machine produced, nor who pressed the keys. The final distinction has to rest on who, or what, the real agent of the work is.

-
    -
  • Assistance is when the human retains the full seat of agency: The human decides what to build, judges -what is correct, rejects what is wrong, understands what is shipped, and can answer for every single line of it. -The machine writes, suggests, drafts, accelerates meaningfully and within reason - but every choice that survives into the -final work is a choice the human made consciously, and understood - or grew to understand - the consequences of.

  • -
  • Substitution is when the machine becomes the agency: the machine decides what to build, -decides what is correct, iterates on its own errors, generates at a scale and speed no single -human can follow - and the human’s role is reduced to providing the name, the account, the -signature, and the marketing launch.

  • -
-
-

The Line can be stated as a single test, and this is the same test that has always separated authorship from couriership: Can the person whose name is on the work answer for it fully? Not “can they describe what it does” - can they account for why it is the way it is, line by line and decision by decision, and the history of how it came to be that way, under questioning, without machine assistance, including by people who know more than they do?

-
-

If they cannot, they did not author the work. They wore it, or in more severe cases, they stole it. And a courier who claims authorship is not a writer; they are a fraud.

-
-
-

What assistance looks like

-

Assisted work is recognisable, and recognisability is itself part of its value. It has the following observable properties:

-
    -
  1. The human can explain it. Under examination, the author can say why this choice was -made, what was rejected, and what they verified. The ability to explain is not a luxury; -it is the very demonstration of authorship. On a large project with many contributors this is partial in -practice - no one can explain every line of a large codebase - but the full capacity must -exist between the authors and the habit must be visible.

  2. -
  3. The human can, and does, reject machine output. An author who has never rejected the machine’s output is not -an author, but an open drain. Rejection is not a stylistic flourish; it is the -mechanism by which any work attains Quality. A record of decisions, including reversals and rejections, -is a sign of agency. A record without rejection is a record of assent-by-default.

  4. -
  5. The human’s judgment is directional. The work contains the human’s actual choices; -things the machine would not have decided on its own, which the human can defend on their -merits. This directionality has many names in engineering: Design, architecture, taste, -judgment. Where it is absent, the work has the uniform, self-consistent texture of -something that was never doubted, and doubt is exactly what anything important requires.

  6. -
  7. Provenance is not concealed. Disclosure of how the work was made is not a legal -requirement in every jurisdiction; it is not even always a practical requirement. But it -is a statement that matters. A project that discloses its process - -including its use of machines - is telling the truth about the relationship between the -human and the work, and that is a property users are entitled to know. Concealment of -provenance is not a stylistic choice; it is deception, made by someone who knows what the -record would otherwise show.

  8. -
  9. The human stands behind the failures. When assisted work fails, the human can say: Yes, I -chose this, under these assumptions, here is why, here is what I got wrong, here is the fix. -That sentence is the entire contract between an author and the people who depend on their work. -A human who cannot say it - because the choices were never theirs - has nothing to offer anyone, and -is by definition entirely replaceable; anyone else could have pressed the same button.

  10. -
-
-
-

What substitution looks like

-

Substitution is equally recognisable, once the pattern is known:

-
    -
  1. Scope that no human could have authored. Output that -structurally cannot have been the product of human authorship, review, and judgment, for -example when the volume exceeds any reasonable human capacity within the span of the project’s life. -When the review capacity and the output volume are incommensurable, the output was not -reviewed. It was generated, then forgotten, and the record was signed blindly.

  2. -
  3. Uniform generation. The work carries no trace of a human’s changing mind. No style -drift, no contested decisions, no “we tried X and it was wrong, so we did Y”. That is, no -signs of actual development, growth and learning. Instead, endless, self-referential -refactoring, renamed abstractions, and the same fluent register in every commit, -every document, every reply. A human’s history shows the wear of judgment. -Pure generation shows the superficial polish of its absence.

  4. -
  5. Discovery by recursive regeneration. When errors are found, they are found by the machine’s own -subsequent runs, not by human scrutiny - and the findings are recorded as planned -“reviews” and “alignments” rather than as the corrections they are. This is the tell: A -machine editing its own previous output, while the edit is recorded as if it were a -scheduled milestone. That’s not review. That’s a continuous generation loop revising itself, -with the attempt of an illusion-preserving voice-over.

  6. -
  7. Laundered provenance. Theatrical concealment machinery: Commit hooks that refuse -attribution of machine generation, contribution policies that declare machine output -“yours”, processes that scrub personal traces from the history, fake identities in the -contributor list. All of these have one function: To make the record say what the “author” -wished had happened, rather than what happened. A project that builds such machinery is -not making a record of its work; it is using automated software to manufacture one.

  8. -
  9. Authorship by assignment. The human’s name is on the work, but the human cannot -answer for it - because the human did not choose any of it, and knows it. The name is not a -signature of authorship; it is a fraudulent claim of ownership over something the claimant did not -make and cannot explain. In every field that has ever dealt with such claims - art, -literature, invention, journalism - this has a name. That forgery is now cheap in software -makes it no more forgivable.

  10. -
-
-
-

Infrastructure Matters

-

For a hobby project, the distinction between assistance and substitution is a personal matter. For infrastructure - code that other people’s communications, safety, and freedom run on - it is a matter of life and safety, and any statement about this must be entirely unapologetic.

-

Infrastructure has one property that distinguishes it from almost all other software: While most of the people who depend on it cannot verify it themselves, it must be verifiable, auditable and open to human inspection, and be engineered in such a way that this is as frictionless and straight-forward as possible.

-

The reference implementation of Reticulum was designed this way, and continues to make that goal a primary one. While it is a complex piece of software, with an intricate array of interlocking and moving parts, the core parts of RNS, the entire Reticulum protocol implementation, the transport logic, the cryptographic primitive interfaces, is less than 15,000 lines of code. That line count even includes the rather extensive comments, incredibly detailed logging facilities and other choices that makes the code easy to read and understand for humans, and easy to operate, verify and debug. It took real dedication, and a lot of hard work to achieve that.

-

Exactly because not everyone can do this, it is a fundamental necessity for infrastructure software that it is designed and authored so that as many as possible can, and that the chain of accountability - who made it, who reviewed it, who answers for it - is held intact at every link. That chain is not decoration. It is the entire mechanism by which normal users decide to trust a system they cannot fully verify. The transparent foundations and the chain combined is the verification for most users.

-

Substitution breaks the chain both at the foundations, and at its load-bearing link: The human at the top can no longer answer, because they were never the maker. Unfortunately, the chain does not always fail visibly; often it fails silently, at the exact moment a question goes deep, or when routing fails subtly at a critical moment.

-

For the user, the result is not a project with a normal and humanly flawed author - it is a project with no author at all; a signature with no substance behind it, a name that cannot be held to account, and a body of work that nobody on earth can (or wants to) explain, defend, or fix in a crisis.

-
-
-

The Fluency Trap

-

It is worth understanding, briefly, why substitution is so attractive and so dangerous at once. Machine-generated output is fluent. It is confident, well-structured, and terminologically self-consistent. At the surface, and from the perspective of someone outside a given craft, it is indistinguishable from the output of a knowledgeable human. Fluency of this kind can be a useful property: It makes assisted work fast. But fluency is not understanding. It is the minimal conformal surface of understanding.

-

The danger is that the surface is exclusively where the substituter invests; for the simple reason that the surface is where most of the desired audience looks, and what yields attention to the project.

-

A human who can tell the difference uses the tool well. A human who cannot is being deceived through means they cannot detect from inside, namely through their own inability to account for the limits imposed by what they do not know.

-

This is why the most important quality for anyone working with systems like Reticulum is not speed, and not vocabulary, but the oldest engineering virtue there is: The honest, uncomfortable, terrorizing knowledge of what you do not know. The substitute never has it. The substitute’s operator, if they ever had it, loses it precisely the moment they accept the fantasy of the substitution process.

-

The tool is not the hazard, plain old human nature is. The tool is a mirror, and mirrors - as has been observed for rather longer than software has existed - show the truth only to those willing and yearning to see their own shortcomings.

-
-
-

Disclosed Assistance Or Laundered Generation

-

The contrast at the centre of this entire discussion can be made clearer with two examples, both of which are publicly accessible, both produced in the same year, by people using the same class of tools.

-

The first is this chapter. It was produced with machine assistance, openly: the structure was directed by the human, the material was assembled with the tool’s help, every conclusion, every line, every slight nuance of meaning and semantics was examined, edited, and accepted or rejected by the human, and the process described publicly - including the precise characteristics of the technological assistance framework used. The human can answer for every claim in these documents, because the human made the decisions the claims rest on.

-

The second is the project examined in the Prns case. It was produced by a machine, at a volume no human could review, and the authorship record was engineered to erase the machine’s role: A commit hook that refuses attribution of machine generation, a contribution policy declaring machine output the human’s own, a fictitious contributor identity, 637,000 lines of unreviewed code. The human cannot answer for the code, because the choices were never made by a human - only the conformal surface of the presentation was.

-

The same tool class. The same technology. Two diametrically opposed relationships to truth and responsibility. The difference is not the tool. The difference is the presence or absence of the human as the agent of the work - and the honesty or concealment with which that relationship is recorded.

-

Assistance leaves the human as the agent and the commons as the source from which the human works and can grow. Substitution removes the human from the loop and converts the commons into fuel for the thing that seeks to replace them, or usurp their work for the profit of the few, human atrophying in the process.

-
-
-

The License In This Context

-

The Reticulum License, examined in What The Reticulum License Is, draws the same line, at the level of the law. Its AI condition does not forbid the use of machine-learning technology to assist development - no reading of the license could support that, and no one associated with this project has ever claimed so. What the condition forbids is the appropriation of the work into the machine itself: The ingestion of the commons as raw material for synthetic replacement, and the concealed fabrication of “prior work” that would bury the real thing.

-

As also noted in Copyright & The Two Legal Layers, plain old copyright law already protects against the case where a process copies the reference implementation into a machine, produces a transformed derivative, and then claims “Copyright, [name of the process operator]”, with no human able to account for the creative decisions in the result. That is simple, petty theft, as it always has been.

-
-
-

The General Principle

-

In these regards, the position of this project can be codensed to something relatively concise:

-
    -
  • Machine assistance in human learning, development, and creation is accepted and useful. -It is not a shameful secret; but it is a tool that needs knowledge, training, understanding, -and great care to use. It should not make anything easier or more fun, it should make the -results better, more carefully considered, and arrive at higher levels of synthesis, not -produce endless streams of code churn.

  • -
  • The line is crossed when the human stops being the agent of the work. When any output is -accepted that the human cannot explain, produced at a scale the human cannot review, or filed under a record -where the human has arranged to conceal the fact.

  • -
  • The line is crossed, when a human claims authorship of work they cannot -answer for or has usurped. Credit follows the ability to answer. It has always worked that way, and -machines do not change the rule - they only make it cheaper and more tempting to break.

  • -
  • The people who depend on infrastructure have the right to know which side of the line -the code they run was made on. That knowledge is the only verification most of -them will ever be able to perform.

  • -
-

Use assistance, by all means, and learn, but don’t substitute. And never sign what you cannot answer for.

-

Somewhere, within the general shape of the conceptual framework outlined in this section, or realistically, a more refined variant of it, I do believe there is the potential of the beginning of a constructive outcome to the immense challenge we are faced with as a species in regards to these problems.

-

At the same time, I reserve my right to revert that position to Butlerian Jihad without prior notice. We are on a knife’s edge, and you know it just as well as I do.

-
-
- -
-

Evaluating a Reticulum Implementation

-
-

The Intention Of This Guide

-

Very few user of network infrastructure can verify the software they run. This is true of the reference implementation as well, and it is precisely why it was engineered the way it was: the entire Reticulum protocol implementation - the transport logic, the cryptographic interfaces, the routing - is less than 15,000 lines of code, including its extensive comments and detailed logging. The reference is designed to be readable by a human in a reasonable sitting. That is a deliberate decision, and it is the standard against which every other implementation should be measured: An implementation that asks for trust while being structurally unreadable is asking for the absurd.

-

Because most users cannot verify the code directly, the evaluation of the project is the evaluation of the software. The framework below exists so that this evaluation can be performed in minutes, by anyone, without expertise - and so that each new “implementation” does not require the kind of full investigation that would otherwise be if not impossible, prohibitevly expensive.

-
-
-

Two Questions That Resolve Most Cases

-

Other considerations in this guide is further corroboration. These two questions allow you to decide the case quickly in most instances, and they should be asked first.

-

Question 1: Does this speak the protocol, or was it just generated from the reference?

-

The copyright and license realities, described in What The Reticulum License Is, is the legal foundation of this distinction. The protocol is public domain: A genuine clean-room implementation, reasoned out and written by humans, is free and welcome, and owes nothing to any license. But an implementation that was directly generated from the reference - reproduced by a copy process, whether manually or through a machine or software - is a derivative of the licensed work, and is subject to both the license’s conditions, and to the ordinary copyright law examined in Copyright & The Two Legal Layers. A primarily machine-generated copy without substantial human creative input is not merely unlicensed; it is a copyright violation that would exist under any license at all.

-

Ask: Does the project say where its protocol knowledge came from? Does it credit the reference implementation, or claim “ground-up” status while its own history shows continuous ingestion of the reference’s source? Does its license carry the original copyright notice? The answers tell you, within minutes, which of two very different kinds of project you are dealing with.

-

Question 2: Who, or what, is the agent of the work?

-

This distinction, established in Assistance Versus Machine Substitution, is the trust foundation of everything else. Is there a human who decided what to build, judged what was correct, rejected what was wrong, understands what was shipped - and can answer for it, line by line and decision by decision, without machine assistance? Or is the machine the agent, with the human’s role reduced to the name, the account, and the signature?

-
-

This test has a sharper form: would the project be meaningfully different if someone else had pressed the button? If the answer is no - if any person could have been the signature and nothing would change - then the “author” is not the agent of the work. They are its facade, and the project is, in the most literal sense, worthless and unowned.

-
-

A project with no owner is a project no one can answer for, and infrastructure with no one to answer for it has no place carrying other people’s traffic.

-
-
-

Other Signals To Look For

-

Once the first two questions are answered, the following signals determine the verdict. None is alone decisive; together they form a pattern that is difficult to fake in the directions that matter.

-
-

Provenance and attribution

-
    -
  • Credit to the reference. Does the project name Reticulum’s reference implementation -in its README, its website, its licenses, and the code? Does it present itself as a port -or as a “ground-up” origin story? Absence of credit, in a field where every implementation -owes its existence to the reference, is not an oversight; it is a decision.

  • -
  • License compliance. For a derivative, the license conditions apply: the copyright -notice and permission notice in all copies and substantial portions, the harm condition, -the AI-training condition. A project whose license file claims a fresh copyright in the -operator’s name, over work that was generated from the reference, has failed the minimum -bar of honesty before any code is read.

  • -
  • Disclosed provenance. Is the use of machine assistance stated openly, or does the -project maintain machinery to conceal it - commit hooks that refuse attribution of -generation, contribution policies that declare machine output the human’s own, scrubbers -for personal traces, fictitious contributor identities? The first is the mark of a human -using a tool. The second is the mark of a record being manufactured.

  • -
  • The author’s history. Is there a prior record - projects, contributions, a body of -work that a person can look at and reasonably assess? A sudden, fully-formed “decade of experience” -appearing from nowhere, attached to a project of unprecedented scope, is a story that -contradicts itself.

  • -
-
-
-

Agency and answerability

-
    -
  • Who answers the questions? When technical questions are asked, does a human answer -with substance - going deeper, explaining trade-offs, admitting uncertainty - or do -answers deflect to process (“the CI verifies it”, “the proofs prove it”, “the audit -covered it”)? Are the answers themself machine generated? Deflection to process is -the substitute’s answer pattern: It names the machinery instead of the reasoning.

  • -
  • Velocity commensurate with authorship. Is the output volume compatible with human -authorship, review, and judgment? An implementation ten (or 42!) times the size of the reference, -produced in a fraction of the time by a single person, is not evidence of brilliance; it -is evidence of unchecked generation. Churn is part of the same signal: A project that -deletes half of everything it adds is not building; it is looping.

  • -
  • Signs of development, growth and learning. Do reversals exist? Contested decisions? -“We tried X and it was wrong, so we did Y”? Humans change their minds, admit mistakes, and -leave the wear of judgment in their history. Pure generation shows “the polish of its -absence”: uniform register, endless renaming, and errors discovered by the generator’s own -later runs, recorded as if they were planned milestones.

  • -
  • Standing behind failures. When something is wrong, does the human say “I chose this, -here is what I got wrong, here is the fix”? Or do corrections arrive as process language, -with nobody accepting that a decision was even made? A project whose history contains no -first-person acceptance of error contains no author.

  • -
-
-
-

Engineering reality

-
    -
  • Honest benchmarks, or none. If performance is claimed, is the baseline real - stock, -current, interpreted RNS, on the modern interface, at equal policy, with methodology -published in advance and reproducible by anyone?

  • -
  • Interop with reality. Is compatibility verified against the current reference -implementation - on real hardware, including the radio and serial interfaces this -protocol family lives on - in both directions, over long transfers, uptimes and edge cases?

  • -
  • The readability standard. Can a competent human read the core of this implementation -in a reasonable sitting, the way the 15,000-line reference can be read? Was human -readability a stated design goal, or an afterthought? A project that cannot be inspected -is a project asking for trust it has not earned and will never attend to.

  • -
  • Nonstandard inventions. Does the project “improve” on the protocol with its own -modes - new channel behaviour on shared spectrum, new rendezvous mechanisms, new crypto, -“post-quantum” replacements, “turbo” variants? Inventions that change behaviour on a -shared network are not features; they are forks of the airwaves, imposed on every other -node that shares the medium. A project that invents on top of a protocol it has not yet -correctly implemented is not advanced; it is dangerous to its neighbours.

  • -
  • The public record. The commit history is public. Read it.

  • -
-
-
-

Network citizenship

-
    -
  • Honest presence. Does the project identify itself honestly on the network, or does -it masquerade as something it is not?

  • -
  • Coexistence. Does it respect the shared medium and network conventions? A node that -is a bad neighbour on the spectrum is a hazard regardless of how well its code works in isolation.

  • -
  • Community participation. Does the author participate in the community as a member - -answering questions, contributing, learning - or only appear to advertise? The second is -not participation; it is exploiting the community as a broadcasting target.

  • -
-
-
-

Monetary and influence signals

-
    -
  • Money under the claim. Donations, funding, or commercial offers premised on claims -like “89× faster” or “formally proven” convert false marketing into a different category -of problem. When money changes hands on the strength of fabricated measurements, the -stakes are no longer theoretical.

  • -
  • Tokens and “investment” vehicles. Any implementation that surrounds itself with a -crypto token, a “coin”, or an investment pitch should be treated as what it is: A project -whose primary purpose is to earn the controller a profit, not open networking.

  • -
  • Marketing-first ordering. A polished website on day three, a twelve-language marketing -presence, a “benchmarks” page and a “get started” funnel existing before the protocol works -is a massive red flag. Real builders ship, then talk. The inverse order is the signature of -a project whose goal is attention.

  • -
-
-
-
-

The Conformal Surface

-

One warning before the synthesis. When you evaluate any project, assume - as a working hypothesis - that its surface is an investment. The marketing, the design, the badge collection, the “proof records”, the fluent and confident prose: This is exactly where substitution invests, for the simple reason that it is where the audience looks first. Fluency is the minimal conformal surface of understanding, and the surface is not evidence of anything except that the author wanted to look convincing. If you find yourself impressed by the surface before you have asked Question 1 and Question 2, you are doing the evaluation in the order the substitute operator hoped you would.

-
-
-

Possible Verdicts

-

The framework proposed here can yield three different verdicts:

-
    -
  1. Meets the standard. A named human who can answer for the work and demonstrably does; -provenance disclosed; attribution given; honest benchmarks; interop -demonstrated against current, real RNS on real media; readable code; a maintenance -history that shows growth and learning. This standard is not high. It is what the -reference implementation has met, in public, for over a decade, and it is what any -project that wants other people’s traffic should be willing to meet in turn.

  2. -
  3. Experimental, but honest. A project that clearly labels itself as in-progress and -experimental (“do not use this yet”, in plain words, as the first thing a user sees) -discloses its provenance, credits the reference, and is being developed in the open by -humans who answer questions. Such projects should be encouraged. They are how the -ecosystem grows, new ideas are tested, and the framework above is designed to make an -honest experimental project easy to recognize, not hard.

  4. -
  5. Does not meet the standard. Unanswerable authorship, concealed provenance, fabricated -measurements, invented protocol behaviour, absent interop, marketing ahead of substance. -Such projects should be treated as unproven regardless of their surface, and as hazards -if they invite users onto shared networks. The community owes them exactly the -skepticism they have earned, and not a moment of engagement beyond that; the network’s -time is better spent elsewhere.

  6. -
-
-
-

These Standards Are Reasonable

-

I’ll close this guide by stating what these standards are not: They are neither a demand for perfection, nor a ban on machine assistance. Assistance is useful, when it is used to arrive at higher levels of synthesis than unaided work. It is not a requirement that an implementation be a clone of the reference; neither is it a requirement that it invents everything from scratch or aspires to be a “clean-room” implementation. Good implementations of the protocol are very welcome, however they come, but working on one requires serious work, considerable skill, and deep competence.

-

The reference implementation has met this standard for years, and its users know what that looks like.

-
-
-
-

The Movement Of A Dozen

-
-

Presentation, Elongated

-

Whenever Reticulum defends itself, its license, or its community - or simply continues to exist - a familiar chorus appears. The register is always similar: concern. Concern about the direction of the project. Concern about its license. Concern about its governance. Concern about its “bullying” of “good-faith contributors”. Concern about the future of the ecosystem at the hands of a single maintainer. The chorus speaks in the plural, presents itself as a body of independent voices, and insists, with some frequency, that it is… not alone.

-

The implicit claim under examination in this section is that there exists a movement of concerned individuals, spread across the ecosystem, all worried about Reticulum, and therefore (for some reason) deserving of a place at the table of decisions.

-

A resolution of this claim requires one step: Count them.

-
-
-

Headcount

-

This “movement” does have a headcount. It is not in the hundreds, and it is not in the dozens. It is a small number of people who appear, in various combinations and under various names, in the following roles:

-
    -
  • Creators of the machine-generated “Reticulum implementations” examined in this chapter.

  • -
  • Creators of articles praising those implementations and lamenting the state of the -reference, its moral shortcomings, its unavailability or lack of something it already has.

  • -
  • Commenters “defending” the implementations when they are evaluated reasonably and critically, -most often by attempting simply to derail the discussion.

  • -
  • Critics of the Reticulum License on “open source” grounds.

  • -
  • “Concerned community members” objecting to the maintainer’s conduct.

  • -
  • Campaigners on online platforms; and

  • -
  • In some cases, a single individual performing several of these roles at once, under -different names, in the same week.

  • -
-

The overlap is well documented at this point, across time and different platforms. The same phrasing, the same arguments, the same links, the same reactive reflexes, the same handful of accounts appearing in the same threads at the same moments. When the sockpuppets are unmasked (and they frequently unmask themselves) the cast does not grow, but rapidly shrinks.

-

This is the most interesting single fact about the “movement”: It isn’t one. It is a synthetic cast, held together mostly by LLM-glue, reddit handles and hysteria. A cast is not a constituency. It performs; it does not represent. The difference matters quite a bit, because a great deal of this “movement’s” imagined power comes from the assumption that the plural form is actual evidence of plurality. If that sentence seemed opaque, notice how many times a statement or article uses “we” instead of “I”, and it will be less so.

-
-
-

Recycled Wine, Fermenting

-

The cast is not only small, it is also, by any evidentiary standard, forgetful. Every major claim this “movement” advances has been advanced before, answered in public, with evidence, and often years ago - and is then advanced again, unchanged and completely ignoring any rationale or evidence presented previously (to the same people), as if the answer had never been written.

-
    -
  • The license argument. Reticulum is “not really open source” because its license is not -approved by a particular orthodoxy. This is answered (now in a new and updated version, for the -fifth time) in full in Open Source Means Open Source. It was also -answered in public, at length, at the time of the license change - including the -arithmetic of who is actually restricted by which license. It is repeated ad infinitum anyway. -Quite a conversation starter, apparently.

  • -
  • The distribution argument. Reticulum is “not properly distributed” because it is not -packaged as a .deb or on some favoured catalogue. Reality: It is available on practically every -platform with a single command, packaged by the community in a wide range of channels, and -capable of updating itself over its own network. The argument has been answered; it is repeated.

  • -
  • The “stepped back” narrative. When the primary mirror of the project moved away from -a public internet platform, it was spun as the founder leaving for good, or now taking on dictatorial -powers, or abandoning his creation, or any variation or nonsensical combination thereof. -Those spins always center around justifying that a vaccuum now needs to be filled. -There was no such vaccuum. The narrative is repeated in various forms whenever convenient.

  • -
  • The enclosure narrative. The license is reframed as a “power grab” or an “enclosure” -of the commons - as if a license whose protocol is public domain and whose conditions -restrict only harm and appropriation were a form of enclosure. The most recent (and mostly incoherent) -expression of this narrative is, once again, fluent, polite, and factually hollow. The -bizarre arguments supporting the “article” include: Defunct and abandoned forks presented -as living, community forks bearing the very Reticulum License mislabeled as AGPL license vehicles -(created by “concerned users”), companion app forks correctly retaining the Creative Commons -licensing used as further evidence of the same and completely abandoned projects propped up -as being “the ecosystem”. Another case of LLM generated nonsense that the “author” didn’t bother checking. -The intent is clear here, simply produce more mass of the bullshit, so it can be further -referenced, regurgitated and linked to.

  • -
  • The “bullying” narrative. Whenever the community declines to treat manufactured -controversies as legitimate, or calmly documents the falsity of claims, or (Lord forbid) -moves a forum post for violating clearly stated forum rules, -the response is the same: The documentation is “bullying”, the community is “excluding” people, the -maintainer is “unprofessional”. In the most recent example, those accusations were made by someone who, in the -same week, publically called for the Reticulum maintainer’s “removal from the equation”, forged a moderator’s -username, attempted to drag unrelated individuals into the campaign, and presented a new -round of outright falsehoods. Contrary to popular belief, the accusation of incivility -is not the last resort of the wronged; it is the standard reflex of the exposed.

  • -
-

None of the “arguments” engages the answers given repeatedly, and extensively. That is the point, and the strategy. The arguments are not produced for the purpose of being engaged; they are produced for the purpose of being repeated - the same wine, poured into new bottles, on a cycle. Each cycle is short, cheap to produce, and (thanks to freely available fluent text generation) now nearly effortless. The intended audience is not the community, which has heard it all before. The intended audience is the newcomer, who has not.

-
-
-

The Method of Madness

-

The method behind the pattern is consistent and simple enough to be described precisely:

-

Bullshit asymmetry

-

Producing a fluent, plausibly-sourced smear is now the work of minutes. Refuting it to the standard this manual demands is the work of days, and the refuted smear is replaced by the next one within the week. This asymmetry isn’t just a fine point of the campaign; it is the campaign. The goal is exhaustion - the slow conversion of the community’s time and attention from building to defending.

-

Weaponized concern

-

The register of the campaign is almost always the register of solicitude: “we are worried about the ecosystem”, “we just want what is best for the community”, “this is about the future of the project”. The concern is directed, without exception, at the things the campaign wants changed, and the change it wants is always the same: The replacement of the reference’s authority with its own. Concern is the costume; capture is the choreography. It is the oldest form of cheap power grab in the book.

-

Event seizure

-

Any event - a license change, a platform move, a forum moderation decision, a subreddit closure, a security notice, even the most banal messages from maintainers or known contributors - are seized, stripped of their actual context, and re-narrated as evidence of the same pre-written story. The subreddit that was closed after its moderator vanished and ceased moderating (according to prior agreement with said moderator) becomes “the maintainer shutting down dissent”. Moving away from a highly centralized online platform becomes “the founder retreating”. The license becomes “the enclosure”. The pattern is so reliable that the story could be pre-written by a lightly mechanized quill; only the date changes. The actual real-world context of these events is documented, in public, in each case; the re-narration proceeds regardless.

-

The legitimacy circuit

-

The campaign invests heavily in the places where legitimacy is minted: Conference talks, blog platforms, social threads, and forums - presented as if these appearances constituted some sort of ecosystem-wide groundswell rather than the same dozen people moving through the same circuit. The LLM-generated articles cite the LLM-generated ports; the ports are praised in the articles; the managers of the ports spread the articles, and echo them on the forums and internet platforms; the conference presentations cite the forums. Random bypassers with the need for a cause watch the conference presentations and take on the holy duty of Internet Soldier. The circuit is closed, self-referential, and (to a reader encountering it fresh) convincing. It is also, entirely, a loop of bullshit.

-

The prior-work minefield

-

The machine-generated implementations have a consequence beyond attention: They flood the space with fake “prior work”. The effect, as the license discussion predicted in the beginning of 2025, is the creation of a minefield of sloppy repositories that can later be matched against any genuine project, and attempt to claim prior art, or even worse: Dissuade actual, honest developers or builders from taking up a real project they might have succeeded with, because they now believe it has already been done. Just as importantly, it also buries real efforts under machine-generated noise. The machine-ports are not failed attempts to contribute. They are (disregarding intentions, but effectively) the campaign’s long game, deployed as inventory.

-
-
-

A Persistent, But Fragile Triad

-

This chapter as a whole has documented three fronts: The legal-ideological attack on the license (Open Source Means Open Source), the substitution and usurpation pattern in machine-generated implementations (Assistance Versus Machine Substitution), and the takeover narratives examined in this section. They are three fronts of the same operation, run by the same cast.

-
    -
  • The license attack and other urgency-engineered narratives are the cover: They supply -the ideological justification for treating the reference as illegitimate, and needing replacement.

  • -
  • The ports are the inventory: they supply the “replacements” that the narrative claims -the ecosystem needs.

  • -
  • The takeover narrative is the objective function: the repeated, persistent, escalating claim that -the reference should be replaced, and that the speakers themself are the natural heirs.

  • -
-

Each front depends on the others, and all three depend on a single, fragile assumption: That their audience cannot count. Restore the count, and the charade collapses into what it is:

-

A small group of people, obsessively orbiting a technology they did not build, synthetically producing the -appearance of a constituency they do not have, in service of a takeover they cannot justify.

-
-
-

What The Pattern Is Not

-

It is important, to also say what this pattern is not, exactly because the campaign depends on being mistaken for such things.

-

It is not criticism

-

The Reticulum community has always had lively, rigorous criticism - of decisions, of priorities, of direction - and it has always engaged that criticism on the merits, and changed course and development priorities many times. The license discussions at the time of the change were extensive, public, and answered in full, including with the critics themselves. Core protocol decisions were always discussed and evaluated in the community. The complex coordination and execution of several protocol upgrades between a loosely organised group of developers, node operators and users have succeeded surprisingly fluidly. All of these are success stories that the “critics” always ignore. Real criticism engages the content and accepts evidence. The campaign engages nothing.

-

It is not a community

-

Communities build things, maintain them, and have skin in the game; long-term users, operators of nodes, authors of real applications, people whose traffic flows through the network every day. A community has a stake. The “movement” only has an agenda.

-

It is not a debate

-

A debate has parties who evaluate arguments and evidence, and as a function of that, the debate moves forward, perpetually. The campaign’s arguments, as shown above, are not designed to survive contact with answers, but to be repeated past them. You cannot debate a loop. You can decline to perform for it, though.

-
-
-

Intent & Consequence

-

While it would provide for interesting speculation, we do not need to know what the individuals in the cast intend, and this document will not speculate about their inner lives. People may believe their own narratives entirely, or partly, or not at all; the pattern is the same in any case, and the pattern is what matters here, since it is the exact thing that has been so insidiously destructive and resource wasting for the actual Reticulum project and community, ever since Reticulum was more widely discovered, popularized, and became a prime target for all sorts of grift known to man.

-

The consequences are very real:

-
    -
  • Time. The community’s scarcest resource is the attention of skilled people who actually build -and maintain the network. The campaign converts that attention into reactive defence, on a cycle -with no end.

  • -
  • Harm. Hastily machine-generated implementations, marketed as drop-in replacements and carrying -traffic onto shared networks, mislead users and can damage the network and its neighbours. -We, the actual reticulum community, all rely on these networks every day, and it is my belief -that we need to defend them. The fabricated measurements and laundered authorship examined in this chapter are not -editorial embellishments; they are the mechanism by which real users are induced to run -unverified, and often destructive software.

  • -
  • Erosion of the currency of trust. Every manufactured scandal, every fluently polite smear, every -fabricated “movement” debases the currency of trust and degrades it for everyone; -including, most of all, the honest projects that come after. This is deep harm, and -it is the primary reason this chapter exists.

  • -
-

Whatever the intent - profit, control, attention, or a simple, obsessive inability to stop - the mechanism is the same, and the response should be calibrated to the mechanism as a whole, not to the individual person acting as the current avatar for it.

-
-
-

Recognition & Response

-

For anyone who encounters the pattern fresh, here is a proposal on how to recognise it, and what to do about it.

-

Recognition: The pattern is recognisable in minutes, once the signs are known:

-
    -
  • Claims that have been answered before, repeated as if never answered.

  • -
  • The register of solemn concern applied, without exception, in one direction.

  • -
  • A severe allergic reaction to answering any questions posed.

  • -
  • A cast that is small, overlapping, and frequently anonymous, with the plural form doing the -work of plurality.

  • -
  • Sockpuppet theater: The same person or footsoldiers thereof appearing as several “supporters” -(and occasionally slipping up mid-performance).

  • -
  • Articles, ports, and forum threads that cite and reference each other in a closed loop.

  • -
  • The immediate, reflexive turn to “bullying”, “exclusion” and “toxicity” whenever a claim is refuted.

  • -
  • A complete absence of engagement with the substance of the answers given, and a complete -absence of stake in the network itself.

  • -
-

Response: A constructive response is one that is proportionate, and does not cost the -community its resources, sanity and dignity:

-
    -
  • Engage the content once, on its merits (if any), with evidence, in public, and then refer to the -existing records. The answers already exist; they do not need to be re-derived for every new -bottle of piss-sour burgundy served from a man behind the curtains. I will do my best to keep this -chapter up to date with such answers and records, for easy reference, but other great resources -exist.

  • -
  • Do not become part of the theater performance. The “movement” feeds on engagement in any form; -attention is its fuel, and outrage is as good as endorsement for them, if not better.

  • -
  • Do the headcount. When the audience is reminded that a “movement” is a dozen people -recycling answered falsehoods, its projected power dissipates. The power was never real; -only the audience’s own assumption of scale can make the illusion effective. A polished-looking -“ground up” implementation might look like the work of many people, but is most likely -just the result of an LLM looping on a laptop for a couple of months (this is weird, but it is -the reality we now live in). Four different users suddenly singing in unison may look like the -representatives of a broader sentiment, but is most likely the same person hiding behind the same keyboard.

  • -
  • Then, spend the saved time on the networks: On building real infrastructure, putting up nodes -and antennas, on real development, real maintenance, real coexistence, and on the people -who actually use Reticulum. The things that we can only do together, as humans. That is -what the fake “movement” cannot do, no matter how fluent and smooth its productions become.

  • -
-
-
-
-

Network Health & Coexistence

-
-

Substance Of The Networks

-

Before examining health, we should consider what is being protected. The networks are not a repository. They are not a website, a social presence, or a set of marketing pages. They are a habitat: A living commons of communication, carried by, from a catious estimate, an intermesh now consisting of over a hundred thousand physical devices, embedded in a plethora of applications and packages, transporting terabytes of traffic every single day - real messages, real coordination, real people - across radios, serial lines, phones, laptops, and backbone servers, in cities, places and ways that no other network reaches.

-

The protocol belongs to humanity; no one owns it. The network belongs to everyone on it, and its membership is defined by nothing other than participation: A node on a mast, a phone in a pocket, a server routing for its neighbourhood, a developer building on the commons. The network has no gatekeepers because it needs none; it is held together by the same physics and mathematics everywhere, and by a shared expectation of how members behave. That expectation - unwritten, unenforced by any central authority, and just as important as any code - is the subject of this section. The Zen of Reticulum calls the software a habitat. A habitat is healthy or unhealthy as a whole, and the health of this one is decided, every day, by the people who live in it.

-
-
-

Health Is A Property Of The Whole

-

A network is not made healthy by the excellence of any single part. It is made healthy by the behaviour of every part on the shared medium, and how they interact. And the shared medium is the point. When your node transmits, it transmits into the same spectrum, the same line, the same time, the same mathematical address space, the same bandwidth that its neighbours depend on.

-

The network cannot be certified by any single project’s claims. A node that is a bad neighbour, that invents its own channel behaviour on shared spectrum, that announces without discipline, that spawns tens of thousands of links to scrape the network, that floods and retries without mercy, is a hazard to every node that shares its medium, regardless of how well its “creator” imagines its own code to work in isolation.

-

On a shared medium, “my software works” is not a complete sentence. The complete sentence is “my software works and coexists”. A network’s health deteriorates with its worst-behaved member, and every member has a responsibility to not be that member. There is no final, technical solution for this; the solutions are human.

-
-
-

A History Of What Works

-

Let’s recall, because it is the ground truth this entire thing rests on, that the coexistence model has worked - is working, right now, at scale. Reticulum’s protocol decisions have been discussed and evaluated in the community, openly, for years. Protocol upgrades have been designed, coordinated, and executed between a loosely organised group of independent developers, node operators, and users - and have succeeded, repeatedly, without any corporate board, without any central command, and without the collapse that the skeptics predict every second week (while screaming for GOVERNANCE). The reference implementation has been developed in the open for over a decade, by a maintainer who answers for it, fixes bugs or issues within hours of their discovery, and is set down in code deliberately kept small enough (the entire protocol implementation under 15,000 lines, including its comments) for a human to read, verify, and challenge.

-

This is the empirical baseline that every narrative examined in A Movement Of A Dozen conveniently ignores, in order to justify:

-
    -
  • The claim that the project is a one-man dictatorship

  • -
  • The claim that the community is excluded

  • -
  • The claim that the networks need saving from their own founders

  • -
-

The reality, documented and reproducible, is that a loose community of humans coordinates complex technical evolution without the apparatus the narratives insist is missing. The health of the networks is not a promise for the future; it is a demonstrated property of the past and present, under the leadership and community model that brought it into existence and navigated it through difficult decisions and situations over a decade. My job is to keep it that way, and you can be damned certain that I take it seriously.

-
-
-

The Role Of The Individual

-

The networks are built by physical acts and human coordination. You cannot prompt an antenna into existence. No model can string up a dipole, or stand in the rain aligning a radio, or coordinate with a neighbour three valleys away about who covers which repeater frequency, or sit down with a newcomer and explain why their first node didn’t announce. These are the acts that make a network organic - present in the physical world, resilient because it is real, valuable because it is human. The machines and technology we use are tools aimed at a well-defined, openly described goal. But the networks themself are composed of intentional human actions, and every node operator is infrastructure for other humans.

-

This has a corollary for the individual: You do not need permission, and you do not need to be an expert, to participate. Run a node. Help a neighbour. Ask questions in the community, and answer the questions you can. Build something small at first. The network grows by these acts, and they are the most meaningful ones - the things that can only be done together, as humans, in the physical world. The parasitic “movement” that haunts the peripheries of this ecosystem cannot do any of this. It cannot build a node because it does not exist, in any real connected and human sense; only people can, and people are precisely what a self-recurrent loop lacks.

-
-
-

The Place Of New Implementations

-

The future of the network includes more implementations - and they are welcome. This chapter is not an argument against implementing Reticulum. The protocol is public domain, by design, and the network is made stronger by genuine diversity: More good implementations, honestly made, mean more resilience, more experimentation, more people who understand the protocol deeply because they have built it themselves. That is the goal: For people to understand and own the technology they rely on. Not to spew out 600,000 line rust ports that fills no meaningful gap or purpose.

-

What the ecosystem needs, from every implementation, is the ordinary floor that Evaluating A Reticulum Implementation establishes, and that the reference has met for over a decade: A human who made it and answers for it; provenance disclosed; attribution given; benchmarks honest or absent; interop demonstrated against the real network; code a human can read. And it needs the one thing that costs nothing and changes everything: Honest labeling.

-

An experimental implementation that says, plainly, as the first thing a user sees: “This is in-progress, experimental, do not depend on it yet, but help if you can”, that is a gift to the ecosystem. It contributes, it invites participation, and it protects users.

-

The opposite, the polished “drop-in replacement”, does damage far beyond its own users: It dissuades the honest developers who might have built the real thing, because the polished slop persuades them the work is already done. That dissuasion is one of the quietest and most expensive harms in this entire affair, and the antidote is honesty at every project’s door.

-
-
-

Expanding The Commons

-

Coexistence, reduced to its essentials, is a short formula; the duties of membership in a network no one owns:

-
    -
  • Be honest. Say what you are, what you built, how you built it, and what you used to -build it. Label experimental work as experimental. Credit the work you build on, including -the reference implementation, in the licenses and in the prose. Concealment is not a -defence; it is a very clear diagnosis marker.

  • -
  • Be owned. Have a human who made the work and can answer for it. Use assistance, don’t -substitute. Never sign what you cannot answer for. The chain of accountability is the -community’s only verification at scale, and every link in it is a person, not a facade.

  • -
  • Be a good neighbour. Respect the shared mediums. Do not invent new behaviour in our -shared space and call it progress. The network belongs to its other members as much as -to you.

  • -
  • Verify against the network. Compatibility is a property demonstrated with actual nodes, -on actual media, against the actual current protocol, over a period of years, not 48 hours. -And not a claim pinned to an old snapshot over loopback.

  • -
  • Participate as a member. Ask, answer, help, build. Advertising at the community is not -the same as being part of it, and it is not the first thing you should do.

  • -
-

None of this is a high bar. It is the bar the reference implementation has met, in public, for over a decade, and the bar the ecosystem has adopted organically itself, successfully, through every protocol upgrade it has ever coordinated, and everything running on Reticulum today, that people actually use. It is not a new requirement; it is the existing, working culture of the networks, now written down so that newcomers can find it and so that those who would exploit us cannot claim they never saw it.

-
-
-

Defence Of The Commons

-

The networks need defending - not because they are under severe physical attacks (yet), but because attention, talent and resources is being stolen from them, trust is being mined against them, and its newcomers are being harvested by the machinery documented in this chapter. Defence, for a commons, has specific shapes, and it is not what the theater would like it to be.

-
    -
  • Defence is documentation: Records kept public, of what was claimed, what was -answered, and what turned out true.

  • -
  • Defence is a headcount: Calm, unexcited reminders that a “movement” might just be a dozen -people having a tantrum.

  • -
  • Defence is honesty in action: The visible, ordinary practice of the good behaviour -by the people who actually run the networks.

  • -
  • And defence is building: Every node raised, every page of real documentation, every -honest implementation, every new user helped into the community with the knowledge to know -what they are doing is more durable than any rebuttal, because it adds to the thing being -defended.

  • -
-
-

The theatre cannot keep up with a network.

-

It can only orbit it, while it recycles its own sour piss.

-
-

A lot of us rely on these networks every day. They carry our messages, our coordination, our friendships, and in some places, our safety. That is exactly why they are a target:

-

They are valuable, free, and very hard to shut down. The response is not to become aggressive. It is to be more of what the network already is: Real, open, owned, and shared.

-

Mark

-
-
-
-

Addendum A: The Prns Case

-
-

Purpose & Scope

-

This section provides a concrete example, which future evaluations can use as a template. Its purpose is not to be the last word on this particular subject, but to be an example of how the ideas in this chapter can be applied, and to make the next such examination easier.

-

The subject of this case study is Prns, a project published at the repository github.com/KenAKAFrosty/Prns, presented as a “ground-up implementation of Reticulum, written in Rust”, marketed as faster, safer, and more strictly tested than the reference, widely promoted with the claim of “up to 89× the throughput” of the reference implementation, and an “author” actively engaged in the tactics described in A Movement Of A Dozen on online platforms. The record described below is the project’s own, and sourced from its GitHub repository, analyzed locally.

-
-
-

The Project In Brief

-
    -
  • Time span. 3,374 commits between 2026-05-26 and 2026-09-12: approximately 110 days, -at a mean rate of ~31 commits per day. Monthly: 132 / 1,137 / 1,097 / 805 / 203.

  • -
  • Authorship. 3,252 of 3,374 commits (96.4%) are authored by a single account. A -second identity, “Prns Tests tests@example.test” - a fictitious identity on a reserved -domain - contributed 35 commits, every one of them website and marketing content.

  • -
  • Size. 3,519 tracked files; 1,983 Rust files; 636,226 lines of Rust; approximately -22.7 MB of Rust source; 69.5 MB of total repository content.

  • -
  • Scope claimed. A daemon (“drop-in replacement” for the reference daemon), embedded -firmware for three microcontroller families (including six ESP32-S3 board variants and -nRF52840), SDKs and bindings for nine languages, a browser-based node and web flasher, a -twelve-language website, a benchmark apparatus with its own publication and “proof” -pipeline, and a validation hub with oracles, fuzzing, mutation testing, and formal-proof -claims.

  • -
  • Marketing claims. “Ground-up implementation of Reticulum”; “up to 89× the -throughput”; “48× smaller peak-memory footprint”; “33× the energy efficiency”; -“Measured, not just claimed”; “Enforced, then audited”; “byte-for-byte wire parity with -the reference implementation”; “works with Sideband, NomadNet, MeshChat, etc.”; -“no_std (no alloc required either)”.

  • -
  • Status at the time of writing. Version 0.3.7; published version history begins at -0.3.0. The repository continues to receive commits.

  • -
-
-
-

Question 1: Is this a clean implementation, or was it directly generated from the reference?

-

The first of the two deciding questions from Evaluating A Reticulum Implementation is settled by the project’s own record. An actual clean-room implementation, reasoned out and written by humans, would owe nothing to any license. The data available shows clearly that Prns is not that.

-
    -
  • The founding state. The first commit, “Scaffold Personal Reticulum suite”, contains -the scaffolding of not one but two ports - personal-rns and personal-lxmf, the latter -being the reference’s companion messaging protocol - together with a daemon scaffold and -a 233-line founding document, docs/build-ethos.md. That document declares the project’s -goal as a “performance-focused drop-in-replacement for rnsd” and its governing principle -as: “Port the contract, not the implementation.” It further states directly to the -machine-loop, that “You owe the reference fidelity at exactly two boundaries: -The wire… The behavior. … Between those two boundaries, the model is yours”. -At this point, the very first commit, effectively the whole of the implementation design -and any relevant decisions are handed over to the LLM.

  • -
  • Continuous ingestion from the reference. 506 commits reference RNS or Reticulum -source behaviour; the word “parity” appears in 49 commit messages. Commits cite the reference -implementation’s own source locations (for example Transport.py:1367) when correcting -their own divergences and mistakes.

  • -
  • Byte-level fidelity as an explicit goal. Within 48 hours of the first commit, the -record shows a “crypto adapter - vetted primitives, byte-exact vs RNS 1.3.1”, and a wire -implementation matching the reference’s packet header layout. The project’s own -validation apparatus decodes wire vectors using the reference implementation’s own -packet parser, and the interop suite consists of real, stock-RNS peer nodes - that is, -the reference source itself, used as the ground truth against which this LLM output is -checked, and committed as accepted and verified after 48 hours of the project’s recorded start.

  • -
  • The marketing mismatch. The same record that documents all of the above describes the -project, publicly, as “a ground-up implementation of Reticulum” - while declining, on its -website, to link to the reference implementation at all: the entire tree mentions the -reference’s author in six files, and the website in none.

  • -
-

Conclusion: the project’s own history and commit record establishes that it was generated directly from the reference implementation’s source code, not reasoned out from the public-domain protocol. It is a derivative of licensed work within the meaning of the Reticulum License and general copyright law. The “ground-up” claim, which carries its entire legal and marketing posture, is disproven by the project’s own history.

-
-
-

Question 2: Who, or what, is the agent of the work?

-

The second deciding question is answered by the application of Assistance Versus Machine Substitution criteria to the project.

-

Scope that no human could have authored

-

636,226 lines of Rust in 110 days is approximately 5,800 lines per day, every day, including weekends - with 149 commits recorded between midnight and 06:00, and Saturday the single busiest weekday. No human review or decision capacity is commensurable with this volume; the review capacity and the output volume are structurally incommensurable, which is the definition of unauthored scope.

-

Uniform generation

-

The record shows no sign of human development, growth, or learning: No contested decisions, no style drift. Instead: endless renaming (in the first 48 hours: State → EngineState → TickInput → TickOutput), uniform fluent register in every commit, and the exact same, synthetic voice in code, documentation, and website copy.

-

Discovery by regeneration

-

Corrections arrived by the machine’s own subsequent runs, recorded as planned milestones. In early July 2026, a series of “core review” commits documented, in the project’s own words:

-
    -
  • announce-ID history “was unbounded per-slot growth” (54cc8d601);

  • -
  • interface announce limiting could “leak a sustained flood” (9d27af75c);

  • -
  • a “phantom-airtime leak” where frames “used to vanish silently” (4ed75ffdd);

  • -
  • Group-packet deduplication “which RNS does not do” - a behaviour the reference never had, -implemented and later removed (edd8e72be);

  • -
  • “both unwrap_or(&[]) arms were unreachable dead defaults” (74bea355e);

  • -
  • a benchmark crate “hasn’t compiled since” a prior rename, undetected for weeks because it -was outside the CI’s workspace (86240b027).

  • -
-

Earlier still: the record shows a “>1 MiB RNS-to-Prns interop stall” closed three weeks in -(cc4d524a3), and a known send-path hang shipped with the note “typed fast-fail a -candidate for later” (a010e6613).

-

Laundered provenance

-

The repository contains a committed commit-message hook (.githooks/commit-msg) that blocks any commit containing attribution for machine generation (“generated with claude/codex/copilot/cursor/gemini”, “co-authored-by”), with the explanation “The committer owns the commit”. The contributing guide is explicit: “AI tools … are welcome … what you submit is yours”, and describes the contributing population as “both human and automated contributors”. The fictitious “Prns Tests” identity completes the picture: A manufactured, unknowable contributor, used for marketing content.

-

Authorship by assignment

-

The human whose name is on the work cannot answer for it, because the choices were never made by a human, and only the general shape and constraints of the outward presentation was. Nothing in the project’s history shows an individual explaining a design decision, accepting a mistake in the first person, or going deeper under technical questioning; the record shows process language and marketing instead. Per the earlier mentioned test: any other person could have pressed the same button, and nothing would be different. The project is effectively unowned, and void of value.

-

By the criteria of Assistance Versus Machine Substitution, this is substitution. The machine was the agent of the work; the “author” was a facade.

-
-
-

The performance claims

-

The project’s central marketing claim - “up to 89× the throughput” of the reference - is also its most completely documented failure, because the project published the evidence of its own measurement. The apparatus, the results, and the problems are all on the record.

-

The claim

-

The README states “up to 89× the throughput”; the on-network marketing page repeats “up to 89× the throughput, 48× smaller peak-memory footprint, and 33× the energy efficiency of stock RNS 1.4.2”; the website’s front page carries the tagline “Measured, not just claimed”.

-

The apparatus

-

The reference is loaded through benchmarks/reference/compiled_reference.py, which installs pyximport and then asserts two things: that RNS.compiled == True, and that at least one RNS module was loaded from a native extension. The project’s own published “proof” records show exactly what those checks captured, on every platform:

-
aarch64-apple-darwin: .../RNS.cpython-313-darwin.so,  Cython 3.2.8, Python 3.13.13
-x86_64-unknown-linux-gnu: .../RNS.cpython-313-x86_64-linux-gnu.so,  gcc 11.4.0
-x86_64-pc-windows-msvc: ~/.prns-oc/lib.win-amd64-cpython-313/RNS.cp313-win_amd64.pyd
-
-
-

In every case the recorded native module is the top-level package only - RNS.so, nothing below it. Nothing named RNS.Transport, RNS.Destination, RNS.Packet, RNS.Link, or RNS.Resource ever appears, because nothing below the top level was ever compiled. Nobody bothered to check or verify anything here.

-

The machine output was simply taken for truth, and a full marketing strategy was spun and produced on it. “RNS 1.4.2 (compiled)” - the label used in every table, chart, and marketing page - is not a mode of RNS that exists, but the hallucinations of a machine, which the humans created a marketing strategy on. In reality, it is the old CRNS development shim, whose top-level-only, debug-oriented “compilation” is slower than plain interpreted RNS, and which was defunct for over a year before these benchmarks were published.

-

Interface choices

-

The harness writes the reference’s configuration itself (benchmarks/reference/participant_node.py): TCPClientInterface for the initiator, TCPServerInterface for the responder and both relay sides, with UDPInterface as the abstract wire. The reference was never given its modern BackboneInterface on any platform, forcing the slowest possible interrfaces. The interface selection is never mentioned in any methodology text.

-

The policy

-

The published default-policy tables record the asymmetry directly: Prns at 500 Mbps / 128 KiB TCP policy against the reference at 10 Mbps / 8 KiB - slyly footnoted as “preserves each implementation’s normal TCP policy” (in the case of RNS, a conservative default policy that was selected to prioritize coexistence on slower, shared links, requiring the user to dial it up to full speed if they knew what they were doing). Raw transport “throughput” ratios of 20.65× (Linux) and 38.84× (Windows) are therefore mostly policy, not engine.

-

The cross-host evidence

-

The published results show the same scenario producing wildly different ratios by host, and the variance is entirely in the reference artifact:

-
- - - - - - - - - - - - - - - - - - - - - - - - - -

Host

Prns, single-packet

“Reference”, single-packet

Ratio

macOS (Apple M4)

37.4k/s

420/s

89.09×

Linux (i7-1260P)

25.3k/s

3.5k/s

7.19×

Windows (Ryzen 5 5600X)

30.8k/s

3.9k/s

8.00×

-
-

Prns is consistent (25–37k/s). The reference artifact collapses to different depths on different hosts, and the single worst collapse - 420 packets per second on macOS - is the cell selected as the global headline. The “89×” is not a measure of Prns; it is a measure of how badly the broken baseline happened to fail on one host.

-

For reality calibration: Stock, interpreted RNS 1.5.4 on an ordinary mid-range laptop, single core, sustains roughly 30,000 single-packet deliveries per second (including ephemeral-key decryption), ~175,000–280,000 packets per second in transit relay, and on the order of 10 Gbps at 16 KiB payloads. The project’s “compiled” artifact measured 420–3,900 packets per second for its single-packet scenario: An order of magnitude or more below stock interpreted RNS. The comparison being marketed was not “Rust vs Python”; it was “an implementation running under optimal conditions versus a mislabeled, broken shim on a slow legacy interface at a severe policy disadvantage”.

-

The equalized rows

-

The project’s own “1 Gbps policy” rows, in which both sides are explicitly configured identically, publish ratios of roughly 2.4–8.4× across all hosts and scenarios - still against the broken baseline, but nowhere near 89×, and consistent with the ordinary, uncontroversial observation that a native implementation tends to outperform an interpreted one in raw throughput. That point needed no fudging to make, and is banal.

-

Interestingly, though, it seems that Prns is really only marginally faster than a Python implementation, running in interpreted mode, on a single CPU core.

-

Assessment

-

The front page says “Measured, not just claimed.” The record shows the opposite ordering: claimed, and arranged to look measured. No methodology section anywhere states what “compiled” means, that it is a deprecated development shim, that it builds debug targets, or which interface the reference was given; the one statement of the policy asymmetry appears in a footnote of the results tables themselves. If they did not need to fudge it, they would have published the method clearly.

-
-
-

Overall Evaluation

-

Evaluated against the conditions of Evaluating A Reticulum Implementation:

-
    -
  • Provenance and attribution. Fails: The fake “ground-up” origin story; zero reference links -on the website; a single well-hidden link in the README; a license file reading -Copyright (c) 2026 The Prns Authors over work directly generated from the licensed reference; -no original copyright or permission notice anywhere.

  • -
  • Agency and answerability. Fails on every criterion of Assistance Versus Machine Substitution: -Unauthored scope, uniform generation, discovery by regeneration, laundered provenance, -authorship by assignment.

  • -
  • Engineering. Fails: The benchmark apparatus above; interop demonstrated only -against a pinned, outdated reference version over loopback while the marketing claims current -parity, interop failures with stock RNS; vendored prebuilt binaries and a republished fork -of an upstream crate (nrf-softdevice 0.1.0-prns.1) with no verifiable provenance; -committed WASM bundles.

  • -
  • Network behaviour. Fails: Invented spectrum behaviour and compatibility claims -about third-party applications of the reference ecosystem (Sideband, NomadNet, MeshChat) -that are not established by any evidence.

  • -
  • Monetary and influence signals. The marketing apparatus - website before protocol, -day-three twelve-language marketing, false benchmark headlines, “audited” and “formal proof” -claims - the conformal surface described in Evaluating A Reticulum Implementation: -is where this project invested.

  • -
-
-
-

Licensing Analysis

-

Applying the two legal layers established in Copyright & The Two Legal Layers:

-

Layer A - copyright infringement (license-independent)

-

The record shows a direct copy roundtrip: the licensed reference implementation fed into a machine, transformed, at a scale and cadence no human could review, with a license file asserting a fresh copyright in the operator’s name and no acknowledgment of the work the project was generated from. No substantial, new human creative input is demonstrated anywhere in the record - the founding document explicitly frames the design as an instructed port of the reference’s contract. A primarily machine-generated copy of a copyrighted work, not accompanied by demonstrable substantial human creative input, is a derivative work over which the copier holds no copyright, and a reproduction of which without the original author’s permission infringes the original author’s copyright. This holds under any license, MIT included. The operator’s copyright claim over the result is, on this evidence, not merely unsubstantiated; it is the absence of one, directly asserted in the project’s own writing.

-

Layer B - license conditions (Reticulum License specific)

-

Modern Reticulum - the current wire format, the AES-256-based link encryption, ratchets, the cryptographic machinery as it exists today - has been published under the Reticulum License since April 15, 2025, effective from release 0.9.4 and every release since. As a derivative of that work, the license’s conditions attach: The copyright and permission notice must be included in all copies and substantial portions; the AI-training condition must be honoured; the harm condition must be honoured. The record shows none of the notice requirements met, and a production process that consists of the licensed work being ingested into a machine and regurgitated, at industrial scale and cadence.

-

The cascading consequence

-

Prns is distributed claiming dual MIT/Apache-2.0 licensing. It is, however, a direct derivative of Reticulum-licensed work, and the claimed grant is therefore void. Every downstream project that builds on Prns believing it is MIT/Apache-licensed is building on a grant that does not exist. The users of Prns, and all software developers using it, have in effect, been placed under conditions they were never told about. The “ground-up” claim is not merely false; it is the sole load-bearing element of an entire licensing posture, and it fails spectacularly.

-
-
-

Consequences

-

The consequences of this pattern are examined in the following sections: The harm to users and to the network in A Movement Of A Dozen, and the human dimension in Network Health & Coexistence. The contribution of this addendum is narrower and intended as an instructive example: This is what a substitution project looks like, in full, on its own record. It is a reference example for Evaluating A Reticulum Implementation, and it demonstrates, in one case, most of the elements this chapter describes.

-
-
-

Verdict

-

Applying the three verdicts of Evaluating A Reticulum Implementation, this project does not meet the standard. It is not “experimental but honest” - it is not labeled as experimental anywhere; the first thing users saw was a polished marketing presence promising a finished, tested, drop-in replacement. It is a project whose central claims are fabricated (the performance apparatus), whose provenance is laundered (the authorship machinery), whose foundation is misrepresented (the “ground-up” story), whose core semantics were learned late and by regeneration, and whose operator cannot possibly answer for any of it. It is also a blatant copyright violation of the reference implementation.

-

The proposed conclusion, for users and for the network: Treat the Prns project as hostile regardless of its polished surface, and as a hazard insofar as it invites users onto shared networks. Nothing in this section is a judgment about the people involved; it is a reading of the results produced, their history and how they came to be, which in the regrettable lack of any human authorship is the only thing that can be expected to be held to account.

-
-
-

Dating

-

This examination reflects the publicly available material as of September 2026; the repository in question continued to receive commits at the time of writing, so readers should treat citations to commits and files as point-in-time evidence. A forensics snapshot is publicly available on GitHub.

-
-
-
- -
-
- -
- -
-
- - - - - - - \ No newline at end of file diff --git a/docs/manual/distributed.html b/docs/manual/distributed.html index 7b5a77cd..bd540f5a 100644 --- a/docs/manual/distributed.html +++ b/docs/manual/distributed.html @@ -7,7 +7,7 @@ - Distributed Development - Reticulum Network Stack 1.5.5 documentation + Distributed Development - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
@@ -204,7 +204,7 @@
- Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation
- + diff --git a/docs/manual/examples.html b/docs/manual/examples.html index 82f25c9d..55446482 100644 --- a/docs/manual/examples.html +++ b/docs/manual/examples.html @@ -7,7 +7,7 @@ - Code Examples - Reticulum Network Stack 1.5.5 documentation + Code Examples - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
@@ -204,7 +204,7 @@
- Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
  • - + diff --git a/docs/manual/forhumans.html b/docs/manual/forhumans.html index fe05cea3..86136c8f 100644 --- a/docs/manual/forhumans.html +++ b/docs/manual/forhumans.html @@ -7,7 +7,7 @@ - An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.5.5 documentation + An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
  • - + diff --git a/docs/manual/genindex.html b/docs/manual/genindex.html index 460d9066..7ca0cb25 100644 --- a/docs/manual/genindex.html +++ b/docs/manual/genindex.html @@ -5,7 +5,7 @@ - Index - Reticulum Network Stack 1.5.5 documentation + Index - Reticulum Network Stack 1.4.2 documentation @@ -178,7 +178,7 @@ - + diff --git a/docs/manual/gettingstartedfast.html b/docs/manual/gettingstartedfast.html index 9e92c660..681a3e01 100644 --- a/docs/manual/gettingstartedfast.html +++ b/docs/manual/gettingstartedfast.html @@ -7,7 +7,7 @@ - Getting Started Fast - Reticulum Network Stack 1.5.5 documentation + Getting Started Fast - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
  • - + diff --git a/docs/manual/git.html b/docs/manual/git.html index f41d7a76..922abc77 100644 --- a/docs/manual/git.html +++ b/docs/manual/git.html @@ -7,7 +7,7 @@ - Git Over Reticulum - Reticulum Network Stack 1.5.5 documentation + Git Over Reticulum - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -267,7 +266,6 @@

      Git Over Reticulum

      This chapter of the manual serves as the technical reference for the distributed software development and project collaboration tools included in RNS. For a conceptual overview, see the Distributed Development chapter.

      A set of utilities for distributed collaborative software development and publishing are included in RNS.

      -_images/rngit.webp

      The system consists of two parts: The rngit node that hosts repositories, and the git-remote-rns helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: rns://DESTINATION_HASH/group/repo.

      If you set a branch to track a Reticulum remote as the default upstream, you can simply use git as you normally would; all commands work transparently and as expected.

      @@ -358,10 +356,6 @@ Repository public/myrepo created

    This creates a bare Git repository at the specified path. You must have create permission for the target group. When a repository is created, the creator automatically receives adm (admin) permissions on the repository through an auto-generated .allowed file.

    -
    -

    Tip

    -

    If you want to use a different primary branch name than master, you can change it inside the bare git repository directory with git symbolic-ref HEAD refs/heads/name_of_other_branch.

    -

    All Command-Line Options (rngit create)

    usage: rngit create [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
                         [-i PATH] [-v] [-q] [--version]
    @@ -822,7 +816,6 @@ def hello_world():
     
  • commit.mu - Individual commit detail page

  • refs.mu - Branches and tags listing

  • stats.mu - Statistics page

  • -
  • no_ident.mu - Displayed to non-identified initiators if blocked

  • Templates can include the following variables:

      @@ -1601,7 +1594,7 @@ done
    -
    + diff --git a/docs/manual/hardware.html b/docs/manual/hardware.html index 18f03933..d3865a1b 100644 --- a/docs/manual/hardware.html +++ b/docs/manual/hardware.html @@ -7,7 +7,7 @@ - Communications Hardware - Reticulum Network Stack 1.5.5 documentation + Communications Hardware - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -677,7 +676,7 @@ can be used with Reticulum. This includes virtual software modems such as
    - + diff --git a/docs/manual/index.html b/docs/manual/index.html index f648ff43..de45c104 100644 --- a/docs/manual/index.html +++ b/docs/manual/index.html @@ -7,7 +7,7 @@ - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -373,49 +372,28 @@ to participate in the development of Reticulum itself.

  • Programs Using Reticulum
  • @@ -830,7 +720,7 @@ to participate in the development of Reticulum itself.

    - + diff --git a/docs/manual/interfaces.html b/docs/manual/interfaces.html index 329437d8..43905620 100644 --- a/docs/manual/interfaces.html +++ b/docs/manual/interfaces.html @@ -7,7 +7,7 @@ - Configuring Interfaces - Reticulum Network Stack 1.5.5 documentation + Configuring Interfaces - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -1152,8 +1151,6 @@ relevant regulation for your location, and to make decisions accordingly.

      discovery_name

      A human-readable name for the interface. This name will be displayed to users on remote systems when they list discovered interfaces. If not specified, the interface name (the section header) will be used.

      -
      discovery_lxmf_address

      An optional LXMF address for contacting the operator of this interface. The address will be displayed to users on remote systems when they view details for discovered interfaces. Useful for getting in touch with other operators to interconnect networks with and coordinating network coverage.

      -
      announce_interval

      The interval in minutes between successive discovery announces for this interface. Default is 360 minutes (6 hours). For stable, long-running infrastructure, higher intervals (12 to 22 hours) are usually sufficient and reduce network load. Minimum allowed value is 5 minutes (but expect to have your announces throttled if using intervals below one hour).

      @@ -1771,44 +1768,6 @@ per second on a given interface.
    under the [reticulum] section of the configuration file, or on a per- interface basis under the relevant interface configuration section.

    -
    -

    Tuning Ingress Queues

    -

    Reticulum uses separate, prioritized queues for processing inbound packets. The default four queues are drained in the following order:

    -
    -
      -
    1. Data and general traffic

    2. -
    3. Announces

    4. -
    5. Path requests

    6. -
    7. Ingress-limited traffic

    8. -
    -
    -

    You can tune the queue sizes from the defaults, for example if you’re running a setup where you want to simply drop most traffic from ingress-limited interfaces. The queue sizes are configured in the [reticulum] section of the config

    -
    -
      -
    • -
      The qlen_in_data option sets the maximum size of the data -queue. Defaults to 1024.
      -
      -
    • -
    • -
      The qlen_in_announce option sets the maximum size of the announce -queue. Defaults to 128.
      -
      -
    • -
    • -
      The qlen_in_pr option sets the maximum size of the path request -queue. Defaults to 128.
      -
      -
    • -
    • -
      The qlen_in_il option sets the maximum size of the ingress limiter -queue. Defaults to 8.
      -
      -
    • -
    -
    -

    If an inbound packet arrives and its target queue is full, the packet is dropped. Reticulum will always completely drain higher-priority queues before starting to drain a lower-priority one. As an example, the ingress limiter queue will only start draining once all other queues are empty.

    -
    @@ -1897,7 +1856,6 @@ queue. Defaults to
  • Announce Rate Control
  • New Destination Rate Limiting
  • Path Request Burst Control
  • -
  • Tuning Ingress Queues
  • @@ -1909,7 +1867,7 @@ queue. Defaults to - + diff --git a/docs/manual/license.html b/docs/manual/license.html index 4250bdc6..140823e7 100644 --- a/docs/manual/license.html +++ b/docs/manual/license.html @@ -3,11 +3,11 @@ - + - Reticulum License - Reticulum Network Stack 1.5.5 documentation + Reticulum License - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
  • + diff --git a/docs/manual/networks.html b/docs/manual/networks.html index f2adbfcd..f1526295 100644 --- a/docs/manual/networks.html +++ b/docs/manual/networks.html @@ -7,7 +7,7 @@ - Building Networks - Reticulum Network Stack 1.5.5 documentation + Building Networks - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -665,7 +664,7 @@ differently than a mobile device roaming between radio cells.

    - + diff --git a/docs/manual/objects.inv b/docs/manual/objects.inv index ed2b71d9..3970fcd4 100644 Binary files a/docs/manual/objects.inv and b/docs/manual/objects.inv differ diff --git a/docs/manual/reference.html b/docs/manual/reference.html index 2c0c93fe..a76c60c4 100644 --- a/docs/manual/reference.html +++ b/docs/manual/reference.html @@ -3,11 +3,11 @@ - + - API Reference - Reticulum Network Stack 1.5.5 documentation + API Reference - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -336,59 +335,10 @@ alterations should be made on a per-interface basis instead.

      to successfully establish links. Currently 5 bits per second.

      -
      -
      -DEFAULT_PER_HOP_TIMEOUT = 6
      -

      The default per-hop timeout value used in various timeout calculations.

      -
      -
      static get_instance()
      -

      Returns the currently running Reticulum instance.

      -
      - -
      -
      -get_first_hop_timeout(destination)
      -

      Returns a best-effort estimate of a reasonable minimum -first-hop timeout value for a given destination hash. -If a path is known, this calculation takes the next-hop -interface’s bitrate into account. If no path is currently -known, returns DEFAULT_PER_HOP_TIMEOUT.

      -
      -
      Parameters:
      -

      destination – A destination hash, as bytes.

      -
      -
      Returns:
      -

      First-hop timeout, in seconds.

      -
      -
      -
      - -
      -
      -get_lowest_interface_bitrate()
      -

      Returns the bitrate of the slowest currently online -interface, or None if no online interface bitrate

      -
      -
      Returns:
      -

      Lowest online interface bitrate in bits per second, or None.

      -
      -
      -
      - -
      -
      -get_medium_path_timeout()
      -

      Returns an estimate of a reasonable minimum path request timeout covering -a full round trip for an MTU on the slowest currently online interface -plus per hop grace

      -
      -
      Returns:
      -

      Timeout in seconds or 0 if it’s unknown.

      -
      -
      +

      Return the currently running Reticulum instance

      @@ -1503,7 +1453,7 @@ statistics, you will be able to retrieve stats such as RSSI, SNRget_expected_rate()
      Returns:
      -

      The expected in-flight data rate of an established link.

      +

      The packet expected in-flight data rate of an established link.

      @@ -2341,14 +2291,14 @@ will announce it.

      -
    + diff --git a/docs/manual/search.html b/docs/manual/search.html index 5284cfa1..0c501680 100644 --- a/docs/manual/search.html +++ b/docs/manual/search.html @@ -8,7 +8,7 @@ -Search - Reticulum Network Stack 1.5.5 documentation +Search - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
  • - + diff --git a/docs/manual/searchindex.js b/docs/manual/searchindex.js index cd5ed683..8414a251 100644 --- a/docs/manual/searchindex.js +++ b/docs/manual/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"A Carrier-Grade Fallacy":[[17,"a-carrier-grade-fallacy"]],"A History Of What Works":[[0,"a-history-of-what-works"]],"A Persistent, But Fragile Triad":[[0,"a-persistent-but-fragile-triad"]],"API Reference":[[11,null]],"ARM64":[[4,"arm64"]],"AX.25 KISS Interface":[[8,"ax-25-kiss-interface"]],"Accessing Repository Pages":[[5,"accessing-repository-pages"]],"Addendum A: The Prns Case":[[0,"addendum-a-the-prns-case"]],"Adding Radio Interfaces":[[4,"adding-radio-interfaces"]],"Agency and answerability":[[0,"agency-and-answerability"]],"An Explanation of Reticulum for Human Beings":[[3,null]],"Anchor In The Flow":[[17,"anchor-in-the-flow"]],"Android":[[4,"android"]],"Announce":[[2,"announce"]],"Announce Propagation Rules":[[14,"announce-propagation-rules"]],"Announce Rate Control":[[8,"announce-rate-control"]],"Announcing Presence":[[17,"announcing-presence"]],"Artifact-Centered Workflows":[[1,"artifact-centered-workflows"]],"Assistance Versus Machine-Substitution":[[0,"assistance-versus-machine-substitution"]],"Author Identity Binding":[[5,"author-identity-binding"]],"Auto Interface":[[8,"auto-interface"]],"Automated Blocking":[[8,"automated-blocking"]],"Automated List Sourcing":[[15,"automated-list-sourcing"]],"Automatic Mirror Synchronization":[[5,"automatic-mirror-synchronization"]],"Backbone Interface":[[8,"backbone-interface"]],"Blackhole Management":[[15,"blackhole-management"]],"Bootstrapping Connectivity":[[4,"bootstrapping-connectivity"]],"Brandolini\u2019s Reference":[[0,null]],"Broadcast":[[2,"broadcast"]],"Browsing & Publishing":[[12,"browsing-publishing"]],"Buffer":[[2,"buffer"]],"Build Personal Infrastructure":[[4,"build-personal-infrastructure"]],"Building Networks":[[10,null]],"Channel":[[2,"channel"]],"Chat":[[12,"chat"]],"Code Examples":[[2,null]],"Columba":[[12,"id8"]],"Combining Hardware Types":[[6,"combining-hardware-types"]],"Command-Line Interaction":[[5,"command-line-interaction"]],"Commit Signing":[[5,"commit-signing"]],"Common Interface Options":[[8,"common-interface-options"]],"Communications Hardware":[[6,null]],"Composable Primitives":[[1,"composable-primitives"]],"Concepts & Overview":[[10,"concepts-overview"]],"Conceptual Overview":[[14,"conceptual-overview"]],"Conclusion":[[0,"conclusion"]],"Configuration":[[5,"configuration"],[5,"id1"]],"Configuration & Data":[[15,"configuration-data"]],"Configuration Example":[[5,"configuration-example"]],"Configuration Methods":[[5,"configuration-methods"]],"Configuring Interfaces":[[8,null]],"Connect to the Distributed Backbone":[[4,"connect-to-the-distributed-backbone"]],"Connecting Remotes":[[8,"connecting-remotes"]],"Connecting Reticulum Instances Over the Internet":[[4,"connecting-reticulum-instances-over-the-internet"]],"Consequences":[[0,"consequences"]],"Contributing to the Global Ret":[[4,"contributing-to-the-global-ret"]],"Copyright & The Two Legal Layers":[[0,"copyright-the-two-legal-layers"]],"Cost Of A Byte":[[17,"cost-of-a-byte"]],"Creating Empty Repositories":[[5,"creating-empty-repositories"]],"Creating RNodes":[[6,"creating-rnodes"]],"Creating Signed Releases":[[5,"creating-signed-releases"]],"Creating a Network With Reticulum":[[4,"creating-a-network-with-reticulum"]],"Creating and Using Custom Interfaces":[[4,"creating-and-using-custom-interfaces"]],"Creating and Using a Network Identity":[[14,"creating-and-using-a-network-identity"]],"Creator Permissions":[[5,"creator-permissions"]],"Cryptographic Attribution":[[5,"cryptographic-attribution"]],"Cryptographic Primitives":[[14,"cryptographic-primitives"]],"Current Status":[[16,"current-status"]],"Current Usage":[[14,"current-usage"]],"Custom Interfaces":[[2,"custom-interfaces"],[8,"custom-interfaces"]],"Customizing Templates":[[5,"customizing-templates"]],"Dating":[[0,"dating"]],"Death To The Address":[[17,"death-to-the-address"]],"Debian Bookworm":[[4,"debian-bookworm"]],"Decentralization Or Uncentralizability?":[[17,"decentralization-or-uncentralizability"]],"Defence Of The Commons":[[0,"defence-of-the-commons"]],"Design Patterns For Post-IP Systems":[[17,"design-patterns-for-post-ip-systems"]],"Destination Naming":[[14,"destination-naming"]],"Destinations":[[14,"destinations"]],"Destinations, Not Addresses":[[10,"destinations-not-addresses"]],"Develop a Program with Reticulum":[[4,"develop-a-program-with-reticulum"]],"Device Flashers":[[12,"device-flashers"]],"Disclosed Assistance Or Laundered Generation":[[0,"disclosed-assistance-or-laundered-generation"]],"Discoverable Interfaces":[[8,"discoverable-interfaces"]],"Discovering Interfaces":[[15,"discovering-interfaces"]],"Discovery Parameters":[[8,"discovery-parameters"]],"Distributed Development":[[1,null]],"Distribution Without Intermediaries":[[1,"distribution-without-intermediaries"]],"Donations":[[13,"donations"]],"Echo":[[2,"echo"]],"Emergent Patterns":[[17,"emergent-patterns"]],"Enabling Discovery":[[8,"enabling-discovery"]],"Enabling the Git Page Node":[[5,"enabling-the-git-page-node"]],"Encryption Is Not A Feature":[[17,"encryption-is-not-a-feature"]],"Engineering reality":[[0,"engineering-reality"]],"Ethernet-based Hardware":[[6,"ethernet-based-hardware"]],"Ethics Of The Tool":[[17,"ethics-of-the-tool"]],"Evaluating a Reticulum Implementation":[[0,"evaluating-a-reticulum-implementation"]],"Example Configuration":[[8,"example-configuration"]],"Expanding The Commons":[[0,"expanding-the-commons"]],"Fabric Of The Independent":[[17,"fabric-of-the-independent"]],"Fallacy Of The Cloud":[[17,"fallacy-of-the-cloud"]],"False Dichotomy":[[0,"false-dichotomy"]],"Filetransfer":[[2,"filetransfer"]],"Finding Your Way":[[4,"finding-your-way"]],"Fixed Serial Port Names":[[15,"fixed-serial-port-names"]],"Flow & Time":[[17,"flow-time"]],"Forking Repositories":[[5,"forking-repositories"]],"Formatting & Syntax Highlighting":[[5,"formatting-syntax-highlighting"]],"Future Implications":[[14,"future-implications"]],"Getting Further":[[14,"getting-further"]],"Getting Started Fast":[[4,null]],"Git Configuration Parameters":[[5,"git-configuration-parameters"]],"Git Over Reticulum":[[5,null],[12,"git-over-reticulum"]],"Goals":[[14,"goals"]],"Headcount":[[0,"headcount"]],"Health Is A Property Of The Whole":[[0,"health-is-a-property-of-the-whole"]],"Heltec LoRa32 v2.0":[[6,"heltec-lora32-v2-0"]],"Heltec LoRa32 v3.0":[[6,"heltec-lora32-v3-0"]],"Heltec LoRa32 v4.0":[[6,"heltec-lora32-v4-0"]],"Heltec T114":[[6,"heltec-t114"]],"Heterogeneous Connectivity":[[10,"heterogeneous-connectivity"]],"Hostile Environments":[[17,"hostile-environments"]],"Hosting Public Entrypoints":[[4,"hosting-public-entrypoints"]],"I. The Foundational Statute: Translation Is Copying":[[0,"i-the-foundational-statute-translation-is-copying"]],"I2P Interface":[[8,"i2p-interface"]],"II. Ideas Are Free, Expressions Protected":[[0,"ii-ideas-are-free-expressions-protected"]],"III. Machine Output Has No Author":[[0,"iii-machine-output-has-no-author"]],"IV. License Conditions Bound the Grant":[[0,"iv-license-conditions-bound-the-grant"]],"Identification":[[2,"example-identify"]],"Identities":[[14,"understanding-identities"]],"Identity & Destination Aliases":[[5,"identity-destination-aliases"]],"Identity and Nomadism":[[17,"identity-and-nomadism"]],"Improving System Configuration":[[15,"improving-system-configuration"]],"Included Utility Programs":[[15,"included-utility-programs"]],"Indices and Tables":[[7,"indices-and-tables"]],"Infrastructure Matters":[[0,"infrastructure-matters"]],"Installation":[[6,"installation"]],"Intent & Consequence":[[0,"intent-consequence"]],"Interface Access Codes":[[14,"interface-access-codes"]],"Interface Modes":[[8,"interface-modes"],[8,"interfaces-modes"]],"Interface Modules & Connectivity Resources":[[12,"interface-modules-connectivity-resources"]],"Interface Types and Devices":[[16,"interface-types-and-devices"]],"Introduction & Basic Functionality":[[14,"introduction-basic-functionality"]],"Introductory Considerations":[[10,"introductory-considerations"]],"KISS Interface":[[8,"kiss-interface"]],"LXMF":[[12,"lxmf"]],"LXMFy":[[12,"lxmfy"]],"LXST":[[12,"id30"]],"LXST Phone":[[12,"lxst-phone"]],"Liberation From Limits":[[17,"liberation-from-limits"]],"Licensing Analysis":[[0,"licensing-analysis"]],"LilyGO LoRa32 v1.0":[[6,"lilygo-lora32-v1-0"]],"LilyGO LoRa32 v2.0":[[6,"lilygo-lora32-v2-0"]],"LilyGO LoRa32 v2.1":[[6,"lilygo-lora32-v2-1"]],"LilyGO T-Beam":[[6,"lilygo-t-beam"]],"LilyGO T-Beam Supreme":[[6,"lilygo-t-beam-supreme"]],"LilyGO T-Deck":[[6,"lilygo-t-deck"]],"LilyGO T-Echo":[[6,"lilygo-t-echo"]],"LilyGO T3S3":[[6,"lilygo-t3s3"]],"Link":[[2,"link"]],"Link Establishment in Detail":[[14,"link-establishment-in-detail"]],"Listeners":[[8,"listeners"]],"Local Blackhole Management":[[15,"local-blackhole-management"]],"Long Archive":[[1,"long-archive"]],"MacOS":[[4,"macos"]],"Managing Group Permissions":[[5,"managing-group-permissions"]],"Managing Repository Permissions":[[5,"managing-repository-permissions"]],"Managing Work Document Permissions":[[5,"managing-work-document-permissions"]],"Manual Synchronization":[[5,"manual-synchronization"]],"Merits Of Scarcity":[[17,"merits-of-scarcity"]],"MeshChatX":[[12,"id6"]],"Messaging & Voice":[[12,"messaging-voice"]],"Micron Parser JS":[[12,"micron-parser-js"]],"Minimal":[[2,"minimal"]],"Mirroring Repositories":[[5,"mirroring-repositories"]],"Mixing Strategies":[[4,"mixing-strategies"]],"Modem73":[[12,"modem73"]],"Monetary and influence signals":[[0,"monetary-and-influence-signals"]],"Motivation":[[14,"motivation"]],"Naming Is Power":[[17,"naming-is-power"]],"Network Health & Coexistence":[[0,"network-health-coexistence"]],"Network Health & Responsibility":[[4,"network-health-responsibility"]],"Network Identities":[[14,"network-identities"]],"Network citizenship":[[0,"network-citizenship"]],"New Destination Rate Limiting":[[8,"new-destination-rate-limiting"]],"Node Types":[[14,"node-types"]],"Nomad Network":[[12,"nomad-network"]],"Obtaining Verified Releases":[[5,"obtaining-verified-releases"]],"Offline Verification":[[5,"offline-verification"]],"On Freedom":[[0,"on-freedom"]],"On The Institutions, In Fairness":[[0,"on-the-institutions-in-fairness"]],"Open Sky":[[17,"open-sky"]],"Open Source Means Open Source":[[0,"open-source-means-open-source"]],"OpenCom XL":[[6,"opencom-xl"]],"OpenWRT":[[4,"openwrt"]],"Other Signals To Look For":[[0,"other-signals-to-look-for"]],"Overall Evaluation":[[0,"overall-evaluation"]],"Packet Prioritisation":[[14,"packet-prioritisation"]],"Packet Radio Modems":[[6,"packet-radio-modems"]],"Partyline":[[12,"partyline"]],"Path Request Burst Control":[[8,"path-request-burst-control"]],"Permission Configuration Locations":[[5,"permission-configuration-locations"]],"Permission Examples":[[5,"permission-examples"]],"Permission Hierarchy":[[5,"permission-hierarchy"]],"Permission Short Forms":[[5,"permission-short-forms"]],"Permission Types":[[5,"permission-types"]],"Permission Validation":[[5,"permission-validation"]],"Permissions":[[5,"permissions"]],"Personal Infrastructure":[[17,"personal-infrastructure"]],"Physics Of Trust":[[17,"physics-of-trust"]],"Pipe Interface":[[8,"pipe-interface"]],"Platform-Specific Install Notes":[[4,"platform-specific-install-notes"]],"Portable Existence":[[17,"portable-existence"]],"Possible Verdicts":[[0,"possible-verdicts"]],"Prerequisites":[[5,"prerequisites"]],"Presentation, Elongated":[[0,"presentation-elongated"]],"Preserving Human Agency":[[17,"preserving-human-agency"]],"Programs Using Reticulum":[[12,null]],"Proposing Work Documents":[[5,"proposing-work-documents"]],"Protocol Specifics":[[14,"protocol-specifics"]],"Protocols":[[12,"protocols"]],"Protocols Over Platforms":[[1,"protocols-over-platforms"]],"Provenance and attribution":[[0,"provenance-and-attribution"]],"Provide Feedback":[[13,"provide-feedback"]],"Public Domain Protocol":[[17,"public-domain-protocol"]],"Public Key Announcements":[[14,"public-key-announcements"]],"Publishing Blackhole Lists":[[15,"publishing-blackhole-lists"]],"Pure-Python Reticulum":[[4,"pure-python-reticulum"]],"Purpose & Scope":[[0,"purpose-scope"]],"Question 1: Is this a clean implementation, or was it directly generated from the reference?":[[0,"question-1-is-this-a-clean-implementation-or-was-it-directly-generated-from-the-reference"]],"Question 2: Who, or what, is the agent of the work?":[[0,"question-2-who-or-what-is-the-agent-of-the-work"]],"RAK4631-based Boards":[[6,"rak4631-based-boards"]],"RISC-V":[[4,"risc-v"]],"RNMon":[[12,"rnmon"]],"RNS FileSync":[[12,"rns-filesync"]],"RNS Page Node":[[12,"rns-page-node"]],"RNode":[[6,"rnode"]],"RNode LoRa Interface":[[8,"rnode-lora-interface"]],"RNode Multi Interface":[[8,"rnode-multi-interface"]],"RRC":[[12,"rrc"]],"Raspberry Pi":[[4,"raspberry-pi"]],"Reaching the Destination":[[14,"reaching-the-destination"]],"Recognition & Response":[[0,"recognition-response"]],"Recycled Wine, Fermenting":[[0,"recycled-wine-fermenting"]],"Reference Implementation":[[16,"reference-implementation"]],"Reference Setup":[[14,"reference-setup"]],"Release Management":[[5,"release-management"]],"Release Storage & Structure":[[5,"release-storage-structure"]],"Remote Management":[[15,"remote-management"]],"Remote Permission Management":[[5,"remote-permission-management"]],"Remote Shell":[[12,"remote-shell"]],"Ren Browser":[[12,"ren-browser"]],"Ren TUI":[[12,"ren-tui"]],"Repository Creation & Management":[[5,"repository-creation-management"]],"Repository Statistics":[[5,"repository-statistics"]],"Repository Structure":[[5,"repository-structure"]],"Requests & Responses":[[2,"requests-responses"]],"Resolving Dependency & Installation Issues":[[4,"resolving-dependency-installation-issues"]],"Resources":[[14,"resources"]],"Restoration":[[1,"restoration"]],"RetiBBS":[[12,"retibbs"]],"Reticulated":[[12,"reticulated"]],"Reticulum License":[[9,null]],"Reticulum Network Stack Manual":[[7,null]],"Reticulum Network Telephone":[[12,"reticulum-network-telephone"]],"Reticulum Relay Chat":[[12,"reticulum-relay-chat"]],"Reticulum Transport":[[14,"reticulum-transport"]],"Reticulum as a System Service":[[15,"reticulum-as-a-system-service"]],"Retipedia":[[12,"retipedia"]],"Roaming Nodes":[[17,"roaming-nodes"]],"Security Considerations":[[8,"security-considerations"]],"Serial Interface":[[8,"serial-interface"]],"Serial Lines & Devices":[[6,"serial-lines-devices"]],"Serving Pages Over Nomad Network":[[5,"serving-pages-over-nomad-network"]],"Sideband":[[12,"sideband"]],"Signing Commits":[[5,"signing-commits"]],"Sources and Authorities":[[0,"sources-and-authorities"]],"Sovereignty Through Infrastructure":[[1,"sovereignty-through-infrastructure"],[17,"sovereignty-through-infrastructure"]],"Standalone Reticulum Installation":[[4,"standalone-reticulum-installation"]],"Start Of The Road":[[1,"start-of-the-road"]],"State Management":[[5,"state-management"]],"Store & Forward":[[17,"store-forward"]],"Substance Of The Networks":[[0,"substance-of-the-networks"]],"Support Reticulum":[[13,null]],"Supported Boards and Devices":[[6,"supported-boards-and-devices"]],"System Administration":[[12,"system-administration"]],"Systemwide Service":[[15,"systemwide-service"]],"TCP Client Interface":[[8,"tcp-client-interface"]],"TCP Server Interface":[[8,"tcp-server-interface"]],"Table Of Contents":[[7,"table-of-contents"]],"The Ability To Disconnect":[[17,"the-ability-to-disconnect"]],"The Announce Mechanism in Detail":[[14,"the-announce-mechanism-in-detail"]],"The Bandwidth Fallacy":[[17,"the-bandwidth-fallacy"]],"The Conformal Surface":[[0,"the-conformal-surface"]],"The Consequences Of The \u201cNot Open Source\u201d Claim":[[0,"the-consequences-of-the-not-open-source-claim"]],"The Distribution Argument":[[0,"the-distribution-argument"]],"The Fluency Trap":[[0,"the-fluency-trap"]],"The General Principle":[[0,"the-general-principle"]],"The Harm Principle":[[17,"the-harm-principle"]],"The Illusion Of The Center":[[17,"the-illusion-of-the-center"]],"The Intention Of This Guide":[[0,"the-intention-of-this-guide"]],"The Interface Is The Medium":[[17,"the-interface-is-the-medium"]],"The License In This Context":[[0,"the-license-in-this-context"]],"The Line":[[0,"the-line"]],"The Method of Madness":[[0,"the-method-of-madness"]],"The Movement Of A Dozen":[[0,"the-movement-of-a-dozen"]],"The Original Architecture":[[1,"the-original-architecture"]],"The Place Of New Implementations":[[0,"the-place-of-new-implementations"]],"The Platform Interregnum":[[1,"the-platform-interregnum"]],"The Prediction, And What Happened":[[0,"the-prediction-and-what-happened"]],"The Project In Brief":[[0,"the-project-in-brief"]],"The Release Workflow":[[5,"the-release-workflow"]],"The Role Of The Individual":[[0,"the-role-of-the-individual"]],"The Work Is Finished":[[17,"the-work-is-finished"]],"The performance claims":[[0,"the-performance-claims"]],"The rncp Utility":[[15,"the-rncp-utility"]],"The rngit Utility":[[5,"the-rngit-utility"],[15,"the-rngit-utility"]],"The rnid Utility":[[15,"the-rnid-utility"]],"The rnodeconf Utility":[[15,"the-rnodeconf-utility"]],"The rnpath Utility":[[15,"the-rnpath-utility"]],"The rnprobe Utility":[[15,"the-rnprobe-utility"]],"The rnsd Utility":[[15,"the-rnsd-utility"]],"The rnsh Utility":[[15,"the-rnsh-utility"]],"The rnstatus Utility":[[15,"the-rnstatus-utility"]],"The rnx Utility":[[15,"the-rnx-utility"]],"The \u201cClean-Room\u201d Delusion":[[0,"the-clean-room-delusion"]],"These Standards Are Reasonable":[[0,"these-standards-are-reasonable"]],"Transport Nodes and Instances":[[10,"transport-nodes-and-instances"]],"Trustless Networking":[[10,"trustless-networking"]],"Try Using a Reticulum-based Program":[[4,"try-using-a-reticulum-based-program"]],"Tuning Ingress Queues":[[8,"tuning-ingress-queues"]],"Two Questions That Resolve Most Cases":[[0,"two-questions-that-resolve-most-cases"]],"UDP Interface":[[8,"udp-interface"]],"Ubuntu Lunar":[[4,"ubuntu-lunar"]],"Understanding Reticulum":[[14,null]],"Unsigned RNode v2.x":[[6,"unsigned-rnode-v2-x"]],"Usage with Reticulum":[[6,"usage-with-reticulum"]],"Userspace Service":[[15,"userspace-service"]],"Using Reticulum on Your System":[[15,null]],"Using the Included Utilities":[[4,"using-the-included-utilities"]],"Utilities & Components":[[12,"utilities-components"]],"V. The Void Grant":[[0,"v-the-void-grant"]],"VI. Fair Use Does Not Rescue This Pattern":[[0,"vi-fair-use-does-not-rescue-this-pattern"]],"Validating Commit Signatures":[[5,"validating-commit-signatures"]],"Verdict":[[0,"verdict"]],"Verified Releases":[[5,"verified-releases"]],"Voice & Telephony":[[12,"voice-telephony"]],"Void Grants: Legal Foundations of Machine-Generated Code":[[0,"void-grants-legal-foundations-of-machine-generated-code"]],"What The Argument Is Actually Doing":[[0,"what-the-argument-is-actually-doing"]],"What The Pattern Is Not":[[0,"what-the-pattern-is-not"]],"What The Reticulum License Is":[[0,"what-the-reticulum-license-is"]],"What This Means For Other Implementations":[[0,"what-this-means-for-other-implementations"]],"What assistance looks like":[[0,"what-assistance-looks-like"]],"What does Reticulum Offer?":[[16,"what-does-reticulum-offer"]],"What is Reticulum?":[[16,null]],"What substitution looks like":[[0,"what-substitution-looks-like"]],"What \u201cOpen\u201d Means Positively":[[0,"what-open-means-positively"]],"Where can Reticulum be Used?":[[16,"where-can-reticulum-be-used"]],"Who Is Restricted By Which License?":[[0,"who-is-restricted-by-which-license"]],"Why This License Exists":[[0,"why-this-license-exists"]],"WiFi-based Hardware":[[6,"wifi-based-hardware"]],"Windows":[[4,"windows"]],"Wire Format":[[14,"wire-format"]],"Work Document Permissions":[[5,"work-document-permissions"]],"Work Documents":[[5,"work-documents"]],"Working With Work Documents":[[5,"working-with-work-documents"]],"Zen of Reticulum":[[17,null]],"Zero-Trust Architectures":[[17,"zero-trust-architectures"]]},"docnames":["brandolinis","distributed","examples","forhumans","gettingstartedfast","git","hardware","index","interfaces","license","networks","reference","software","support","understanding","using","whatis","zen"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["brandolinis.rst","distributed.rst","examples.rst","forhumans.rst","gettingstartedfast.rst","git.rst","hardware.rst","index.rst","interfaces.rst","license.rst","networks.rst","reference.rst","software.rst","support.rst","understanding.rst","using.rst","whatis.rst","zen.rst"],"indexentries":{"__init__() (rns.rawchannelreader method)":[[11,"RNS.RawChannelReader.__init__",false]],"__init__() (rns.rawchannelwriter method)":[[11,"RNS.RawChannelWriter.__init__",false]],"accepts_links() (rns.destination method)":[[11,"RNS.Destination.accepts_links",false]],"add_message_handler() (rns.channel.channel method)":[[11,"RNS.Channel.Channel.add_message_handler",false]],"add_ready_callback() (rns.rawchannelreader method)":[[11,"RNS.RawChannelReader.add_ready_callback",false]],"advertise() (rns.resource method)":[[11,"RNS.Resource.advertise",false]],"announce() (rns.destination method)":[[11,"RNS.Destination.announce",false]],"announce_cap (rns.reticulum attribute)":[[11,"RNS.Reticulum.ANNOUNCE_CAP",false]],"app_and_aspects_from_name() (rns.destination static method)":[[11,"RNS.Destination.app_and_aspects_from_name",false]],"await_path() (rns.transport static method)":[[11,"RNS.Transport.await_path",false]],"blackhole_identity() (rns.transport static method)":[[11,"RNS.Transport.blackhole_identity",false]],"blackhole_sources() (rns.reticulum static method)":[[11,"RNS.Reticulum.blackhole_sources",false]],"buffer (class in rns)":[[11,"RNS.Buffer",false]],"cancel() (rns.resource method)":[[11,"RNS.Resource.cancel",false]],"channel (class in rns.channel)":[[11,"RNS.Channel.Channel",false]],"clear_default_app_data() (rns.destination method)":[[11,"RNS.Destination.clear_default_app_data",false]],"concluded() (rns.requestreceipt method)":[[11,"RNS.RequestReceipt.concluded",false]],"create_bidirectional_buffer() (rns.buffer static method)":[[11,"RNS.Buffer.create_bidirectional_buffer",false]],"create_keys() (rns.destination method)":[[11,"RNS.Destination.create_keys",false]],"create_reader() (rns.buffer static method)":[[11,"RNS.Buffer.create_reader",false]],"create_writer() (rns.buffer static method)":[[11,"RNS.Buffer.create_writer",false]],"current_ratchet_id() (rns.identity static method)":[[11,"RNS.Identity.current_ratchet_id",false]],"curve (rns.identity attribute)":[[11,"RNS.Identity.CURVE",false]],"curve (rns.link attribute)":[[11,"RNS.Link.CURVE",false]],"decrypt() (rns.destination method)":[[11,"RNS.Destination.decrypt",false]],"decrypt() (rns.identity method)":[[11,"RNS.Identity.decrypt",false]],"default_per_hop_timeout (rns.reticulum attribute)":[[11,"RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT",false]],"deregister_announce_handler() (rns.transport static method)":[[11,"RNS.Transport.deregister_announce_handler",false]],"deregister_request_handler() (rns.destination method)":[[11,"RNS.Destination.deregister_request_handler",false]],"destination (class in rns)":[[11,"RNS.Destination",false]],"discovered_interfaces() (rns.reticulum static method)":[[11,"RNS.Reticulum.discovered_interfaces",false]],"enable_ratchets() (rns.destination method)":[[11,"RNS.Destination.enable_ratchets",false]],"encrypt() (rns.destination method)":[[11,"RNS.Destination.encrypt",false]],"encrypt() (rns.identity method)":[[11,"RNS.Identity.encrypt",false]],"encrypted_mdu (rns.packet attribute)":[[11,"RNS.Packet.ENCRYPTED_MDU",false]],"enforce_ratchets() (rns.destination method)":[[11,"RNS.Destination.enforce_ratchets",false]],"establishment_timeout_per_hop (rns.link attribute)":[[11,"RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP",false]],"expand_name() (rns.destination static method)":[[11,"RNS.Destination.expand_name",false]],"from_bytes() (rns.identity static method)":[[11,"RNS.Identity.from_bytes",false]],"from_file() (rns.identity static method)":[[11,"RNS.Identity.from_file",false]],"full_hash() (rns.identity static method)":[[11,"RNS.Identity.full_hash",false]],"get_age() (rns.link method)":[[11,"RNS.Link.get_age",false]],"get_channel() (rns.link method)":[[11,"RNS.Link.get_channel",false]],"get_data_size() (rns.resource method)":[[11,"RNS.Resource.get_data_size",false]],"get_establishment_rate() (rns.link method)":[[11,"RNS.Link.get_establishment_rate",false]],"get_expected_rate() (rns.link method)":[[11,"RNS.Link.get_expected_rate",false]],"get_first_hop_timeout() (rns.reticulum method)":[[11,"RNS.Reticulum.get_first_hop_timeout",false]],"get_hash() (rns.resource method)":[[11,"RNS.Resource.get_hash",false]],"get_instance() (rns.reticulum static method)":[[11,"RNS.Reticulum.get_instance",false]],"get_lowest_interface_bitrate() (rns.reticulum method)":[[11,"RNS.Reticulum.get_lowest_interface_bitrate",false]],"get_mdu() (rns.link method)":[[11,"RNS.Link.get_mdu",false]],"get_medium_path_timeout() (rns.reticulum method)":[[11,"RNS.Reticulum.get_medium_path_timeout",false]],"get_mode() (rns.link method)":[[11,"RNS.Link.get_mode",false]],"get_mtu() (rns.link method)":[[11,"RNS.Link.get_mtu",false]],"get_parts() (rns.resource method)":[[11,"RNS.Resource.get_parts",false]],"get_private_key() (rns.destination method)":[[11,"RNS.Destination.get_private_key",false]],"get_private_key() (rns.identity method)":[[11,"RNS.Identity.get_private_key",false]],"get_progress() (rns.requestreceipt method)":[[11,"RNS.RequestReceipt.get_progress",false]],"get_progress() (rns.resource method)":[[11,"RNS.Resource.get_progress",false]],"get_public_key() (rns.identity method)":[[11,"RNS.Identity.get_public_key",false]],"get_q() (rns.link method)":[[11,"RNS.Link.get_q",false]],"get_q() (rns.packet method)":[[11,"RNS.Packet.get_q",false]],"get_random_hash() (rns.identity static method)":[[11,"RNS.Identity.get_random_hash",false]],"get_remote_identity() (rns.link method)":[[11,"RNS.Link.get_remote_identity",false]],"get_request_id() (rns.requestreceipt method)":[[11,"RNS.RequestReceipt.get_request_id",false]],"get_response() (rns.requestreceipt method)":[[11,"RNS.RequestReceipt.get_response",false]],"get_response_time() (rns.requestreceipt method)":[[11,"RNS.RequestReceipt.get_response_time",false]],"get_rssi() (rns.link method)":[[11,"RNS.Link.get_rssi",false]],"get_rssi() (rns.packet method)":[[11,"RNS.Packet.get_rssi",false]],"get_rtt() (rns.packetreceipt method)":[[11,"RNS.PacketReceipt.get_rtt",false]],"get_segments() (rns.resource method)":[[11,"RNS.Resource.get_segments",false]],"get_snr() (rns.link method)":[[11,"RNS.Link.get_snr",false]],"get_snr() (rns.packet method)":[[11,"RNS.Packet.get_snr",false]],"get_status() (rns.packetreceipt method)":[[11,"RNS.PacketReceipt.get_status",false]],"get_status() (rns.requestreceipt method)":[[11,"RNS.RequestReceipt.get_status",false]],"get_transfer_size() (rns.resource method)":[[11,"RNS.Resource.get_transfer_size",false]],"has_path() (rns.transport static method)":[[11,"RNS.Transport.has_path",false]],"hash() (rns.destination static method)":[[11,"RNS.Destination.hash",false]],"hash_from_name_and_identity() (rns.destination static method)":[[11,"RNS.Destination.hash_from_name_and_identity",false]],"hops_to() (rns.transport static method)":[[11,"RNS.Transport.hops_to",false]],"identify() (rns.link method)":[[11,"RNS.Link.identify",false]],"identity (class in rns)":[[11,"RNS.Identity",false]],"inactive_for() (rns.link method)":[[11,"RNS.Link.inactive_for",false]],"interface_discovery_sources() (rns.reticulum static method)":[[11,"RNS.Reticulum.interface_discovery_sources",false]],"is_compressed() (rns.resource method)":[[11,"RNS.Resource.is_compressed",false]],"is_ready_to_send() (rns.channel.channel method)":[[11,"RNS.Channel.Channel.is_ready_to_send",false]],"keepalive (rns.link attribute)":[[11,"RNS.Link.KEEPALIVE",false]],"keepalive_timeout_factor (rns.link attribute)":[[11,"RNS.Link.KEEPALIVE_TIMEOUT_FACTOR",false]],"keysize (rns.identity attribute)":[[11,"RNS.Identity.KEYSIZE",false]],"link (class in rns)":[[11,"RNS.Link",false]],"link_mtu_discovery (rns.reticulum attribute)":[[11,"RNS.Reticulum.LINK_MTU_DISCOVERY",false]],"link_mtu_discovery() (rns.reticulum static method)":[[11,"RNS.Reticulum.link_mtu_discovery",false]],"load_private_key() (rns.destination method)":[[11,"RNS.Destination.load_private_key",false]],"load_private_key() (rns.identity method)":[[11,"RNS.Identity.load_private_key",false]],"load_public_key() (rns.identity method)":[[11,"RNS.Identity.load_public_key",false]],"mdu (rns.channel.channel property)":[[11,"RNS.Channel.Channel.mdu",false]],"messagebase (class in rns)":[[11,"RNS.MessageBase",false]],"minimum_bitrate (rns.reticulum attribute)":[[11,"RNS.Reticulum.MINIMUM_BITRATE",false]],"msgtype (rns.messagebase attribute)":[[11,"RNS.MessageBase.MSGTYPE",false]],"mtu (rns.reticulum attribute)":[[11,"RNS.Reticulum.MTU",false]],"next_hop() (rns.transport static method)":[[11,"RNS.Transport.next_hop",false]],"next_hop_interface() (rns.transport static method)":[[11,"RNS.Transport.next_hop_interface",false]],"no_data_for() (rns.link method)":[[11,"RNS.Link.no_data_for",false]],"no_inbound_for() (rns.link method)":[[11,"RNS.Link.no_inbound_for",false]],"no_outbound_for() (rns.link method)":[[11,"RNS.Link.no_outbound_for",false]],"pack() (rns.messagebase method)":[[11,"RNS.MessageBase.pack",false]],"packet (class in rns)":[[11,"RNS.Packet",false]],"packetreceipt (class in rns)":[[11,"RNS.PacketReceipt",false]],"pathfinder_m (rns.transport attribute)":[[11,"RNS.Transport.PATHFINDER_M",false]],"plain_mdu (rns.packet attribute)":[[11,"RNS.Packet.PLAIN_MDU",false]],"pub_to_file() (rns.identity method)":[[11,"RNS.Identity.pub_to_file",false]],"publish_blackhole_enabled() (rns.reticulum static method)":[[11,"RNS.Reticulum.publish_blackhole_enabled",false]],"ratchet_count (rns.destination attribute)":[[11,"RNS.Destination.RATCHET_COUNT",false]],"ratchet_expiry (rns.identity attribute)":[[11,"RNS.Identity.RATCHET_EXPIRY",false]],"ratchet_interval (rns.destination attribute)":[[11,"RNS.Destination.RATCHET_INTERVAL",false]],"ratchetsize (rns.identity attribute)":[[11,"RNS.Identity.RATCHETSIZE",false]],"rawchannelreader (class in rns)":[[11,"RNS.RawChannelReader",false]],"rawchannelwriter (class in rns)":[[11,"RNS.RawChannelWriter",false]],"recall() (rns.identity static method)":[[11,"RNS.Identity.recall",false]],"recall_app_data() (rns.identity static method)":[[11,"RNS.Identity.recall_app_data",false]],"register_announce_handler() (rns.transport static method)":[[11,"RNS.Transport.register_announce_handler",false]],"register_message_type() (rns.channel.channel method)":[[11,"RNS.Channel.Channel.register_message_type",false]],"register_request_handler() (rns.destination method)":[[11,"RNS.Destination.register_request_handler",false]],"remote_management_enabled() (rns.reticulum static method)":[[11,"RNS.Reticulum.remote_management_enabled",false]],"remove_message_handler() (rns.channel.channel method)":[[11,"RNS.Channel.Channel.remove_message_handler",false]],"remove_ready_callback() (rns.rawchannelreader method)":[[11,"RNS.RawChannelReader.remove_ready_callback",false]],"request() (rns.link method)":[[11,"RNS.Link.request",false]],"request_path() (rns.transport static method)":[[11,"RNS.Transport.request_path",false]],"requestreceipt (class in rns)":[[11,"RNS.RequestReceipt",false]],"required_discovery_value() (rns.reticulum static method)":[[11,"RNS.Reticulum.required_discovery_value",false]],"resend() (rns.packet method)":[[11,"RNS.Packet.resend",false]],"resource (class in rns)":[[11,"RNS.Resource",false]],"reticulum (class in rns)":[[11,"RNS.Reticulum",false]],"send() (rns.channel.channel method)":[[11,"RNS.Channel.Channel.send",false]],"send() (rns.packet method)":[[11,"RNS.Packet.send",false]],"set_default_app_data() (rns.destination method)":[[11,"RNS.Destination.set_default_app_data",false]],"set_delivery_callback() (rns.packetreceipt method)":[[11,"RNS.PacketReceipt.set_delivery_callback",false]],"set_link_closed_callback() (rns.link method)":[[11,"RNS.Link.set_link_closed_callback",false]],"set_link_established_callback() (rns.destination method)":[[11,"RNS.Destination.set_link_established_callback",false]],"set_max_request_size() (rns.destination method)":[[11,"RNS.Destination.set_max_request_size",false]],"set_packet_callback() (rns.destination method)":[[11,"RNS.Destination.set_packet_callback",false]],"set_packet_callback() (rns.link method)":[[11,"RNS.Link.set_packet_callback",false]],"set_proof_requested_callback() (rns.destination method)":[[11,"RNS.Destination.set_proof_requested_callback",false]],"set_proof_strategy() (rns.destination method)":[[11,"RNS.Destination.set_proof_strategy",false]],"set_ratchet_interval() (rns.destination method)":[[11,"RNS.Destination.set_ratchet_interval",false]],"set_remote_identified_callback() (rns.link method)":[[11,"RNS.Link.set_remote_identified_callback",false]],"set_resource_callback() (rns.link method)":[[11,"RNS.Link.set_resource_callback",false]],"set_resource_concluded_callback() (rns.link method)":[[11,"RNS.Link.set_resource_concluded_callback",false]],"set_resource_started_callback() (rns.link method)":[[11,"RNS.Link.set_resource_started_callback",false]],"set_resource_strategy() (rns.link method)":[[11,"RNS.Link.set_resource_strategy",false]],"set_retained_ratchets() (rns.destination method)":[[11,"RNS.Destination.set_retained_ratchets",false]],"set_timeout() (rns.packetreceipt method)":[[11,"RNS.PacketReceipt.set_timeout",false]],"set_timeout_callback() (rns.packetreceipt method)":[[11,"RNS.PacketReceipt.set_timeout_callback",false]],"should_use_implicit_proof() (rns.reticulum static method)":[[11,"RNS.Reticulum.should_use_implicit_proof",false]],"sign() (rns.destination method)":[[11,"RNS.Destination.sign",false]],"sign() (rns.identity method)":[[11,"RNS.Identity.sign",false]],"stale_grace (rns.link attribute)":[[11,"RNS.Link.STALE_GRACE",false]],"stale_time (rns.link attribute)":[[11,"RNS.Link.STALE_TIME",false]],"teardown() (rns.link method)":[[11,"RNS.Link.teardown",false]],"to_file() (rns.identity method)":[[11,"RNS.Identity.to_file",false]],"track_phy_stats() (rns.link method)":[[11,"RNS.Link.track_phy_stats",false]],"transport (class in rns)":[[11,"RNS.Transport",false]],"transport_enabled() (rns.reticulum static method)":[[11,"RNS.Reticulum.transport_enabled",false]],"truncated_hash() (rns.identity static method)":[[11,"RNS.Identity.truncated_hash",false]],"truncated_hashlength (rns.identity attribute)":[[11,"RNS.Identity.TRUNCATED_HASHLENGTH",false]],"unblackhole_identity() (rns.transport static method)":[[11,"RNS.Transport.unblackhole_identity",false]],"unpack() (rns.messagebase method)":[[11,"RNS.MessageBase.unpack",false]],"validate() (rns.identity method)":[[11,"RNS.Identity.validate",false]]},"objects":{"RNS":[[11,0,1,"","Buffer"],[11,0,1,"","Destination"],[11,0,1,"","Identity"],[11,0,1,"","Link"],[11,0,1,"","MessageBase"],[11,0,1,"","Packet"],[11,0,1,"","PacketReceipt"],[11,0,1,"","RawChannelReader"],[11,0,1,"","RawChannelWriter"],[11,0,1,"","RequestReceipt"],[11,0,1,"","Resource"],[11,0,1,"","Reticulum"],[11,0,1,"","Transport"]],"RNS.Buffer":[[11,1,1,"","create_bidirectional_buffer"],[11,1,1,"","create_reader"],[11,1,1,"","create_writer"]],"RNS.Channel":[[11,0,1,"","Channel"]],"RNS.Channel.Channel":[[11,1,1,"","add_message_handler"],[11,1,1,"","is_ready_to_send"],[11,2,1,"","mdu"],[11,1,1,"","register_message_type"],[11,1,1,"","remove_message_handler"],[11,1,1,"","send"]],"RNS.Destination":[[11,3,1,"","RATCHET_COUNT"],[11,3,1,"","RATCHET_INTERVAL"],[11,1,1,"","accepts_links"],[11,1,1,"","announce"],[11,1,1,"","app_and_aspects_from_name"],[11,1,1,"","clear_default_app_data"],[11,1,1,"","create_keys"],[11,1,1,"","decrypt"],[11,1,1,"","deregister_request_handler"],[11,1,1,"","enable_ratchets"],[11,1,1,"","encrypt"],[11,1,1,"","enforce_ratchets"],[11,1,1,"","expand_name"],[11,1,1,"","get_private_key"],[11,1,1,"","hash"],[11,1,1,"","hash_from_name_and_identity"],[11,1,1,"","load_private_key"],[11,1,1,"","register_request_handler"],[11,1,1,"","set_default_app_data"],[11,1,1,"","set_link_established_callback"],[11,1,1,"","set_max_request_size"],[11,1,1,"","set_packet_callback"],[11,1,1,"","set_proof_requested_callback"],[11,1,1,"","set_proof_strategy"],[11,1,1,"","set_ratchet_interval"],[11,1,1,"","set_retained_ratchets"],[11,1,1,"","sign"]],"RNS.Identity":[[11,3,1,"","CURVE"],[11,3,1,"","KEYSIZE"],[11,3,1,"","RATCHETSIZE"],[11,3,1,"","RATCHET_EXPIRY"],[11,3,1,"","TRUNCATED_HASHLENGTH"],[11,1,1,"","current_ratchet_id"],[11,1,1,"","decrypt"],[11,1,1,"","encrypt"],[11,1,1,"","from_bytes"],[11,1,1,"","from_file"],[11,1,1,"","full_hash"],[11,1,1,"","get_private_key"],[11,1,1,"","get_public_key"],[11,1,1,"","get_random_hash"],[11,1,1,"","load_private_key"],[11,1,1,"","load_public_key"],[11,1,1,"","pub_to_file"],[11,1,1,"","recall"],[11,1,1,"","recall_app_data"],[11,1,1,"","sign"],[11,1,1,"","to_file"],[11,1,1,"","truncated_hash"],[11,1,1,"","validate"]],"RNS.Link":[[11,3,1,"","CURVE"],[11,3,1,"","ESTABLISHMENT_TIMEOUT_PER_HOP"],[11,3,1,"","KEEPALIVE"],[11,3,1,"","KEEPALIVE_TIMEOUT_FACTOR"],[11,3,1,"","STALE_GRACE"],[11,3,1,"","STALE_TIME"],[11,1,1,"","get_age"],[11,1,1,"","get_channel"],[11,1,1,"","get_establishment_rate"],[11,1,1,"","get_expected_rate"],[11,1,1,"","get_mdu"],[11,1,1,"","get_mode"],[11,1,1,"","get_mtu"],[11,1,1,"","get_q"],[11,1,1,"","get_remote_identity"],[11,1,1,"","get_rssi"],[11,1,1,"","get_snr"],[11,1,1,"","identify"],[11,1,1,"","inactive_for"],[11,1,1,"","no_data_for"],[11,1,1,"","no_inbound_for"],[11,1,1,"","no_outbound_for"],[11,1,1,"","request"],[11,1,1,"","set_link_closed_callback"],[11,1,1,"","set_packet_callback"],[11,1,1,"","set_remote_identified_callback"],[11,1,1,"","set_resource_callback"],[11,1,1,"","set_resource_concluded_callback"],[11,1,1,"","set_resource_started_callback"],[11,1,1,"","set_resource_strategy"],[11,1,1,"","teardown"],[11,1,1,"","track_phy_stats"]],"RNS.MessageBase":[[11,3,1,"","MSGTYPE"],[11,1,1,"","pack"],[11,1,1,"","unpack"]],"RNS.Packet":[[11,3,1,"","ENCRYPTED_MDU"],[11,3,1,"","PLAIN_MDU"],[11,1,1,"","get_q"],[11,1,1,"","get_rssi"],[11,1,1,"","get_snr"],[11,1,1,"","resend"],[11,1,1,"","send"]],"RNS.PacketReceipt":[[11,1,1,"","get_rtt"],[11,1,1,"","get_status"],[11,1,1,"","set_delivery_callback"],[11,1,1,"","set_timeout"],[11,1,1,"","set_timeout_callback"]],"RNS.RawChannelReader":[[11,1,1,"","__init__"],[11,1,1,"","add_ready_callback"],[11,1,1,"","remove_ready_callback"]],"RNS.RawChannelWriter":[[11,1,1,"","__init__"]],"RNS.RequestReceipt":[[11,1,1,"","concluded"],[11,1,1,"","get_progress"],[11,1,1,"","get_request_id"],[11,1,1,"","get_response"],[11,1,1,"","get_response_time"],[11,1,1,"","get_status"]],"RNS.Resource":[[11,1,1,"","advertise"],[11,1,1,"","cancel"],[11,1,1,"","get_data_size"],[11,1,1,"","get_hash"],[11,1,1,"","get_parts"],[11,1,1,"","get_progress"],[11,1,1,"","get_segments"],[11,1,1,"","get_transfer_size"],[11,1,1,"","is_compressed"]],"RNS.Reticulum":[[11,3,1,"","ANNOUNCE_CAP"],[11,3,1,"","DEFAULT_PER_HOP_TIMEOUT"],[11,3,1,"","LINK_MTU_DISCOVERY"],[11,3,1,"","MINIMUM_BITRATE"],[11,3,1,"","MTU"],[11,1,1,"","blackhole_sources"],[11,1,1,"","discovered_interfaces"],[11,1,1,"","get_first_hop_timeout"],[11,1,1,"","get_instance"],[11,1,1,"","get_lowest_interface_bitrate"],[11,1,1,"","get_medium_path_timeout"],[11,1,1,"","interface_discovery_sources"],[11,1,1,"","link_mtu_discovery"],[11,1,1,"","publish_blackhole_enabled"],[11,1,1,"","remote_management_enabled"],[11,1,1,"","required_discovery_value"],[11,1,1,"","should_use_implicit_proof"],[11,1,1,"","transport_enabled"]],"RNS.Transport":[[11,3,1,"","PATHFINDER_M"],[11,1,1,"","await_path"],[11,1,1,"","blackhole_identity"],[11,1,1,"","deregister_announce_handler"],[11,1,1,"","has_path"],[11,1,1,"","hops_to"],[11,1,1,"","next_hop"],[11,1,1,"","next_hop_interface"],[11,1,1,"","register_announce_handler"],[11,1,1,"","request_path"],[11,1,1,"","unblackhole_identity"]]},"objnames":{"0":["py","class","Python class"],"1":["py","method","Python method"],"2":["py","property","Python property"],"3":["py","attribute","Python attribute"]},"objtypes":{"0":"py:class","1":"py:method","2":"py:property","3":"py:attribute"},"terms":{"":[1,2,4,5,7,8,10,11,12,14,15,16,17],"0":[0,2,4,5,8,11,14,15,16],"00":[0,5,14,15],"000":0,"00000000":14,"0000001":0,"00000100":14,"00000111":14,"01":[5,14],"0100":5,"01010000":14,"02":4,"03":5,"04":5,"05":[0,2,5],"0536":15,"06":0,"06915":8,"06982":0,"08":[2,15],"09":[0,5],"097":0,"0d7334d411d00120cbad24edf355fdd2":5,"0f4259fef4521ab75a3409e353fe9073eb10783b4912a6a9937c57bf44a62c1":15,"0x0101":2,"0x20":2,"0x7d":2,"0x7e":2,"0x91c421ddfb8a30a49a71d63447ddb54cebe3465":13,"0xf000":[2,11],"1":[2,4,5,7,8,11,14,15,17],"10":[0,5,8,14,15],"100":[0,2,8,14],"1000":2,"10000000":14,"101":0,"102":0,"1024":[2,8],"103":0,"106":0,"107":0,"1081":0,"1087":0,"109":17,"11":[0,5,14],"110":0,"111":0,"1115":0,"1121":0,"115":14,"115200":[2,8,15],"1178a8f1fad405bf2ad153bf5036bdfd":15,"118":8,"12":[0,4,5,8,15],"1200":14,"122":0,"125":8,"125000":8,"1260p":0,"127":8,"128":[0,8,10,11,14],"13":[0,15],"132":0,"13425ec15b621c1d928589718000d814":[10,14],"1367":0,"137":0,"1373":0,"1381":0,"14":[5,8],"140":0,"144":0,"146":0,"1476":0,"149":0,"15":[0,5,8],"150":[8,15],"150m":8,"153cb870b4665b8c1c348896292b0bad":5,"15kb":17,"15m":15,"16":[0,5,8,10,14,15,17],"16190":0,"16192":0,"1625":8,"1625000":8,"167":14,"168":17,"17":[0,15],"1711":0,"1716230400":5,"1726dbad538775b5bf9b0ea25a4079c8":15,"175":0,"18":[0,5,15],"1800":11,"187":15,"1879":0,"188":0,"1884":0,"19":0,"192":17,"1976":0,"1980":0,"1984":0,"1985":0,"1989":0,"1990":0,"1991":0,"1992":0,"1997":0,"1998":0,"1999":0,"1a54d64db7e8beca6f2c6cd17b0cb479":5,"1b03013c25f1c2ca068a4f080b844a10":15,"1h":15,"2":[2,4,5,7,8,11,14,15],"20":[0,5,8,14,15],"200":[8,15],"2005":1,"2008":0,"201":8,"2010":0,"2016":[9,16],"2017":0,"2018":0,"2021":0,"2023":[0,4,15],"2024":5,"2025":[0,5],"2026":[0,5,9,17],"203":0,"21":[0,4],"21a8daa6d9c3d3b8aab6e94b6bcb0e33":5,"22":[0,5,8],"2225fdeecaf6e2db4556c3c2d7637294":15,"226":0,"229":0,"23":[0,5,15],"2316":15,"233":0,"236":0,"23h":15,"24":[5,8,15],"2400":12,"2400000000":8,"25":[0,2,7,16],"250":17,"252":0,"255":[8,15],"25519":11,"256":[0,5,8,10,11,14,15,16],"2592000":11,"26":0,"27":15,"280":0,"29":[0,8],"297":[14,16],"29716":8,"2b489d06eaf7c543808c76a5332a447d":15,"2b9ec651326d9bc274119054c70fb75":15,"2d":0,"2d03725b327348980d570f739a3a5708":15,"2d882c5586e548d79b5af27bca1776dc":15,"2f":2,"2owjajquafianpecac":4,"3":[0,2,4,5,8,12,14,15,16],"30":[0,5,8,11,15],"30602def3b3506a28ed33db6f60cc6c9":15,"31":0,"313":0,"32":[4,5,11,14],"3278":8,"327c1b2f87c9353e01769b01090b18f2":17,"32m":15,"33":[0,8],"34":[8,17],"340":0,"345":0,"35":0,"360":[5,8,11],"3600":[2,4,8],"37":[0,5],"374":0,"37428":15,"37429":15,"37k":0,"38":[0,5,15],"383":11,"3865":15,"399ea050ce0eed1816c300bcb0840938":15,"3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o":15,"3b87":8,"3d":0,"3k":0,"4":[0,5,11,14,15],"40m":15,"42":[0,5],"420":0,"4242":[4,8],"4251":8,"42671":8,"430":14,"4343":8,"44":16,"44318":8,"45":[2,5,14],"46":15,"464":11,"465":14,"469":15,"471":0,"48":0,"48555":8,"49":[0,15],"490":0,"49555":8,"4965":15,"499":0,"4e":8,"4ed75ffdd":0,"4faf1b2e0a077e6a9d92fa051f256038":14,"4g":17,"4ghz":8,"4k":0,"5":[0,2,5,8,11,14,15,16,17],"500":[0,8,11,14,16,17],"5001":8,"501":0,"506":0,"508":0,"50824b711717f97c2fb1166ceddd5ea9":5,"51":[8,14],"512":[8,11,14,16],"519":0,"52":15,"521c87a83afb8f29e4455e77930b973b":15,"5233":0,"5245a8efe1788c6a1cd36144a270e13b":15,"53":[0,5],"535":0,"539":0,"54cc8d601":0,"55":8,"56":15,"5600x":0,"564":2,"566":0,"56m":15,"57":0,"5757":8,"58":0,"5858":8,"59":15,"593":0,"598":0,"5caf":8,"5d78":8,"5k":0,"5urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq":8,"6":[4,5,8,11,12,15],"60":[2,8,15],"600":[0,8],"629":0,"63":15,"636":0,"637":0,"64":[4,14,15],"65":0,"67":0,"687":0,"68a4aa91ac350c4087564e8a69f84e86":15,"69":0,"693":0,"7":[0,4,8,14,15],"709":0,"71":17,"71e5":8,"72":8,"720":[8,11],"7200":8,"73":[8,15],"730":0,"737":0,"73cbd378bb0286ed11a707c13447bb1":15,"74":15,"74195":8,"74bea355":0,"781":15,"7822":15,"7a55144adf826958a9529a3bcf08b149":15,"8":[0,2,8,15],"80":[13,15],"800":0,"8001":8,"805":0,"809":15,"82":0,"83":[14,15],"83b7328926fed0d2e6a10a7671f9e237":17,"84":0,"84fpy1qbxhcgdseepynmhthcrgmx4nffbytz2gkytoqhvvhjp8eaw1z1eedrnkd19b3b8nilcgvxzkv17ummmeescrpya5w":13,"86":15,"86240b027":0,"865":8,"865600000":8,"867":8,"867200000":8,"868":14,"88":[0,8],"886":0,"89":[0,8],"8a37cdd16938ce79861561adbd59023a":5,"8dd57a738226809646089335a6b03695":15,"8f3a21c9d84e927b":5,"8f7e6d5":5,"8f7e6d5c8f7e6d5c8f7e6d5c8f7e6d5c8f7e6d5":5,"8k":0,"9":[0,4,5,15],"90":5,"900":[0,14,15],"9037":15,"921600":15,"928":0,"94":0,"940":0,"941bed5e228775e5a8079fc38b1ccf3f":15,"959e10e5efc1bd9d97a4083babe51dea":5,"96":[0,14],"9600":2,"9710b86":5,"9710b86ba12c42d1d8f30f74fe509286":5,"9710b86ba12c4f2":5,"982":0,"983":0,"984b74a3f768bef236af4371e6f248cd":15,"99":[0,14],"99714":8,"9a":5,"9d27af75c":0,"9fb6d773498fb3feda407ed8ef2c3229":15,"9h":15,"9k":0,"9th":0,"A":[1,2,4,5,7,8,9,10,11,12,14,15],"AND":9,"AS":9,"And":[1,2,7,17],"As":[0,1,4,5,6,8,11,12,13,14,15,16],"At":[0,4,14],"BE":9,"BUT":9,"Be":[0,5,11,15,17],"Being":0,"But":[1,7,13,14,17],"By":[1,2,4,5,7,8,11,14,15,17],"FOR":9,"For":[1,4,5,7,8,10,11,12,14,15,16],"IN":[2,9,11],"If":[0,1,2,4,5,6,8,10,11,12,13,14,15,16,17],"In":[1,2,4,5,6,7,8,10,11,12,14,15,16],"It":[0,1,2,4,5,6,8,10,11,12,14,15,16,17],"Its":0,"NO":9,"NOT":9,"No":[2,4,7,8,10,14,15,16,17],"Not":7,"OF":9,"OR":9,"Of":[10,14],"On":[1,2,4,5,7,8,10,15,17],"One":[0,4,5,6,8,11,14,17],"Or":[5,7,8,14,15,16],"Such":[0,1,8],"THE":9,"TO":9,"That":[1,5,7,12,14,17],"The":[2,4,6,7,8,9,10,11,12,13,16],"Their":0,"Then":[0,1,2,4],"There":[0,1,4,10,14,15,17],"These":[1,4,5,7,8,10,11,12,14,15],"To":[2,4,5,6,7,8,10,14,15,16],"WITH":9,"Will":11,"With":[1,7,8,10,12,14,15,17],"_":14,"__":14,"______":14,"_______":14,"________":14,"________________":14,"__future__":2,"__init__":[2,11],"__main__":2,"__name__":2,"__str__":2,"_no_us":11,"a010e6613":0,"a1b2c3d4e5f686ba12c42d1ba12ef1aa":5,"a4d":8,"a79f":8,"aarch64":[0,4],"ab":2,"abandon":[0,1],"abc":5,"abil":[0,1,4,5,6,7,9,15,16],"abl":[0,1,2,4,5,8,11,14,15],"abolish":17,"abort":[1,2,5,8],"about":[0,2,4,5,6,8,10,11,14,15,17],"abov":[0,4,5,6,8,9,14,15],"abridg":0,"absenc":0,"absent":0,"absolut":[0,8,13,16,17],"abstract":[0,10,11,14,17],"abstractmethod":11,"absurd":0,"abund":[6,17],"abus":[0,15],"academ":1,"acceler":0,"accept":[0,1,5,8,11,13,14,15,17],"accept_al":[2,11],"accept_app":11,"accept_non":11,"accepts_link":11,"access":[0,1,4,7,8,10,11,15,17],"access_point":[8,10],"accid":[0,12,17],"accommod":[11,14],"accomod":17,"accompani":0,"accord":[0,2,4,5,10,14],"accordingli":[2,8],"account":[0,1,10,11,17],"accumul":0,"accus":0,"achiev":[0,1,4,8,10,11,14,16],"acknowledg":[0,10,16],"acquir":0,"across":[0,1,2,5,6,8,10,11,14,15,17],"act":[0,1,4,8,11,14,15,17],"action":[0,1,2,9,15],"activ":[0,1,2,4,5,8,11,12,14,15],"actor":[1,14,15,17],"actual":[1,2,4,5,7,8,10,14,15,17],"ad":[0,1,2,5,6,7,8,10,11,12,14,15,16,17],"adapt":[0,4,6,17],"add":[0,1,2,4,5,8,10,11,15],"add_argu":2,"add_message_handl":[2,11],"add_ready_callback":11,"addendum":7,"addict":17,"addit":[0,5,6,8,10,11,12,14,15,16],"addition":[4,5,6,8,14],"addr":15,"address":[0,1,2,4,5,7,8,11,14,15,16],"adher":[8,11],"adjust":[0,4,5,11,15,17],"adm":5,"admin":[5,8],"administ":[0,5],"administr":[0,5,7,10,14,15,17],"admit":0,"adopt":[0,14],"adress":[11,14],"advanc":[0,12,14],"advantag":11,"advers":16,"adversari":[0,4,10,13,17],"advertis":[0,2,8,11],"advic":[0,13],"advis":[8,14],"ae":[0,11,14,16],"aetherlab":12,"af73":8,"aff":0,"affair":0,"affect":[0,4,8,17],"affili":1,"affin":8,"affirm":0,"afford":10,"after":[0,2,4,5,8,10,11,14,15],"afterthought":[0,17],"ag":[0,1],"again":[0,8,14,15],"against":[0,1,4,5,8,14,17],"agenc":[7,14],"agenda":0,"agent":[7,14,17],"aggress":0,"agnost":[10,14],"agnostic":[14,17],"ago":[0,15],"agpl":0,"agreement":[0,1,17],"ahead":[0,10],"ai":[0,17],"aim":[0,1,4,7,12,14],"air":[4,6],"airmax":6,"airtim":[0,8,10,17],"airtime_limit_long":8,"airtime_limit_short":8,"airwav":0,"akin":14,"alexand":0,"algorithm":[0,1,9,10,17],"alia":5,"alias":7,"aliased_nam":5,"alic":[5,14],"alien":17,"align":[0,4,15,17],"aliv":[11,14],"all":[0,1,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17],"allerg":0,"allevi":6,"alloc":[0,8,10,11,14,15],"allow":[0,1,2,4,5,6,8,10,11,12,14,15,16,17],"allow_al":[2,11],"allow_list":11,"allow_non":11,"allowed_hash":15,"allowed_ident":15,"allowed_list":11,"allowedsignersfil":5,"almost":[0,1,6,8,14,15,17],"alon":[0,8],"along":[5,8,11,14,15],"alongsid":[5,10],"alpha":12,"alreadi":[0,2,4,5,6,10,11,12,14,15,17],"also":[0,1,2,4,5,6,7,8,10,11,12,14,15,16,17],"altai":0,"alter":[4,11,14],"altern":[0,2,4,5,8,15],"although":[4,14,16],"altitud":8,"alwai":[0,1,4,5,8,10,11,14,15,17],"am":17,"amateur":[8,16],"ambigu":0,"amd64":0,"america":0,"among":[1,17],"amongst":[0,9],"amount":[0,2,8,10,11,14,16,17],"amplifi":1,"amsterdam":[8,15],"an":[0,1,2,4,5,6,8,9,10,11,12,14,15,16,17],"analog":[12,17],"analysi":[1,5,7],"analyt":13,"analyz":[0,1],"ancestri":0,"anchor":7,"andi":0,"android":[7,8,12,15],"ani":[0,1,2,4,5,6,8,9,10,11,12,13,14,15,16],"annonuc":8,"annot":2,"annouce_cap":8,"announc":[0,4,5,7,10,11,15],"announce_cap":[8,11],"announce_handl":2,"announce_interv":[5,8],"announce_packet_hash":11,"announce_rate_grac":[4,8],"announce_rate_penalti":[4,8],"announce_rate_target":[4,8],"announced_ident":[2,11],"announceloop":2,"announces_from_intern":8,"announces_to_intern":8,"announcesampl":2,"annoy":15,"anonym":[0,4,10,11,14,16],"anoth":[0,5,8,10,11,14,15,17],"answer":[14,15,17],"antenna":[0,6,17],"anticip":[0,5],"antidot":0,"anxieti":17,"anymor":[4,17],"anyon":[0,1,2,4,5,6,8,10,11,14,15,17],"anyth":[0,1,5,6,8,12,14,15,16,17],"anywai":[0,8],"anywher":[0,1,2,8,17],"ap":8,"apach":0,"api":[4,5,7,14,16,17],"apk":4,"app":[0,2,4,11,12,14,17],"app_and_aspects_from_nam":11,"app_data":[2,11],"app_nam":[2,11],"app_timeout":2,"appar":[0,1],"apparatu":0,"appeal":0,"appear":[0,17],"append":[2,14,15],"appl":0,"appli":[0,1,5,8,11,14,17],"applic":[0,1,2,8,10,11,12,14,15,16,17],"appreci":5,"approach":[0,1,4,6,10,14,15],"appropri":[0,1,5,8,10,17],"approv":[0,1,10,14,17],"approxim":[0,8,15],"april":[0,4],"apt":4,"ar":[1,2,4,5,6,7,8,10,11,12,13,14,15,16,17],"arbit":17,"arbitrari":[5,11,14,15,16],"arch":[0,4,17],"architect":[13,17],"architectur":[0,4,7,12],"archiv":[0,5,7,12],"area":[1,4,6,8,10,14,16],"arg":[2,15],"argon":2,"argpars":2,"argument":[2,5,7,11,15],"argumentpars":2,"aris":9,"arithmet":0,"arm":0,"arm64":7,"armi":17,"armor":[5,17],"around":[0,4,8,14,17],"arrai":[0,4],"arrang":[0,1],"arriv":[0,2,8,14,17],"art":[0,17],"articl":0,"artifact":[0,5,7],"artifacts_dir":5,"artifex":0,"artifici":[0,9],"artist":0,"arx":15,"ascii":5,"ask":[0,2,4,6,11,12,17],"aspect":[2,4,6,8,10,11,14,15],"aspect_filt":[2,11],"aspir":0,"assembl":0,"assent":0,"assert":[0,1,17],"assess":0,"assign":[0,2,10,14,15,17],"assist":[7,12],"associ":[0,5,9,11,14,15],"assum":[0,8,10,14,17],"assumpt":[0,10,13,17],"assuredli":17,"asymmetr":[14,15,16],"asymmetri":[0,1],"asynchron":[10,17],"atop":1,"atrophi":0,"attach":[0,1,12,15],"attached_interfac":11,"attack":[0,4],"attain":[0,4],"attempt":[0,2,4,5,6,8,11,15],"attend":0,"attent":[0,1,13,17],"attest":[0,5],"attract":0,"attribut":[1,7,11],"atx":15,"audienc":0,"audio":12,"audit":0,"auth":15,"authent":[1,5,8,10,11,12,14,15,16],"author":[1,7,9,14,15,17],"authoris":[0,15],"authorit":[0,5,14,16],"authorship":[0,1],"auto":[4,5,6,7,11,14,15],"auto_compress":11,"autoconfigur":16,"autoconnect_announces_to_intern":15,"autoconnect_discovered_interfac":[8,15],"autoconnect_interface_grav":8,"autoconnect_interface_mod":15,"autoconnect_unverified_implement":15,"autodiscov":8,"autoinstal":[6,15],"autointerfac":[4,6,8,15],"autom":[0,1,4,5,6,7,12,13,14],"automat":[2,4,7,8,10,11,12,14,15,16,17],"autonom":[1,10,14,17],"autonomi":[1,16,17],"avail":[0,1,2,4,5,6,7,8,10,11,12,14,15,16,17],"avatar":0,"averag":14,"avoid":[6,14,15],"awai":[0,1,2,8,14,15,17],"await_path":11,"awar":[0,4,5,8,14],"ax":[7,16],"ax25kissinterfac":8,"axiom":0,"b":[0,2,5,15],"b32":8,"back":[0,1,2,5,8,14,16,17],"backbon":[0,6,7,10,15,17],"backboneinterfac":[0,4,8,15],"backend":[8,14],"background":[4,8,15],"backhaul":[6,10],"backup":[1,15],"backward":0,"bad":0,"badg":0,"badli":0,"baker":0,"balanc":[0,8],"ban":0,"banal":0,"band":[6,14,17],"bandwidth":[0,4,7,8,10,11,12,14,15,16],"bar":[0,17],"bare":[0,1,2,5,8,17],"barrier":[1,8,14,17],"base":[0,1,5,7,8,10,11,12,14,15,16,17],"base32":[8,15],"base64":[5,15],"baselin":0,"bash":[8,15],"bash_sourc":5,"basi":[0,1,8,11,14,15,16],"basic":[0,2,4,7,8,10,11,12,15],"batch":5,"batter":0,"batteri":17,"baud":[8,15],"baud_flash":15,"baudrat":2,"bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6":13,"bc7291552be7a58f361522990465165c":[15,16],"beacon":8,"bear":0,"beast":17,"beat":17,"beauti":[1,12],"becam":[0,1],"becaus":[0,1,5,10,14,17],"becki":2,"becom":[0,1,2,4,5,8,10,14,17],"bedrock":0,"been":[0,1,2,4,5,6,8,10,11,12,13,14,15,16,17],"befor":[0,2,4,5,8,10,11,13,14,15,17],"beg":17,"begin":[0,2,6,11,17],"begun":[0,11],"behalf":[8,10],"behav":[0,6,12],"behavior":[0,8,12,17],"behaviour":[0,4,8,15],"behind":[0,4,8,13,14,17],"being":[0,1,4,5,8,11,12,14,17],"beings":[0,1,9,14],"belief":[0,14],"believ":[0,1,17],"belong":[0,8,10,14,15,17],"below":[0,6,8,15],"benchmark":0,"bend":17,"benefici":[8,15],"beneficiari":0,"berlin":17,"besid":0,"best":[0,4,10,11,14,17],"beta":0,"better":[0,1,4,8,10,13,14,17],"between":[0,1,2,8,10,11,12,14,15,17],"beyond":[0,1,5],"bi":[11,14],"bicker":0,"bidirect":14,"big":17,"bill":17,"billion":[10,14],"billionair":17,"bin":[4,8,15],"binari":[0,2,4,8,10,11,14,15,17],"bind":[0,7,8],"bio":0,"bit":[0,2,4,5,8,10,11,12,14,15,16,17],"bitcoin":13,"bitrat":[2,8,11,12],"bizarr":0,"black":1,"blackhol":[4,7,10,11,14],"blackhole_ident":11,"blackhole_sourc":[11,15],"blackhole_update_interv":15,"blackholed_ident":15,"blackholeupdat":15,"blatant":0,"ble":8,"bless":0,"blindli":[0,15,17],"blizzard":0,"blob":[2,5,14],"block":[0,4,5,7,10,11,12,14,15,17],"block_fast_flap":8,"blockchain":17,"blocklist":15,"blog":0,"blood":17,"blueprint":17,"bluetooth":15,"board":[0,4,7,8,12,14,16],"boat":17,"bob":5,"bobs_nod":5,"bodi":0,"bogu":8,"boil":6,"bond":17,"book":[0,12,17],"bookworm":7,"bool":11,"boot":[4,15],"bootstrap":[7,8,15],"bootstrap_onli":[4,8],"borrow":17,"bot":12,"both":[0,1,2,4,5,6,8,10,11,12,14,15,16],"bother":0,"bottl":0,"bounc":[4,17],"bound":[7,10,17],"boundari":[0,1,5,8,10,14],"box":[1,6],"brain":0,"branch":[1,5,15],"brandolini":7,"breach":0,"breadcrumb":5,"breadth":0,"break":[0,4,11,14,17],"breath":[0,4,17],"breviti":15,"bridg":[6,8],"brief":7,"briefli":[0,6,14],"brillianc":0,"bring":[0,8,15],"brittl":[4,17],"broad":[4,6],"broadcast":[0,4,7,8,10,11,12,14,15,17],"broadcast_destin":2,"broadcastloop":2,"broader":[0,4,14],"broken":[0,4,8,15,17],"brought":[0,8,15],"brows":[1,5,7,17],"browser":[0,5,7,17],"buffer":[7,8,11,16],"bufferedread":11,"bufferedrwpair":11,"bufferedwrit":11,"bufferexampl":2,"bug":[0,5,12,13],"bui":17,"build":[0,1,2,5,6,7,8,12,14,16,17],"builder":[0,17],"built":[0,1,4,8,10,12,14,15,16,17],"bulletin":12,"bulli":0,"bullshit":0,"bundl":[0,2],"bureaucraci":17,"bureaucrat":[16,17],"burgundi":0,"buri":0,"burrow":0,"burst":[7,15],"busiest":0,"busy":1,"butlerian":0,"button":[0,17],"bw":15,"bypass":[0,8,13,17],"byte":[0,2,7,10,11,14,15,16],"bytes":2,"c":[0,2,5,9,15],"c50cc4e4f7838b6c31f60ab9032cbc62":15,"c89b4da064bf66d280f0e4d8abfd9806":15,"ca":0,"cabl":[8,12,17],"cach":[8,10,15],"cadenc":0,"cafe":17,"cage":17,"cal":0,"calcul":[10,11,14,15],"calibr":0,"call":[0,1,2,10,11,12,14,16,17],"callabl":[2,11],"callback":[2,11],"callsign":8,"calm":0,"calmli":0,"cambrian":0,"came":[0,5],"camp":1,"campaign":0,"can":[0,1,2,4,5,6,7,8,10,11,12,13,14,15,17],"cancel":[0,5,11],"candid":[0,10],"cannot":[0,1,2,4,5,8,10,16,17],"canon":0,"cap":8,"capabl":[0,1,2,4,8,10,12,14],"capac":[0,4,6,10,11,14],"capit":[1,17],"captur":[0,1,17],"car":10,"carambola":2,"card":[1,12,17],"care":[0,1,4,5,10,11,15,16,17],"carefulli":[0,4],"cargo_build_target":4,"caricatur":0,"carri":[0,1,8,10,11,14,15,16,17],"carrier":[7,10,16],"cascad":0,"case":[1,2,4,5,6,7,8,10,12,14,15],"cast":0,"castl":17,"casual":0,"cat":15,"catalogu":0,"catastroph":[0,1],"categori":[0,6],"catiou":0,"caught":0,"caus":[0,4,8],"caution":8,"cb":[11,14],"cbc":[14,16],"cc4d524a3":0,"cd":5,"cdma":8,"ceas":[0,1,17],"cede":1,"cell":[0,10],"cellular":17,"cement":17,"censor":[1,14,15,17],"censorship":[14,16],"center":[0,7],"centr":0,"central":[0,1,4,5,6,8,10,12,14,15,17],"centralis":14,"centric":1,"centuri":1,"ceo":17,"certain":[0,2,8,10,14,15,17],"certainti":0,"certif":[0,17],"certifi":[0,1],"cetera":0,"chain":[0,5,8,17],"challeng":[0,4,12,14,17],"chanc":6,"chang":[0,1,2,4,5,8,11,14,15,16,17],"channel":[0,1,4,5,7,8,10,11,13,14,16,17],"channelarg":2,"channelexampl":2,"chaotic":17,"chapter":[0,1,4,5,6,8,10,11,12,14,15,16],"charact":[0,2,5,8,15],"characteris":0,"characterist":[0,8,10,14,17],"charad":0,"charg":[0,9],"chart":[0,5],"charter":14,"chase":17,"chat":7,"cheap":[0,6,8,14],"cheaper":0,"cheapli":4,"check":[0,2,4,5,11,12,14,15,17],"checksum":[5,11,16],"chmod":5,"choic":[0,4,10,17],"choke":17,"choos":[0,1,2,4,8,12,14,17],"choreographi":0,"choru":0,"chose":[0,14],"chosen":14,"chunk":2,"churn":0,"ci":0,"cific":8,"ciphertext":11,"ciphertext_token":11,"cir":0,"circl":17,"circuit":0,"circul":0,"circumst":[13,14,17],"citat":0,"cite":0,"citi":[0,17],"cl":2,"claim":[5,7,9,14],"claimant":0,"clariti":17,"class":[0,2,10,11,16],"classifi":0,"claud":0,"clean":7,"cleanest":0,"clear":[0,2,8,11,15,17],"clear_default_app_data":11,"clear_screen":2,"clearer":0,"clearli":[0,17],"click":5,"client":[1,2,5,6,7,11,12,15,17],"client_buffer_readi":2,"client_config":5,"client_connect":2,"client_disconnect":2,"client_ident":[2,5],"client_loop":2,"client_message_receiv":2,"client_packet_receiv":2,"client_request":2,"climat":17,"clone":[0,5,14],"close":[0,2,10,11,15],"closed_callback":11,"closer":[12,14],"closest":14,"closur":0,"cloth":17,"cloud":[1,7,8,12],"cluster":[1,14],"co":[0,5,6,8,16],"code":[1,4,5,6,7,8,10,12,13,15,16,17],"codebas":0,"codens":0,"codex":0,"codic":1,"codifi":0,"codingr":8,"coexist":[7,10],"coffe":17,"coher":17,"coin":0,"collabor":[1,5,12,14,15],"collaps":[0,1,4,17],"collect":[0,1,4,13],"collis":10,"colon":17,"color":5,"columba":7,"columbia":0,"com":[0,2,5,13,17],"combin":[0,1,4,5,7,12,14,15,17],"combinatori":17,"come":[0,1,4,8,10,14,15,17],"comma":[8,15],"command":[0,1,2,4,7,8,12,15],"commend":17,"commensur":0,"comment":[0,1,5,8,15],"commentari":0,"commerci":[0,17],"commiss":0,"commit":[0,1,4,7,12],"committ":0,"committe":[0,17],"common":[1,2,4,6,7,10,14,15,16,17],"commonli":[0,5,6,8],"commun":[0,1,2,4,5,7,8,10,11,12,13,14,15,16,17],"communica":8,"compani":17,"companion":[0,12],"compar":[10,17],"comparison":0,"compass":17,"compat":[0,1,2,4,5,6,8,11,12,14,15,16],"compet":[0,1,17],"compil":[0,4],"compiled_refer":0,"complain":17,"complaint":0,"complement":0,"complet":[0,1,2,4,5,6,8,10,11,12,14,15,16],"complex":[0,1,4,8,13,14,17],"compli":10,"complianc":0,"compon":[6,7,13,14,17],"compos":[0,5,7,14],"comprehens":[1,12,13],"compress":[2,11,14,15,16],"compromis":[1,14,17],"comput":[0,1,4,8,14,16,17],"computation":[8,15],"concaten":[11,15],"conceal":0,"conceiv":[0,1,4,6],"concept":[0,1,7,14,15,17],"conceptu":[0,1,5,7],"concern":[0,17],"concert":14,"concis":0,"conclud":[0,2,11],"conclus":7,"concret":[0,17],"concurr":[6,14,15],"condens":0,"condit":[4,7,9,14,16,17],"conduct":[0,1],"conduit":17,"conf":[4,12],"confer":0,"confess":0,"confid":0,"config":[2,4,5,8,10,15,17],"configarg":2,"configdir":11,"configobj":5,"configpath":2,"configur":[0,1,2,4,6,7,10,11,12,14,16],"configuraion":6,"configuraiton":15,"configure_devic":2,"confirm":[5,10,14,16,17],"conflict":[4,10],"conform":[1,7],"confront":17,"confus":[0,6,14],"congest":14,"conglomer":17,"conjunct":8,"connect":[0,1,2,5,6,7,9,11,13,14,15,16,17],"conscienc":17,"consciou":0,"conscious":[0,1],"conscript":17,"consent":1,"consequ":[1,7,10,14],"conserv":[0,8],"consid":[0,2,4,8,11,12,14,15,16,17],"consider":[0,7,14],"consist":[0,5,14,15],"consol":15,"constant":[1,11,17],"constantli":[14,17],"constitu":0,"constitut":[0,1,4,14],"constrain":[10,11],"constraint":[0,17],"construct":[0,1,5,14,16,17],"constructor":2,"consult":0,"consum":[0,5,10,11,17],"consumpt":[10,17],"contact":[0,8,10,12,14,17],"contain":[0,1,2,5,8,10,11,14,15],"contamin":0,"contempl":17,"contemporari":1,"contend":14,"content":[0,1,2,4,5,10,15],"contest":0,"context":[4,7,14,15,17],"contin":[2,4,17],"continu":[0,1,5,8,11,13,15,16,17],"contract":[0,9],"contractu":0,"contradict":0,"contrari":[0,17],"contrast":[0,1],"contribut":[0,1,7,9,10,13],"contributor":[0,1],"control":[0,1,2,4,5,6,7,10,11,12,14,15,16,17],"controversi":0,"convei":[0,2,17],"conveni":[0,1,2,5,15,17],"convent":[0,2,10],"converg":[4,10,14,17],"convers":[0,1,5,12,14,17],"convert":[0,1,5],"convinc":0,"convincingli":0,"cook":4,"coordin":[0,8,10,11,14,16,17],"copi":[1,5,7,9,15],"copier":0,"copilot":0,"copyleft":0,"copyright":[1,7,9],"core":[0,1,10,14,15,16,17],"corner":17,"corollari":0,"corp":0,"corpor":[0,1,17],"corpu":0,"correct":[0,1,2,5,8,10,14],"correctli":[0,2,5,10],"correspond":[5,10,14,17],"corrobor":0,"corrupt":[1,5],"cosmologi":0,"cost":[0,6,7,8,14,16],"costum":0,"could":[0,1,2,4,8,10,11,14,15,16,17],"counsel":0,"count":[0,5,12,14,17],"counter":2,"coupl":0,"courier":0,"couriership":0,"cours":[0,4,5,8,10,14,17],"court":0,"coven":[0,17],"cover":[0,4,5,6,10,11,14,16],"coverag":[8,10],"cp313":0,"cpp":0,"cpu":[0,2,8,10,14],"cpuinfo":15,"cpython":0,"cr":15,"craft":[0,1,5,17],"craftsman":17,"crash":17,"crate":0,"creat":[0,1,2,7,8,10,11,12,15,16,17],"create_bidirectional_buff":[2,11],"create_kei":11,"create_read":11,"create_receipt":[2,11],"create_writ":11,"creation":[0,1,7,9,10,12,14,15,16],"creativ":[0,1,17],"creator":[0,1,7,14,17],"credenti":[8,14],"credit":0,"crippl":0,"crisi":0,"criteria":0,"criterion":0,"critic":[0,1,10,14,15],"crn":0,"cron":1,"cross":[0,8,12],"crowd":17,"crucial":10,"cruft":17,"crypto":0,"cryptograph":[0,1,4,7,8,10,15,16,17],"cryptographi":[1,4,14,16],"crystal":17,"ctrl":2,"cull":11,"culmin":17,"cultur":[0,1],"cumbersom":5,"curl":8,"currenc":0,"current":[0,1,2,4,5,7,8,11,12,15,17],"current_download":2,"current_filenam":2,"current_ratchet_id":11,"cursor":0,"curtain":0,"curv":[10,11,14,16],"curve25519":[10,11,14,16],"custodian":17,"custom":[0,6,7,11,12,14,16],"custom_network_nam":8,"customis":8,"cut":[0,17],"cv":0,"cycl":0,"cynic":17,"cython":0,"d":[0,5,15],"d09285e660cfe27cee6d9a0beb58b7e0":5,"d56a4fa02c0a77b3575935aedd90bdb2":15,"daemon":[0,2,4,8,11,12,15],"dai":[0,5,11,12,17],"daili":[0,5],"damag":[0,1,9,17],"damn":0,"danc":17,"dang":0,"danger":[0,17],"dark":17,"darwin":0,"data":[0,1,2,4,5,6,7,8,10,11,14,16,17],"data_buff":2,"data_port":8,"databas":[1,12,15,17],"databit":[2,8,15],"datacent":4,"datar":8,"dataset":[0,9,17],"date":[2,4,5,7,15],"datetim":2,"dawn":0,"db":[2,15],"dbm":[2,8,15],"dead":0,"deadlin":1,"deal":[0,8,9],"dealt":0,"death":[0,7],"deb":0,"debas":0,"debat":0,"debian":[0,7,14,15],"debug":[0,4,15],"decad":[0,1,6,13,17],"deceiv":0,"decentr":[4,7,8,15],"decentralis":16,"decept":0,"decid":[0,1,8,10,11,14,17],"decim":8,"decis":[0,1,5,8,10,14,17],"declar":[0,14],"declin":0,"decod":[0,2,8,15],"decor":0,"decoupl":1,"decreas":[5,8,15],"decrypt":[0,8,10,11,14,15],"dedic":[0,5,8,14,16,17],"dedupl":0,"deed":0,"deep":[0,4,17],"deeper":0,"deepli":[0,1,17],"deepseek":0,"def":[2,5],"default":[0,2,4,5,6,8,10,11,12,14,15],"default_ar_grac":8,"default_ar_penalti":8,"default_ar_target":8,"default_grav":8,"default_ifac_s":2,"default_per_hop_timeout":11,"defenc":7,"defenceless":0,"defend":0,"defens":0,"defin":[0,2,5,8,11,14,15,16,17],"definit":[0,2,4,5,14,15,17],"deflect":0,"defunct":[0,15],"degrad":[0,10],"degre":[0,8],"delai":[8,12,14,15,17],"deleg":[1,5,14],"delet":[0,1,5,17],"deliber":0,"delimit":2,"deliv":[0,2,4,10,11,12,17],"deliveri":[0,1,2,5,10,11,12,16],"delus":7,"delusion":0,"demand":[0,5,11,17],"demonstr":[0,1,2,4,8,17],"deni":[1,5,17],"denomin":[0,1],"densiti":0,"depend":[0,1,2,5,6,7,8,10,11,14,15,17],"depict":0,"deploi":[0,14,17],"deploy":[0,5],"deprec":0,"depriorit":17,"depth":[0,17],"derail":0,"deregist":11,"deregister_announce_handl":11,"deregister_request_handl":11,"deriv":[0,4,10,11,14,16],"descend":0,"describ":[0,5,6,8,11,14],"descript":[0,2,5,15],"descriptor":2,"deseri":2,"deserializ":2,"deserv":0,"design":[0,1,4,5,6,7,8,10,12,13,14,15,16],"desir":[0,2,6,8,14],"desk":17,"desktop":[8,12],"dest_len":2,"destin":[0,2,4,7,11,15,17],"destination_1":2,"destination_2":2,"destination_clos":2,"destination_hash":[2,5,11,15],"destination_hexhash":2,"destroi":[1,17],"destruct":[0,17],"detach":[5,8,15],"detail":[0,1,2,4,5,7,8,11,15,16],"detect":[0,2,5],"deterior":0,"determin":[0,4,10,11,14,17],"determinist":5,"detriment":15,"dev":[2,4,5,8,15],"devalu":0,"devel":4,"develop":[0,5,6,7,9,11,12,13,14,15,16,17],"deviant":0,"devic":[0,2,4,7,8,10,11,14,15,17],"dfsg":0,"dh":11,"dhcp":[6,8,10,15],"di":17,"diagnosi":0,"dial":[0,12],"diametr":0,"dichotomi":7,"dict":2,"dictat":[15,17],"dictatori":0,"dictatorship":[0,17],"dictionari":15,"did":[0,1,2,12,17],"didn":0,"diff":5,"differ":[0,1,2,4,5,6,8,10,11,12,14,15,16,17],"differenti":15,"diffi":[10,14],"difficult":[0,4,6,15],"difficulti":[8,15],"digit":[1,4,6,14,16,17],"digniti":[0,1,17],"dilut":1,"dimens":0,"dinner":17,"dipol":0,"dir":[2,5],"dire":6,"direct":[0,1,2,4,5,6,10,11,12,14,15],"direction":[0,14],"directli":[1,4,5,6,7,8,9,10,11,12,14,15,16,17],"directori":[2,4,5,8,10,15,17],"dirnam":5,"dirt":0,"dirti":0,"dirtiest":0,"disabl":[8,14,15],"disadvantag":0,"disagre":0,"disappear":[8,15,17],"disassoci":14,"discard":[4,11,14],"discern":[0,1],"disciplin":[0,1,17],"disclos":7,"disclosur":0,"disconnect":[2,4,7,8],"discourag":1,"discov":[0,1,4,7,8,10,11,14,17],"discover":[4,7,15],"discover_interfac":15,"discovered_interfac":11,"discoveri":[0,4,5,7,11,12,14,15],"discovery_bandwidth":8,"discovery_encrypt":8,"discovery_frequ":8,"discovery_lxmf_address":8,"discovery_modul":8,"discovery_nam":8,"discovery_port":8,"discovery_scop":8,"discovery_stamp_valu":8,"discret":1,"discrimin":[0,10],"discuss":[0,1,5,6,14],"disk":[2,5,11,15],"dismiss":0,"displai":[2,5,8,10,12,14,15],"disproven":0,"disput":0,"disqualifi":0,"disregard":0,"disrupt":[1,8],"dissemin":1,"dissent":0,"dissip":0,"dissolv":[10,17],"dissuad":0,"dissuas":0,"dist":5,"distanc":[6,14,15],"distant":[4,11,14],"distinct":[0,1,8,10,14,17],"distinguish":[0,10,14],"distribut":[2,5,7,9,10,11,12,14,15,16,17],"district":0,"dive":4,"diverg":[0,1],"divers":[0,1],"divid":11,"divmod":2,"dn":[6,8,12,17],"dna":17,"dnf":4,"do":[1,2,4,5,6,7,8,9,10,11,14,15,17],"doc":0,"doc_id":5,"doctrin":0,"document":[0,1,4,7,9,11,12,14,15,16,17],"doe":[1,2,4,5,6,7,8,11,12,14,15,17],"doer":0,"doesn":[1,8,10,17],"dollar":[0,17],"domain":[0,1,4,7,8,14,15,16],"domin":17,"don":[0,2,4,5,10,11,14,15,17],"donat":[0,7],"done":[0,2,4,5,8,14,15,17],"door":[0,17],"dot":[14,15],"doubt":[0,4],"dowload":4,"down":[0,1,2,6,8,11,12,15,17],"download":[1,2,4,5,8,10,15],"download_began":2,"download_conclud":2,"download_finish":2,"download_start":2,"download_tim":2,"downstream":[0,15],"dozen":7,"draft":0,"drag":[0,12],"drain":[0,8],"dramat":0,"drastic":15,"draught":0,"draw":[0,17],"drawn":[0,17],"dress":0,"drift":0,"drink":0,"drive":[1,13],"driver":[15,16],"droid":4,"drone":17,"drop":[0,5,8,10,11,12,14,15,17],"drown":1,"dsrdtr":2,"dual":[0,6,15],"dualiti":0,"dublin":15,"due":[1,2],"dumb":17,"dump":[8,15],"duplex":[14,16],"durabl":[0,1],"durat":15,"dure":[0,5,15],"duti":0,"dynam":[1,4,5,8,12,15,17],"dysfunct":13,"dysregul":1,"e":[2,5,12,15],"e5c032d3ec4e64a6aca9927ba8ab73336780f6d71790":15,"e702c42ba8":15,"e7536ee90bd4a440e130490b87a25124":15,"each":[0,1,2,4,5,8,10,11,12,14,15],"earli":[0,12,17],"earlier":[0,14],"earliest":0,"earn":0,"earth":0,"eas":[8,12,14],"easi":[0,4,5,6,8,12,14,15,16],"easier":[0,4,5,8,10,15,16],"easiest":[4,6,8,14],"easili":[0,1,4,5,8,10,14,15,16,17],"eastern":14,"eaten":0,"ec_pr_freq":8,"ecdh":[14,16],"echo":[0,5,7],"echo_destin":2,"echo_request":2,"econom":1,"economi":17,"ecosystem":[0,1,4,8,12,14,15,17],"ed25519":[1,5,14,16],"edd8e72b":0,"edg":0,"edit":[0,4,5,15],"editor":[5,15],"editori":0,"educ":0,"ee":5,"eeprom":15,"effect":[0,1,5,8,14,15],"effici":[0,2,4,8,10,11,12,14,15,16,17],"effort":[0,6,11,17],"effortless":0,"effortlessli":12,"eg":15,"egress":8,"egress_control":8,"ei":2,"eight":0,"either":[0,4,5,6,10,11,14,15,17],"elaps":[5,8],"electromagnet":17,"eleg":17,"element":[0,10,17],"elev":1,"elif":2,"elig":0,"elimin":[1,10,17],"ellipsoid":8,"ellipt":[10,11,14,16],"elong":7,"els":[0,1,2,10,12,13,14,16,17],"elsewher":0,"email":[1,5,12],"emb":5,"embed":[0,1,5,6,12],"embellish":0,"embodi":0,"embrac":17,"emerg":[1,7,12],"emiss":12,"emot":1,"empir":0,"emploi":[0,1,6,14],"empow":[0,16],"empti":[2,7,8,17],"emul":[4,15],"enabl":[1,2,4,6,7,10,11,12,14,15,16,17],"enable_ratchet":11,"enable_remote_manag":15,"enable_transport":[8,14,15],"encapsul":[8,16],"enclos":5,"enclosur":0,"encod":[0,2,5,11,12,15,17],"encount":[0,4,10],"encourag":[0,1,4,17],"encrypt":[0,1,2,4,5,7,8,10,11,12,14,15,16],"encrypted_mdu":11,"encumbr":0,"end":[0,2,5,8,10,11,12,14,16],"endeavor":[0,1],"endless":[0,8,17],"endors":0,"endow":1,"endpoint":[2,10,11,12,14,17],"energi":[0,4,17],"enforc":[0,8,11,14,15],"enforce_ratchet":11,"engag":[0,1,17],"engin":[1,4,12,13,17],"enginest":0,"enhanc":14,"enjoy":1,"enorm":0,"enough":[0,1,4,6,8,12,14],"ensur":[0,1,2,4,5,8,10,11,12,14,15,16,17],"ensurepath":4,"entail":0,"enter":[1,2,15],"enterpris":0,"entertain":0,"enthusiast":0,"entir":[0,1,2,4,5,8,10,11,12,14,15,16,17],"entireti":0,"entiti":[1,10,14,15,17],"entitl":[0,1],"entri":[1,2,4,14,15,17],"entropi":17,"entrypoint":[7,8,15],"enumer":2,"envelop":[5,11,17],"environ":[0,1,4,5,7,8,10,13,14,15],"environment":14,"environmentlogg":14,"eof":8,"ephemer":[0,1,10,11,14,16],"epub":7,"equal":[0,1,10,11,14,17],"equat":0,"equip":[8,10,14],"equival":[0,5,8,17],"era":17,"eras":0,"eridanu":12,"erod":17,"eros":0,"erron":13,"error":[0,2,4,5,8,13,15,17],"esc":2,"esc_mask":2,"escal":0,"escap":[0,2,15],"esoter":1,"esp32":[0,6],"especi":[0,4,5,6,8,15],"essenc":0,"essenti":[0,1,4,5,8,12,14,15,17],"establish":[0,1,2,4,8,10,11,12,15,16,17],"established_callback":11,"establishment_timeout_per_hop":11,"estim":[0,11],"et":0,"etc":[0,5,15],"eth0":8,"eth1":8,"ether":17,"ethereum":13,"ethernet":[4,7,8,10,12,14,16,17],"ethic":[0,7],"etho":0,"evalu":[1,7],"evapor":17,"even":[0,1,2,4,5,8,10,11,12,14,15,16,17],"event":[0,8,9,17],"eventu":11,"ever":[0,14],"everi":[0,1,2,4,5,8,10,11,14,15,17],"everydai":4,"everyon":[0,1,4,5,12,13,14,17],"everyth":[0,1,2,5,10,12,13,14,17],"everywher":0,"evict":17,"evid":0,"evidentiari":0,"evolut":0,"evolv":[0,1,4,10,14],"exact":[0,5,6,8,14],"exactli":[0,5,11,14,17],"examin":0,"exampl":[0,1,4,6,7,10,11,12,14,15,16],"example_util":2,"exampleannouncehandl":2,"exampleconfig":[4,15],"exampleinterfac":2,"exce":[0,2,8,11],"excel":[0,12],"except":[0,2,8,11],"excerpt":0,"excess":[8,15],"exchang":[1,10,11,14,16],"exclud":[0,5,11,14],"exclus":[0,1,5],"execstart":15,"execstartpr":15,"execut":[0,1,2,4,5,8,11,15],"exemplifi":0,"exercis":[0,1],"exhaust":[0,6,12,14,15],"exist":[1,2,4,5,6,7,8,10,12,14,15,16],"exit":[1,2,5,8,11,15],"expand":[5,7,10,12,14,16],"expand_nam":11,"expans":[1,5,17],"expect":[0,2,5,8,11,13,14,15],"expens":[0,6,8],"experi":[0,10,12,14,15,17],"experienc":2,"experiment":[0,15],"expert":0,"expertis":0,"expir":[8,11],"expiri":[8,11],"explain":[0,6,11],"explan":[0,14,15],"explicit":[0,11],"explicitli":[0,8,10,14,15],"exploit":[0,1],"explor":[2,14,16,17],"explos":0,"export":[4,15],"expos":[0,8,11,15],"exposur":[0,8],"express":[1,5,7,9],"extend":[0,1,2,11],"extens":[0,5,8,15,16],"extent":0,"extern":[0,1,2,4,5,8,11,15,16,17],"extra":[0,4,8,15],"extract":[1,5,15,17],"extract_id":5,"extrem":[0,4,12,14,15,16,17],"ey":0,"f":[0,2,4,5,15],"f2d31b2e080e5d4e358d32822ee4a3b7":5,"f4":8,"f53a1c4278e0726bb73fcc623d6ce763":15,"fabric":[0,7,10],"facad":0,"face":[0,8],"facebook":1,"facil":0,"facilit":[12,14],"fact":[0,1,4,8,16],"factor":[0,8,11,15],"factual":0,"fade":17,"fail":[0,1,2,4,5,8,11,17],"failed_callback":[2,11],"failur":[0,1,4,5,8,11,17],"fair":7,"faith":0,"fake":[0,15],"fall":0,"fallaci":7,"fallback":[5,17],"fallen":17,"fals":[2,7,8,10,11],"falsehood":0,"falsiti":0,"famili":[0,4],"familiar":[0,1,12],"fantasi":[0,17],"far":[0,1,4,10,14],"farcic":0,"fart":0,"fashion":13,"fast":[0,7,8,10,11,12,14],"fast_flapping_block_tim":8,"fast_flapping_grac":8,"fast_flapping_threshold":8,"faster":[0,4,8,14,17],"fastest":[8,14],"fat":17,"favor":4,"favour":0,"fe80":4,"featur":[0,4,5,7,8,10,12,14,15,16],"feb":0,"fed":0,"feder":[0,12,14,15,17],"fedora":4,"fee":0,"feed":[0,1,4,11,17],"feedback":[0,1,2,7],"feel":[12,17],"feist":0,"ferment":7,"fernet":14,"fetch":[1,5,8,15],"few":[0,1,4,6,8,10,12,14,15,16,17],"fewer":11,"ff":15,"ffcffb4e255e156e77f79b82c13086a6":5,"fi":[5,13],"fiber":[6,17],"fibr":16,"fiction":0,"fictiti":0,"fidel":0,"field":[0,5,14,17],"fifth":0,"fight":0,"figur":8,"file":[0,1,2,4,5,6,8,9,10,11,12,14,15,16],"file_resourc":2,"file_s":2,"filelist":2,"filelist_data":2,"filelist_receiv":2,"filelist_timeout_job":2,"filenam":2,"filesync":7,"filetransf":[7,11],"fill":0,"filter":[1,2,10,14,15],"final":[0,2,5,11,14,17],"find":[0,1,5,7,8,12,14,15,17],"find_spec":2,"fine":[0,1,5,8,15],"fingerprint":17,"finish":[0,7],"finit":17,"fire":17,"firewal":[0,4,8,10,17],"firm":0,"firmli":0,"firmwar":[0,4,6,14,15],"firmware_hash":15,"first":[0,1,2,4,5,8,10,11,14,15,17],"fit":[1,2,9,17],"five":[2,17],"fix":[0,5,7,14,17],"fixed_mtu":8,"flag":[0,2,4,5,11,14,15],"flap":8,"flash":[0,15],"flasher":[0,4,7],"flat":[0,17],"flaw":[0,17],"fleet":14,"flesh":17,"flexibl":[4,5,6,8,10,12,15,16],"flexibli":5,"fli":14,"flicker":17,"flight":11,"flip":17,"float":[2,8,11],"flood":[0,8,15],"floor":0,"flourish":0,"flow":[0,1,2,4,7,8,10,14],"flow_control":8,"fluenci":7,"fluent":0,"fluentli":0,"fluid":17,"fluiditi":17,"fluidli":[0,17],"flush":2,"focu":[1,15],"focus":[0,12,14],"folder":[1,2,5],"folklor":0,"follow":[0,1,2,4,5,6,8,9,11,13,14,15,16,17],"font":5,"footnot":0,"footprint":0,"footsoldi":0,"forbid":[0,17],"forbidden":0,"forc":[0,1,2,15,17],"forcibli":[11,15],"foreground":4,"forens":0,"forese":0,"forev":[0,17],"forg":[0,17],"forgeri":0,"forget":[0,10,15],"forgiv":0,"forgotten":[0,1],"fork":[0,1,7],"forker":5,"form":[0,4,7,8,10,11,14,15,17],"formal":0,"format":[0,1,2,7,8,12,15,16],"formula":0,"forth":2,"fortifi":1,"fortiori":0,"fortun":17,"forum":0,"forward":[0,1,5,7,8,10,11,12,14,15,16],"forward_ip":8,"forward_port":8,"found":[0,2,4,5,8,11,14,15,17],"foundat":[1,7,10,13,14,16,17],"founder":0,"four":[0,8],"fourth":0,"fraction":0,"fragil":[1,7,17],"frame":[0,2,8],"framework":[0,12],"fraud":0,"fraudul":0,"free":[4,7,9,10,12,13,16,17],"freedom":[7,14,17],"freedv":6,"freeli":[0,14],"freq":15,"frequenc":[0,4,6,8,14,15],"frequent":[0,8,10],"fresh":0,"frictionless":0,"friend":[4,17],"friendship":0,"from":[1,2,4,5,6,7,8,9,10,11,12,13,14,15,16],"from_byt":11,"from_fil":11,"from_identity_hash":11,"fromhex":2,"front":[0,5,14],"fruit":2,"ftdi_ft230x_basic_uart_43891ckm":15,"fudg":0,"fuel":[0,17],"full":[0,1,2,4,5,8,10,11,12,14,15,16],"full_hash":11,"full_nam":[11,15],"fulli":[0,2,4,5,6,8,12,14,15,16],"fun":0,"funcion":11,"function":[0,1,2,4,5,6,7,8,9,10,11,12,13,15,16,17],"fund":0,"fundament":[0,1,6,10,14,15,17],"funnel":0,"furnish":9,"further":[0,1,4,7,8,15,17],"futur":[0,1,5,7,10,15,17],"fuzz":0,"fw":15,"g":[2,5,14,15],"ga":2,"gain":[1,6,11,14,17],"galact":14,"game":0,"gap":[0,4],"gaslit":17,"gatekeep":[0,1,14,17],"gatewai":[2,4,8,10,14,15,17],"gbp":[0,15],"gcc":0,"gear":17,"gemini":0,"gen_tim":5,"gener":[1,2,4,5,6,7,8,10,11,14,15,16,17],"generalis":16,"genuin":[0,15],"geograph":[1,4,8],"geoid":8,"get":[0,1,2,5,6,7,8,10,11,12,15,17],"get_ag":11,"get_channel":[2,11],"get_config_obj":2,"get_data_s":11,"get_establishment_r":11,"get_expected_r":11,"get_external_ip":8,"get_first_hop_timeout":11,"get_hash":11,"get_inst":11,"get_lowest_interface_bitr":11,"get_mdu":11,"get_medium_path_timeout":11,"get_mod":11,"get_mtu":11,"get_packet_rssi":2,"get_packet_snr":2,"get_part":11,"get_private_kei":11,"get_progress":[2,11],"get_public_kei":11,"get_q":11,"get_random_hash":[2,11],"get_remote_ident":[2,11],"get_request_id":11,"get_respons":11,"get_response_tim":11,"get_rssi":11,"get_rtt":[2,11],"get_seg":11,"get_snr":11,"get_statu":11,"get_transfer_s":11,"geti2p":8,"ghost":17,"ghostscript":0,"ghz":14,"gi":2,"giant":17,"gift":[0,17],"gigabit":[10,17],"gigabyt":16,"gile":0,"git":[1,7,15],"githook":0,"github":[0,1,2,4,5],"gitlab":1,"give":[4,8,14,15,17],"given":[0,8,10,11,14,15,17],"glanc":0,"global":[0,2,5,7,8,10,11,14,15,16],"globe":17,"glue":[0,6],"gmail":1,"gnu":0,"go":[0,2,8,10,12,14,17],"goal":[0,4,7,8,10,16],"goe":[0,17],"goldsmith":0,"gone":0,"good":[0,1,4,5,6,10,14,15],"goodwil":17,"googl":0,"got":[0,2,17],"got_respons":2,"govern":[0,14,17],"government":1,"gp":8,"gpg":5,"gpio":12,"gpl":0,"grab":0,"grace":11,"gracefulli":[8,10],"grade":7,"graham":0,"grain":[1,5,15],"grammar":1,"grant":[1,5,7,9,15,17],"granular":5,"grape":2,"graph":1,"gratefulli":13,"graviti":[8,17],"great":[0,4,5,12,14,15,17],"greater":[0,5,11,14,15,16],"greatli":[4,8],"green":17,"grep":5,"grew":0,"grid":[12,17],"grift":0,"gross":17,"ground":[0,1,17],"groundswel":0,"group":[0,1,7,8,11,12,14,15],"group_id":8,"group_nam":5,"group_root":5,"groupinstal":4,"grow":[0,1,4,17],"grown":17,"growth":[0,1,4],"guarante":[0,5,10,13,14],"guard":17,"guess":[0,8],"gui":12,"guid":[4,6,7,12,14],"guidanc":0,"guidelin":[0,4],"guilt":12,"gun":17,"gw":8,"gz":5,"h":[0,5,15],"ha":[1,2,5,6,7,8,10,11,12,13,14,15,16,17],"habit":[0,17],"habitat":[0,17],"hack":16,"had":[0,1,14],"half":[0,14,16],"hallucin":0,"halo":0,"hammer":17,"hancom":0,"hand":[0,2,17],"handheld":14,"handl":[0,1,2,4,5,6,8,10,11,12,14,15,16],"handler":[2,11,15,17],"handshak":17,"hang":0,"happen":[2,4,5,7,8,11,14,17],"happili":0,"har":0,"hard":[0,4,6,8,17],"hardcod":17,"hardlin":10,"hardwar":[0,1,2,4,7,8,11,12,14,15,16,17],"harm":[0,7,9],"harper":0,"harvest":[0,1],"has_path":[2,11],"hasattr":2,"hash":[1,2,5,10,11,14,15,17],"hash1":14,"hash2":14,"hash_from_name_and_ident":11,"hashlib":14,"hashmap":2,"hasn":0,"hastili":0,"hater":0,"haunt":0,"have":[0,1,2,4,5,6,8,10,11,12,13,14,15,16,17],"hazard":[0,11],"hdlc":2,"he":[0,1],"head":5,"headcount":7,"header":[0,2,5,8,11,14],"header_1":14,"header_2":14,"headlin":0,"health":7,"healthi":0,"hear":[14,17],"heard":[0,1,11,12,14,15],"heart":17,"heavi":[0,17],"heavili":0,"height":8,"heir":0,"held":[0,8,15],"helium":2,"hellman":[10,14],"hello":5,"hello_world":5,"help":[0,2,4,5,10,12,13,14,15,17],"helper":[2,5,15],"here":[0,1,2,4,8,14,15,17],"herebi":[0,9],"herein":0,"heterogen":[4,7,16],"hex":[2,15],"hexadecim":[2,5,14,15],"hexbyt":15,"hf":[12,17],"hi":0,"hidden":[0,15],"hide":[0,4,17],"hierarch":[16,17],"hierarchi":[7,17],"high":[0,6,8,10,14,15,16,17],"higher":[0,1,8,10,16],"highest":11,"highli":[0,5,8,13,14,15],"highlight":7,"hijack":[0,17],"hint":[2,5],"hire":0,"histor":1,"histori":[1,5,7,12],"hit":[0,2],"hkdf":14,"hmac":[14,16],"hobbi":0,"hobbyist":0,"hoc":[16,17],"hold":[0,8,11,14,17],"holder":[0,9,10],"hole":17,"holi":0,"hollow":[0,17],"home":[1,4,5,10,12,15,17],"homebrew":8,"honest":0,"honesti":0,"honestli":0,"honour":0,"hook":[0,1,5],"hop":[8,10,11,14,15,16,17],"hope":0,"hopefulli":10,"hops_to":11,"hospit":0,"host":[0,1,2,5,7,8,10,12,14,15],"hostil":[0,7],"hostnam":[8,17],"hotspot":17,"hour":[0,2,5,8,15,17],"hous":[4,17],"how":[0,1,2,4,5,6,8,10,12,14,15,16,17],"howev":[0,4,5,8,10,14],"http":[1,2,5,12,13,17],"hub":[0,8,12,15],"hum":17,"human":[0,1,2,7,8,9,10,11,13,14],"humanli":0,"hundr":[0,1,10,12,17],"hungri":6,"hw_mtu":2,"hwrev":15,"hypothesi":0,"hypothet":0,"hysteria":0,"hz":[8,12,15],"i":[1,2,4,5,6,7,8,9,10,11,12,13,14,15],"i2p":[4,7,10,12,16],"i2p_tunnel":8,"i2pd":[4,8],"i2pinterfac":[4,8],"i7":0,"ia":15,"ibm":0,"ic":6,"ic_burst_freq":8,"ic_burst_freq_new":8,"ic_burst_hold":8,"ic_burst_penalti":8,"ic_held_release_interv":8,"ic_max_held_announc":8,"ic_new_tim":8,"ic_pr_burst_freq":8,"ic_pr_burst_freq_new":8,"icmp":12,"icon":[5,17],"id":[0,5,8,11,14,15],"id_callsign":8,"id_interv":8,"id_regex":5,"idea":[1,4,5,7,14,15,17],"ideal":[12,15,16],"ident":[0,1,2,4,7,8,10,11,12,15,16],"identif":[7,8,16],"identifi":[0,2,5,8,10,11,14,15,16],"identifyexampl":2,"identity_data":15,"identity_hash":11,"ideolog":0,"ideologi":0,"ie":15,"if00":15,"ifac":[2,4,8,10,14,15],"ifac_s":8,"ifconf":2,"ifconfig":15,"ignor":[0,5,8,10,11,13,14,15],"ignored_devic":8,"ii":7,"iii":7,"illegitim":0,"illus":[0,7],"illustr":[0,2,8,14],"imag":[0,17],"imagin":[0,1,10,17],"immedi":[0,1,5,8,15,17],"immens":[0,17],"immort":17,"immun":1,"immut":[1,17],"impact":[8,10,11,14],"impati":17,"imperson":10,"implement":[1,2,5,7,8,10,11,12,14,15,17],"impli":9,"implic":[0,7,8,10],"implicit":[0,10,11,14],"implicitli":15,"import":[0,1,2,4,6,8,10,11,14,15],"importantli":[0,1,4,17],"importlib":2,"impos":0,"imposs":[0,14,17],"impostor":0,"impract":15,"impress":0,"improv":[0,4,7,8,12,13,17],"in_fram":2,"in_wait":2,"inabl":0,"inaccess":0,"inactive_for":11,"inadvert":15,"inbound":[2,8,11],"inbox":10,"inc":0,"incant":0,"incent":1,"incivil":0,"includ":[0,1,2,5,6,7,8,9,10,11,12,14,16],"inclus":10,"incoher":0,"incom":[2,8,11,15],"incommensur":0,"incompat":[1,11,14],"incompet":1,"incomplet":10,"inconveni":1,"increas":[5,8,11,15],"incredibli":[0,1,17],"increment":1,"incur":[1,8],"indefinit":[1,15],"independ":[0,1,5,7,11,14],"index":[2,4,7],"indic":[2,5,11],"indiffer":17,"indirectli":[0,9,14],"indistinguish":0,"individu":[1,4,5,7,8,11,13,14,15,16,17],"induc":0,"industri":[0,1],"inertia":0,"inevit":[4,14],"infer":[0,14],"infinit":17,"infinitum":0,"influenc":1,"influx":[8,12],"influxdb":12,"info":[5,11,14,15,17],"inform":[0,1,2,4,5,7,8,10,11,12,13,14,15,17],"infrastructur":[5,6,7,8,10,12,14,15],"infring":0,"ingest":[0,17],"ingo":11,"ingress":[2,7],"ingress_control":8,"inhabit":17,"inher":[1,10],"inherit":[0,5],"init":4,"initi":[0,2,5,8,10,11,14,15,16],"initialis":[2,5,11,15],"inner":0,"input":[0,2,15],"inquiri":0,"insert":14,"insid":[0,5,14,17],"insidi":[0,1],"insight":17,"insist":0,"insofar":0,"inspect":[0,4,5,8,10,14,15],"inspir":[12,17],"instal":[0,1,2,5,7,8,14,15,16],"instanc":[0,1,2,7,8,11,12,14,15,17],"instance_control_port":15,"instance_nam":15,"instant":[0,17],"instanti":11,"instantli":15,"instead":[0,1,2,4,5,8,11,14,15,17],"institut":[1,7,17],"institution":0,"instruct":[0,4,5,17],"instrument":0,"insuffici":0,"int":[2,11],"intact":0,"integ":[2,11],"integr":[0,1,4,5,12,14,15,17],"intellig":[0,1,9,17],"intend":[0,4,8,10,14,15],"intens":15,"intent":[7,10,17],"intention":[0,12,14],"inter":[4,11,14],"inter_byte_timeout":2,"interact":[0,1,2,4,7,10,11,12,14,15,17],"intercept":10,"interchang":[5,8],"interconnect":[4,8,10,14],"interest":[0,12,14,17],"interestingli":0,"interfac":[0,1,5,6,7,10,11],"interface_class":2,"interface_discovery_sourc":[11,15],"interface_en":15,"interfer":[10,15],"interlock":0,"intermedi":[10,17],"intermediari":[7,8,14],"intermesh":0,"intermitt":[1,8,10,17],"intern":[0,1,2,5,8,11,14,15,17],"internal_1":8,"internet":[0,1,7,8,10,12,14,15,16,17],"internetwork":4,"interop":0,"interoper":[6,10,12,14,16],"interpret":[0,5],"interregnum":7,"interrfac":0,"interrupt":[5,8],"interv":[5,8,11,15],"intervent":15,"intiat":2,"intim":17,"intox":0,"intric":0,"introduc":[1,5,14,15,17],"introduct":7,"introductori":7,"intuit":[10,14,16],"invalid":[2,5,8,11],"invari":17,"invent":0,"inventori":0,"invers":[0,14],"invert":[0,17],"invest":[0,1,10],"investig":[0,5],"invis":[4,8],"invit":[0,8],"invok":[0,5],"involv":[0,1,5,10,13],"io":6,"iodin":12,"ioerror":2,"iot":6,"ip":[0,4,6,7,8,10,14,15,16],"ipv4":8,"ipv6":[4,8,15],"irc":12,"ironi":0,"irrelev":[1,13],"irrespons":17,"is_compress":11,"is_connected_to_shared_inst":2,"is_open":2,"is_path_respons":11,"is_ready_to_send":[2,11],"isdir":2,"isfil":2,"isinst":2,"ism":[6,14],"isn":[0,17],"isol":[0,4,8,15],"isp":[8,10,17],"issu":[0,1,5,7,8,10],"issuer":0,"iter":0,"its":[0,1,5,6,8,9,10,11,12,14,15,17],"itself":[0,1,5,7,10,12,14,15,17],"iv":[7,14,16],"j":[7,15],"jacobsen":0,"jail":15,"jame":0,"jan":[0,5],"javascript":12,"jihad":0,"job":[0,1,2],"join":[2,4,10,12,14],"journal":0,"journei":10,"jsc":0,"json":[15,17],"judg":0,"judgment":[0,1],"judici":0,"juli":0,"jurisdict":0,"just":[0,1,2,4,5,6,8,10,12,14,15,16,17],"justif":0,"justifi":0,"k":[2,15],"katzer":0,"kb":15,"kbp":15,"keep":[0,2,4,5,10,11,14,15,16,17],"keepal":[11,14],"keepalive_timeout_factor":11,"kei":[0,1,2,5,7,8,10,11,15,16,17],"kenakafrosti":0,"kept":[0,8,10,11,14],"kernel":[1,8,15,16],"keyboard":0,"keyboardinterrupt":2,"keyerror":11,"keypad":12,"keypair":[10,14],"keyr":17,"keyserv":5,"keyset":[14,15,16],"keysiz":11,"keystor":10,"keyword":5,"khz":8,"ki":2,"kib":0,"kill":[11,14,17],"kilobit":12,"kilomet":1,"kind":[0,1,6,8,9,10,12,14,17],"kindergarten":0,"kiss":[6,7,15,16],"kiss_fram":8,"kissinterfac":[8,15],"knew":0,"knife":0,"knock":17,"know":[0,1,2,4,5,6,8,10,11,14,15,17],"knowledg":[0,10,14],"known":[0,2,8,11,14,15,16,17],"ko":13,"krypton":2,"l":[5,8,15],"la":15,"label":[0,1],"labor":[0,17],"lack":[0,1,14],"laid":14,"lake":1,"lament":0,"lan":8,"landlord":17,"landmark":0,"landscap":0,"languag":[0,1,5,9,12],"lantern":17,"laptop":[0,10,17],"larg":[0,2,6,8,10,11,14,15,16,17],"larger":[10,14,15],"laser":8,"last":[0,2,5,8,11,15,17],"last_read_m":2,"last_unit":2,"lat":8,"late":0,"latenc":[4,10,14,16,17],"later":[0,2,4,5,8,12,15],"latest":[1,2,4,5,8,11,12,15],"latest_buff":2,"latest_client_link":2,"latitud":8,"latter":0,"laucnh":4,"launch":[0,12,15],"launder":7,"law":[0,17],"layer":[1,6,7,8,10,11,12,14,15,16,17],"layout":0,"lc":0,"lcd":12,"lead":[0,4,14,17],"leadership":0,"leak":[0,4],"learn":[0,2,4,9,10,13,14,17],"leas":[0,17],"leash":17,"least":[0,4,5,6,8,10,14,15,16],"leav":[0,1,10,12,14,17],"ledger":[14,17],"leech":1,"left":[1,8,11,14,15],"legaci":[0,10,17],"legal":[1,7,8,17],"legisl":[0,8],"legitim":[0,4,10,15],"legitimaci":0,"len":[2,17],"lend":0,"length":[0,2,5,8,11,14],"less":[0,2,4,6,8,11,14,16,17],"let":[0,1,2,4,5,8,10,12,14,15,17],"letter":0,"level":[0,1,5,6,8,10,12,14,15],"lever":17,"leverag":0,"li":[1,17],"liabil":[0,9],"liabl":9,"lib":0,"liber":[6,7],"liberapai":13,"libffi":4,"librari":[1,4,12,17],"librn":12,"licens":[7,14,16,17],"license":0,"licensor":0,"lie":17,"life":[0,1,17],"lifelin":17,"lifespan":1,"lift":[11,15,17],"light":[1,5,15,17],"lightli":0,"lightweight":16,"like":[1,2,4,5,6,7,8,10,11,12,14,15,16,17],"likewis":5,"limit":[0,2,4,5,6,7,9,10,12,14],"line":[1,2,4,7,8,12,14,15,16,17],"linger":15,"link":[0,1,4,5,6,7,8,10,11,12,15,16,17],"link_clos":2,"link_establish":2,"link_id":[2,11],"link_mtu_discoveri":11,"linkexampl":2,"linux":[0,1,4,5,6,8,12,14],"liquid":17,"list":[0,1,2,4,5,6,7,8,11,12,14,16,17],"list_deliv":2,"list_fil":2,"list_filt":15,"list_packet":2,"list_receipt":2,"list_timeout":2,"listdir":2,"listen":[1,2,4,5,7,12,14,15,17],"listen_ip":8,"listen_on":[4,8],"listen_port":8,"liter":[0,15],"literari":0,"literatur":0,"lithograph":0,"litig":0,"litter":17,"littl":[0,6,12,14],"live":[0,1,4,11,12,17],"lki":14,"lkr":14,"ll":[0,2,16,17],"llama":0,"llc":0,"llm":[0,12],"ln":15,"load":[0,2,4,5,6,8,11,15,16,17],"load_private_kei":11,"load_public_kei":11,"local":[0,1,2,4,5,7,8,10,11,12,14,16,17],"locat":[0,4,7,8,10,12,14,15,17],"location_cmd":8,"lock":[0,17],"log":[0,1,2,4,5,8,15,17],"log_crit":2,"log_error":2,"log_info":2,"log_verbos":2,"logdest":11,"logfil":15,"logic":[0,14,17],"login":15,"loginctl":15,"loglevel":[2,11,15],"lon":8,"long":[0,2,4,5,6,7,8,10,11,14,15,17],"longer":[0,2,4,8,14,15,17],"longest":8,"longitud":8,"look":[2,4,5,7,8,10,12,14,15,17],"loop":[0,2],"loopback":0,"loophol":0,"loos":0,"lora":[1,4,6,7,10,12,14,16,17],"lorawan":[6,14],"lord":0,"lose":[0,1],"loss":[1,8,10,12,14],"lost":17,"lot":[0,10,14,17],"loud":0,"loudest":[0,17],"low":[0,1,4,5,6,8,10,12,14,16,17],"lower":[2,4,8,15],"lowest":[1,11],"lowli":17,"ltu":6,"lunar":7,"luxuri":0,"lxmf":[0,5,7,8,10,11,15],"lxmfy":7,"lxst":7,"m":[2,4,5,14,15],"m4":0,"mac":[8,14],"machin":[1,4,7,9,14,15,17],"machineri":[0,12],"maco":[0,7,12],"mad":7,"made":[0,4,5,8,11,14,17],"madman":0,"magic":0,"magnitud":0,"mai":[0,1,4,5,8,10,11,12,14,15,17],"mail":0,"mailbox":12,"mailmap":5,"main":[0,2,5],"maintain":[0,1,4,5,10,14,15,16],"mainten":[0,4,10,14,17],"major":[0,5],"make":[0,1,2,4,5,6,8,10,12,14,15,16,17],"maker":0,"malici":[1,4,5,10,14,15],"malinform":13,"man":0,"manag":[0,1,4,7,10,11,12,14,16,17],"mani":[0,2,4,5,6,8,10,11,12,14,15,16,17],"manifest":[1,5],"manipul":[14,16,17],"manjaro":4,"manner":[0,5,10,12],"manual":[0,1,2,4,8,10,11,12,14,15,16,17],"manufactur":[0,6,8],"map":[8,12,14,15,17],"mar":0,"margin":0,"mark":[0,4,5,8,9,11,14,16,17],"markdown":5,"marker":0,"market":[0,1,17],"markqvist":[2,13],"markup":[5,12],"mask":1,"masquerad":0,"mass":[0,17],"massiv":[0,8],"mast":0,"master":[2,5,11,17],"match":[0,1,2,5,8,14,15,17],"materi":[0,12],"math":17,"mathemat":[0,17],"matter":[1,4,7,8,10,17],"matur":15,"max":15,"max_request_s":11,"max_response_s":11,"maximum":[2,8,11,14,15],"mayb":17,"mb":[0,5],"mbp":[0,15],"md":[0,5],"mdu":[2,11],"mdy":0,"me":[0,8,17],"mean":[1,4,5,6,7,8,10,11,14,15,17],"meaning":[0,4],"meaningfulli":0,"meaningless":[0,1],"meant":0,"meantim":11,"measur":[0,1,10,14,17],"mechan":[0,1,4,5,7,8,10,15,16],"media":[0,5,17],"mediat":[0,1],"mediev":17,"medium":[0,1,2,6,7,8,10,11,12,14,16],"meet":[0,1,17],"megaphon":17,"member":[0,4,14],"membership":0,"memori":[0,4,10,14],"men":1,"mental":17,"mention":[0,8,14],"menu":2,"menu_mod":2,"merchant":9,"merci":0,"mere":[0,1,17],"merg":[0,1,9,15],"merit":[0,7],"mesh":[0,4,8,10,14,15,16,17],"meshchat":0,"meshchatx":7,"messag":[0,2,4,5,7,10,11,14,15,17],"message_class":11,"messagebas":[2,7,11],"messagecallbacktyp":11,"messeng":[10,14],"met":[0,1,4,17],"meta":5,"metadata":[1,4,5,8,14,15,17],"metaphor":17,"metavar":2,"meter":8,"meth":2,"method":[2,4,7,8,11,14],"methodologi":[0,14,15],"metric":[1,12],"mevpekyafshak5wr":8,"mhz":[8,14],"mi":2,"mib":0,"microcontrol":[0,14],"micromanag":17,"micron":[5,7],"microsoft":0,"microsystem":0,"microwav":10,"mid":0,"middl":1,"middlemen":1,"midjournei":0,"midnight":0,"might":[0,1,4,5,8,10,14,15,17],"migrat":1,"mikrotik":6,"mileston":0,"millimet":6,"million":[0,17],"millisecond":[2,10,15,17],"mind":[0,1,10,17],"mindset":17,"mine":[0,17],"minefield":0,"miner":17,"minim":[0,1,6,7,8,14],"minimalsampl":2,"minimum":[0,2,4,8,11,14,15,17],"minimum_bitr":11,"miniscul":17,"minor":5,"mint":0,"minut":[0,2,4,5,8,14,17],"mirror":[0,1,7,10,12,15],"mirror_interv":5,"mischaracteris":0,"misconcept":0,"misconfigur":4,"mislabel":0,"mislead":[0,4,17],"mismatch":0,"misrepres":0,"miss":[0,2,4,5],"missil":17,"mistak":[0,4],"mistaken":0,"misunderstand":13,"mit":0,"mitig":10,"mix":[7,8,10,15],"mixtur":16,"mobil":[8,10,12,14],"mode":[0,1,2,4,5,6,7,10,11,12,14,15,16,17],"model":[0,1,9,10,15,17],"modem":[7,8,10,11,12,14,16,17],"modem73":7,"modem73interfac":12,"moder":[0,8],"modern":[0,4,6,12,17],"modif":[0,1,5],"modifi":[0,1,5,8,9,10,14,15],"modul":[0,2,4,5,6,7,8,10,14,15,16],"modular":12,"moment":[0,14,17],"momentarili":8,"mon":5,"monei":0,"monero":13,"monet":1,"monitor":[4,8,12,14,15],"monolith":1,"monopol":1,"month":0,"monthli":0,"mood":0,"moon":2,"moral":[0,17],"more":[0,1,2,4,5,6,8,10,11,12,13,14,15,16,17],"moron":0,"moscow":[12,14],"most":[1,4,6,7,8,10,11,12,14,15,17],"mostli":[0,8,10,14],"motion":0,"motiv":[0,7],"mountain":[0,17],"move":[0,5,8,10,14,15,17],"movement":7,"msg":0,"msgpack":5,"msgtype":[2,11],"msvc":0,"mtu":[2,8,11,12,14,16],"mu":5,"much":[0,4,8,10,12,14,15,16,17],"muddi":0,"multi":[1,7,10,12,14,15,16],"multicast":8,"multicast_address_typ":8,"multilater":14,"multipl":[1,2,5,8,10,12,14,15],"multiplex":[2,15],"multipoint":14,"multitud":4,"mumbl":12,"music":[0,1,12],"must":[0,1,2,4,5,8,10,11,14,15,17],"mutat":0,"mutual":17,"mw":8,"my":[0,5,8,14,17],"my_fil":15,"my_ident":15,"my_network":[14,15],"my_network_ident":8,"my_nod":5,"myapp":5,"mycal":8,"myfork":5,"myfriend":17,"mymirror":5,"myrepo":5,"myriad":14,"mysteri":17,"n":[0,2,5,14,15],"name":[0,2,4,5,7,8,10,11],"name_of_other_branch":5,"namespac":2,"nano":5,"narg":2,"narr":0,"narrat":0,"narrow":0,"nat":[4,8],"nation":[0,1],"nativ":[0,2,10,12],"natur":[0,1,8],"navig":[0,1,5],"ncurs":12,"nearbi":[8,10],"nearest":8,"nearli":0,"nears":14,"neat":10,"neccessari":8,"necesarri":8,"necess":[0,1],"necessari":[0,1,5,6,8,10,11,14,15],"necessarili":14,"need":[0,1,2,4,5,6,7,8,10,11,12,13,14,15,16,17],"needless":8,"neg":[4,8],"neglig":[14,17],"negoti":[1,17],"neighbor":17,"neighbour":0,"neighbourhood":0,"neither":[0,11,14],"neon":2,"neopixel":15,"nerd":5,"net":8,"netcat":8,"network":[1,2,6,8,11,15,16,17],"network_ident":[8,14,15],"network_nam":[4,8],"neutral":[0,10,17],"never":[0,1,8,10,11,14,17],"new":[1,2,4,5,7,10,11,14,15,17],"new_id":15,"new_ident":15,"newcom":0,"newer":[11,14],"newest":14,"newli":[8,11,14],"newlin":[8,15],"next":[0,1,4,5,8,11,14,17],"next_hop":11,"next_hop_interfac":11,"nice":17,"nicknam":14,"nilli":0,"nine":0,"nixo":0,"no1cll":8,"no_data_for":11,"no_id":5,"no_inbound_for":11,"no_outbound_for":11,"no_std":0,"noauth":15,"nobl":2,"noble_ga":2,"noble_gas":2,"nobodi":[0,5],"nocheck":15,"node":[0,1,4,7,8,15,16],"node_nam":5,"noid":15,"nois":[0,1,8,11,13,17],"noisi":17,"nomad":[1,7],"nomadnet":[0,5,12],"non":[0,4,5,8,11,12,14,17],"none":[0,2,4,5,8,11,14,15],"nonetheless":0,"nonexclus":0,"noninfring":9,"nonsens":0,"nonstandard":0,"nor":[0,4,17],"normal":[0,1,2,4,5,8,11,14,15],"nostalgia":1,"notabl":12,"notar":5,"notaris":0,"notat":[14,15],"note":[0,1,2,5,6,7,8,11,14,15],"noth":[0,5,6,10,16,17],"notic":[0,5,8,9,14,15],"notif":[1,2,11],"novel":0,"now":[0,2,4,5,14,15,17],"nowher":0,"np":15,"nrf":0,"nrf52":6,"nrf52840":0,"nt":2,"nuanc":0,"null":5,"num":2,"number":[0,2,4,5,8,10,11,12,14,15,17],"nurtur":1,"o":[0,2,4,5,8,14,15,16],"obj":5,"object":[0,2,5,11,17],"oblig":0,"obscur":[0,1,4],"observ":[0,1,10,17],"obsess":0,"obsolet":17,"obstacl":17,"obtain":[0,1,4,6,7,9,11,14],"obviou":0,"obvious":14,"oc":0,"occas":0,"occasion":0,"occupi":17,"occur":[0,2,8,15,16],"ocean":17,"ocur":11,"odd":2,"odin":12,"off":[0,4,8,10,12,14,15,16,17],"offer":[0,1,4,7,8,10,11,12,14,15,17],"offic":[0,10],"offici":0,"offlin":[0,1,2,4,7,10,12],"often":[0,1,4,8,10,14,15,17],"oganesson":2,"old":[0,1,2,4,6,12,13,14,17],"older":[0,15],"oldest":0,"omit":11,"on_interfac":11,"onc":[0,2,4,5,6,8,11,14,15,16,17],"one":[0,1,2,4,5,6,8,10,11,12,14,15,16,17],"ones":[0,4,8,10,15],"oneself":14,"ongo":12,"onli":[0,1,2,4,5,6,8,10,11,12,14,15,16,17],"onlin":[0,2,11,15,17],"onto":[0,4,14,17],"ontolog":1,"opaqu":[0,10],"open":[1,2,4,5,6,7,8,10,11,12,13,14,15,16],"open_port":2,"openli":0,"openmodem":[8,16],"openssl":[4,14],"openwrt":7,"oper":[0,1,4,5,6,8,10,11,12,14,15,16,17],"opinion":[1,13],"opkg":4,"opportun":4,"opportunist":10,"oppos":[0,5],"opposit":[0,2,8],"oppress":17,"opt":14,"optic":[6,16,17],"optim":[0,1,4,5,10,17],"option":[1,2,4,5,6,7,10,11,12,14,15,17],"oracl":0,"orbit":0,"ord":2,"order":[0,1,2,8,11,15],"ordinari":0,"organ":[0,1,4,5,10,14,15,17],"organis":[0,8],"organiz":1,"orient":[0,14],"origin":[0,2,5,7,10,14,17],"orthodoxi":0,"orthogon":1,"os":4,"osi":0,"other":[1,2,4,5,6,7,8,9,10,11,12,14,15,17],"otherwis":[0,2,5,8,9,11],"our":[0,2,8,14,17],"ourselv":10,"out":[0,2,6,8,9,11,12,14,15,17],"outag":1,"outbound":[8,11,14],"outcom":0,"outdat":0,"outgo":[2,8,11,14,15],"outlast":1,"outlin":[0,4,6,14],"outperform":0,"output":[1,5,7,8,15],"outrag":0,"outright":0,"outsid":[0,11,16,17],"outward":0,"over":[0,2,6,7,8,10,11,13,14,15,16,17],"overal":[7,14,15],"overcom":14,"overhead":[4,6,8,14,17],"overlai":4,"overlap":0,"overli":13,"overlord":17,"overrid":[2,5],"oversight":[0,14],"overview":[5,7,8],"overwhelm":[0,1,8,10,11],"overwrit":15,"ow":0,"own":[0,1,2,4,5,10,11,12,14,15,16,17],"owner":[0,2,17],"ownership":[0,1,17],"p":[2,5,15],"pace":1,"pack":[2,11],"packag":[0,1,2,4,5,8,12,14],"packb":2,"packed_s":2,"packet":[0,1,2,4,7,8,10,11,12,15,16,17],"packet_callback":2,"packet_deliv":2,"packet_hash":2,"packet_receipt":[2,11],"packet_timed_out":2,"packetreceipt":[2,7,11],"pad":[14,16],"page":[0,1,7,14,15,17],"page_cont":5,"pagin":5,"paid":1,"painstak":0,"painstakingli":0,"pair":[8,11,14,15],"pamac":4,"panic":[2,15,17],"panic_on_interface_error":[2,15],"panopticon":17,"paper":12,"par":[2,4],"paradigm":0,"paradox":17,"parallel":4,"paralysi":17,"param":2,"paramet":[2,6,7,11,14,15],"paranoia":17,"parasit":[0,17],"pariti":[0,2,8,15,16],"parity_even":2,"parity_non":2,"parity_odd":2,"pars":[2,5],"parse_arg":2,"parser":[0,2,5,7],"part":[0,1,2,4,5,8,10,11,12,14,15,17],"parti":[0,1,5],"partial":[0,5],"particip":[0,1,4,7,8,10,14,15,17],"participant_nod":0,"particular":[0,1,2,6,8,9,14,15],"particularli":[1,5,8,15],"partli":0,"partner":17,"partylin":7,"pass":[2,5,6,8,10,11,14,15,17],"passphras":[4,8,10,14],"passport":14,"past":[0,15],"patch":[1,5,17],"patent":0,"path":[0,1,2,4,5,6,7,10,11,12,14,15,17],"path_respons":11,"pathfinder_m":11,"patient":17,"pattern":[1,5,6,7,10,14],"paydai":0,"payload":[0,2,5,8,11,14,15],"pc":0,"pdf":7,"peac":17,"peach":2,"peak":[0,5],"peer":[0,1,2,4,5,8,11,12,14,15,17],"penalti":8,"pend":[2,14],"peopl":[0,1,4,12,14,17],"per":[0,1,5,8,10,11,12,14,15,16,17],"percent":[2,8],"percentag":11,"perfect":0,"perfectli":[1,4],"perform":[1,2,4,5,7,8,10,11,12,14,15,17],"perhap":[4,17],"period":[0,2,5,8,11,14,15],"peripher":12,"peripheri":0,"perlmutt":0,"perm":5,"perman":[4,8,15],"permiss":[0,1,7,8,9,15,17],"permissionless":[1,14,15],"permit":9,"perpetu":[0,17],"persecut":14,"persist":[1,7,8,15,17],"person":[0,7,8,9,10,13,14],"personag":1,"perspect":[0,1,8,10,11,14,16],"persuad":0,"pervers":0,"pet":2,"petit":17,"petti":0,"phantom":0,"phenomenon":0,"philosophi":[1,13,14,16,17],"phoenix":0,"phone":[0,4,7,10,17],"phonebook":12,"phonorecord":0,"photo":17,"photocopi":0,"phrase":0,"phy":15,"physic":[0,1,2,4,5,6,7,8,10,11,12,14,15,16],"pi":[1,2,7,10,14,15,16,17],"pick":[0,14],"pictur":0,"piec":[0,14],"pierian":0,"pillar":17,"pin":0,"ping":[15,17],"pip":[0,2,4,5,6,8,15],"pip3":4,"pipe":[7,12,15,16,17],"pipeinterfac":[6,8,12],"pipelin":0,"pipx":4,"piss":0,"pitch":0,"pitfal":14,"pkcs7":[14,16],"pkg":4,"place":[2,4,5,7,8,10,14,15,17],"plaform":6,"plain":[0,2,6,8,11,14,17],"plain_mdu":11,"plainli":0,"plaintext":[2,11,17],"plan":[0,10,14,17],"planet":[1,17],"planetari":[16,17],"platform":[0,2,5,6,7,12,14,15,17],"plausibl":0,"plausibli":0,"pleas":[2,4,8,11,15,17],"plenti":[10,14],"plethora":0,"plu":11,"plug":[15,17],"plugin":[12,17],"plural":0,"pmr":14,"pocket":0,"poetri":17,"point":[0,1,4,5,6,8,10,12,13,14,15,17],"pointer":4,"polici":[0,1,15,17],"polish":0,"polit":[0,17],"pomelo":2,"pope":0,"popul":[0,11],"popular":[0,16],"port":[0,2,4,6,7,8,10,14,16,17],"port0":15,"portabl":[1,7,8,10,14],"portion":[0,9],"pose":[0,4,14],"posit":[2,5,7,8,15],"posix":4,"possess":[0,8,14,17],"possibl":[1,4,5,6,7,8,10,11,12,14,15,16],"possibli":[0,4,14],"post":[0,2,7],"postfix":15,"postur":0,"potenti":[0,1,2,4,5,8,13,14,15,16,17],"pour":0,"power":[0,1,4,6,7,8,10,12,14,15,16],"powershel":4,"ppp":2,"pr":15,"practic":[0,1,5,10,14,16,17],"prais":0,"pre":[0,4,10,11,14],"preambl":[8,15],"prebuilt":0,"preced":[0,2,5],"preciou":10,"precis":[0,1],"precompil":4,"predatori":17,"predic":1,"predict":[7,14],"preexist":0,"prefer":[0,4,5,13,14],"prefer_ipv6":8,"prejudic":0,"premis":[0,10,17],"prepar":[0,2,14],"preposter":0,"prerequisit":[7,11],"prescrib":1,"presenc":[0,1,7,8,11],"present":[1,7,8,10,12,14,15,17],"preserv":[0,1,4,7,11],"preshar":14,"press":[0,2],"pressur":1,"prestig":0,"pretend":[0,12,14],"prettyhexrep":2,"prevent":[0,1,5,8,10,15,17],"preview":5,"previou":[0,2],"previous":[0,8,11,14],"price":17,"primari":[0,1,5,14,17],"primarili":[0,8,10,12],"primarlii":4,"prime":0,"primit":[0,4,5,7],"princip":0,"principl":[1,7,10,14,16],"print":[2,5,8,15],"print_filelist":2,"print_help":2,"print_menu":2,"prior":0,"priorit":[0,8,10,14,17],"prioriti":[0,8,14],"prioritis":[7,8,10,11],"privaci":[4,8,12,17],"privat":[1,4,5,8,10,11,12,13,14,15,16,17],"private_ret":4,"privileg":[1,15,17],"prn":7,"probabl":[0,2,4,5,10,14,15,16],"probe":15,"problem":[0,1,4,5,14,15,17],"problemat":5,"proc":15,"proce":0,"procedur":[0,4,5,11,14],"process":[0,1,2,4,5,6,8,10,11,14,15,17],"process_incom":2,"process_outgo":2,"proclaim":0,"proclam":0,"produc":[0,1],"product":[0,1,15,17],"profil":17,"profit":[0,17],"profound":[10,17],"profoundli":17,"program":[0,2,5,6,7,8,11,14,16],"program_setup":2,"programm":[0,14],"programmat":[1,14],"progress":[0,2,5,11,15,16],"progress_callback":11,"prohibitevli":0,"project":[1,5,7,12,13,17],"promis":[0,4],"promot":0,"prompt":[0,2,4],"prompter":0,"proof":[0,1,2,5,8,10,11,14,15,17],"proof_packet":2,"proof_requested_callback":11,"proof_strategi":11,"prop":0,"propag":[0,7,8,10,11,12,15,17],"proper":4,"properli":[0,4],"properti":[1,2,7,10,11],"proport":14,"proportion":0,"propos":[0,7],"prose":0,"prospect":1,"protect":[1,4,7,15,17],"protocol":[0,4,5,6,7,8,10,16],"prototyp":12,"proudli":0,"prove":[0,2,5,10,14,17],"prove_al":[2,11],"prove_app":11,"prove_non":11,"proven":[1,5,11,14],"provid":[0,1,2,4,5,6,7,8,9,10,11,12,14,15,16,17],"provis":[0,6,15],"prv_byte":11,"prx":15,"pseudo":15,"psycholog":[1,17],"ptt":12,"ptx":15,"pub_byt":11,"pub_to_fil":11,"public":[0,2,5,7,8,10,11,12,13,15,16],"public_inform":2,"publicli":[0,4,8,14,15],"publish":[0,1,4,5,7,8,9,11,14],"publish_blackhol":15,"publish_blackhole_en":11,"publish_ifac":8,"pull":[0,1,5],"puppet":17,"purchas":[4,14,16],"pure":[0,7,14],"purg":11,"purport":0,"purpos":[1,4,5,6,7,8,9,10,11,12,14,15,17],"purposefulli":[0,9],"pursu":1,"push":[1,5,12,17],"put":[0,1,2,6,8,15],"pwd":5,"py":[0,2,5,14,15],"py3":[4,5],"pyca":[4,14],"pyd":0,"pygment":5,"pyseri":[2,4],"python":[0,1,2,5,7,8,11,14,15,16,17],"python3":[2,4,6],"pyximport":0,"q":[2,5,15],"q8_k_xl":0,"qlen_in_announc":8,"qlen_in_data":8,"qlen_in_il":8,"qlen_in_pr":8,"qr":[10,12],"qua":0,"qualiti":[0,1,11,15,17],"quant":0,"quantiti":1,"quantum":0,"queri":[2,4,5,11,15,17],"question":[4,6,7,8],"queu":[11,15],"queue":[7,14,15],"quick":15,"quickli":[0,8,12,14,16],"quiet":[5,8,15,17],"quietest":0,"quill":0,"quinc":2,"quirk":4,"quit":[0,2,4],"quot":[0,5],"qvist":[9,16],"r":[0,2,5,8,14,15],"radic":17,"radio":[0,1,7,8,10,11,12,14,15,16,17],"radon":2,"rain":0,"rais":[0,1,2,11],"rak":6,"ram":[0,4,8],"ramp":0,"randint":2,"random":[0,1,2,11,13,14],"random_text_gener":2,"randomis":14,"randomli":[2,14],"rang":[0,1,2,4,5,6,8,10,14,15,16,17],"rapidli":[0,8],"rare":[4,8],"raspberri":[1,7,10,14,15,16],"rasperri":4,"ratchet":[0,11,14],"ratchet_count":11,"ratchet_expiri":11,"ratchet_id_receiv":11,"ratchet_interv":11,"ratchets":11,"ratchets_path":11,"rate":[0,2,4,7,11,12,15],"rather":[0,1,4,11,15,17],"ratio":[0,1,11],"rational":0,"raw":[0,2,5,6,11],"rawchannelread":[7,11],"rawchannelwrit":[7,11],"rawiobas":11,"rawmu":5,"rb":2,"re":[0,1,2,8,10,11,14,15,17],"reach":[0,1,4,7,8,10,11,15,17],"reachabl":[1,2,4,8,10,11,12,14,15],"reachable_on":8,"reaction":0,"reactiv":[0,1],"read":[0,1,2,4,5,8,10,11,12,14,15,16,17],"read_loop":2,"readabl":[0,1,2,8,11,14,15,17],"reader":[0,2,11],"readi":[2,4,6,8,11,16,17],"readili":16,"readm":[0,5],"readwrit":5,"ready_byt":[2,11],"ready_callback":11,"real":[0,1,4,6,8,10,12,15,16,17],"realist":0,"realiti":[1,4,17],"realiz":17,"realli":[0,8,11,15,17],"realm":17,"realtim":12,"reappear":8,"reason":[4,7,8,11,14,15],"reassembl":14,"reboot":17,"rebroadcast":10,"rebuilt":17,"rebutt":0,"recal":[0,2,5,11,15],"recall_app_data":11,"recap":14,"recast":0,"receipt":[2,10,11,14],"receiv":[0,2,4,5,8,10,11,13,14,15,17],"receive_path_respons":11,"receive_stream_id":11,"received_announc":[2,11],"recent":[0,11,14],"recept":11,"reception_rssi":2,"reception_snr":2,"reception_stat":2,"recip":4,"recipi":[1,5,14,17],"reciproc":0,"reclaim":17,"recogn":[0,14,17],"recognis":[0,15],"recognit":[1,7,17],"recombin":1,"recommend":[1,2,4,8,14],"recondit":15,"reconnect":[2,10,17],"reconnect_port":2,"reconstruct":14,"record":[0,1,4,5,14],"record_stat":5,"recov":1,"recoveri":8,"recreat":14,"recur":0,"recurr":0,"recurs":[0,8,11,14],"recursive_pr":8,"recycl":7,"red":0,"reddit":0,"redistribut":0,"reduc":[0,8,15],"redund":[4,10],"reestablish":1,"ref":5,"refactor":[0,5],"refer":[1,2,4,5,7,8,12,15,17],"referenc":0,"referenti":0,"refin":[0,13],"reflect":[0,1],"reflex":0,"refram":0,"refresh":15,"refus":[0,10],"refut":0,"reg":0,"regain":[1,4],"regard":[0,1,12,14],"regardless":[0,1,5,8],"regener":0,"region":8,"regist":[0,2,5,11,15],"register_announce_handl":[2,11],"register_message_typ":[2,11],"register_request_handl":[2,11],"registr":0,"registrar":17,"registri":[1,17],"regrett":0,"regul":8,"regular":15,"regularli":0,"regulatori":17,"regurgit":[0,17],"reid":0,"reimplement":0,"reintroduc":1,"reject":[0,5,17],"rel":[0,5,6,8,10,14],"relai":[0,4,7],"relat":[0,4,5],"relationship":[0,1,10,14,15,17],"releas":[0,1,4,7,8,14,15,17],"relev":[0,2,4,5,6,8,11,14,15,17],"reli":[0,4,5,8,10,13,14,15,17],"reliabl":[0,4,6,8,10,11,14,16,17],"relianc":[4,17],"relief":10,"reload":15,"rem":2,"remain":[0,1,5,8,10,11,12,14,17],"remaind":0,"rememb":[8,10,11,14],"remind":0,"remot":[2,7,11,14],"remote_ident":[2,5,11],"remote_identifi":2,"remote_management_allow":15,"remote_management_en":11,"remote_nod":5,"remote_p":2,"remotesensor":14,"remov":[0,1,5,11,15,17],"remove_message_handl":11,"remove_ready_callback":11,"ren":7,"renam":0,"render":[5,12,17],"rendezv":0,"rent":17,"rental":0,"reopen":5,"rep":0,"repeat":0,"repeatedli":[0,1,8],"replac":[0,1,2,4,10,11,12,14,15,17],"repli":[0,2,15],"replic":[1,6,14],"reply_data":2,"reply_messag":2,"reply_text":2,"repo":[5,15],"repo_nam":5,"report":[0,8,11,13,15],"repositori":[0,1,4,7,12,14,15,17],"repres":[0,1,10,14,17],"represent":[2,11,17],"reproduc":0,"reproduct":0,"republish":0,"repudi":5,"repurpos":4,"reput":[14,15],"request":[0,1,5,7,10,11,14,15,16,17],"request_destin":2,"request_fail":2,"request_id":[2,11],"request_packet":2,"request_path":[2,11],"request_receipt":[2,11],"request_receiv":2,"requested_at":[2,11],"requestexampl":2,"requestor":8,"requestreceipt":[7,11],"requir":[0,1,2,4,5,8,10,11,12,14,15,16,17],"require_shared_inst":11,"required_discovery_valu":[11,15],"rerout":17,"rescu":7,"research":1,"resend":11,"reserv":[0,2,10,11,17],"reset":5,"reshap":1,"resid":[8,12,17],"resili":[0,1,4,10,12,15,16,17],"resist":10,"resiz":15,"resolut":[0,4,5,8,14],"resolv":[5,7,8,10,14,15],"resort":0,"resourc":[0,2,4,7,8,10,11,13,15,17],"resource_callback":11,"resource_sending_conclud":2,"resource_strategi":11,"respawn":8,"respawn_delai":8,"respawn_interv":8,"respect":[0,1,5,8,10,16,17],"respond":[0,1,2,10,11,15],"respond_to_prob":15,"respons":[1,7,8,11,14,16,17],"response_callback":[2,11],"response_gener":[2,11],"rest":[0,10,16,17],"restart":[8,15],"restartsec":15,"restor":[0,4,7,8,17],"restrict":[5,7,8,9,10,15,17],"resubmit":5,"result":[0,1,2,4,8,14,15],"ret":[7,14],"retain":[0,1,5,8,11],"retained_ratchet":11,"retibb":7,"reticul":7,"reticulum":[1,2,8,10,11],"retipedia":7,"retransmiss":[10,14],"retransmit":[8,14],"retreat":0,"retri":[0,2,5,11,14],"retriev":[1,5,11,15],"return":[1,2,8,11,15,17],"reveal":[4,11,14,16],"revers":[0,15],"revert":0,"review":[0,1,14],"revis":[0,15],"revisit":0,"revok":[5,17],"revolut":17,"revolutionari":1,"rewrit":0,"rfe":15,"rhetor":0,"rich":12,"ridicul":[0,14],"right":[0,1,4,5,8,9,17],"rigid":17,"rigor":0,"rington":12,"rippl":17,"risc":7,"risk":[5,14],"riski":17,"rm":5,"rmap":4,"rn":[0,1,2,4,5,6,7,11,14,15,17],"rncp":7,"rngc":5,"rngit":[1,7,12],"rngit_config":5,"rnid":[5,7,14],"rnmon":7,"rnode":[4,7,12,14,15,16],"rnodeconf":[6,7,12],"rnodef3b9":8,"rnodeinterfac":[4,6,8,15],"rnodemultiinterfac":8,"rnpath":[4,7,8],"rnphone":12,"rnprobe":[4,7],"rns_bin_dir":15,"rns_config":5,"rns_remot":5,"rnsconfig":[5,15],"rnsd":[0,4,7],"rnsh":[7,12],"rnspure":[4,14],"rnstatu":[4,7,8,14],"rnstransport":[8,15],"rnx":7,"road":[7,17],"roadmap":17,"roam":[7,8,10],"robot":14,"robust":[4,8,17],"role":[7,10],"roll":8,"rom":15,"room":[7,12,17],"root":[0,5,14],"rotat":[8,11,14,15],"rou":0,"roughli":0,"round":[0,2,11,15],"roundtrip":0,"rout":[0,4,6,8,10,11,12,14,15,16,17],"router":[4,8,10,15,17],"row":0,"rpc":15,"rpc_kei":15,"rprogress":2,"rrc":7,"rrcd":12,"rsg":[1,5],"rsm":[1,5],"rssi":[2,11,15],"rtsct":2,"rtt":[2,11,14],"rttstring":2,"rubber":17,"rule":[0,1,4,5,7,8,10,15,17],"run":[0,1,2,4,5,6,8,10,11,12,14,15,16,17],"runtim":[2,14],"rural":0,"rust":[0,4],"rvp":5,"rw":5,"rx":[8,15],"rxb":2,"ryzen":0,"s3":0,"sa":12,"safe":[5,14,15,17],"safeguard":17,"safer":0,"safeti":[0,1],"sai":[0,1,17],"said":[0,5,14],"sake":[0,1],"sale":0,"same":[0,1,2,4,5,6,8,10,11,12,14,15,17],"sand":17,"saniti":0,"saroni":0,"satellit":10,"satisfi":[0,14],"satur":1,"saturdai":0,"save":[0,5,11,14,15],"save_error":2,"saved_filenam":2,"saw":[0,12],"scaffold":0,"scalabl":[15,16],"scale":[0,14,16,17],"scan":[4,8],"scandal":0,"scarc":17,"scarcest":0,"scarciti":7,"scatter":17,"scenario":[0,4,8,14],"schedul":[0,5,11],"scheme":[1,5,6,8,15],"scholarship":0,"school":[0,12],"scienc":0,"scope":[5,7,8],"scrape":[0,17],"scratch":[0,6,17],"scream":0,"screen":[0,2],"script":[1,4,5,6,8,15],"scrub":0,"scrubber":0,"scrutini":[0,14],"sd":1,"sdk":0,"se":5,"sea":8,"seal":0,"seamless":[8,12,14],"seamlessli":[8,10,12],"search":[7,11,12],"seat":0,"second":[0,2,8,10,11,12,14,15,16,17],"secreci":[8,11,12,14,16],"secret":[0,10,14],"section":[0,4,5,6,8,10,12,14,15],"secur":[0,1,4,5,7,10,12,14,15,16,17],"sed":5,"see":[0,2,4,5,8,10,11,14,15,16,17],"seek":[0,1,14,17],"seem":0,"seen":[0,8,14,17],"segment":[2,4,8,10,11,14,15,16],"seiz":0,"seizur":0,"selden":0,"select":[0,2,4,5,8,17],"self":[0,1,2,4,5,14,16,17],"sell":[0,9],"semant":0,"semi":[5,15],"semtech":6,"send":[0,2,5,8,10,11,14,15,17],"send_stream_id":11,"sender":[2,4,10,14,15],"sens":[0,1,10,17],"sensibl":[4,6,8],"sensit":17,"sensor":[10,14],"sent":[2,4,10,11,14,15,17],"sentenc":0,"sentiment":[0,14],"separ":[0,1,2,8,10,12,14,15],"sept":0,"septemb":0,"seq":5,"sequenc":[0,2,5,11,14,15,16],"sequenti":[5,16],"serfdom":17,"seri":[0,6],"serial":[0,2,4,7,10,14,16,17],"serialinterfac":[6,8],"seriou":[0,1],"serious":0,"serv":[0,1,2,6,7,8,10,12,13,14,15,17],"serve_nomadnet":5,"serve_path":2,"server":[0,1,2,4,5,6,7,10,11,12,14,15,17],"server_buffer_readi":2,"server_callback":2,"server_client_connect":2,"server_destin":2,"server_fil":2,"server_ident":2,"server_link":2,"server_loop":2,"server_message_receiv":2,"server_packet_receiv":2,"servic":[0,1,4,5,7,8,10,12,14,17],"session":[4,12,14,15],"set":[0,1,2,4,5,6,8,10,11,12,14,15,16,17],"set_default_app_data":11,"set_delivery_callback":[2,11],"set_link_closed_callback":[2,11],"set_link_established_callback":[2,11],"set_max_request_s":11,"set_packet_callback":[2,11],"set_proof_requested_callback":11,"set_proof_strategi":[2,11],"set_ratchet_interv":11,"set_remote_identified_callback":[2,11],"set_resource_callback":11,"set_resource_concluded_callback":[2,11],"set_resource_started_callback":[2,11],"set_resource_strategi":[2,11],"set_retained_ratchet":11,"set_timeout":[2,11],"set_timeout_callback":[2,11],"settl":0,"setup":[2,4,5,7,8,15],"sever":[0,4,5,10,11,12,15],"sf":15,"sh":8,"sha":[10,11,14],"sha256":[5,14,16],"shall":[0,9,14],"shallow":0,"shame":0,"shape":[0,10,17],"share":[0,1,2,4,10,11,12,14,15,17],"share_inst":15,"shared_instance_port":15,"shared_instance_typ":[11,15],"sharper":0,"shatter":0,"shelf":[14,16],"shell":[1,4,5,7,15,17],"shelter":1,"shift":[1,17],"shim":[0,5],"shine":17,"ship":[0,14,17],"shop":[2,17],"short":[0,6,7,8,14,15],"shortcom":0,"shorter":8,"shorthand":[8,15],"shortlog":5,"shot":4,"should":[0,1,2,4,5,8,10,11,12,14,15,17],"should_ingress_limit":2,"should_quit":2,"should_use_implicit_proof":11,"shout":17,"shove":1,"show":[0,4,5,15,17],"shown":[0,1,2,13],"shrink":0,"shut":[0,1,17],"side":[0,1,8,15,16],"sideband":[0,4,7,15],"sign":[0,1,7,8,10,11,14,15,17],"signal":[1,2,7,8,11,14,15,17],"signatur":[0,1,7,10,11,14,15,16,17],"signer":5,"signifi":14,"signific":10,"significantli":[8,11],"signingkei":5,"silent":[0,15],"silk":0,"similar":[0,1,2,4,5,6,8,11,12,15,16],"simpl":[0,2,4,5,6,11,12,14,15,16,17],"simpler":[5,8,10,16],"simplest":[8,14],"simpli":[0,1,2,4,5,6,8,10,12,13,14,15,17],"simplic":14,"simplifi":2,"simplyequip":6,"simul":[12,14],"simultan":[4,14],"sinc":[0,2,4,5,8,10,11,14,17],"sine":0,"sing":0,"singl":[0,1,2,4,5,8,10,11,14,15,16,17],"singular":14,"sit":[0,4,17],"site":[1,8,10,17],"situat":[0,4,5,6,8,10,14,15],"six":0,"size":[0,2,4,5,8,11,14,15],"size_str":2,"skeptic":0,"skill":[0,1],"skin":0,"skip":[2,5],"sky":7,"slap":17,"slave":17,"sleep":[2,4,15],"slice":[0,2,17],"slight":0,"slightli":[2,4,17],"slip":0,"slop":0,"sloppi":[0,17],"slot":0,"slottim":[8,15],"slow":[0,2,8,10,12,14,17],"slower":[0,8,11,14],"slowest":[0,11],"slyli":0,"small":[0,2,4,8,10,11,14,16,17],"smaller":[0,11],"smallest":4,"smear":0,"smooth":0,"snapshot":0,"sneez":0,"snippet":15,"snr":[2,11,15],"so":[0,1,2,4,5,6,8,9,10,11,12,13,14,15,16,17],"sober":0,"social":[0,1,4,17],"societi":17,"socket":[8,15,16,17],"sockpuppet":0,"soft":17,"softdevic":0,"softwar":[0,1,4,5,6,8,9,12,14,15,17],"solar":17,"soldier":0,"sole":[0,8],"solemn":0,"solicitud":0,"solid":[0,13],"solo":1,"solut":[0,1,12,14,15],"solv":[1,14,17],"some":[0,1,2,4,6,8,10,11,12,14,15,17],"some_program":5,"some_program_1":5,"some_remot":5,"somehow":0,"someon":[0,1,4,5,8,15,17],"someth":[0,1,5,8,10,14,17],"somethign":2,"sometim":[0,6,8,12,17],"somewhat":4,"somewher":[0,17],"soon":[5,11,15],"sort":[0,8,14,15],"soul":17,"sound":[0,12,17],"soundmodem":8,"sour":0,"sourc":[1,2,4,5,6,7,10,11,12,14,16,17],"source_":5,"sovereign":[1,4,14,17],"sovereignli":17,"sovereignti":[7,16],"space":[0,1,2,4,6,10,14,16,17],"spam":[4,8,14,15],"spammi":15,"span":[0,8,10,14],"spawn":[0,8,12],"spe":8,"speak":[0,1,13,17],"speaker":0,"spec":14,"speci":0,"special":[0,12,14,15],"specif":[0,1,2,5,6,7,8,10,11,15,16,17],"specifi":[0,1,2,4,5,6,8,10,11,14,15],"spectacularli":0,"spectrum":[0,8,14,17],"specul":0,"speech":0,"speed":[0,2,8,11,14,15,17],"spend":0,"spent":0,"spew":0,"sphere":17,"spin":0,"spinner":17,"spirit":[0,12],"split":2,"spoof":[5,17],"spread":[0,8,15],"spreadingfactor":8,"spring":0,"spun":0,"squelch":8,"ssh":[5,12,15],"ssid":8,"ssl":17,"stabil":[0,10,17],"stabl":[4,8,10,14,16],"stack":[2,4,10,11,12,14,15,16,17],"stackexchang":12,"stage":14,"stai":[2,8,14,15,17],"stake":[0,1],"stale":[10,11,15],"stale_grac":11,"stale_tim":11,"stall":0,"stamp":[8,11,15],"stanc":[1,17],"stand":[0,1,17],"standalon":[7,12],"standard":[1,5,6,7,8,12,14,15],"start":[0,2,5,6,7,8,10,11,12,14,15,17],"starter":0,"startup":[2,4,15],"stat":[5,11,15],"state":[0,1,2,7,10,17],"statement":0,"static":[4,5,8,11,17],"staticmethod":2,"station":8,"stationari":[10,15,17],"statist":[2,7,8,11,13],"stats_ignore_ident":5,"statu":[0,2,4,5,7,8,11,12,14,15,17],"statut":7,"statutori":0,"stderr":15,"stdin":[8,15],"stdio":16,"stdout":[2,5,8,15],"steel":17,"stem":13,"step":[0,2,4,5,6,17],"stewardship":17,"still":[0,1,2,4,5,8,10,14,15,17],"stock":[0,2],"stole":0,"stolen":0,"stone":17,"stop":[0,11,16,17],"stopbit":[2,8,15],"storag":[4,7,8,12,14,15,17],"store":[1,2,5,7,11,12,14,15],"store_tru":2,"stori":0,"storm":17,"str":[2,11],"straight":0,"straightforward":8,"strang":17,"stranger":17,"strangl":17,"strateg":10,"strategi":[0,1,7,8,11],"strawman":0,"stream":[0,1,2,11,12,14,15,17],"stream_id":[2,11],"street":17,"strength":[0,11,16,17],"strict":[1,17],"strictli":[0,8,14],"string":[0,2,11,17],"stringmessag":2,"strip":[0,1,5,17],"strong":[4,5,14,17],"stronger":[0,4],"strongli":4,"struct":2,"structur":[0,1,2,7,10,11,14,16,17],"studi":[0,1],"style":[0,5,8],"stylist":0,"sub":[2,8],"subclass":[2,11],"subcommand":5,"subdirectori":5,"subinterfac":8,"subject":[0,4,9,15,16,17],"sublicens":[0,9],"submit":[0,1],"subnet":[4,10],"subreddit":0,"subscrib":[4,15,17],"subsequ":[0,2,5,14],"subset":8,"subsid":8,"subsist":0,"substanc":7,"substant":0,"substanti":[0,1,9],"substitut":7,"substrat":[1,17],"subtl":17,"subtli":[0,17],"succe":[4,5,15],"succeed":[0,2,11],"succesfulli":10,"success":[0,1,4,5,8,11],"successful":11,"successfulli":[0,2,11,14],"sudden":0,"suddenli":[0,17],"sudo":[4,15],"sue":0,"suffer":10,"suffic":14,"suffici":[0,4,8,14,15,16],"suffix":2,"suggest":0,"suit":[0,1,4,6,12,14,15,17],"suitabl":[2,4,8,10,14,15,16],"sum":[0,17],"sun":0,"super":2,"superfici":0,"supersed":8,"superstructur":1,"supp":0,"suppli":[0,2,8,11,14,15,16],"supplier":4,"support":[0,1,2,4,5,7,8,10,11,12,14,15,16],"suprem":[0,1],"sure":[0,2,4,5,6,8,12,15],"surfac":[7,17],"surgic":0,"surprisingli":0,"surrend":1,"surround":[0,1,17],"surveil":[1,14,17],"surviv":[0,4,14,15,17],"suspend":1,"sustain":[0,17],"swim":1,"switch":[6,8,10,14,15,17],"sx1262":6,"sx1268":6,"sx1276":6,"sx1278":6,"sx1280":6,"sy":2,"syllog":0,"symbol":[0,5],"symlink":15,"symmetr":[11,14],"sync":5,"syncer":5,"synchron":[1,7,12,17],"syntax":7,"synthesi":0,"synthet":[0,17],"system":[0,1,2,4,5,6,7,8,9,10,11,13,14,16],"systemat":0,"systemctl":15,"systemd":15,"t":[0,1,2,4,5,8,10,11,14,15,17],"tabl":[0,4,8,10,14,15,17],"tablet":[0,4],"tackl":12,"tactic":0,"tag":[5,11],"taglin":0,"tail":8,"tailor":14,"take":[0,1,2,4,5,8,11,12,14,15,16,17],"taken":[0,2,5,14,15,17],"takeov":0,"talent":0,"talk":[0,12,17],"tamper":[1,5,17],"tangerin":2,"tangibl":0,"tantrum":0,"tap":[6,17],"tar":5,"target":[0,2,4,5,8,12,15,17],"target_hash":11,"target_host":8,"target_port":[8,15],"task":[5,15],"tast":0,"taught":17,"tcp":[0,4,6,7,10,14,15,16,17],"tcpclientinterfac":[0,4,6,8],"tcpinterfac":15,"tcpserverinterfac":[0,4,8],"tdata":2,"teach":[0,15,17],"teahous":[1,13],"team":[0,5],"teardown":[2,11],"teardown_reason":2,"tech":17,"technic":[0,1,5,12],"technician":17,"technolog":[0,1],"technologi":[0,4,10,17],"teffect":2,"telco":17,"telecom":17,"telemetri":[12,13],"telephon":[0,7],"telephoni":7,"televis":17,"tell":[0,2,4,8,10,17],"temperatur":14,"templat":[0,7],"temporari":[4,8,15],"temporarili":[0,8,10],"tempt":0,"ten":[0,14],"tenanc":17,"tenant":17,"tend":[0,8],"tenuou":17,"terabyt":0,"term":[0,1,4,8,10,14,17],"termin":[0,2,4,5,11,12,15],"terminolog":0,"terminologi":[10,14],"termux":4,"terrifi":17,"territori":1,"terror":0,"test":[0,4,5,12,14,15,17],"testimoni":1,"testnet":15,"tether":17,"text":[0,2,12,14,15],"textur":[0,17],"tfile":2,"tgz":[5,15],"thaler":0,"than":[0,1,2,4,5,8,10,11,12,13,14,15,16,17],"thank":[0,5],"theater":0,"theatr":0,"theatric":[0,1],"theft":0,"thei":[0,1,2,4,5,8,10,11,12,14,15,17],"theirs":0,"them":[0,1,2,4,5,6,8,10,12,14,15,16,17],"theme":5,"themself":0,"themselv":[0,1,5,8,10,15],"theoret":0,"theori":[0,1,17],"thereaft":14,"therefor":[0,4,14,16,17],"thereof":0,"thi":[1,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17],"thin":0,"thing":[0,2,4,10,12],"think":[4,6,8,10,13,14,17],"thinner":0,"third":[0,1,5],"those":[0,1,4,10,14,15,17],"though":[0,1,4,8,10,14],"thought":[0,17],"thousand":[0,1,4,8,14,15,17],"thread":[0,1,2,5],"threat":[0,15],"three":[0,4,14,17],"threshold":8,"throough":8,"throttl":[8,10],"through":[0,2,4,5,6,7,8,10,11,12,14,15,16],"throughout":[10,14],"throughput":[0,1,11,12,14,16],"thrown":[11,17],"thu":[10,11,14],"ti":[2,10,14],"tickinput":0,"tickoutput":0,"tighten":17,"tightrop":17,"tild":15,"time":[0,1,2,4,5,6,7,8,10,11,12,13,14,15],"time_since_last":2,"timefram":1,"timeout":[2,11,15,17],"timeoutarg":2,"timer":5,"timescal":1,"timestamp":[2,5,11,14,17],"timestr":2,"tion":8,"titl":[0,5],"tl":17,"tmp":15,"tnc":[6,8,11,15,16],"to_fil":11,"todai":[0,1,14,17],"togeth":[0,6,10,14,15,17],"toggl":15,"token":[0,11,14,16],"tokyo":17,"told":[0,17],"toler":8,"tomorrow":17,"too":[0,2,10,17],"took":0,"tool":[0,1,4,5,6,7,8,10,12,14,15,16],"toolkit":[4,10],"top":[0,1,12,14,15,17],"topic":[4,14],"topograph":8,"topographi":[4,10,14,17],"topologi":[4,8,10,12,14,16,17],"torn":[11,15],"tort":9,"tortur":17,"torvald":1,"total":[0,5,11,14,15,16],"total_s":2,"touch":[8,17],"toward":[0,2,8,14],"tower":17,"toxic":[0,1],"trace":0,"track":[0,1,5,11,15,17],"track_phy_stat":11,"trade":[0,10,14],"tradit":[0,5,10,14,17],"tradition":1,"traffic":[0,2,4,8,10,11,14,15,17],"train":[0,9,17],"trajectori":0,"transceiv":[4,6,8,12,14,16],"transfer":[0,2,5,11,12,14,15,16],"transfer_s":2,"transform":[0,16],"transient":17,"transistor":17,"transit":0,"translat":[7,10],"transmiss":[4,8,11,14,17],"transmit":[0,2,5,8,14,17],"transmitt":17,"transpar":[0,5,6,8,10,14,15],"transport":[0,1,2,4,5,7,8,11,12,15,16,17],"transport_en":11,"transport_ident":15,"trap":7,"travel":[1,17],"travers":[1,4,8,11,14,17],"treat":[0,1,5,10,14,17],"tree":[0,1,5],"tremend":10,"tri":[0,2],"triad":7,"trigger":8,"trip":[2,11,15],"trivial":[14,16],"troll":0,"troubl":17,"true":[0,1,2,4,8,11,12,15,17],"truli":[0,6,14,17],"truncat":[11,14,15],"truncated_hash":11,"truncated_hashlength":[2,11],"trust":[0,1,4,5,7,8,10,14,15],"trustless":[7,14,16],"trustworthi":[10,15],"truth":[0,5],"try":[2,5,7,8,10,11,12],"ttime":2,"ttransfer":2,"ttyacm0":8,"ttyusb0":[2,8,15],"ttyusb1":8,"ttyusb2":8,"tui":7,"tun":[6,8],"tun0":8,"tune":7,"tunnel":[4,8,10,14,16,17],"tupl":11,"turbo":0,"turn":[0,1,2,8,15,17],"turnaround":8,"tutori":4,"twelv":0,"two":[1,2,5,6,7,8,10,11,14,15],"tx":[8,15],"txb":2,"txp":15,"txpower":8,"txt":[5,15],"txtail":[8,15],"type":[0,2,4,7,8,10,11,15],"typeerror":11,"typic":[5,8,10,14],"tyrant":17,"u":[0,14,15,17],"ubiqu":6,"ubuntu":[7,15],"uci":4,"udp":[7,10,14,15,16],"udpinterfac":[0,8],"uhf":[12,15,16],"ui":[12,17],"ultim":[4,14,17],"umsgpack":2,"unaid":0,"unanswer":0,"unapologet":0,"unauthor":0,"unavail":[0,5],"unblackhol":15,"unblackhole_ident":11,"unblock":15,"unbound":[0,17],"uncar":17,"uncencrypt":2,"uncentraliz":7,"uncertainti":0,"unchang":[0,10],"uncheck":0,"uncomfort":0,"uncontamin":0,"uncontroversi":0,"uncoordin":[4,17],"under":[0,1,5,8,11,13,14,15,16,17],"underli":[2,8,10,11,14,16],"undermin":[0,17],"underneath":17,"undersea":17,"understand":[0,1,4,7,8,10,11,15,16,17],"understood":[0,17],"underwai":4,"underwear":17,"undetect":0,"unencrypt":[2,11,14],"unencumb":0,"unenforc":0,"unequivoc":14,"unexamin":0,"unexcit":0,"unexplor":10,"unfair":0,"unforeseen":1,"unforg":[10,16],"unfortun":[0,4],"unfre":0,"unhealthi":0,"unicod":5,"unicode_icon":5,"unidentifi":2,"unidirect":2,"unifi":[10,14],"uniform":0,"uninform":1,"uninterest":2,"uniqu":[2,8,10,11,14,15,16,17],"unison":0,"unit":[0,1,2,4,11,15],"univers":17,"unix":[5,11],"unknow":0,"unknown":[0,2,8,10,11,14,15],"unlawfulli":0,"unlearn":17,"unless":[2,4,8,10,11,14],"unlicens":[0,14],"unlik":[1,5,14],"unlock":15,"unmask":0,"unmeasur":0,"unmedi":1,"unnecessari":8,"unorganis":10,"unown":0,"unpack":[2,11],"unpackb":2,"unpaid":17,"unplug":[15,17],"unpreced":0,"unprofession":0,"unprotect":0,"unproven":0,"unreach":0,"unread":0,"unrecover":[2,15],"unrel":0,"unreli":8,"unrestrict":0,"unreview":0,"unsatur":1,"unset":14,"unsubstanti":0,"unsupport":11,"until":[2,4,8,11],"untrust":[5,15],"untrustworthi":17,"unus":15,"unverifi":[0,15],"unwant":[14,15],"unwrap_or":0,"unwritten":0,"up":[0,1,2,4,5,6,8,10,11,12,14,15,16,17],"updat":[0,1,2,4,5,11,12,14,15,17],"upgrad":[0,4,11,14],"upkeep":8,"upload":5,"upon":[0,1,2,8,14],"upset":2,"upstream":[0,1,5,12,15],"uptim":[0,17],"urandom":[14,16],"urgenc":[0,1],"urgent":17,"uri":12,"url":[1,5,15,17],"urwid":12,"us":[1,2,5,6,7,8,9,10,11,17],"usabl":[5,8,14],"usag":[5,7,8,11,12,13,15],"usb":[1,6,14,15],"useabl":14,"useless":[1,17],"user":[0,1,2,4,5,8,10,12,14,15,16,17],"user_input":2,"userland":16,"usernam":[0,17],"usernameher":15,"usr":[8,15],"usual":[0,4,8,10],"usurp":0,"utf":2,"util":[2,6,7,8,14,17],"utilis":[2,4,8,11,12,14,15,16],"ux":12,"v":[5,7,15],"v4":0,"vaccuum":0,"valid":[0,1,2,4,7,8,10,11,14,15,17],"vallei":0,"valu":[0,1,2,4,5,8,11,14,15,17],"valuabl":[0,1,5,6,13,17],"valueerror":[2,11],"vanish":[0,17],"vanishingli":[0,17],"var":5,"vari":[4,8,14,15],"variabl":[2,4,5,11],"varianc":0,"variant":0,"variat":0,"varieti":[8,12,14,15,16],"variou":[0,2,4,8,10,11,12,14,15,16],"vast":[0,1,4,14,17],"vastli":10,"vau001480196":0,"vc":0,"ve":10,"vector":0,"vehicl":[0,8],"veloc":[0,1],"vendor":[0,2,6,16],"ventur":17,"venu":1,"verbatim":0,"verbos":[5,11,15],"verdict":7,"veri":[0,1,4,5,6,8,10,11,12,13,14,15,16,17],"verif":[0,1,7,10,11,12,14,15,17],"verifi":[0,1,2,4,7,8,10,11,14,15,16,17],"versa":16,"version":[0,1,2,4,5,14,15,17],"versu":7,"vet":0,"vhf":[12,14,16],"vi":[5,7],"via":[1,4,5,6,8,10,12,13,14,15,16,17],"viabl":10,"vice":16,"video":17,"view":[1,4,5,8,12,13,14,15],"viewabl":5,"vim":5,"vindic":0,"violat":[0,8],"violenc":0,"violent":0,"viral":0,"virtu":0,"virtual":[4,6,8,10,14,15,16],"virtuou":17,"visibl":[0,1,2,5,8],"visibli":0,"visual":[0,5,14,17],"vital":17,"vocabulari":0,"voic":[0,1,7,13,17],"voicemail":12,"void":7,"volum":0,"volunt":4,"voluntari":4,"vouch":17,"vpn":[10,17],"vport":8,"vulner":10,"vv":15,"vvv":4,"w":[5,15],"wa":[1,2,5,6,7,8,10,11,14,15,16,17],"wai":[0,1,2,5,7,8,10,11,12,14,15,17],"wait":[0,2,4,8,12,14,15,17],"waiv":0,"walk":17,"wall":[0,17],"wander":17,"want":[0,1,2,4,5,8,10,12,13,14,15,17],"wantedbi":15,"warhol":0,"warn":[0,15],"warrant":16,"warranti":9,"wash":17,"wasm":0,"wast":[0,8,10,13,17],"watch":0,"wave":[0,6,17],"wb":2,"we":[0,1,2,4,6,8,10,13,14,17],"weaken":14,"weapon":[0,1,17],"weaponis":0,"wear":[0,17],"weather":17,"weav":17,"web":[0,4,12,17],"websit":[0,4,8,17],"week":0,"weekdai":0,"weekend":0,"weigh":17,"weight":[0,1,5,15,17],"weird":0,"welcom":[0,1],"well":[0,1,5,6,8,10,11,12,13,14,15,16,17],"went":2,"were":[0,1,2,5,12,17],"what":[1,2,4,5,7,8,10,11,14,15,17],"whatev":[0,10,11,14],"whatsoev":0,"wheel":[4,5],"when":[0,1,2,4,5,6,8,10,11,12,14,15,17],"whenev":[0,2,11,14],"where":[0,1,4,5,6,7,8,10,11,12,14,15,17],"whereupon":14,"wherev":17,"whether":[0,2,4,5,8,9,10,11,14,17],"which":[1,2,4,5,7,8,9,10,11,14,15,16,17],"while":[0,1,2,4,5,6,8,10,11,12,14,15,16,17],"whim":17,"whitelist":14,"whl":[4,5],"who":[1,4,5,7,8,10,14,15,17],"whole":[7,15,17],"wholesal":0,"whom":[0,9,17],"whose":[0,15,17],"why":[1,7,17],"wide":[0,4,5,6,8,10,12,14,15,16],"wider":[0,4,8,10,14,15],"wield":[0,1],"wifi":[4,7,8,10,12,14,15,16,17],"wikipedia":12,"wild":[5,15,17],"wildcard":[2,5],"wilder":17,"wildli":0,"willi":0,"willing":[0,15],"win":0,"win_amd64":0,"window":[0,7,8,12,15],"windowsil":17,"wine":7,"wipe":15,"wire":[0,6,7,8,12,16,17],"wireless":[6,10],"wirelessli":8,"wish":[0,2,8,14,15,17],"witchcraft":0,"withhold":0,"within":[0,1,2,4,5,8,10,11,14,17],"without":[0,2,4,5,6,7,8,9,10,12,14,15,16,17],"wlan0":8,"wolf":6,"won":2,"word":[0,1,4,17],"wore":0,"work":[1,4,6,7,8,10,12,14,15,16],"workabl":10,"workflow":7,"workspac":0,"world":[0,1,4,6,8,10,12,15,16,17],"worldwid":0,"worri":[0,10],"wors":0,"worst":0,"worth":[0,1,4],"worthless":0,"would":[0,1,5,8,11,14,15,17],"wrap":5,"write":[0,1,2,4,5,6,8,11,12,14,15,17],"write_timeout":2,"writer":[0,2,11],"written":[0,2,5,14,16,17],"wrong":[0,2,4,8,17],"wrote":[0,2,8],"x":[0,5,11,14,15],"x25519":[14,16],"x86_64":[0,5],"xenon":2,"xonxoff":2,"y":[0,2,5],"ye":[0,4,5,8,14,15,17],"year":[0,14,17],"yearn":0,"yet":[0,1,2,8,14,17],"yggdrasil":8,"yi":2,"yield":0,"you":[0,1,2,4,5,6,7,8,10,11,12,13,14,15,16,17],"young":0,"your":[0,1,2,5,6,7,8,10,12,13,14,16,17],"yourself":[0,1,4,14,15,16,17],"z":2,"zarya":0,"zen":[0,7,16],"zero":[0,7,8,11,12,16],"zi":2,"zim":12,"zip":5,"zone":[0,1,14],"zshrc":4},"titles":["Brandolini\u2019s Reference","Distributed Development","Code Examples","An Explanation of Reticulum for Human Beings","Getting Started Fast","Git Over Reticulum","Communications Hardware","Reticulum Network Stack Manual","Configuring Interfaces","Reticulum License","Building Networks","API Reference","Programs Using Reticulum","Support Reticulum","Understanding Reticulum","Using Reticulum on Your System","What is Reticulum?","Zen of Reticulum"],"titleterms":{"":0,"0":6,"1":[0,6],"2":0,"25":8,"A":[0,17],"And":0,"Beings":3,"But":0,"By":0,"For":[0,17],"In":[0,17],"No":0,"Not":[0,10,17],"Of":[0,1,7,17],"On":0,"Or":[0,17],"That":0,"The":[0,1,5,14,15,17],"These":0,"To":[0,17],"With":[4,5],"abil":17,"access":[5,14],"actual":0,"ad":4,"addendum":0,"address":[10,17],"administr":12,"agenc":[0,17],"agent":0,"alias":5,"an":3,"analysi":0,"anchor":17,"android":4,"announc":[2,8,14,17],"answer":0,"api":11,"ar":0,"architectur":[1,17],"archiv":1,"argument":0,"arm64":4,"artifact":1,"assist":0,"attribut":[0,5],"author":[0,5],"auto":8,"autom":[8,15],"automat":5,"ax":8,"backbon":[4,8],"bandwidth":17,"base":[4,6],"basic":14,"beam":6,"bind":5,"blackhol":15,"block":8,"board":6,"bookworm":4,"bootstrap":4,"bound":0,"brandolini":0,"brief":0,"broadcast":2,"brows":12,"browser":12,"buffer":2,"build":[4,10],"burst":8,"byte":17,"can":16,"carrier":17,"case":0,"center":[1,17],"channel":2,"chat":12,"citizenship":0,"claim":0,"clean":0,"client":8,"cloud":17,"code":[0,2,14],"coexist":0,"columba":12,"combin":6,"command":5,"commit":5,"common":[0,8],"commun":6,"compon":12,"compos":1,"concept":10,"conceptu":14,"conclus":0,"condit":0,"configur":[5,8,15],"conform":0,"connect":[4,8,10,12],"consequ":0,"consider":[8,10],"content":7,"context":0,"contribut":4,"control":8,"copi":0,"copyright":0,"cost":17,"creat":[4,5,6,14],"creation":5,"creator":5,"cryptograph":[5,14],"current":[14,16],"custom":[2,4,5,8],"data":15,"date":0,"death":17,"debian":4,"decentr":17,"deck":6,"defenc":0,"delus":0,"depend":4,"design":17,"destin":[5,8,10,14],"detail":14,"develop":[1,4],"devic":[6,12,16],"dichotomi":0,"directli":0,"disclos":0,"disconnect":17,"discov":15,"discover":8,"discoveri":8,"distribut":[0,1,4],"do":0,"document":5,"doe":[0,16],"domain":17,"donat":13,"dozen":0,"echo":[2,6],"elong":0,"emerg":17,"empti":5,"enabl":[5,8],"encrypt":17,"engin":0,"entrypoint":4,"environ":17,"establish":14,"ethernet":6,"ethic":17,"evalu":0,"exampl":[2,5,8],"exist":[0,17],"expand":0,"explan":3,"express":0,"fabric":17,"fair":0,"fallaci":17,"fals":0,"fast":4,"featur":17,"feedback":13,"ferment":0,"filesync":12,"filetransf":2,"find":4,"finish":17,"fix":15,"flasher":12,"flow":17,"fluenci":0,"fork":5,"form":5,"format":[5,14],"forward":17,"foundat":0,"fragil":0,"free":0,"freedom":0,"from":[0,17],"function":14,"further":14,"futur":14,"gener":0,"get":[4,14],"git":[5,12],"global":4,"goal":14,"grade":17,"grant":0,"group":5,"guid":0,"ha":0,"happen":0,"hardwar":6,"harm":17,"headcount":0,"health":[0,4],"heltec":6,"heterogen":10,"hierarchi":5,"highlight":5,"histori":0,"host":4,"hostil":17,"human":[3,17],"i":[0,16,17],"i2p":8,"idea":0,"ident":[5,14,17],"identif":2,"ii":0,"iii":0,"illus":17,"implement":[0,16],"implic":14,"improv":15,"includ":[4,15],"independ":17,"indic":7,"individu":0,"influenc":0,"infrastructur":[0,1,4,17],"ingress":8,"instal":[4,6],"instanc":[4,10],"institut":0,"intent":0,"interact":5,"interfac":[2,4,8,12,14,15,16,17],"intermediari":1,"internet":4,"interregnum":1,"introduct":14,"introductori":10,"ip":17,"issu":4,"iv":0,"j":12,"kei":14,"kiss":8,"launder":0,"layer":0,"legal":0,"liber":17,"licens":[0,9],"like":0,"lilygo":6,"limit":[8,17],"line":[0,5,6],"link":[2,14],"list":15,"listen":8,"local":15,"locat":5,"long":1,"look":0,"lora":8,"lora32":6,"lunar":4,"lxmf":12,"lxmfy":12,"lxst":12,"machin":0,"maco":4,"mad":0,"manag":[5,15],"manual":[5,7],"matter":0,"mean":0,"mechan":14,"medium":17,"merit":17,"meshchatx":12,"messag":12,"method":[0,5],"micron":12,"minim":2,"mirror":5,"mix":4,"mode":8,"modem":6,"modem73":12,"modul":12,"monetari":0,"most":0,"motiv":14,"movement":0,"multi":8,"name":[14,15,17],"network":[0,4,5,7,10,12,14],"new":[0,8],"node":[5,10,12,14,17],"nomad":[5,12,17],"note":4,"obtain":5,"offer":16,"offlin":5,"open":[0,17],"opencom":6,"openwrt":4,"option":8,"origin":1,"other":0,"output":0,"over":[1,4,5,12],"overal":0,"overview":[10,14],"packet":[6,14],"page":[5,12],"paramet":[5,8],"parser":12,"partylin":12,"path":8,"pattern":[0,17],"perform":0,"permiss":5,"persist":0,"person":[4,17],"phone":12,"physic":17,"pi":4,"pipe":8,"place":0,"platform":[1,4],"port":15,"portabl":17,"posit":0,"possibl":0,"post":17,"power":17,"predict":0,"prerequisit":5,"presenc":17,"present":0,"preserv":17,"primit":[1,14],"principl":[0,17],"prioritis":14,"prn":0,"program":[4,12,15],"project":0,"propag":14,"properti":0,"propos":5,"protect":0,"protocol":[1,12,14,17],"proven":0,"provid":13,"public":[4,14,17],"publish":[12,15],"pure":4,"purpos":0,"python":4,"question":0,"queue":8,"radio":[4,6],"rak4631":6,"raspberri":4,"rate":8,"reach":14,"realiti":0,"reason":0,"recognit":0,"recycl":0,"refer":[0,11,14,16],"relai":12,"releas":5,"remot":[5,8,12,15],"ren":12,"repositori":5,"request":[2,8],"rescu":0,"resolv":[0,4],"resourc":[12,14],"respons":[0,2,4],"restor":1,"restrict":0,"ret":4,"retibb":12,"reticul":12,"reticulum":[0,3,4,5,6,7,9,12,13,14,15,16,17],"retipedia":12,"risc":4,"rn":12,"rncp":15,"rngit":[5,15],"rnid":15,"rnmon":12,"rnode":[6,8],"rnodeconf":15,"rnpath":15,"rnprobe":15,"rnsd":15,"rnsh":15,"rnstatu":15,"rnx":15,"road":1,"roam":17,"role":0,"room":0,"rrc":12,"rule":14,"scarciti":17,"scope":0,"secur":8,"serial":[6,8,15],"serv":5,"server":8,"servic":15,"setup":14,"shell":12,"short":5,"sideband":12,"sign":5,"signal":0,"signatur":5,"sky":17,"sourc":[0,15],"sovereignti":[1,17],"specif":[4,14],"stack":7,"standalon":4,"standard":0,"start":[1,4],"state":5,"statist":5,"statu":16,"statut":0,"storag":5,"store":17,"strategi":4,"structur":5,"substanc":0,"substitut":0,"support":[6,13],"suprem":6,"surfac":0,"synchron":5,"syntax":5,"system":[12,15,17],"systemwid":15,"t":6,"t114":6,"t3s3":6,"tabl":7,"tcp":8,"telephon":12,"telephoni":12,"templat":5,"thi":0,"through":[1,17],"time":17,"tool":17,"translat":0,"transport":[10,14],"trap":0,"triad":0,"trust":17,"trustless":10,"try":4,"tui":12,"tune":8,"two":0,"type":[5,6,14,16],"ubuntu":4,"udp":8,"uncentraliz":17,"understand":14,"unsign":6,"us":[0,4,12,14,15,16],"usag":[6,14],"userspac":15,"util":[4,5,12,15],"v":[0,4],"v1":6,"v2":6,"v3":6,"v4":6,"valid":5,"verdict":0,"verif":5,"verifi":5,"versu":0,"vi":0,"voic":12,"void":0,"wa":0,"wai":4,"what":[0,16],"where":16,"which":0,"who":0,"whole":0,"why":0,"wifi":6,"window":4,"wine":0,"wire":14,"without":1,"work":[0,5,17],"workflow":[1,5],"x":6,"xl":6,"your":[4,15],"zen":17,"zero":17}}) \ No newline at end of file +Search.setIndex({"alltitles":{"A Carrier-Grade Fallacy":[[16,"a-carrier-grade-fallacy"]],"API Reference":[[10,null]],"ARM64":[[3,"arm64"]],"AX.25 KISS Interface":[[7,"ax-25-kiss-interface"]],"Accessing Repository Pages":[[4,"accessing-repository-pages"]],"Adding Radio Interfaces":[[3,"adding-radio-interfaces"]],"An Explanation of Reticulum for Human Beings":[[2,null]],"Anchor In The Flow":[[16,"anchor-in-the-flow"]],"Android":[[3,"android"]],"Announce":[[1,"announce"]],"Announce Propagation Rules":[[13,"announce-propagation-rules"]],"Announce Rate Control":[[7,"announce-rate-control"]],"Announcing Presence":[[16,"announcing-presence"]],"Artifact-Centered Workflows":[[0,"artifact-centered-workflows"]],"Author Identity Binding":[[4,"author-identity-binding"]],"Auto Interface":[[7,"auto-interface"]],"Automated Blocking":[[7,"automated-blocking"]],"Automated List Sourcing":[[14,"automated-list-sourcing"]],"Automatic Mirror Synchronization":[[4,"automatic-mirror-synchronization"]],"Backbone Interface":[[7,"backbone-interface"]],"Blackhole Management":[[14,"blackhole-management"]],"Bootstrapping Connectivity":[[3,"bootstrapping-connectivity"]],"Broadcast":[[1,"broadcast"]],"Buffer":[[1,"buffer"]],"Build Personal Infrastructure":[[3,"build-personal-infrastructure"]],"Building Networks":[[9,null]],"Channel":[[1,"channel"]],"Code Examples":[[1,null]],"Combining Hardware Types":[[5,"combining-hardware-types"]],"Command-Line Interaction":[[4,"command-line-interaction"]],"Commit Signing":[[4,"commit-signing"]],"Common Interface Options":[[7,"common-interface-options"]],"Communications Hardware":[[5,null]],"Composable Primitives":[[0,"composable-primitives"]],"Concepts & Overview":[[9,"concepts-overview"]],"Conceptual Overview":[[13,"conceptual-overview"]],"Configuration":[[4,"configuration"],[4,"id1"]],"Configuration & Data":[[14,"configuration-data"]],"Configuration Example":[[4,"configuration-example"]],"Configuration Methods":[[4,"configuration-methods"]],"Configuring Interfaces":[[7,null]],"Connect to the Distributed Backbone":[[3,"connect-to-the-distributed-backbone"]],"Connecting Remotes":[[7,"connecting-remotes"]],"Connecting Reticulum Instances Over the Internet":[[3,"connecting-reticulum-instances-over-the-internet"]],"Contributing to the Global Ret":[[3,"contributing-to-the-global-ret"]],"Cost Of A Byte":[[16,"cost-of-a-byte"]],"Creating Empty Repositories":[[4,"creating-empty-repositories"]],"Creating RNodes":[[5,"creating-rnodes"]],"Creating Signed Releases":[[4,"creating-signed-releases"]],"Creating a Network With Reticulum":[[3,"creating-a-network-with-reticulum"]],"Creating and Using Custom Interfaces":[[3,"creating-and-using-custom-interfaces"]],"Creating and Using a Network Identity":[[13,"creating-and-using-a-network-identity"]],"Creator Permissions":[[4,"creator-permissions"]],"Cryptographic Attribution":[[4,"cryptographic-attribution"]],"Cryptographic Primitives":[[13,"cryptographic-primitives"]],"Current Status":[[15,"current-status"]],"Current Usage":[[13,"current-usage"]],"Custom Interfaces":[[1,"custom-interfaces"],[7,"custom-interfaces"]],"Customizing Templates":[[4,"customizing-templates"]],"Death To The Address":[[16,"death-to-the-address"]],"Debian Bookworm":[[3,"debian-bookworm"]],"Decentralization Or Uncentralizability?":[[16,"decentralization-or-uncentralizability"]],"Design Patterns For Post-IP Systems":[[16,"design-patterns-for-post-ip-systems"]],"Destination Naming":[[13,"destination-naming"]],"Destinations":[[13,"destinations"]],"Destinations, Not Addresses":[[9,"destinations-not-addresses"]],"Develop a Program with Reticulum":[[3,"develop-a-program-with-reticulum"]],"Discoverable Interfaces":[[7,"discoverable-interfaces"]],"Discovering Interfaces":[[14,"discovering-interfaces"]],"Discovery Parameters":[[7,"discovery-parameters"]],"Distributed Development":[[0,null]],"Distribution Without Intermediaries":[[0,"distribution-without-intermediaries"]],"Donations":[[12,"donations"]],"Echo":[[1,"echo"]],"Emergent Patterns":[[16,"emergent-patterns"]],"Enabling Discovery":[[7,"enabling-discovery"]],"Enabling the Git Page Node":[[4,"enabling-the-git-page-node"]],"Encryption Is Not A Feature":[[16,"encryption-is-not-a-feature"]],"Ethernet-based Hardware":[[5,"ethernet-based-hardware"]],"Ethics Of The Tool":[[16,"ethics-of-the-tool"]],"Example Configuration":[[7,"example-configuration"]],"Fabric Of The Independent":[[16,"fabric-of-the-independent"]],"Fallacy Of The Cloud":[[16,"fallacy-of-the-cloud"]],"Filetransfer":[[1,"filetransfer"]],"Finding Your Way":[[3,"finding-your-way"]],"Fixed Serial Port Names":[[14,"fixed-serial-port-names"]],"Flow & Time":[[16,"flow-time"]],"Forking Repositories":[[4,"forking-repositories"]],"Formatting & Syntax Highlighting":[[4,"formatting-syntax-highlighting"]],"Future Implications":[[13,"future-implications"]],"Getting Further":[[13,"getting-further"]],"Getting Started Fast":[[3,null]],"Git Configuration Parameters":[[4,"git-configuration-parameters"]],"Git Over Reticulum":[[4,null]],"Goals":[[13,"goals"]],"Heltec LoRa32 v2.0":[[5,"heltec-lora32-v2-0"]],"Heltec LoRa32 v3.0":[[5,"heltec-lora32-v3-0"]],"Heltec LoRa32 v4.0":[[5,"heltec-lora32-v4-0"]],"Heltec T114":[[5,"heltec-t114"]],"Heterogeneous Connectivity":[[9,"heterogeneous-connectivity"]],"Hostile Environments":[[16,"hostile-environments"]],"Hosting Public Entrypoints":[[3,"hosting-public-entrypoints"]],"I2P Interface":[[7,"i2p-interface"]],"Identification":[[1,"example-identify"]],"Identities":[[13,"understanding-identities"]],"Identity & Destination Aliases":[[4,"identity-destination-aliases"]],"Identity and Nomadism":[[16,"identity-and-nomadism"]],"Improving System Configuration":[[14,"improving-system-configuration"]],"Included Utility Programs":[[14,"included-utility-programs"]],"Indices and Tables":[[6,"indices-and-tables"]],"Installation":[[5,"installation"]],"Interface Access Codes":[[13,"interface-access-codes"]],"Interface Modes":[[7,"interface-modes"],[7,"interfaces-modes"]],"Interface Modules & Connectivity Resources":[[11,"interface-modules-connectivity-resources"]],"Interface Types and Devices":[[15,"interface-types-and-devices"]],"Introduction & Basic Functionality":[[13,"introduction-basic-functionality"]],"Introductory Considerations":[[9,"introductory-considerations"]],"KISS Interface":[[7,"kiss-interface"]],"LXMF":[[11,"lxmf"]],"LXMF Interactive Client":[[11,"lxmf-interactive-client"]],"LXMFy":[[11,"lxmfy"]],"LXST":[[11,"id16"]],"LXST Phone":[[11,"lxst-phone"]],"Liberation From Limits":[[16,"liberation-from-limits"]],"LilyGO LoRa32 v1.0":[[5,"lilygo-lora32-v1-0"]],"LilyGO LoRa32 v2.0":[[5,"lilygo-lora32-v2-0"]],"LilyGO LoRa32 v2.1":[[5,"lilygo-lora32-v2-1"]],"LilyGO T-Beam":[[5,"lilygo-t-beam"]],"LilyGO T-Beam Supreme":[[5,"lilygo-t-beam-supreme"]],"LilyGO T-Deck":[[5,"lilygo-t-deck"]],"LilyGO T-Echo":[[5,"lilygo-t-echo"]],"LilyGO T3S3":[[5,"lilygo-t3s3"]],"Link":[[1,"link"]],"Link Establishment in Detail":[[13,"link-establishment-in-detail"]],"Listeners":[[7,"listeners"]],"Local Blackhole Management":[[14,"local-blackhole-management"]],"Long Archive":[[0,"long-archive"]],"MacOS":[[3,"macos"]],"Managing Group Permissions":[[4,"managing-group-permissions"]],"Managing Repository Permissions":[[4,"managing-repository-permissions"]],"Managing Work Document Permissions":[[4,"managing-work-document-permissions"]],"Manual Synchronization":[[4,"manual-synchronization"]],"Merits Of Scarcity":[[16,"merits-of-scarcity"]],"MeshChatX":[[11,"meshchatx"]],"Micron Parser JS":[[11,"micron-parser-js"]],"Minimal":[[1,"minimal"]],"Mirroring Repositories":[[4,"mirroring-repositories"]],"Mixing Strategies":[[3,"mixing-strategies"]],"Motivation":[[13,"motivation"]],"Naming Is Power":[[16,"naming-is-power"]],"Network Health & Responsibility":[[3,"network-health-responsibility"]],"Network Identities":[[13,"network-identities"]],"New Destination Rate Limiting":[[7,"new-destination-rate-limiting"]],"Node Types":[[13,"node-types"]],"Nomad Network":[[11,"nomad-network"]],"Obtaining Verified Releases":[[4,"obtaining-verified-releases"]],"Offline Verification":[[4,"offline-verification"]],"Open Sky":[[16,"open-sky"]],"OpenCom XL":[[5,"opencom-xl"]],"OpenWRT":[[3,"openwrt"]],"Packet Prioritisation":[[13,"packet-prioritisation"]],"Packet Radio Modems":[[5,"packet-radio-modems"]],"Path Request Burst Control":[[7,"path-request-burst-control"]],"Permission Configuration Locations":[[4,"permission-configuration-locations"]],"Permission Examples":[[4,"permission-examples"]],"Permission Hierarchy":[[4,"permission-hierarchy"]],"Permission Short Forms":[[4,"permission-short-forms"]],"Permission Types":[[4,"permission-types"]],"Permission Validation":[[4,"permission-validation"]],"Permissions":[[4,"permissions"]],"Personal Infrastructure":[[16,"personal-infrastructure"]],"Physics Of Trust":[[16,"physics-of-trust"]],"Pipe Interface":[[7,"pipe-interface"]],"Platform-Specific Install Notes":[[3,"platform-specific-install-notes"]],"Portable Existence":[[16,"portable-existence"]],"Prerequisites":[[4,"prerequisites"]],"Preserving Human Agency":[[16,"preserving-human-agency"]],"Programs & Utilities":[[11,"programs-utilities"]],"Programs Using Reticulum":[[11,null]],"Proposing Work Documents":[[4,"proposing-work-documents"]],"Protocol Specifics":[[13,"protocol-specifics"]],"Protocols":[[11,"protocols"]],"Protocols Over Platforms":[[0,"protocols-over-platforms"]],"Provide Feedback":[[12,"provide-feedback"]],"Public Domain Protocol":[[16,"public-domain-protocol"]],"Public Key Announcements":[[13,"public-key-announcements"]],"Publishing Blackhole Lists":[[14,"publishing-blackhole-lists"]],"Pure-Python Reticulum":[[3,"pure-python-reticulum"]],"RAK4631-based Boards":[[5,"rak4631-based-boards"]],"RBrowser":[[11,"rbrowser"]],"RISC-V":[[3,"risc-v"]],"RNMon":[[11,"rnmon"]],"RNS FileSync":[[11,"rns-filesync"]],"RNS Page Node":[[11,"rns-page-node"]],"RNode":[[5,"rnode"]],"RNode LoRa Interface":[[7,"rnode-lora-interface"]],"RNode Multi Interface":[[7,"rnode-multi-interface"]],"RRC":[[11,"rrc"]],"Raspberry Pi":[[3,"raspberry-pi"]],"Reaching the Destination":[[13,"reaching-the-destination"]],"Reference Implementation":[[15,"reference-implementation"]],"Reference Setup":[[13,"reference-setup"]],"Release Management":[[4,"release-management"]],"Release Storage & Structure":[[4,"release-storage-structure"]],"Remote Management":[[14,"remote-management"]],"Remote Permission Management":[[4,"remote-permission-management"]],"Remote Shell":[[11,"remote-shell"]],"Repository Creation & Management":[[4,"repository-creation-management"]],"Repository Statistics":[[4,"repository-statistics"]],"Repository Structure":[[4,"repository-structure"]],"Requests & Responses":[[1,"requests-responses"]],"Resolving Dependency & Installation Issues":[[3,"resolving-dependency-installation-issues"]],"Resources":[[13,"resources"]],"Restoration":[[0,"restoration"]],"RetiBBS":[[11,"retibbs"]],"Reticulum License":[[8,null]],"Reticulum Network Stack Manual":[[6,null]],"Reticulum Network Telephone":[[11,"reticulum-network-telephone"]],"Reticulum Relay Chat":[[11,"reticulum-relay-chat"]],"Reticulum Transport":[[13,"reticulum-transport"]],"Reticulum as a System Service":[[14,"reticulum-as-a-system-service"]],"Retipedia":[[11,"retipedia"]],"Roaming Nodes":[[16,"roaming-nodes"]],"Security Considerations":[[7,"security-considerations"]],"Serial Interface":[[7,"serial-interface"]],"Serial Lines & Devices":[[5,"serial-lines-devices"]],"Serving Pages Over Nomad Network":[[4,"serving-pages-over-nomad-network"]],"Sideband":[[11,"sideband"]],"Signing Commits":[[4,"signing-commits"]],"Sovereignty Through Infrastructure":[[0,"sovereignty-through-infrastructure"],[16,"sovereignty-through-infrastructure"]],"Standalone Reticulum Installation":[[3,"standalone-reticulum-installation"]],"Start Of The Road":[[0,"start-of-the-road"]],"State Management":[[4,"state-management"]],"Store & Forward":[[16,"store-forward"]],"Support Reticulum":[[12,null]],"Supported Boards and Devices":[[5,"supported-boards-and-devices"]],"Systemwide Service":[[14,"systemwide-service"]],"TCP Client Interface":[[7,"tcp-client-interface"]],"TCP Server Interface":[[7,"tcp-server-interface"]],"Table Of Contents":[[6,"table-of-contents"]],"The Ability To Disconnect":[[16,"the-ability-to-disconnect"]],"The Announce Mechanism in Detail":[[13,"the-announce-mechanism-in-detail"]],"The Bandwidth Fallacy":[[16,"the-bandwidth-fallacy"]],"The Harm Principle":[[16,"the-harm-principle"]],"The Illusion Of The Center":[[16,"the-illusion-of-the-center"]],"The Interface Is The Medium":[[16,"the-interface-is-the-medium"]],"The Original Architecture":[[0,"the-original-architecture"]],"The Platform Interregnum":[[0,"the-platform-interregnum"]],"The Release Workflow":[[4,"the-release-workflow"]],"The Work Is Finished":[[16,"the-work-is-finished"]],"The rncp Utility":[[14,"the-rncp-utility"]],"The rngit Utility":[[4,"the-rngit-utility"],[14,"the-rngit-utility"]],"The rnid Utility":[[14,"the-rnid-utility"]],"The rnodeconf Utility":[[14,"the-rnodeconf-utility"]],"The rnpath Utility":[[14,"the-rnpath-utility"]],"The rnprobe Utility":[[14,"the-rnprobe-utility"]],"The rnsd Utility":[[14,"the-rnsd-utility"]],"The rnsh Utility":[[14,"the-rnsh-utility"]],"The rnstatus Utility":[[14,"the-rnstatus-utility"]],"The rnx Utility":[[14,"the-rnx-utility"]],"Transport Nodes and Instances":[[9,"transport-nodes-and-instances"]],"Trustless Networking":[[9,"trustless-networking"]],"Try Using a Reticulum-based Program":[[3,"try-using-a-reticulum-based-program"]],"UDP Interface":[[7,"udp-interface"]],"Ubuntu Lunar":[[3,"ubuntu-lunar"]],"Understanding Reticulum":[[13,null]],"Unsigned RNode v2.x":[[5,"unsigned-rnode-v2-x"]],"Usage with Reticulum":[[5,"usage-with-reticulum"]],"Userspace Service":[[14,"userspace-service"]],"Using Reticulum on Your System":[[14,null]],"Using the Included Utilities":[[3,"using-the-included-utilities"]],"Validating Commit Signatures":[[4,"validating-commit-signatures"]],"Verified Releases":[[4,"verified-releases"]],"What does Reticulum Offer?":[[15,"what-does-reticulum-offer"]],"What is Reticulum?":[[15,null]],"Where can Reticulum be Used?":[[15,"where-can-reticulum-be-used"]],"WiFi-based Hardware":[[5,"wifi-based-hardware"]],"Windows":[[3,"windows"]],"Wire Format":[[13,"wire-format"]],"Work Document Permissions":[[4,"work-document-permissions"]],"Work Documents":[[4,"work-documents"]],"Working With Work Documents":[[4,"working-with-work-documents"]],"Zen of Reticulum":[[16,null]],"Zero-Trust Architectures":[[16,"zero-trust-architectures"]]},"docnames":["distributed","examples","forhumans","gettingstartedfast","git","hardware","index","interfaces","license","networks","reference","software","support","understanding","using","whatis","zen"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["distributed.rst","examples.rst","forhumans.rst","gettingstartedfast.rst","git.rst","hardware.rst","index.rst","interfaces.rst","license.rst","networks.rst","reference.rst","software.rst","support.rst","understanding.rst","using.rst","whatis.rst","zen.rst"],"indexentries":{"__init__() (rns.rawchannelreader method)":[[10,"RNS.RawChannelReader.__init__",false]],"__init__() (rns.rawchannelwriter method)":[[10,"RNS.RawChannelWriter.__init__",false]],"accepts_links() (rns.destination method)":[[10,"RNS.Destination.accepts_links",false]],"add_message_handler() (rns.channel.channel method)":[[10,"RNS.Channel.Channel.add_message_handler",false]],"add_ready_callback() (rns.rawchannelreader method)":[[10,"RNS.RawChannelReader.add_ready_callback",false]],"advertise() (rns.resource method)":[[10,"RNS.Resource.advertise",false]],"announce() (rns.destination method)":[[10,"RNS.Destination.announce",false]],"announce_cap (rns.reticulum attribute)":[[10,"RNS.Reticulum.ANNOUNCE_CAP",false]],"app_and_aspects_from_name() (rns.destination static method)":[[10,"RNS.Destination.app_and_aspects_from_name",false]],"await_path() (rns.transport static method)":[[10,"RNS.Transport.await_path",false]],"blackhole_identity() (rns.transport static method)":[[10,"RNS.Transport.blackhole_identity",false]],"blackhole_sources() (rns.reticulum static method)":[[10,"RNS.Reticulum.blackhole_sources",false]],"buffer (class in rns)":[[10,"RNS.Buffer",false]],"cancel() (rns.resource method)":[[10,"RNS.Resource.cancel",false]],"channel (class in rns.channel)":[[10,"RNS.Channel.Channel",false]],"clear_default_app_data() (rns.destination method)":[[10,"RNS.Destination.clear_default_app_data",false]],"concluded() (rns.requestreceipt method)":[[10,"RNS.RequestReceipt.concluded",false]],"create_bidirectional_buffer() (rns.buffer static method)":[[10,"RNS.Buffer.create_bidirectional_buffer",false]],"create_keys() (rns.destination method)":[[10,"RNS.Destination.create_keys",false]],"create_reader() (rns.buffer static method)":[[10,"RNS.Buffer.create_reader",false]],"create_writer() (rns.buffer static method)":[[10,"RNS.Buffer.create_writer",false]],"current_ratchet_id() (rns.identity static method)":[[10,"RNS.Identity.current_ratchet_id",false]],"curve (rns.identity attribute)":[[10,"RNS.Identity.CURVE",false]],"curve (rns.link attribute)":[[10,"RNS.Link.CURVE",false]],"decrypt() (rns.destination method)":[[10,"RNS.Destination.decrypt",false]],"decrypt() (rns.identity method)":[[10,"RNS.Identity.decrypt",false]],"deregister_announce_handler() (rns.transport static method)":[[10,"RNS.Transport.deregister_announce_handler",false]],"deregister_request_handler() (rns.destination method)":[[10,"RNS.Destination.deregister_request_handler",false]],"destination (class in rns)":[[10,"RNS.Destination",false]],"discovered_interfaces() (rns.reticulum static method)":[[10,"RNS.Reticulum.discovered_interfaces",false]],"enable_ratchets() (rns.destination method)":[[10,"RNS.Destination.enable_ratchets",false]],"encrypt() (rns.destination method)":[[10,"RNS.Destination.encrypt",false]],"encrypt() (rns.identity method)":[[10,"RNS.Identity.encrypt",false]],"encrypted_mdu (rns.packet attribute)":[[10,"RNS.Packet.ENCRYPTED_MDU",false]],"enforce_ratchets() (rns.destination method)":[[10,"RNS.Destination.enforce_ratchets",false]],"establishment_timeout_per_hop (rns.link attribute)":[[10,"RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP",false]],"expand_name() (rns.destination static method)":[[10,"RNS.Destination.expand_name",false]],"from_bytes() (rns.identity static method)":[[10,"RNS.Identity.from_bytes",false]],"from_file() (rns.identity static method)":[[10,"RNS.Identity.from_file",false]],"full_hash() (rns.identity static method)":[[10,"RNS.Identity.full_hash",false]],"get_age() (rns.link method)":[[10,"RNS.Link.get_age",false]],"get_channel() (rns.link method)":[[10,"RNS.Link.get_channel",false]],"get_data_size() (rns.resource method)":[[10,"RNS.Resource.get_data_size",false]],"get_establishment_rate() (rns.link method)":[[10,"RNS.Link.get_establishment_rate",false]],"get_expected_rate() (rns.link method)":[[10,"RNS.Link.get_expected_rate",false]],"get_hash() (rns.resource method)":[[10,"RNS.Resource.get_hash",false]],"get_instance() (rns.reticulum static method)":[[10,"RNS.Reticulum.get_instance",false]],"get_mdu() (rns.link method)":[[10,"RNS.Link.get_mdu",false]],"get_mode() (rns.link method)":[[10,"RNS.Link.get_mode",false]],"get_mtu() (rns.link method)":[[10,"RNS.Link.get_mtu",false]],"get_parts() (rns.resource method)":[[10,"RNS.Resource.get_parts",false]],"get_private_key() (rns.destination method)":[[10,"RNS.Destination.get_private_key",false]],"get_private_key() (rns.identity method)":[[10,"RNS.Identity.get_private_key",false]],"get_progress() (rns.requestreceipt method)":[[10,"RNS.RequestReceipt.get_progress",false]],"get_progress() (rns.resource method)":[[10,"RNS.Resource.get_progress",false]],"get_public_key() (rns.identity method)":[[10,"RNS.Identity.get_public_key",false]],"get_q() (rns.link method)":[[10,"RNS.Link.get_q",false]],"get_q() (rns.packet method)":[[10,"RNS.Packet.get_q",false]],"get_random_hash() (rns.identity static method)":[[10,"RNS.Identity.get_random_hash",false]],"get_remote_identity() (rns.link method)":[[10,"RNS.Link.get_remote_identity",false]],"get_request_id() (rns.requestreceipt method)":[[10,"RNS.RequestReceipt.get_request_id",false]],"get_response() (rns.requestreceipt method)":[[10,"RNS.RequestReceipt.get_response",false]],"get_response_time() (rns.requestreceipt method)":[[10,"RNS.RequestReceipt.get_response_time",false]],"get_rssi() (rns.link method)":[[10,"RNS.Link.get_rssi",false]],"get_rssi() (rns.packet method)":[[10,"RNS.Packet.get_rssi",false]],"get_rtt() (rns.packetreceipt method)":[[10,"RNS.PacketReceipt.get_rtt",false]],"get_segments() (rns.resource method)":[[10,"RNS.Resource.get_segments",false]],"get_snr() (rns.link method)":[[10,"RNS.Link.get_snr",false]],"get_snr() (rns.packet method)":[[10,"RNS.Packet.get_snr",false]],"get_status() (rns.packetreceipt method)":[[10,"RNS.PacketReceipt.get_status",false]],"get_status() (rns.requestreceipt method)":[[10,"RNS.RequestReceipt.get_status",false]],"get_transfer_size() (rns.resource method)":[[10,"RNS.Resource.get_transfer_size",false]],"has_path() (rns.transport static method)":[[10,"RNS.Transport.has_path",false]],"hash() (rns.destination static method)":[[10,"RNS.Destination.hash",false]],"hash_from_name_and_identity() (rns.destination static method)":[[10,"RNS.Destination.hash_from_name_and_identity",false]],"hops_to() (rns.transport static method)":[[10,"RNS.Transport.hops_to",false]],"identify() (rns.link method)":[[10,"RNS.Link.identify",false]],"identity (class in rns)":[[10,"RNS.Identity",false]],"inactive_for() (rns.link method)":[[10,"RNS.Link.inactive_for",false]],"interface_discovery_sources() (rns.reticulum static method)":[[10,"RNS.Reticulum.interface_discovery_sources",false]],"is_compressed() (rns.resource method)":[[10,"RNS.Resource.is_compressed",false]],"is_ready_to_send() (rns.channel.channel method)":[[10,"RNS.Channel.Channel.is_ready_to_send",false]],"keepalive (rns.link attribute)":[[10,"RNS.Link.KEEPALIVE",false]],"keepalive_timeout_factor (rns.link attribute)":[[10,"RNS.Link.KEEPALIVE_TIMEOUT_FACTOR",false]],"keysize (rns.identity attribute)":[[10,"RNS.Identity.KEYSIZE",false]],"link (class in rns)":[[10,"RNS.Link",false]],"link_mtu_discovery (rns.reticulum attribute)":[[10,"RNS.Reticulum.LINK_MTU_DISCOVERY",false]],"link_mtu_discovery() (rns.reticulum static method)":[[10,"RNS.Reticulum.link_mtu_discovery",false]],"load_private_key() (rns.destination method)":[[10,"RNS.Destination.load_private_key",false]],"load_private_key() (rns.identity method)":[[10,"RNS.Identity.load_private_key",false]],"load_public_key() (rns.identity method)":[[10,"RNS.Identity.load_public_key",false]],"mdu (rns.channel.channel property)":[[10,"RNS.Channel.Channel.mdu",false]],"messagebase (class in rns)":[[10,"RNS.MessageBase",false]],"minimum_bitrate (rns.reticulum attribute)":[[10,"RNS.Reticulum.MINIMUM_BITRATE",false]],"msgtype (rns.messagebase attribute)":[[10,"RNS.MessageBase.MSGTYPE",false]],"mtu (rns.reticulum attribute)":[[10,"RNS.Reticulum.MTU",false]],"next_hop() (rns.transport static method)":[[10,"RNS.Transport.next_hop",false]],"next_hop_interface() (rns.transport static method)":[[10,"RNS.Transport.next_hop_interface",false]],"no_data_for() (rns.link method)":[[10,"RNS.Link.no_data_for",false]],"no_inbound_for() (rns.link method)":[[10,"RNS.Link.no_inbound_for",false]],"no_outbound_for() (rns.link method)":[[10,"RNS.Link.no_outbound_for",false]],"pack() (rns.messagebase method)":[[10,"RNS.MessageBase.pack",false]],"packet (class in rns)":[[10,"RNS.Packet",false]],"packetreceipt (class in rns)":[[10,"RNS.PacketReceipt",false]],"pathfinder_m (rns.transport attribute)":[[10,"RNS.Transport.PATHFINDER_M",false]],"plain_mdu (rns.packet attribute)":[[10,"RNS.Packet.PLAIN_MDU",false]],"pub_to_file() (rns.identity method)":[[10,"RNS.Identity.pub_to_file",false]],"publish_blackhole_enabled() (rns.reticulum static method)":[[10,"RNS.Reticulum.publish_blackhole_enabled",false]],"ratchet_count (rns.destination attribute)":[[10,"RNS.Destination.RATCHET_COUNT",false]],"ratchet_expiry (rns.identity attribute)":[[10,"RNS.Identity.RATCHET_EXPIRY",false]],"ratchet_interval (rns.destination attribute)":[[10,"RNS.Destination.RATCHET_INTERVAL",false]],"ratchetsize (rns.identity attribute)":[[10,"RNS.Identity.RATCHETSIZE",false]],"rawchannelreader (class in rns)":[[10,"RNS.RawChannelReader",false]],"rawchannelwriter (class in rns)":[[10,"RNS.RawChannelWriter",false]],"recall() (rns.identity static method)":[[10,"RNS.Identity.recall",false]],"recall_app_data() (rns.identity static method)":[[10,"RNS.Identity.recall_app_data",false]],"register_announce_handler() (rns.transport static method)":[[10,"RNS.Transport.register_announce_handler",false]],"register_message_type() (rns.channel.channel method)":[[10,"RNS.Channel.Channel.register_message_type",false]],"register_request_handler() (rns.destination method)":[[10,"RNS.Destination.register_request_handler",false]],"remote_management_enabled() (rns.reticulum static method)":[[10,"RNS.Reticulum.remote_management_enabled",false]],"remove_message_handler() (rns.channel.channel method)":[[10,"RNS.Channel.Channel.remove_message_handler",false]],"remove_ready_callback() (rns.rawchannelreader method)":[[10,"RNS.RawChannelReader.remove_ready_callback",false]],"request() (rns.link method)":[[10,"RNS.Link.request",false]],"request_path() (rns.transport static method)":[[10,"RNS.Transport.request_path",false]],"requestreceipt (class in rns)":[[10,"RNS.RequestReceipt",false]],"required_discovery_value() (rns.reticulum static method)":[[10,"RNS.Reticulum.required_discovery_value",false]],"resend() (rns.packet method)":[[10,"RNS.Packet.resend",false]],"resource (class in rns)":[[10,"RNS.Resource",false]],"reticulum (class in rns)":[[10,"RNS.Reticulum",false]],"send() (rns.channel.channel method)":[[10,"RNS.Channel.Channel.send",false]],"send() (rns.packet method)":[[10,"RNS.Packet.send",false]],"set_default_app_data() (rns.destination method)":[[10,"RNS.Destination.set_default_app_data",false]],"set_delivery_callback() (rns.packetreceipt method)":[[10,"RNS.PacketReceipt.set_delivery_callback",false]],"set_link_closed_callback() (rns.link method)":[[10,"RNS.Link.set_link_closed_callback",false]],"set_link_established_callback() (rns.destination method)":[[10,"RNS.Destination.set_link_established_callback",false]],"set_max_request_size() (rns.destination method)":[[10,"RNS.Destination.set_max_request_size",false]],"set_packet_callback() (rns.destination method)":[[10,"RNS.Destination.set_packet_callback",false]],"set_packet_callback() (rns.link method)":[[10,"RNS.Link.set_packet_callback",false]],"set_proof_requested_callback() (rns.destination method)":[[10,"RNS.Destination.set_proof_requested_callback",false]],"set_proof_strategy() (rns.destination method)":[[10,"RNS.Destination.set_proof_strategy",false]],"set_ratchet_interval() (rns.destination method)":[[10,"RNS.Destination.set_ratchet_interval",false]],"set_remote_identified_callback() (rns.link method)":[[10,"RNS.Link.set_remote_identified_callback",false]],"set_resource_callback() (rns.link method)":[[10,"RNS.Link.set_resource_callback",false]],"set_resource_concluded_callback() (rns.link method)":[[10,"RNS.Link.set_resource_concluded_callback",false]],"set_resource_started_callback() (rns.link method)":[[10,"RNS.Link.set_resource_started_callback",false]],"set_resource_strategy() (rns.link method)":[[10,"RNS.Link.set_resource_strategy",false]],"set_retained_ratchets() (rns.destination method)":[[10,"RNS.Destination.set_retained_ratchets",false]],"set_timeout() (rns.packetreceipt method)":[[10,"RNS.PacketReceipt.set_timeout",false]],"set_timeout_callback() (rns.packetreceipt method)":[[10,"RNS.PacketReceipt.set_timeout_callback",false]],"should_use_implicit_proof() (rns.reticulum static method)":[[10,"RNS.Reticulum.should_use_implicit_proof",false]],"sign() (rns.destination method)":[[10,"RNS.Destination.sign",false]],"sign() (rns.identity method)":[[10,"RNS.Identity.sign",false]],"stale_grace (rns.link attribute)":[[10,"RNS.Link.STALE_GRACE",false]],"stale_time (rns.link attribute)":[[10,"RNS.Link.STALE_TIME",false]],"teardown() (rns.link method)":[[10,"RNS.Link.teardown",false]],"to_file() (rns.identity method)":[[10,"RNS.Identity.to_file",false]],"track_phy_stats() (rns.link method)":[[10,"RNS.Link.track_phy_stats",false]],"transport (class in rns)":[[10,"RNS.Transport",false]],"transport_enabled() (rns.reticulum static method)":[[10,"RNS.Reticulum.transport_enabled",false]],"truncated_hash() (rns.identity static method)":[[10,"RNS.Identity.truncated_hash",false]],"truncated_hashlength (rns.identity attribute)":[[10,"RNS.Identity.TRUNCATED_HASHLENGTH",false]],"unblackhole_identity() (rns.transport static method)":[[10,"RNS.Transport.unblackhole_identity",false]],"unpack() (rns.messagebase method)":[[10,"RNS.MessageBase.unpack",false]],"validate() (rns.identity method)":[[10,"RNS.Identity.validate",false]]},"objects":{"RNS":[[10,0,1,"","Buffer"],[10,0,1,"","Destination"],[10,0,1,"","Identity"],[10,0,1,"","Link"],[10,0,1,"","MessageBase"],[10,0,1,"","Packet"],[10,0,1,"","PacketReceipt"],[10,0,1,"","RawChannelReader"],[10,0,1,"","RawChannelWriter"],[10,0,1,"","RequestReceipt"],[10,0,1,"","Resource"],[10,0,1,"","Reticulum"],[10,0,1,"","Transport"]],"RNS.Buffer":[[10,1,1,"","create_bidirectional_buffer"],[10,1,1,"","create_reader"],[10,1,1,"","create_writer"]],"RNS.Channel":[[10,0,1,"","Channel"]],"RNS.Channel.Channel":[[10,1,1,"","add_message_handler"],[10,1,1,"","is_ready_to_send"],[10,2,1,"","mdu"],[10,1,1,"","register_message_type"],[10,1,1,"","remove_message_handler"],[10,1,1,"","send"]],"RNS.Destination":[[10,3,1,"","RATCHET_COUNT"],[10,3,1,"","RATCHET_INTERVAL"],[10,1,1,"","accepts_links"],[10,1,1,"","announce"],[10,1,1,"","app_and_aspects_from_name"],[10,1,1,"","clear_default_app_data"],[10,1,1,"","create_keys"],[10,1,1,"","decrypt"],[10,1,1,"","deregister_request_handler"],[10,1,1,"","enable_ratchets"],[10,1,1,"","encrypt"],[10,1,1,"","enforce_ratchets"],[10,1,1,"","expand_name"],[10,1,1,"","get_private_key"],[10,1,1,"","hash"],[10,1,1,"","hash_from_name_and_identity"],[10,1,1,"","load_private_key"],[10,1,1,"","register_request_handler"],[10,1,1,"","set_default_app_data"],[10,1,1,"","set_link_established_callback"],[10,1,1,"","set_max_request_size"],[10,1,1,"","set_packet_callback"],[10,1,1,"","set_proof_requested_callback"],[10,1,1,"","set_proof_strategy"],[10,1,1,"","set_ratchet_interval"],[10,1,1,"","set_retained_ratchets"],[10,1,1,"","sign"]],"RNS.Identity":[[10,3,1,"","CURVE"],[10,3,1,"","KEYSIZE"],[10,3,1,"","RATCHETSIZE"],[10,3,1,"","RATCHET_EXPIRY"],[10,3,1,"","TRUNCATED_HASHLENGTH"],[10,1,1,"","current_ratchet_id"],[10,1,1,"","decrypt"],[10,1,1,"","encrypt"],[10,1,1,"","from_bytes"],[10,1,1,"","from_file"],[10,1,1,"","full_hash"],[10,1,1,"","get_private_key"],[10,1,1,"","get_public_key"],[10,1,1,"","get_random_hash"],[10,1,1,"","load_private_key"],[10,1,1,"","load_public_key"],[10,1,1,"","pub_to_file"],[10,1,1,"","recall"],[10,1,1,"","recall_app_data"],[10,1,1,"","sign"],[10,1,1,"","to_file"],[10,1,1,"","truncated_hash"],[10,1,1,"","validate"]],"RNS.Link":[[10,3,1,"","CURVE"],[10,3,1,"","ESTABLISHMENT_TIMEOUT_PER_HOP"],[10,3,1,"","KEEPALIVE"],[10,3,1,"","KEEPALIVE_TIMEOUT_FACTOR"],[10,3,1,"","STALE_GRACE"],[10,3,1,"","STALE_TIME"],[10,1,1,"","get_age"],[10,1,1,"","get_channel"],[10,1,1,"","get_establishment_rate"],[10,1,1,"","get_expected_rate"],[10,1,1,"","get_mdu"],[10,1,1,"","get_mode"],[10,1,1,"","get_mtu"],[10,1,1,"","get_q"],[10,1,1,"","get_remote_identity"],[10,1,1,"","get_rssi"],[10,1,1,"","get_snr"],[10,1,1,"","identify"],[10,1,1,"","inactive_for"],[10,1,1,"","no_data_for"],[10,1,1,"","no_inbound_for"],[10,1,1,"","no_outbound_for"],[10,1,1,"","request"],[10,1,1,"","set_link_closed_callback"],[10,1,1,"","set_packet_callback"],[10,1,1,"","set_remote_identified_callback"],[10,1,1,"","set_resource_callback"],[10,1,1,"","set_resource_concluded_callback"],[10,1,1,"","set_resource_started_callback"],[10,1,1,"","set_resource_strategy"],[10,1,1,"","teardown"],[10,1,1,"","track_phy_stats"]],"RNS.MessageBase":[[10,3,1,"","MSGTYPE"],[10,1,1,"","pack"],[10,1,1,"","unpack"]],"RNS.Packet":[[10,3,1,"","ENCRYPTED_MDU"],[10,3,1,"","PLAIN_MDU"],[10,1,1,"","get_q"],[10,1,1,"","get_rssi"],[10,1,1,"","get_snr"],[10,1,1,"","resend"],[10,1,1,"","send"]],"RNS.PacketReceipt":[[10,1,1,"","get_rtt"],[10,1,1,"","get_status"],[10,1,1,"","set_delivery_callback"],[10,1,1,"","set_timeout"],[10,1,1,"","set_timeout_callback"]],"RNS.RawChannelReader":[[10,1,1,"","__init__"],[10,1,1,"","add_ready_callback"],[10,1,1,"","remove_ready_callback"]],"RNS.RawChannelWriter":[[10,1,1,"","__init__"]],"RNS.RequestReceipt":[[10,1,1,"","concluded"],[10,1,1,"","get_progress"],[10,1,1,"","get_request_id"],[10,1,1,"","get_response"],[10,1,1,"","get_response_time"],[10,1,1,"","get_status"]],"RNS.Resource":[[10,1,1,"","advertise"],[10,1,1,"","cancel"],[10,1,1,"","get_data_size"],[10,1,1,"","get_hash"],[10,1,1,"","get_parts"],[10,1,1,"","get_progress"],[10,1,1,"","get_segments"],[10,1,1,"","get_transfer_size"],[10,1,1,"","is_compressed"]],"RNS.Reticulum":[[10,3,1,"","ANNOUNCE_CAP"],[10,3,1,"","LINK_MTU_DISCOVERY"],[10,3,1,"","MINIMUM_BITRATE"],[10,3,1,"","MTU"],[10,1,1,"","blackhole_sources"],[10,1,1,"","discovered_interfaces"],[10,1,1,"","get_instance"],[10,1,1,"","interface_discovery_sources"],[10,1,1,"","link_mtu_discovery"],[10,1,1,"","publish_blackhole_enabled"],[10,1,1,"","remote_management_enabled"],[10,1,1,"","required_discovery_value"],[10,1,1,"","should_use_implicit_proof"],[10,1,1,"","transport_enabled"]],"RNS.Transport":[[10,3,1,"","PATHFINDER_M"],[10,1,1,"","await_path"],[10,1,1,"","blackhole_identity"],[10,1,1,"","deregister_announce_handler"],[10,1,1,"","has_path"],[10,1,1,"","hops_to"],[10,1,1,"","next_hop"],[10,1,1,"","next_hop_interface"],[10,1,1,"","register_announce_handler"],[10,1,1,"","request_path"],[10,1,1,"","unblackhole_identity"]]},"objnames":{"0":["py","class","Python class"],"1":["py","method","Python method"],"2":["py","property","Python property"],"3":["py","attribute","Python attribute"]},"objtypes":{"0":"py:class","1":"py:method","2":"py:property","3":"py:attribute"},"terms":{"":[0,1,3,4,7,9,13,14,15,16],"0":[1,3,4,7,10,13,14,15],"00":[4,13,14],"00000000":13,"00000100":13,"00000111":13,"01":[4,13],"0100":4,"01010000":13,"02":3,"03":4,"04":4,"05":[1,4],"0536":14,"06915":7,"08":[1,14],"09":4,"0d7334d411d00120cbad24edf355fdd2":4,"0f4259fef4521ab75a3409e353fe9073eb10783b4912a6a9937c57bf44a62c1":14,"0x0101":1,"0x20":1,"0x7d":1,"0x7e":1,"0x91c421ddfb8a30a49a71d63447ddb54cebe3465":12,"0xf000":[1,10],"1":[1,3,4,7,10,13,14,16],"10":[4,7,13,14],"100":[1,7,13],"1000":1,"10000000":13,"1024":1,"109":16,"11":[4,13],"115":13,"115200":[1,7,14],"1178a8f1fad405bf2ad153bf5036bdfd":14,"118":7,"12":[3,4,7,14],"1200":13,"125":7,"125000":7,"127":7,"128":[9,10,13],"13":14,"13425ec15b621c1d928589718000d814":[9,13],"14":[4,7],"15":[4,7],"150":[7,14],"150m":7,"153cb870b4665b8c1c348896292b0bad":4,"15kb":16,"15m":14,"16":[4,7,9,13,14,16],"1625":7,"1625000":7,"167":13,"168":16,"17":14,"1716230400":4,"1726dbad538775b5bf9b0ea25a4079c8":14,"18":[4,14],"1800":10,"187":14,"192":16,"1a54d64db7e8beca6f2c6cd17b0cb479":4,"1b03013c25f1c2ca068a4f080b844a10":14,"1h":14,"2":[1,3,4,7,10,13,14],"20":[4,7,13,14],"200":[7,14],"2005":0,"201":7,"2016":[8,15],"2023":[3,14],"2024":4,"2025":4,"2026":[4,8,16],"21":3,"21a8daa6d9c3d3b8aab6e94b6bcb0e33":4,"22":[4,7],"2225fdeecaf6e2db4556c3c2d7637294":14,"23":[4,14],"2316":14,"23h":14,"24":[4,7,14],"2400000000":7,"25":[1,6,15],"250":16,"255":[7,14],"25519":10,"256":[4,7,9,10,13,14,15],"2592000":10,"27":14,"29":7,"297":[13,15],"29716":7,"2b489d06eaf7c543808c76a5332a447d":14,"2b9ec651326d9bc274119054c70fb75":14,"2d03725b327348980d570f739a3a5708":14,"2d882c5586e548d79b5af27bca1776dc":14,"2f":1,"2owjajquafianpecac":3,"3":[1,3,4,7,13,14,15],"30":[4,7,10,14],"30602def3b3506a28ed33db6f60cc6c9":14,"32":[3,4,10,13],"3278":7,"327c1b2f87c9353e01769b01090b18f2":16,"32m":14,"33":7,"34":[7,16],"360":[4,7,10],"3600":[1,3,7],"37":4,"37428":14,"37429":14,"38":[4,14],"383":10,"3865":14,"399ea050ce0eed1816c300bcb0840938":14,"3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o":14,"3b87":7,"4":[4,10,13,14],"40m":14,"42":4,"4242":[3,7],"4251":7,"42671":7,"430":13,"4343":7,"44":15,"44318":7,"45":[1,4,13],"46":14,"464":10,"465":13,"469":14,"48555":7,"49":14,"49555":7,"4965":14,"4e":7,"4faf1b2e0a077e6a9d92fa051f256038":13,"4g":16,"4ghz":7,"5":[1,4,7,10,13,14,15,16],"500":[7,10,13,15,16],"5001":7,"50824b711717f97c2fb1166ceddd5ea9":4,"51":[7,13],"512":[7,10,13,15],"52":14,"521c87a83afb8f29e4455e77930b973b":14,"5245a8efe1788c6a1cd36144a270e13b":14,"53":4,"55":7,"56":14,"564":1,"56m":14,"5757":7,"5858":7,"59":14,"5caf":7,"5d78":7,"5urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq":7,"6":[3,4,7,10,14],"60":[1,7,14],"600":7,"63":14,"64":[3,13,14],"68a4aa91ac350c4087564e8a69f84e86":14,"7":[3,7,13,14],"71":16,"71e5":7,"72":7,"720":[7,10],"7200":7,"73":[7,14],"73cbd378bb0286ed11a707c13447bb1":14,"74":14,"74195":7,"781":14,"7822":14,"7a55144adf826958a9529a3bcf08b149":14,"8":[1,7,14],"80":[12,14],"8001":7,"809":14,"83":[13,14],"83b7328926fed0d2e6a10a7671f9e237":16,"84fpy1qbxhcgdseepynmhthcrgmx4nffbytz2gkytoqhvvhjp8eaw1z1eedrnkd19b3b8nilcgvxzkv17ummmeescrpya5w":12,"86":14,"865":7,"865600000":7,"867":7,"867200000":7,"868":13,"88":7,"89":7,"8a37cdd16938ce79861561adbd59023a":4,"8dd57a738226809646089335a6b03695":14,"8f3a21c9d84e927b":4,"8f7e6d5":4,"8f7e6d5c8f7e6d5c8f7e6d5c8f7e6d5c8f7e6d5":4,"9":[3,4,14],"90":4,"900":[13,14],"9037":14,"921600":14,"941bed5e228775e5a8079fc38b1ccf3f":14,"959e10e5efc1bd9d97a4083babe51dea":4,"96":13,"9600":1,"9710b86":4,"9710b86ba12c42d1d8f30f74fe509286":4,"9710b86ba12c4f2":4,"984b74a3f768bef236af4371e6f248cd":14,"99":13,"99714":7,"9a":4,"9fb6d773498fb3feda407ed8ef2c3229":14,"9h":14,"A":[0,1,3,4,6,7,8,9,10,11,13,14],"AND":8,"AS":8,"And":[0,1,16],"As":[0,3,4,5,7,10,11,12,13,14,15],"At":[3,13],"BE":8,"BUT":8,"Be":[4,10,14,16],"But":[0,12,13,16],"By":[0,1,3,4,7,10,13,14,16],"FOR":8,"For":[0,3,4,6,7,9,10,13,14,15],"IN":[1,8,10],"If":[0,1,3,4,5,7,9,10,11,12,13,14,15,16],"In":[0,1,3,4,5,6,7,9,10,11,13,14,15],"It":[0,1,3,4,5,7,9,10,11,13,14,15,16],"NO":8,"NOT":8,"No":[1,3,7,9,13,14,15,16],"Not":6,"OF":8,"OR":8,"Of":[9,13],"On":[0,1,3,4,7,9,14,16],"One":[3,4,5,7,10,13,16],"Or":[4,6,7,13,14,15],"Such":[0,7],"THE":8,"TO":8,"That":[0,4,11,13,16],"The":[1,3,5,6,7,8,9,10,11,12,15],"Then":[0,1,3],"There":[0,3,9,13,14,16],"These":[0,3,4,7,9,10,11,13,14],"To":[1,3,4,5,6,7,9,13,14,15],"WITH":8,"Will":10,"With":[0,6,7,9,11,13,14,16],"_":13,"__":13,"______":13,"_______":13,"________":13,"________________":13,"__future__":1,"__init__":[1,10],"__main__":1,"__name__":1,"__str__":1,"_no_us":10,"a1b2c3d4e5f686ba12c42d1ba12ef1aa":4,"a4d":7,"a79f":7,"aarch64":3,"ab":1,"abandon":0,"abc":4,"abil":[0,3,4,5,6,8,14,15],"abl":[0,1,3,4,7,10,13,14],"abolish":16,"abort":[0,1,4,7],"about":[1,3,4,5,7,9,10,13,14,16],"abov":[3,4,5,7,8,13,14],"absolut":[7,12,15,16],"abstract":[9,10,13,16],"abstractmethod":10,"abund":[5,16],"abus":14,"academ":0,"accept":[0,4,7,10,12,13,14,16],"accept_al":[1,10],"accept_app":10,"accept_non":10,"accepts_link":10,"access":[0,3,6,7,9,10,14,16],"access_point":[7,9],"accid":[11,16],"accommod":[10,13],"accomod":16,"accord":[1,3,4,9,13],"accordingli":[1,7],"account":[0,9,16],"achiev":[0,3,7,9,10,13,15],"acknowledg":[9,15],"across":[0,1,4,5,7,9,10,13,14,16],"act":[0,3,7,10,13,14,16],"action":[0,1,8,14],"activ":[0,1,3,4,7,10,13,14],"actor":[0,13,14,16],"actual":[0,1,3,4,7,9,13,14,16],"ad":[0,1,4,5,6,7,9,10,11,13,14,15,16],"adapt":[3,5,16],"add":[0,1,3,4,7,9,10,14],"add_argu":1,"add_message_handl":[1,10],"add_ready_callback":10,"addict":16,"addit":[4,5,7,9,10,11,13,14,15],"addition":[3,4,5,7,13],"addr":14,"address":[0,1,3,4,6,7,10,13,14,15],"adher":[7,10],"adjust":[3,4,10,14,16],"adm":4,"admin":[4,7],"administ":4,"administr":[4,9,11,13,14,16],"adopt":13,"adress":[10,13],"advanc":[11,13],"advantag":10,"advers":15,"adversari":[3,9,12,16],"advertis":[1,7,10],"advic":12,"advis":[7,13],"ae":[10,13,15],"af73":7,"affect":[3,7,16],"affili":[0,11],"affin":7,"afford":9,"after":[1,3,4,7,9,10,13,14],"afterthought":16,"ag":0,"again":[7,13,14],"against":[0,3,4,7,13,16],"agenc":[6,13],"agent":[13,16],"agnost":[9,13],"agnostic":[13,16],"ago":14,"agreement":[0,16],"ahead":9,"ai":16,"aim":[0,3,6,13],"air":[3,5],"airmax":5,"airtim":[7,9,16],"airtime_limit_long":7,"airtime_limit_short":7,"akin":13,"algorithm":[0,8,9,16],"alia":4,"alias":6,"aliased_nam":4,"alic":[4,13],"alien":16,"align":[3,14,16],"aliv":[10,13],"all":[0,1,3,4,5,6,7,8,9,10,11,12,13,14,15,16],"allevi":5,"alloc":[7,9,10,13,14],"allow":[0,1,3,4,5,7,9,10,11,13,14,15,16],"allow_al":[1,10],"allow_list":10,"allow_non":10,"allowed_hash":14,"allowed_ident":14,"allowed_list":10,"allowedsignersfil":4,"almost":[0,5,7,13,14,16],"alon":7,"along":[4,7,10,13,14],"alongsid":[4,9],"alreadi":[1,3,4,5,9,10,11,13,14,16],"also":[0,1,3,4,5,6,7,9,10,11,13,14,15,16],"alter":[3,10,13],"altern":[1,3,4,7,14],"although":[3,13,15],"altitud":7,"alwai":[0,3,4,7,9,10,13,14,16],"am":16,"amateur":[7,15],"among":[0,16],"amongst":8,"amount":[1,7,9,10,13,15,16],"amplifi":0,"amsterdam":[7,14],"an":[0,1,3,4,5,7,8,9,10,11,13,14,15,16],"analog":[11,16],"analysi":[0,4],"analyt":12,"analyz":0,"anchor":6,"android":[6,7,11,14],"ani":[0,1,3,4,5,7,8,9,10,11,12,13,14,15],"annonuc":7,"annot":1,"annouce_cap":7,"announc":[3,4,6,9,10,11,14],"announce_cap":[7,10],"announce_handl":1,"announce_interv":[4,7],"announce_packet_hash":10,"announce_rate_grac":[3,7],"announce_rate_penalti":[3,7],"announce_rate_target":[3,7],"announced_ident":[1,10],"announceloop":1,"announces_from_intern":7,"announces_to_intern":7,"announcesampl":1,"annoy":14,"anonym":[3,9,10,13,15],"anoth":[4,7,9,10,13,14,16],"answer":[13,14,16],"antenna":[5,16],"anticip":4,"anxieti":16,"anymor":[3,16],"anyon":[0,1,3,4,5,7,9,10,13,14,16],"anyth":[0,4,5,7,11,13,14,15,16],"anywai":7,"anywher":[0,1,7,16],"ap":7,"api":[3,4,6,13,15,16],"apk":3,"app":[1,3,10,13,16],"app_and_aspects_from_nam":10,"app_data":[1,10],"app_nam":[1,10],"app_timeout":1,"appar":0,"appear":16,"append":[1,13,14],"appli":[0,4,7,10,13,16],"applic":[0,1,7,9,10,11,13,14,15,16],"appreci":4,"approach":[0,3,5,9,13,14],"appropri":[0,4,7,9,16],"approv":[0,9,13,16],"approxim":[7,14],"april":3,"apt":3,"ar":[0,1,3,4,5,7,9,10,11,12,13,14,15,16],"arbit":16,"arbitrari":[4,10,13,14,15],"arch":[3,16],"architect":[12,16],"architectur":[3,6,11],"archiv":[4,6,11],"area":[0,3,5,7,9,13,15],"arg":[1,14],"argon":1,"argpars":1,"argument":[1,4,10,14],"argumentpars":1,"aris":8,"arm64":6,"armi":16,"armor":[4,16],"around":[3,7,13,16],"arrai":3,"arrang":0,"arriv":[1,13,16],"art":16,"artifact":[4,6],"artifacts_dir":4,"artifici":8,"arx":14,"ascii":4,"ask":[1,3,5,10,11,16],"aspect":[1,3,5,7,9,10,13,14],"aspect_filt":[1,10],"assert":[0,16],"assign":[1,9,13,14,16],"assist":11,"associ":[4,8,10,13,14],"assum":[7,9,13,16],"assumpt":[9,12,16],"assuredli":16,"asymmetr":[13,14,15],"asymmetri":0,"asynchron":[9,16],"atop":0,"attach":[0,11,14],"attached_interfac":10,"attack":3,"attain":3,"attempt":[1,3,4,5,7,10,14],"attent":[0,12,16],"attest":4,"attribut":[0,6,10],"atx":14,"auth":14,"authent":[0,4,7,9,10,11,13,14,15],"author":[0,6,8,13,14,16],"authoris":14,"authorit":[4,13,15],"authorship":0,"auto":[3,4,5,6,10,13,14],"auto_compress":10,"autoconfigur":15,"autoconnect_announces_to_intern":14,"autoconnect_discovered_interfac":[7,14],"autoconnect_interface_grav":7,"autoconnect_interface_mod":14,"autodiscov":7,"autoinstal":[5,14],"autointerfac":[3,5,7,14],"autom":[0,3,4,5,6,11,12,13],"automat":[1,3,6,7,9,10,11,13,14,15,16],"autonom":[0,9,13,16],"autonomi":[0,15,16],"avail":[0,1,3,4,5,6,7,9,10,11,13,14,15,16],"averag":13,"avoid":[5,13,14],"awai":[0,1,7,13,14,16],"await_path":10,"awar":[3,4,7,13],"ax":[6,15],"ax25kissinterfac":7,"b":[1,4,14],"b32":7,"back":[0,1,4,7,13,15,16],"backbon":[5,6,9,14,16],"backboneinterfac":[3,7,14],"backend":[7,13],"background":[3,7,14],"backhaul":[5,9],"backup":[0,14],"balanc":7,"band":[5,13,16],"bandwidth":[3,6,7,9,10,11,13,14,15],"bar":16,"bare":[0,1,4,7,16],"barrier":[0,7,13,16],"base":[0,4,6,7,9,10,11,13,14,15,16],"base32":[7,14],"base64":[4,14],"bash":[7,14],"bash_sourc":4,"basi":[0,7,10,13,14,15],"basic":[1,3,6,7,9,10,14],"batch":4,"batteri":16,"baud":[7,14],"baud_flash":14,"baudrat":1,"bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6":12,"bc7291552be7a58f361522990465165c":[14,15],"beacon":7,"beast":16,"beat":16,"beauti":[0,11],"becam":0,"becaus":[0,4,9,13,16],"becki":1,"becom":[0,1,3,4,7,9,13,16],"been":[0,1,3,4,5,7,9,10,11,12,13,14,15,16],"befor":[1,3,4,7,9,10,12,13,14,16],"beg":16,"begin":[1,5,10,16],"begun":10,"behalf":[7,9],"behav":[5,11],"behavior":[7,16],"behaviour":[3,7,14],"behind":[3,7,12,13,16],"being":[0,3,4,7,10,13,16],"beings":[0,8,13],"belief":13,"believ":[0,16],"belong":[7,9,13,14,16],"below":[5,7,14],"bend":16,"benefici":[7,14],"berlin":16,"best":[3,9,13,16],"better":[0,3,7,9,12,13,16],"between":[0,1,7,9,10,11,13,14,16],"beyond":[0,4],"bi":[10,13],"bidirect":13,"big":16,"bill":16,"billion":[9,13],"billionair":16,"bin":[3,7,14],"binari":[1,3,7,9,10,13,14,16],"bind":[6,7],"bit":[1,3,4,7,9,10,11,13,14,15,16],"bitcoin":12,"bitrat":[1,7,10],"black":0,"blackhol":[3,6,9,10,13],"blackhole_ident":10,"blackhole_sourc":[10,14],"blackhole_update_interv":14,"blackholed_ident":14,"blackholeupdat":14,"ble":7,"blindli":[14,16],"blob":[1,4,13],"block":[3,4,6,9,10,11,13,14,16],"block_fast_flap":7,"blockchain":16,"blocklist":14,"blood":16,"blueprint":16,"bluetooth":14,"board":[3,6,7,11,13,15],"boat":16,"bob":4,"bobs_nod":4,"bogu":7,"boil":5,"bond":16,"book":16,"bookworm":6,"bool":10,"boot":[3,14],"bootstrap":[6,7,14],"bootstrap_onli":[3,7],"borrow":16,"bot":11,"both":[0,1,3,4,5,7,9,10,11,13,14,15],"bounc":[3,16],"bound":[9,16],"boundari":[0,4,7,9,13],"box":[0,5],"branch":[0,4,14],"breadcrumb":4,"break":[3,10,13,16],"breath":[3,16],"breviti":14,"bridg":[5,7],"briefli":[5,13],"bring":[7,14],"brittl":[3,16],"broad":[3,5],"broadcast":[3,6,7,9,10,13,14,16],"broadcast_destin":1,"broadcastloop":1,"broader":[3,13],"broken":[3,7,16],"brought":[7,14],"brows":[0,4,11,16],"browser":[4,11,16],"buffer":[6,7,10,15],"bufferedread":10,"bufferedrwpair":10,"bufferedwrit":10,"bufferexampl":1,"bug":[4,11,12],"bui":16,"build":[0,1,4,5,6,7,11,13,15,16],"builder":16,"built":[0,3,7,9,11,13,14,15,16],"bulletin":11,"bundl":1,"bureaucraci":16,"bureaucrat":[15,16],"burst":6,"busy":0,"button":16,"bw":14,"bypass":[7,12,16],"byte":[1,6,9,10,13,14,15],"bytes":1,"c":[1,4,8,14],"c50cc4e4f7838b6c31f60ab9032cbc62":14,"c89b4da064bf66d280f0e4d8abfd9806":14,"cabl":[7,16],"cach":[7,9,14],"cafe":16,"cage":16,"calcul":[9,10,13,14],"call":[0,1,9,10,11,13,15,16],"callabl":[1,10],"callback":[1,10],"callsign":7,"came":4,"camp":0,"can":[0,1,3,4,5,6,7,9,10,11,12,13,14,16],"cancel":[4,10],"candid":9,"cannot":[0,1,3,4,7,9,15,16],"cap":7,"capabl":[0,1,3,7,9,13],"capac":[3,5,9,10,13],"capit":[0,16],"captur":[0,16],"car":9,"carambola":1,"card":[0,16],"care":[0,3,4,9,10,14,15,16],"carefulli":3,"cargo_build_target":3,"carri":[0,7,9,10,13,14,15,16],"carrier":[6,9,15],"case":[0,1,3,4,5,7,9,13,14],"castl":16,"cat":14,"catastroph":0,"categori":5,"caus":[3,7],"caution":7,"cb":[10,13],"cbc":[13,15],"cd":4,"cdma":7,"ceas":[0,16],"cede":0,"cell":9,"cellular":16,"cement":16,"censor":[0,13,14,16],"censorship":[13,15],"center":6,"central":[0,3,4,5,7,9,11,13,14,16],"centralis":13,"centric":0,"centuri":0,"ceo":16,"certain":[1,7,9,12,13,14,16],"certif":16,"certifi":0,"chain":[4,7,16],"challeng":[3,11,13,16],"chanc":5,"chang":[0,1,3,4,7,10,13,14,15,16],"channel":[0,3,4,6,7,9,10,12,13,15,16],"channelarg":1,"channelexampl":1,"chaotic":16,"chapter":[0,3,4,5,7,9,10,11,13,14,15],"charact":[1,4,7,14],"characterist":[7,9,13,16],"charg":8,"chart":4,"charter":13,"chase":16,"chat":6,"cheap":[5,7,13],"cheapli":3,"check":[1,3,4,10,13,14,16],"checksum":[4,10,15],"chmod":4,"choic":[3,9,16],"choke":16,"choos":[0,1,3,7,11,13,16],"chose":13,"chosen":13,"chunk":1,"cific":7,"ciphertext":10,"ciphertext_token":10,"circl":16,"circumst":[12,13,16],"citi":16,"cl":1,"claim":[4,8,13],"clariti":16,"class":[1,9,10,15],"clear":[1,7,10,14,16],"clear_default_app_data":10,"clear_screen":1,"clearli":16,"click":4,"client":[0,1,4,5,6,10,14,16],"client_buffer_readi":1,"client_config":4,"client_connect":1,"client_disconnect":1,"client_ident":[1,4],"client_loop":1,"client_message_receiv":1,"client_packet_receiv":1,"client_request":1,"climat":16,"clone":[4,13],"close":[1,9,10,14],"closed_callback":10,"closer":[11,13],"closest":13,"cloth":16,"cloud":[0,6,7,11],"cluster":[0,13],"co":[4,5,7,15],"code":[0,3,4,5,6,7,9,11,12,14,15,16],"codic":0,"codingr":7,"coexist":9,"coffe":16,"coher":16,"collabor":[0,4,13,14],"collaps":[0,3,16],"collect":[0,3,12],"collis":9,"colon":16,"color":4,"com":[1,4,12,16],"combin":[0,3,4,6,11,13,14,16],"combinatori":16,"come":[0,3,7,9,13,16],"comma":[7,14],"command":[0,1,3,6,7,11,14],"commend":16,"comment":[0,4,7,14],"commerci":16,"commit":[0,3,6,11],"committe":16,"common":[0,1,3,5,6,9,13,14,15,16],"commonli":[4,5,7],"commun":[0,1,3,4,6,7,9,10,11,12,13,14,15,16],"communica":7,"compani":16,"compar":[9,16],"compass":16,"compat":[0,1,3,4,5,7,10,11,13,14,15],"compet":[0,16],"compil":3,"complain":16,"complet":[0,1,3,4,5,7,9,10,11,13,14,15],"complex":[0,3,7,12,13,16],"compli":9,"compon":[5,12,13,16],"compos":[4,6,13],"comprehens":[0,11,12],"compress":[1,10,13,14,15],"compromis":[0,13,16],"comput":[0,3,7,13,15,16],"computation":[7,14],"concaten":[10,14],"conceiv":[0,3,5],"concept":[0,6,13,14,16],"conceptu":[0,4,6],"concern":16,"concert":13,"conclud":[1,10],"concret":16,"concurr":[5,13,14],"condit":[3,8,13,15,16],"conduct":0,"conduit":16,"conf":[3,11],"config":[1,3,4,7,9,14,16],"configarg":1,"configdir":10,"configobj":4,"configpath":1,"configur":[0,1,3,5,6,9,10,13,15],"configuraion":5,"configuraiton":14,"configure_devic":1,"confirm":[4,9,13,15,16],"conflict":[3,9],"conform":0,"confront":16,"confus":[5,13],"congest":13,"conglomer":16,"conjunct":7,"connect":[0,1,4,5,6,8,10,12,13,14,15,16],"conscienc":16,"conscious":0,"conscript":16,"consent":0,"consequ":[0,9,13],"conserv":7,"consid":[1,3,7,10,11,13,14,15,16],"consider":[6,13],"consist":[4,13,14],"consol":14,"constant":[0,10,16],"constantli":[13,16],"constitut":[0,3,13],"constrain":[9,10],"constraint":16,"construct":[0,4,13,15,16],"constructor":1,"consum":[4,9,10,16],"consumpt":[9,16],"contact":[9,11,13,16],"contain":[0,1,4,7,9,10,13,14],"contempl":16,"contemporari":0,"contend":13,"content":[0,1,3,4,9,11,14],"context":[3,13,14,16],"contin":[1,3,16],"continu":[0,4,7,10,12,14,15,16],"contract":8,"contrari":16,"contrast":0,"contribut":[0,6,8,9,12],"contributor":0,"control":[0,1,3,4,5,6,9,10,11,13,14,15,16],"convei":[1,16],"conveni":[0,1,4,14,16],"convent":[1,9],"converg":[3,9,13,16],"convers":[0,4,11,13,16],"convert":[0,4],"cook":3,"coordin":[7,9,10,13,15,16],"copi":[0,4,8,14],"copyright":[0,8],"core":[0,9,13,14,15,16],"corner":16,"corpor":[0,16],"correct":[0,1,4,7,9,13],"correctli":[1,4,9],"correspond":[4,9,13,16],"corrupt":[0,4],"cost":[5,6,7,13,15],"could":[0,1,3,7,9,10,13,14,15,16],"count":[4,11,13,16],"counter":1,"cours":[3,4,7,9,13,16],"coven":16,"cover":[3,4,5,9,13,15],"coverag":9,"cpu":[1,7,9,13],"cpuinfo":14,"cr":14,"craft":[0,4,16],"craftsman":16,"crash":16,"creat":[0,1,6,7,9,10,11,14,15,16],"create_bidirectional_buff":[1,10],"create_kei":10,"create_read":10,"create_receipt":[1,10],"create_writ":10,"creation":[0,6,8,9,11,13,14,15],"creativ":[0,16],"creator":[0,6,13,16],"credenti":[7,13],"critic":[0,9,13,14],"cron":0,"cross":[7,11],"crowd":16,"crucial":9,"cruft":16,"cryptograph":[0,3,6,7,9,14,15,16],"cryptographi":[0,3,13,15],"crystal":16,"ctrl":1,"cull":10,"culmin":16,"cultur":0,"cumbersom":4,"curl":7,"current":[0,1,3,4,6,7,10,11,14,16],"current_download":1,"current_filenam":1,"current_ratchet_id":10,"curv":[9,10,13,15],"curve25519":[9,10,13,15],"custodian":16,"custom":[5,6,10,11,13,15],"custom_network_nam":7,"customis":7,"cut":16,"cynic":16,"d":[4,14],"d09285e660cfe27cee6d9a0beb58b7e0":4,"d56a4fa02c0a77b3575935aedd90bdb2":14,"daemon":[1,3,7,10,11,14],"dai":[4,10,11,16],"daili":4,"damag":[0,8,16],"danc":16,"danger":16,"dark":16,"data":[0,1,3,4,5,6,7,9,10,13,15,16],"data_buff":1,"data_port":7,"databas":[0,11,14,16],"databit":[1,7,14],"datacent":3,"datar":7,"dataset":[8,16],"date":[1,3,4,11,14],"datetim":1,"db":[1,14],"dbm":[1,7,14],"deadlin":0,"deal":[7,8],"death":6,"debian":[6,13,14],"debug":[3,14],"decad":[0,5,12,16],"decentr":[3,6,7,14],"decentralis":15,"decid":[0,7,9,10,13,16],"decim":7,"decis":[0,4,7,9,13,16],"declar":13,"decod":[1,7,14],"decoupl":0,"decreas":[4,7,14],"decrypt":[7,9,10,13,14],"dedic":[4,7,13,15,16],"deep":[3,16],"deepli":[0,16],"def":[1,4],"default":[1,3,4,5,7,9,10,11,13,14],"default_ar_grac":7,"default_ar_penalti":7,"default_ar_target":7,"default_grav":7,"default_ifac_s":1,"defin":[1,4,7,10,13,14,15,16],"definit":[1,3,4,13,14,16],"defunct":14,"degrad":9,"degre":7,"delai":[7,11,13,14,16],"deleg":[0,4,13],"delet":[0,4,16],"delimit":1,"deliv":[1,3,9,10,11,16],"deliveri":[0,1,4,9,10,11,15],"demand":[4,10,16],"demonstr":[0,1,3,7,16],"deni":[0,4,16],"denomin":0,"depend":[0,1,4,5,6,7,9,10,13,14,16],"deploi":[13,16],"deploy":4,"depriorit":16,"depth":16,"deregist":10,"deregister_announce_handl":10,"deregister_request_handl":10,"deriv":[3,9,10,13,15],"describ":[4,5,7,10,13],"descript":[1,4,14],"descriptor":1,"deseri":1,"deserializ":1,"design":[0,3,4,5,6,7,9,11,12,13,14,15],"desir":[1,5,7,13],"desk":16,"desktop":[7,11],"dest_len":1,"destin":[1,3,6,10,14,16],"destination_1":1,"destination_2":1,"destination_clos":1,"destination_hash":[1,4,10,14],"destination_hexhash":1,"destroi":[0,16],"destruct":16,"detach":[4,7],"detail":[0,1,3,4,6,7,10,14,15],"detect":[1,4],"determin":[3,9,10,13,16],"determinist":4,"detriment":14,"dev":[1,3,4,7,14],"devel":3,"develop":[4,5,6,8,10,11,12,13,14,15,16],"devic":[1,3,6,7,9,10,11,13,14,16],"dh":10,"dhcp":[5,7,9,14],"di":16,"dict":1,"dictat":[14,16],"dictatorship":16,"dictionari":14,"did":[0,1,11,16],"diff":4,"differ":[0,1,3,4,5,7,9,10,11,13,14,15,16],"differenti":14,"diffi":[9,13],"difficult":[3,5,14],"difficulti":[7,14],"digit":[0,3,5,13,15,16],"digniti":[0,16],"dilut":0,"dinner":16,"dir":[1,4],"dire":5,"direct":[0,1,3,4,5,9,10,11,13,14],"direction":13,"directli":[0,3,4,5,7,8,9,10,11,13,14,15,16],"directori":[1,3,4,7,9,14,16],"dirnam":4,"disabl":[7,13,14],"disappear":[7,14,16],"disassoci":13,"discard":[3,10,13],"discern":0,"disciplin":[0,16],"disconnect":[1,3,6,7],"discourag":0,"discov":[0,3,6,7,9,10,11,13,16],"discover":[3,6,14],"discover_interfac":14,"discovered_interfac":10,"discoveri":[3,4,6,10,13,14],"discovery_bandwidth":7,"discovery_encrypt":7,"discovery_frequ":7,"discovery_modul":7,"discovery_nam":7,"discovery_port":7,"discovery_scop":7,"discovery_stamp_valu":7,"discret":0,"discrimin":9,"discuss":[0,4,5,13],"disk":[1,4,10,14],"displai":[1,4,7,9,11,13,14],"disrupt":[0,7],"dissemin":0,"dissolv":[9,16],"dist":4,"distanc":[5,13,14],"distant":[3,10,13],"distinct":[0,7,9,13,16],"distinguish":[9,13],"distribut":[1,4,6,8,9,10,11,13,14,15,16],"dive":3,"diverg":0,"divers":0,"divid":10,"divmod":1,"dn":[5,7,11,16],"dna":16,"dnf":3,"do":[0,1,3,4,5,7,8,9,10,13,14,16],"doc_id":4,"document":[0,3,6,8,10,11,13,14,15,16],"doe":[0,1,3,4,5,6,7,10,11,13,14,16],"doesn":[0,7,9,16],"dollar":16,"domain":[0,3,6,7,13,14,15],"domin":16,"don":[1,3,4,9,10,13,14,16],"donat":6,"done":[1,3,4,7,13,14,16],"door":16,"dot":[13,14],"doubt":3,"dowload":3,"down":[0,1,5,7,10,14,16],"download":[0,1,3,4,7,9,11,14],"download_began":1,"download_conclud":1,"download_finish":1,"download_start":1,"download_tim":1,"downstream":14,"drag":11,"drastic":14,"draw":16,"drawn":16,"drive":[0,12],"driver":[14,15],"droid":3,"drone":16,"drop":[4,7,9,10,11,13,14,16],"drown":0,"dsrdtr":1,"dual":[5,14],"dublin":14,"due":[0,1],"dumb":16,"dump":[7,14],"duplex":[13,15],"durabl":0,"durat":14,"dure":[4,14],"dynam":[0,3,4,7,11,14,16],"dysfunct":12,"dysregul":0,"e":[1,4,14],"e5c032d3ec4e64a6aca9927ba8ab73336780f6d71790":14,"e702c42ba8":14,"e7536ee90bd4a440e130490b87a25124":14,"each":[0,1,3,4,7,9,10,11,13,14],"earli":16,"earlier":13,"eas":[7,13],"easi":[3,4,5,7,11,13,14,15],"easier":[3,4,7,9,14,15],"easiest":[3,5,7,13],"easili":[0,3,4,7,9,13,14,15,16],"eastern":13,"ec_pr_freq":7,"ecdh":[13,15],"echo":[4,6],"echo_destin":1,"echo_request":1,"econom":0,"economi":16,"ecosystem":[0,3,7,11,13,14,16],"ed25519":[0,4,13,15],"edit":[3,4,14],"editor":[4,14],"ee":4,"eeprom":14,"effect":[0,4,7,13,14],"effici":[1,3,7,9,10,11,13,14,15,16],"effort":[5,16],"effortlessli":11,"eg":14,"egress":7,"egress_control":7,"ei":1,"either":[3,4,5,9,10,13,14,16],"elaps":[4,7],"electromagnet":16,"eleg":16,"element":[9,16],"elev":0,"elif":1,"elimin":[0,9,16],"ellipsoid":7,"ellipt":[9,10,13,15],"els":[0,1,9,11,12,13,15,16],"email":[0,4,11],"emb":4,"embed":[0,4,5],"embrac":16,"emerg":[0,6,11],"emiss":11,"emot":0,"emploi":[0,5,13],"empow":15,"empti":[1,6,16],"emul":[3,14],"enabl":[0,1,3,5,6,9,10,11,13,14,15,16],"enable_ratchet":10,"enable_remote_manag":14,"enable_transport":[7,13,14],"encapsul":[7,15],"enclos":4,"encod":[1,4,10,11,14,16],"encount":[3,9],"encourag":[0,3,16],"encrypt":[0,1,3,4,6,7,9,10,11,13,14,15],"encrypted_mdu":10,"end":[1,4,7,9,10,11,13,15],"endeavor":0,"endless":[7,16],"endow":0,"endpoint":[1,9,10,11,13,16],"energi":[3,16],"enforc":[7,10,13,14],"enforce_ratchet":10,"engag":[0,16],"engin":[0,3,11,12,16],"enhanc":13,"enjoy":0,"enough":[0,3,5,7,11,13],"ensur":[0,1,3,4,7,9,10,11,13,14,15,16],"ensurepath":3,"enter":[0,1,14],"entir":[0,1,3,4,7,9,10,11,13,14,15,16],"entiti":[0,9,13,14,16],"entitl":0,"entri":[0,1,3,13,14,16],"entropi":16,"entrypoint":[6,7,14],"enumer":1,"envelop":[4,10,16],"environ":[0,3,4,6,7,9,12,13,14],"environment":13,"environmentlogg":13,"eof":7,"ephemer":[0,9,10,13,15],"epub":6,"equal":[0,9,10,13,16],"equip":[7,9,13],"equival":[4,7,16],"era":16,"erod":16,"erron":12,"error":[1,3,4,7,12,14,16],"esc":1,"esc_mask":1,"escap":[1,14],"esoter":0,"esp32":5,"especi":[3,4,5,7,14],"essenti":[0,3,4,7,11,13,14,16],"establish":[0,1,3,7,9,10,11,14,15,16],"established_callback":10,"establishment_timeout_per_hop":10,"etc":[4,14],"eth0":7,"eth1":7,"ether":16,"ethereum":12,"ethernet":[3,6,7,9,11,13,15,16],"ethic":6,"evalu":0,"evapor":16,"even":[0,1,3,4,7,9,10,11,13,14,15,16],"event":[7,8,16],"eventu":10,"ever":13,"everi":[0,1,3,4,7,9,10,13,14,16],"everydai":3,"everyon":[0,3,4,11,12,13,16],"everyth":[0,1,4,9,11,12,13,16],"evict":16,"evolv":[0,3,9,13],"exact":[4,5,7,13],"exactli":[4,10,13,16],"exampl":[0,3,5,6,9,10,13,14,15],"example_util":1,"exampleannouncehandl":1,"exampleconfig":[3,14],"exampleinterfac":1,"exce":[1,7,10],"except":[1,7,10],"excess":[7,14],"exchang":[0,9,10,13,15],"exclud":[4,10,13],"exclus":[0,4],"execstart":14,"execstartpr":14,"execut":[0,1,3,4,7,10,14],"exercis":0,"exhaust":[5,11,13,14],"exist":[0,1,3,4,5,6,7,9,11,13,14,15],"exit":[0,1,4,7,10,14],"expand":[4,9,11,13,15],"expand_nam":10,"expans":[0,4,16],"expect":[1,4,7,10,12,13,14],"expens":[5,7],"experi":[9,11,13,14,16],"experienc":1,"expir":[7,10],"expiri":[7,10],"explain":[5,10],"explan":[13,14],"explicit":10,"explicitli":[7,9,13,14],"exploit":0,"explor":[1,11,13,15,16],"export":[3,14],"expos":[7,10,14],"exposur":7,"express":[0,4,8],"extend":[0,1,10],"extens":[4,7,11,14,15],"extern":[0,1,3,4,7,10,14,15,16],"extra":[3,7,14],"extract":[0,4,14,16],"extract_id":4,"extrem":[3,11,13,14,15,16],"f":[1,3,4,14],"f2d31b2e080e5d4e358d32822ee4a3b7":4,"f4":7,"f53a1c4278e0726bb73fcc623d6ce763":14,"fabric":[6,9],"face":[7,11],"facebook":0,"facilit":[11,13],"fact":[0,3,7,15],"factor":[7,10,14],"fade":16,"fail":[0,1,3,4,7,10,16],"failed_callback":[1,10],"failur":[0,3,4,7,10,16],"fake":14,"fallaci":6,"fallback":[4,16],"fallen":16,"fals":[1,7,9,10],"famili":3,"familiar":0,"fantasi":16,"far":[0,3,9,13],"fashion":12,"fast":[6,7,9,10,11,13],"fast_flapping_block_tim":7,"fast_flapping_grac":7,"fast_flapping_threshold":7,"faster":[3,7,13,16],"fastest":[7,13],"fat":16,"favor":3,"favorit":11,"fe80":3,"featur":[3,4,6,7,9,11,12,13,14,15],"feder":[11,13,14,16],"fedora":3,"feed":[0,3,10,16],"feedback":[0,1,6],"feel":[11,16],"fernet":13,"fetch":[0,4,7,14],"few":[0,3,5,7,9,11,13,14,15,16],"fewer":10,"ff":14,"ffcffb4e255e156e77f79b82c13086a6":4,"fi":[4,12],"fiber":[5,16],"fibr":15,"field":[4,13,16],"figur":7,"file":[0,1,3,4,5,7,8,9,10,11,13,14,15],"file_resourc":1,"file_s":1,"filelist":1,"filelist_data":1,"filelist_receiv":1,"filelist_timeout_job":1,"filenam":1,"filesync":6,"filetransf":[6,10],"filter":[0,1,9,13,14],"final":[1,4,10,13,16],"find":[0,4,6,7,11,13,14,16],"find_spec":1,"fine":[0,4,7,14],"fingerprint":16,"finish":6,"finit":16,"fire":16,"firewal":[3,7,9,16],"firmwar":[3,5,13,14],"firmware_hash":14,"first":[0,1,3,4,7,9,10,13,14,16],"fit":[0,1,8,16],"five":[1,16],"fix":[4,6,13,16],"fixed_mtu":7,"flag":[1,3,4,10,13,14],"flap":7,"flash":14,"flasher":3,"flat":16,"flaw":16,"fleet":13,"flesh":16,"flexibl":[3,4,5,7,9,11,14,15],"flexibli":4,"fli":13,"flicker":16,"flight":10,"flip":16,"float":[1,7,10],"flood":[7,14],"flow":[0,1,3,6,7,9,13],"flow_control":7,"fluid":16,"fluiditi":16,"fluidli":16,"flush":1,"focu":[0,14],"focus":[11,13],"folder":[0,1,4],"follow":[0,1,3,4,5,7,8,10,12,13,14,15,16],"font":4,"forbid":16,"forc":[0,1,14,16],"forcibli":[10,14],"foreground":3,"forev":16,"forg":16,"forget":[9,14],"forgotten":0,"fork":[0,6,11],"forker":4,"form":[3,6,7,9,10,13,14,16],"format":[0,1,6,7,11,14,15],"forth":1,"fortifi":0,"fortun":16,"forward":[0,4,6,7,9,10,11,13,14,15],"forward_ip":7,"forward_port":7,"found":[1,3,4,7,10,13,14,16],"foundat":[0,9,12,13,15,16],"fragil":[0,16],"frame":[1,7],"framework":11,"free":[3,8,9,11,12,15,16],"freedom":[13,16],"freedv":5,"freeli":13,"freq":14,"frequenc":[3,5,7,13,14],"frequent":[7,9],"friend":[3,16],"friendli":11,"from":[0,1,3,4,5,6,7,8,9,10,11,12,13,14,15],"from_byt":10,"from_fil":10,"from_identity_hash":10,"fromhex":1,"front":[4,13],"frontend":11,"fruit":1,"ftdi_ft230x_basic_uart_43891ckm":14,"fuel":16,"full":[0,1,3,4,7,9,10,11,13,14,15],"full_hash":10,"full_nam":[10,14],"fulli":[1,3,4,5,7,11,13,14,15],"funcion":10,"function":[0,1,3,4,5,6,7,8,9,10,11,12,14,15,16],"fundament":[0,5,9,13,14,16],"furnish":8,"further":[0,3,6,7,14,16],"futur":[0,4,6,9,11,14,16],"fw":14,"g":[1,4,13,14],"ga":1,"gain":[0,5,10,13,16],"galact":13,"gap":3,"gaslit":16,"gatekeep":[0,13,16],"gatewai":[1,3,7,9,13,14,16],"gbp":14,"gear":16,"gen_tim":4,"gener":[0,1,3,4,5,7,9,10,13,14,15,16],"generalis":15,"genuin":14,"geograph":[0,3,7],"geoid":7,"get":[0,1,4,5,6,7,9,10,11,14,16],"get_ag":10,"get_channel":[1,10],"get_config_obj":1,"get_data_s":10,"get_establishment_r":10,"get_expected_r":10,"get_external_ip":7,"get_hash":10,"get_inst":10,"get_mdu":10,"get_mod":10,"get_mtu":10,"get_packet_rssi":1,"get_packet_snr":1,"get_part":10,"get_private_kei":10,"get_progress":[1,10],"get_public_kei":10,"get_q":10,"get_random_hash":[1,10],"get_remote_ident":[1,10],"get_request_id":10,"get_respons":10,"get_response_tim":10,"get_rssi":10,"get_rtt":[1,10],"get_seg":10,"get_snr":10,"get_statu":10,"get_transfer_s":10,"geti2p":7,"ghost":16,"ghz":13,"gi":1,"giant":16,"gift":16,"gigabit":[9,16],"gigabyt":15,"git":[0,6,14],"github":[0,1,3,4],"gitlab":0,"give":[3,7,13,14,16],"given":[7,9,10,13,14,16],"global":[1,4,6,7,9,10,13,14,15],"globe":16,"glue":5,"gmail":0,"go":[1,7,9,13,16],"goal":[3,6,7,9,11,15],"goe":16,"good":[0,3,4,5,9,13,14],"goodwil":16,"got":[1,16],"got_respons":1,"govern":[13,16],"government":0,"gp":7,"gpg":4,"gpio":11,"grace":10,"gracefulli":[7,9],"grade":6,"grain":[0,4,14],"grammar":0,"grant":[0,4,8,14,16],"granular":4,"grape":1,"graph":0,"graphic":11,"gratefulli":12,"graviti":[7,16],"great":[3,4,13,14,16],"greater":[4,10,13,14,15],"greatli":[3,7],"green":16,"grep":4,"grid":[11,16],"gross":16,"ground":[0,16],"group":[0,6,7,10,11,13,14],"group_id":7,"group_nam":4,"group_root":4,"groupinstal":3,"grow":[0,3,16],"grown":16,"growth":[0,3],"guarante":[4,9,12,13],"guard":16,"guess":7,"gui":11,"guid":[3,5,11,13],"guidelin":3,"guilt":11,"gun":16,"gw":7,"gz":4,"h":[4,14],"ha":[0,1,4,5,7,9,10,11,12,13,14,15,16],"habit":16,"habitat":16,"hack":15,"had":[0,13],"half":[13,15],"hammer":16,"hand":[1,16],"handheld":13,"handl":[0,1,3,4,5,7,9,10,11,13,14,15],"handler":[1,10,14,16],"handshak":16,"happen":[1,3,4,7,10,13,16],"hard":[3,5,7,16],"hardcod":16,"hardlin":9,"hardwar":[0,1,3,6,7,10,11,13,14,15,16],"harm":[6,8],"harvest":0,"has_path":[1,10],"hasattr":1,"hash":[0,1,4,9,10,13,14,16],"hash1":13,"hash2":13,"hash_from_name_and_ident":10,"hashlib":13,"hashmap":1,"have":[0,1,3,4,5,7,9,10,11,12,13,14,15,16],"hazard":10,"hdlc":1,"he":0,"header":[1,4,7,10,13],"header_1":13,"header_2":13,"health":6,"hear":[13,16],"heard":[0,10,11,13,14],"heart":16,"heavi":16,"height":7,"held":[7,14],"helium":1,"hellman":[9,13],"hello":4,"hello_world":4,"help":[1,3,4,9,12,13,14,16],"helper":[1,4,14],"here":[0,1,3,7,13,14,16],"herebi":8,"heterogen":[3,6,15],"hex":[1,14],"hexadecim":[1,4,13,14],"hexbyt":14,"hf":[11,16],"hidden":14,"hide":[3,16],"hierarch":[15,16],"hierarchi":[6,16],"high":[5,7,9,13,14,15,16],"higher":[0,7,9,15],"highest":10,"highli":[4,7,12,13,14],"highlight":6,"hijack":16,"hint":[1,4],"histor":0,"histori":[0,4,11],"hit":1,"hkdf":13,"hmac":[13,15],"hoc":[15,16],"hold":[7,10,13,16],"holder":[8,9],"hole":16,"hollow":16,"home":[0,3,4,9,11,14,16],"homebrew":7,"hook":[0,4],"hop":[7,9,10,13,14,15,16],"hopefulli":9,"hops_to":10,"host":[0,1,4,6,7,9,11,13,14],"hostil":6,"hostnam":[7,16],"hotspot":16,"hour":[1,4,7,14,16],"hous":[3,16],"how":[0,1,3,4,5,7,9,11,13,14,15,16],"howev":[3,4,7,9,13],"http":[0,1,4,11,12,16],"hub":[7,11,14],"hum":16,"human":[0,1,6,7,8,9,10,12,13],"hundr":[0,9,11,16],"hungri":5,"hw_mtu":1,"hwrev":14,"hz":[7,14],"i":[0,1,3,4,5,6,7,8,9,10,11,12,13,14],"i2p":[3,6,9,11,15],"i2p_tunnel":7,"i2pd":[3,7],"i2pinterfac":[3,7],"ia":14,"ic":5,"ic_burst_freq":7,"ic_burst_freq_new":7,"ic_burst_hold":7,"ic_burst_penalti":7,"ic_held_release_interv":7,"ic_max_held_announc":7,"ic_new_tim":7,"ic_pr_burst_freq":7,"ic_pr_burst_freq_new":7,"icmp":11,"icon":[4,16],"id":[4,7,10,13,14],"id_callsign":7,"id_interv":7,"id_regex":4,"idea":[0,3,4,13,14,16],"ideal":[11,14,15],"ident":[0,1,3,6,7,9,10,11,14,15],"identif":[6,7,15],"identifi":[1,7,9,10,13,14,15],"identifyexampl":1,"identity_data":14,"identity_hash":10,"ie":14,"if00":14,"ifac":[1,3,7,9,13,14],"ifac_s":7,"ifconf":1,"ifconfig":14,"ignor":[4,7,9,10,12,13,14],"ignored_devic":7,"illus":6,"illustr":[1,7,13],"imag":16,"imagin":[0,9,16],"immedi":[0,4,7,14,16],"immens":16,"immort":16,"immun":0,"immut":[0,16],"impact":[7,9,10,13],"impati":16,"imperson":9,"implement":[0,1,4,6,7,9,10,11,12,13,16],"impli":8,"implic":[6,7,9],"implicit":[9,10,13],"implicitli":14,"import":[0,1,3,5,7,9,10,12,13,14],"importantli":[0,3,16],"importlib":1,"imposs":[13,16],"impract":14,"improv":[3,6,7,11,12,16],"in_fram":1,"in_wait":1,"inactive_for":10,"inadvert":14,"inbound":[1,7,10],"inbox":9,"incent":0,"includ":[0,1,4,5,6,7,8,9,10,11,13,15],"inclus":9,"incom":[1,7,10,14],"incompat":[0,10,13],"incompet":0,"incomplet":9,"inconveni":0,"increas":[4,7,10,14],"incredibli":[0,16],"increment":0,"incur":[0,7],"indefinit":[0,14],"independ":[0,4,6,10,13],"index":[1,3,6],"indic":[1,4,10],"indiffer":16,"indirectli":[8,13],"individu":[0,3,4,7,10,12,13,14,15,16],"industri":0,"inevit":[3,13],"infer":13,"infinit":16,"influenc":0,"influx":[7,11],"influxdb":11,"info":[4,10,13,14,16],"inform":[0,1,3,4,6,7,9,10,11,12,13,14,16],"infrastructur":[4,5,6,7,9,11,13,14],"ingest":16,"ingo":10,"ingress":[1,7],"ingress_control":7,"inhabit":16,"inher":[0,9],"inherit":4,"init":3,"initi":[1,4,7,9,10,13,14,15],"initialis":[1,4,10,14],"input":[1,14],"insert":13,"insid":[4,13,16],"insidi":0,"insight":16,"inspect":[3,4,7,9,13,14],"inspir":16,"instal":[0,1,4,6,7,13,14,15],"instanc":[0,1,6,7,10,11,13,14,16],"instance_control_port":14,"instance_nam":14,"instant":16,"instanti":10,"instantli":14,"instead":[0,1,3,4,7,10,13,14,16],"institut":[0,16],"instruct":[3,4,16],"int":[1,10],"integ":[1,10],"integr":[0,3,4,11,13,14,16],"intellig":[0,8,16],"intend":[3,7,9,13,14],"intens":14,"intent":[9,16],"intention":[11,13],"inter":[3,10,13],"inter_byte_timeout":1,"interact":[0,1,3,6,9,10,13,14,16],"intercept":9,"interchang":[4,7],"interconnect":[3,7,9,13],"interest":[11,13,16],"interfac":[0,4,5,6,9,10],"interface_class":1,"interface_discovery_sourc":[10,14],"interface_en":14,"interfer":[9,14],"intermedi":[9,16],"intermediari":[6,7,13],"intermitt":[0,7,9,16],"intern":[0,1,4,7,10,13,14,16],"internal_1":7,"internet":[0,6,7,9,11,13,14,15,16],"internetwork":3,"interoper":[5,9,11,13,15],"interpret":4,"interregnum":6,"interrupt":[4,7],"interv":[4,7,10,14],"intervent":14,"intiat":1,"intim":16,"introduc":[0,4,13,14,16],"introduct":6,"introductori":6,"intuit":[9,13,15],"invalid":[1,4,7,10],"invari":16,"invers":13,"invert":16,"invest":[0,9],"investig":4,"invis":[3,7],"invit":7,"invok":4,"involv":[0,4,9,12],"io":5,"iodin":11,"ioerror":1,"iot":5,"ip":[3,5,6,7,9,13,14,15],"ipv4":7,"ipv6":[3,7,14],"irc":11,"irrelev":[0,12],"irrespons":16,"is_compress":10,"is_connected_to_shared_inst":1,"is_open":1,"is_path_respons":10,"is_ready_to_send":[1,10],"isdir":1,"isfil":1,"isinst":1,"ism":[5,13],"isn":16,"isol":[3,7,14],"isp":[7,9,16],"issu":[0,4,6,7,9],"its":[0,4,5,7,8,9,10,13,14,16],"itself":[0,4,6,9,11,13,14,16],"iv":[13,15],"j":[6,14],"jail":14,"jan":4,"javascript":11,"job":[0,1],"join":[1,3,9,11,13],"journei":9,"json":[14,16],"judgment":0,"just":[0,1,3,4,5,7,9,11,13,14,15,16],"k":[1,14],"kb":14,"kbp":14,"keep":[1,3,4,9,10,13,14,15,16],"keepal":[10,13],"keepalive_timeout_factor":10,"kei":[0,1,4,6,7,9,10,14,15,16],"kept":[7,9,10,13],"kernel":[0,7,14,15],"keyboardinterrupt":1,"keyerror":10,"keypad":11,"keypair":[9,13],"keyr":16,"keyserv":4,"keyset":[13,14,15],"keysiz":10,"keystor":9,"keyword":4,"khz":7,"ki":1,"kill":[10,13,16],"kilomet":0,"kind":[0,5,7,8,9,11,13,16],"kiss":[5,6,11,14,15],"kiss_fram":7,"kissinterfac":[7,14],"knock":16,"know":[0,1,3,4,5,7,9,10,13,14,16],"knowledg":[9,13],"known":[1,7,10,13,14,15,16],"ko":12,"krypton":1,"l":[4,7,14],"la":14,"label":0,"labor":16,"lack":[0,13],"laid":13,"lake":0,"lan":7,"landlord":16,"languag":[0,4,8,11],"lantern":16,"laptop":[9,16],"larg":[1,5,7,9,10,13,14,15,16],"larger":[9,13,14],"laser":7,"last":[1,4,7,10,14,16],"last_read_m":1,"last_unit":1,"lat":7,"latenc":[3,9,13,15,16],"later":[1,3,4,7,11,14],"latest":[0,1,3,4,7,10,14],"latest_buff":1,"latest_client_link":1,"latitud":7,"laucnh":3,"launch":[11,14],"law":16,"layer":[0,5,7,9,10,11,13,14,15,16],"lcd":11,"lead":[3,13,16],"leak":3,"learn":[1,3,8,9,12,13,16],"leas":16,"leash":16,"least":[3,4,5,7,9,13,14,15],"leav":[0,9,11,13,16],"ledger":[13,16],"leech":0,"left":[0,7,10,13,14],"legaci":[9,16],"legal":[0,7,16],"legisl":7,"legitim":[3,9,14],"len":[1,16],"length":[1,4,7,10,13],"less":[1,3,5,7,10,13,15,16],"let":[0,1,3,4,7,9,11,13,14,16],"level":[0,4,5,7,9,13,14],"lever":16,"li":[0,16],"liabil":8,"liabl":8,"liber":[5,6],"liberapai":12,"libffi":3,"librari":[0,3,11,16],"licens":[6,13,15,16],"lie":16,"life":[0,16],"lifelin":16,"lifespan":0,"lift":[10,14,16],"light":[0,4,14,16],"lightweight":15,"like":[0,1,3,4,5,7,9,10,11,13,14,15,16],"likewis":4,"limit":[1,3,4,5,6,8,9,11,13],"line":[0,1,3,6,7,11,13,14,15,16],"linger":14,"link":[0,3,4,5,6,7,9,10,11,14,15,16],"link_clos":1,"link_establish":1,"link_id":[1,10],"link_mtu_discoveri":10,"linkexampl":1,"linux":[0,3,4,5,7,11,13],"liquid":16,"list":[0,1,3,4,5,6,7,10,11,13,15,16],"list_deliv":1,"list_fil":1,"list_filt":14,"list_packet":1,"list_receipt":1,"list_timeout":1,"listdir":1,"listen":[0,1,3,4,6,11,13,14,16],"listen_ip":7,"listen_on":[3,7],"listen_port":7,"liter":14,"litter":16,"littl":[5,11,13],"live":[0,3,10,11,16],"lki":13,"lkr":13,"ll":[1,15,16],"llm":11,"ln":14,"load":[1,3,4,5,7,10,14,15,16],"load_private_kei":10,"load_public_kei":10,"local":[0,1,3,4,6,7,9,10,11,13,15,16],"locat":[3,6,7,9,13,14,16],"location_cmd":7,"lock":16,"log":[0,1,3,4,7,14,16],"log_crit":1,"log_error":1,"log_info":1,"log_verbos":1,"logdest":10,"logfil":14,"logic":[13,16],"login":14,"loginctl":14,"loglevel":[1,10,14],"lon":7,"long":[1,3,4,5,6,7,9,10,13,14,16],"longer":[1,3,7,13,14,16],"longest":7,"longitud":7,"look":[1,3,4,7,9,11,13,14,16],"loop":1,"lora":[0,3,5,6,9,11,13,15,16],"lorawan":[5,13],"lose":0,"loss":[0,7,9,13],"lost":16,"lot":[9,13,16],"loudest":16,"low":[0,3,4,5,7,9,11,13,15,16],"lower":[1,3,7,14],"lowest":0,"lowli":16,"ltu":5,"lunar":6,"lxmf":[4,6,7,9,10,14],"lxmfy":6,"lxst":6,"m":[1,3,4,13,14],"mac":[7,13],"machin":[0,3,8,13,14,16],"machineri":11,"maco":[6,11],"made":[3,4,7,10,13,16],"mai":[0,3,4,7,9,10,11,13,14,16],"mailbox":11,"mailmap":4,"main":[1,4],"maintain":[0,3,4,9,11,13,14,15],"mainten":[3,9,13,16],"major":4,"make":[0,1,3,4,5,7,9,11,12,13,14,15,16],"malici":[0,3,4,9,13,14],"malinform":12,"manag":[0,3,6,9,10,11,13,15,16],"mani":[1,3,4,5,7,9,10,11,13,14,15,16],"manifest":[0,4],"manipul":[13,15,16],"manjaro":3,"manner":[4,9,11],"manual":[0,1,3,7,9,10,13,14,15,16],"manufactur":[5,7],"map":[7,11,13,14,16],"mark":[3,4,7,8,10,13,15,16],"markdown":4,"market":[0,16],"markqvist":[1,12],"markup":[4,11],"mask":0,"mass":16,"massiv":7,"master":[1,4,10,16],"match":[0,1,4,7,13,14,16],"math":16,"mathemat":16,"matter":[0,3,7,9,16],"matur":14,"max":14,"max_request_s":10,"max_response_s":10,"maximum":[1,7,10,13,14],"mayb":16,"mb":4,"mbp":14,"md":4,"mdu":[1,10],"me":[7,16],"mean":[0,3,4,5,7,9,10,13,14,16],"meaning":3,"meaningless":0,"meantim":10,"measur":[0,9,13,16],"mechan":[0,3,4,6,7,9,14,15],"media":[4,16],"mediat":0,"mediev":16,"medium":[0,1,5,6,7,9,10,11,13,15],"meet":[0,16],"megaphon":16,"member":[3,13],"memori":[3,9,13],"men":0,"mental":16,"mention":[7,13],"menu":1,"menu_mod":1,"merchant":8,"mere":[0,16],"merg":[0,8,14],"merit":6,"mesh":[3,7,9,13,14,15,16],"meshchat":11,"meshchatx":6,"messag":[1,3,4,9,10,11,13,14,16],"message_class":10,"messagebas":[1,6,10],"messagecallbacktyp":10,"messeng":[9,13],"met":[0,3,16],"meta":4,"metadata":[0,3,4,7,13,14,16],"metaphor":16,"metavar":1,"meter":7,"meth":1,"method":[1,3,6,7,10,13],"methodologi":[13,14],"metric":[0,11],"mevpekyafshak5wr":7,"mhz":[7,13],"mi":1,"microcontrol":13,"micromanag":16,"micron":[4,6],"microwav":9,"middl":0,"middlemen":0,"might":[0,3,4,7,9,13,14,16],"migrat":0,"mikrotik":5,"millimet":5,"million":16,"millisecond":[1,9,14,16],"mind":[0,9,16],"mindset":16,"mine":16,"miner":16,"minim":[0,5,6,7,13],"minimalsampl":1,"minimum":[1,3,7,10,13,14,16],"minimum_bitr":10,"miniscul":16,"minor":4,"minut":[1,3,4,7,13,16],"mirror":[0,6,9,14],"mirror_interv":4,"misconfigur":3,"mislead":[3,16],"miss":[1,3,4],"missil":16,"mistak":3,"misunderstand":12,"mitig":9,"mix":[6,7,9,14],"mixtur":15,"mobil":[7,9,13],"mode":[0,1,3,4,5,6,9,10,11,13,14,15,16],"model":[0,8,9,14,16],"modem":[6,7,9,10,11,13,15,16],"modem73":11,"moder":7,"modern":[3,5,11,16],"modif":[0,4],"modifi":[0,4,7,8,9,13,14],"modul":[1,3,4,5,6,7,9,13,14,15],"modular":11,"moment":[13,16],"momentarili":7,"mon":4,"monero":12,"monet":0,"monitor":[3,7,11,13,14],"monolith":0,"monopol":0,"moon":1,"moral":16,"more":[0,1,3,4,5,7,9,10,11,12,13,14,15,16],"moscow":13,"most":[0,3,5,7,9,10,11,13,14,16],"mostli":[7,9,13],"motiv":6,"mountain":16,"move":[4,7,9,13,14,16],"msgpack":4,"msgtype":[1,10],"mtu":[1,7,10,13,15],"mu":4,"much":[3,7,9,11,13,14,15,16],"multi":[0,6,9,11,13,14,15],"multicast":7,"multicast_address_typ":7,"multilater":13,"multipl":[0,1,4,7,9,11,13,14],"multiplex":[1,14],"multipoint":13,"multitud":3,"music":0,"must":[0,1,3,4,7,9,10,13,14,16],"mutual":16,"mw":7,"my":[4,7,13,16],"my_fil":14,"my_ident":14,"my_network":[13,14],"my_network_ident":7,"my_nod":4,"myapp":4,"mycal":7,"myfork":4,"myfriend":16,"mymirror":4,"myrepo":4,"myriad":13,"mysteri":16,"n":[1,4,13,14],"name":[1,3,4,6,7,9,10],"namespac":1,"nano":4,"narg":1,"nat":[3,7],"nation":0,"nativ":[1,9],"natur":[0,7],"navig":[0,4],"nearbi":[7,9],"nearest":7,"nears":13,"neat":9,"neccessari":7,"necesarri":7,"necess":0,"necessari":[0,4,5,7,9,10,13,14],"necessarili":13,"need":[0,1,3,4,5,6,7,9,10,11,12,13,14,15,16],"needless":7,"neg":[3,7],"neglig":[13,16],"negoti":[0,16],"neighbor":16,"neither":[10,13],"neon":1,"neopixel":14,"nerd":4,"net":7,"netcat":7,"network":[0,1,5,7,10,14,15,16],"network_ident":[7,13,14],"network_nam":[3,7],"neutral":[9,16],"never":[0,7,9,10,13,16],"new":[0,1,3,4,6,9,10,13,14,16],"new_id":14,"new_ident":14,"newer":[10,13],"newest":13,"newli":[7,10,13],"newlin":[7,14],"next":[0,3,4,7,10,13,16],"next_hop":10,"next_hop_interfac":10,"nice":16,"nicknam":13,"no1cll":7,"no_data_for":10,"no_inbound_for":10,"no_outbound_for":10,"noauth":14,"nobl":1,"noble_ga":1,"noble_gas":1,"nobodi":4,"nocheck":14,"node":[0,3,6,7,14,15],"node_nam":4,"noid":14,"nois":[0,7,10,12,16],"noisi":16,"nomad":[0,6],"nomadnet":[4,11],"nomadnetwork":11,"non":[3,7,10,11,13,16],"none":[1,3,4,7,10,13,14],"noninfring":8,"nor":[3,16],"normal":[0,1,3,4,7,10,13,14],"nostalgia":0,"notabl":11,"notar":4,"notat":[13,14],"note":[0,1,4,5,6,7,10,13,14],"noth":[4,5,9,15,16],"notic":[4,7,8,13,14],"notif":[0,1,10],"now":[1,3,4,13,14,16],"np":14,"nrf52":5,"nt":1,"null":4,"num":1,"number":[1,3,4,7,9,10,11,13,14,16],"nurtur":0,"o":[1,3,4,7,13,14,15],"obj":4,"object":[1,4,10,16],"obscur":[0,3],"observ":[0,9,16],"obsolet":16,"obstacl":16,"obtain":[0,3,5,6,8,10,13],"obvious":13,"occupi":16,"occur":[1,7,14,15],"ocean":16,"ocur":10,"odd":1,"ofdm":11,"off":[3,7,9,11,13,14,15,16],"offer":[0,3,6,7,9,10,11,13,14,16],"offic":9,"offlin":[0,1,3,6,9,11],"often":[0,3,7,9,13,14,16],"oganesson":1,"old":[0,1,3,5,12,13,16],"older":14,"omit":10,"on_interfac":10,"onc":[1,3,4,5,7,10,13,14,15,16],"one":[0,1,3,4,5,7,9,10,11,13,14,15,16],"ones":[3,7,9,14],"oneself":13,"ongo":11,"onli":[0,1,3,4,5,7,9,10,13,14,15,16],"onlin":[1,14,16],"onto":[3,13,16],"ontolog":0,"opaqu":9,"open":[0,1,3,4,5,6,7,9,10,11,12,13,14,15],"open_port":1,"openmodem":[7,15],"openssl":[3,13],"openwrt":6,"oper":[0,3,4,5,7,9,10,13,14,15,16],"opinion":[0,12],"opkg":3,"opportun":3,"opportunist":9,"oppos":4,"opposit":[1,7],"oppress":16,"opt":13,"optic":[5,15,16],"optim":[0,3,4,9,16],"option":[0,1,3,4,5,6,9,10,11,13,14,16],"ord":1,"order":[0,1,10,14],"organ":[0,3,4,9,13,14,16],"organis":[7,12],"organiz":0,"orient":13,"origin":[1,4,6,9,11,13,16],"orthogon":0,"os":3,"other":[0,1,3,4,5,7,8,9,10,11,13,14,16],"otherwis":[1,4,7,8,10],"our":[1,7,13,16],"ourselv":9,"out":[1,5,7,8,10,11,13,14,16],"outag":0,"outbound":[7,10,13],"outgo":[1,7,10,13,14],"outlast":0,"outlin":[3,5,13],"output":[0,4,7,14],"outsid":[10,15,16],"over":[1,5,6,7,9,10,11,12,13,14,15,16],"overal":[13,14],"overcom":13,"overhead":[3,5,7,13,16],"overlai":3,"overli":12,"overlord":16,"overrid":[1,4],"oversight":13,"overview":[4,6,7],"overwhelm":[0,7,9,10],"overwrit":14,"own":[0,1,3,4,9,10,13,14,15,16],"owner":[1,16],"ownership":[0,16],"p":[1,4,14],"pace":0,"pack":[1,10],"packag":[0,1,3,4,7,11,13],"packb":1,"packed_s":1,"packet":[0,1,3,6,7,9,10,11,14,15,16],"packet_callback":1,"packet_deliv":1,"packet_hash":1,"packet_receipt":[1,10],"packet_timed_out":1,"packetreceipt":[1,6,10],"pad":[13,15],"page":[0,6,13,14,16],"page_cont":4,"pagin":4,"paid":0,"pair":[7,10,13,14],"pamac":3,"panic":[1,14,16],"panic_on_interface_error":[1,14],"panopticon":16,"paper":11,"par":[1,3],"paradox":16,"parallel":3,"paralysi":16,"param":1,"paramet":[1,5,6,10,13,14],"paranoia":16,"parasit":16,"pariti":[1,7,14,15],"parity_even":1,"parity_non":1,"parity_odd":1,"pars":[1,4],"parse_arg":1,"parser":[1,4,6],"part":[0,1,3,4,7,9,10,11,13,14,16],"parti":[0,4],"partial":4,"particip":[0,3,6,7,9,13,14,16],"particular":[0,1,5,7,8,13,14],"particularli":[0,4,7,14],"partner":16,"pass":[1,4,5,7,9,10,13,14,16],"passphras":[3,7,9,13],"passport":13,"past":14,"patch":[0,4,16],"path":[0,1,3,4,5,6,9,10,13,14,16],"path_respons":10,"pathfinder_m":10,"patient":16,"pattern":[0,4,5,6,9,13],"payload":[1,4,7,10,13,14],"pdf":6,"peac":16,"peach":1,"peak":4,"peer":[0,1,3,4,7,10,11,13,14,16],"penalti":7,"pend":[1,13],"peopl":[0,3,11,13,16],"per":[0,4,7,9,10,11,13,14,15,16],"percent":[1,7],"percentag":10,"perfectli":[0,3],"perform":[0,1,3,4,7,9,10,11,13,14,16],"perhap":[3,16],"period":[1,4,7,10,13,14],"peripher":11,"perm":4,"perman":[3,7,14],"permiss":[0,6,7,8,14,16],"permissionless":[0,13,14],"permit":8,"perpetu":16,"persecut":13,"persist":[0,7,14,16],"person":[6,7,8,9,12,13],"personag":0,"perspect":[0,7,9,10,13,15],"pet":1,"petit":16,"philosophi":[0,12,13,15,16],"phone":[3,6,9,16],"phonebook":11,"photo":16,"phy":14,"physic":[0,1,3,4,5,6,7,9,10,11,13,14,15],"pi":[0,1,6,9,13,14,15,16],"pick":13,"piec":13,"pillar":16,"ping":[14,16],"pip":[1,3,4,5,7,14],"pip3":3,"pipe":[6,11,14,15,16],"pipeinterfac":[5,7,11],"pipx":3,"pitfal":13,"pkcs7":[13,15],"pkg":3,"place":[1,3,4,7,9,13,14,16],"plaform":5,"plain":[1,5,7,10,13,16],"plain_mdu":10,"plaintext":[1,10,16],"plan":[9,13,16],"planet":[0,16],"planetari":[15,16],"platform":[1,4,5,6,11,13,14,16],"pleas":[1,3,7,10,14,16],"plenti":[9,13],"plug":[14,16],"plugin":[11,16],"pmr":13,"poetri":16,"point":[0,3,4,5,7,9,11,12,13,14,16],"pointer":3,"polici":[0,14,16],"polit":16,"pomelo":1,"popul":10,"popular":15,"port":[1,3,5,6,7,9,13,15,16],"port0":14,"portabl":[0,6,7,9,13],"portion":8,"pose":[3,13],"posit":[1,4,7,14],"posix":3,"possess":[7,13,16],"possibl":[0,3,4,5,7,9,10,11,13,14,15],"possibli":[3,13],"post":[1,6],"postfix":14,"potenti":[0,1,3,4,7,12,13,14,15,16],"power":[0,3,5,6,7,9,11,13,14,15],"powershel":3,"ppp":1,"practic":[0,4,9,13,15,16],"pre":[3,9,10,13],"preambl":[7,14],"preced":[1,4],"preciou":9,"precis":0,"precompil":3,"predatori":16,"predic":0,"predict":13,"prefer":[3,4,12,13],"prefer_ipv6":7,"premis":[9,16],"prepar":[1,13],"prerequisit":[6,10],"prescrib":0,"presenc":[0,6,7,10],"present":[0,7,9,11,13,14,16],"preserv":[0,3,6,10],"preshar":13,"press":1,"pressur":0,"pretend":[11,13],"prettyhexrep":1,"prevent":[0,4,7,9,14,16],"preview":4,"previou":1,"previous":[7,10,13],"price":16,"primari":[0,13,16],"primarili":[7,9,11],"primarlii":3,"primit":[3,4,6],"principl":[0,6,9,13,15],"print":[1,4,7,14],"print_filelist":1,"print_help":1,"print_menu":1,"priorit":[9,13,16],"prioriti":13,"prioritis":[6,7,9,10],"privaci":[3,7,11,16],"privat":[0,3,4,7,9,10,11,12,13,14,15,16],"private_ret":3,"privileg":[0,14,16],"probabl":[1,3,4,9,13,14,15],"probe":14,"problem":[0,3,4,13,14,16],"problemat":4,"proc":14,"procedur":[3,4,10,13],"process":[0,1,3,4,5,7,9,10,13,14,16],"process_incom":1,"process_outgo":1,"produc":0,"product":[0,14,16],"profil":16,"profit":16,"profound":[9,16],"profoundli":16,"program":[1,4,5,6,7,10,13,15],"program_setup":1,"programm":13,"programmat":[0,13],"progress":[1,4,10,14,15],"progress_callback":10,"project":[0,4,11,12,16],"promis":3,"prompt":[1,3],"proof":[0,1,4,7,9,10,13,14,16],"proof_packet":1,"proof_requested_callback":10,"proof_strategi":10,"propag":[6,7,9,10,11,14,16],"proper":3,"properli":3,"properti":[0,1,9,10],"proport":13,"propos":6,"prospect":0,"protect":[0,3,14,16],"protocol":[3,4,5,6,7,9,15],"prove":[1,4,9,13,16],"prove_al":[1,10],"prove_app":10,"prove_non":10,"proven":[0,4,10,13],"provid":[0,1,3,4,5,6,7,8,9,10,11,13,14,15,16],"provis":[5,14],"prv_byte":10,"pseudo":14,"psycholog":[0,16],"pub_byt":10,"pub_to_fil":10,"public":[1,4,6,7,9,10,12,14,15],"public_inform":1,"publicli":[3,7,13,14],"publish":[0,3,4,6,7,8,10,13],"publish_blackhol":14,"publish_blackhole_en":10,"publish_ifac":7,"pull":[0,4],"puppet":16,"purchas":[3,13,15],"pure":[6,13],"purg":10,"purpos":[0,3,4,5,7,8,9,10,11,13,14,16],"purposefulli":8,"pursu":0,"push":[0,4,11,16],"put":[0,1,5,7,14],"pwd":4,"py":[1,4,13,14],"py3":[3,4],"pyca":[3,13],"pygment":4,"pyseri":[1,3],"python":[0,1,4,6,7,10,13,14,15,16],"python3":[1,3,5],"q":[1,4,14],"qr":[9,11],"qualiti":[0,10,14,16],"quantiti":0,"queri":[1,3,4,10,14,16],"question":[3,5,7],"queu":[10,14],"queue":[7,13],"quick":14,"quickli":[7,11,12,13,15],"quiet":[4,7,14,16],"quinc":1,"quirk":3,"quit":[1,3],"quot":4,"qvist":[8,15],"r":[1,4,7,13,14],"radic":16,"radio":[0,6,7,9,10,11,13,14,15,16],"radon":1,"rais":[0,1,10],"rak":5,"ram":[3,7],"randint":1,"random":[0,1,10,12,13],"random_text_gener":1,"randomis":13,"randomli":[1,13],"rang":[0,1,3,4,5,7,9,13,14,15,16],"rapidli":7,"rare":[3,7],"raspberri":[0,6,9,13,14,15],"rasperri":3,"ratchet":[10,13],"ratchet_count":10,"ratchet_expiri":10,"ratchet_id_receiv":10,"ratchet_interv":10,"ratchets":10,"ratchets_path":10,"rate":[1,3,6,10,11,14],"rather":[0,3,10,11,14,16],"ratio":[0,10],"raw":[1,4,5,10],"rawchannelread":[6,10],"rawchannelwrit":[6,10],"rawiobas":10,"rawmu":4,"rb":1,"rbrowser":6,"re":[0,1,7,9,10,13,14,16],"reach":[0,3,6,7,9,10,14,16],"reachabl":[0,1,3,7,9,10,11,13,14],"reachable_on":7,"reactiv":0,"read":[0,1,3,4,7,9,10,13,14,15,16],"read_loop":1,"readabl":[0,1,7,10,13,14,16],"reader":[1,10],"readi":[1,3,5,7,10,15,16],"readili":15,"readm":4,"readwrit":4,"ready_byt":[1,10],"ready_callback":10,"real":[0,3,5,7,9,11,14,15,16],"realiti":[0,3,12,16],"realiz":16,"realli":[7,10,14,16],"realm":16,"reappear":7,"reason":[3,7,10,13,14],"reassembl":13,"reboot":16,"rebroadcast":9,"rebuilt":16,"recal":[1,4,10,14],"recall_app_data":10,"recap":13,"receipt":[1,9,10,13],"receiv":[1,3,4,7,9,10,12,13,14,16],"receive_path_respons":10,"receive_stream_id":10,"received_announc":[1,10],"recent":[10,13],"recept":10,"reception_rssi":1,"reception_snr":1,"reception_stat":1,"recip":3,"recipi":[0,4,13,16],"reclaim":16,"recogn":[13,16],"recognis":14,"recognit":[0,16],"recombin":0,"recommend":[0,1,3,7,13],"recondit":14,"reconnect":[1,9,16],"reconnect_port":1,"reconstruct":13,"record":[0,3,4,13],"record_stat":4,"recov":0,"recoveri":7,"recreat":13,"recurs":[7,10,13],"recursive_pr":7,"reduc":[7,14],"redund":[3,9],"reestablish":0,"ref":4,"refactor":4,"refer":[0,1,3,4,6,7,11,14,16],"refin":12,"reflect":0,"refresh":14,"refus":9,"regain":[0,3],"regard":[0,13],"regardless":[0,4,7],"region":7,"regist":[1,4,10,14],"register_announce_handl":[1,10],"register_message_typ":[1,10],"register_request_handl":[1,10],"registrar":16,"registri":[0,16],"regul":7,"regular":14,"regulatori":16,"regurgit":16,"reintroduc":0,"reject":[4,16],"rel":[4,5,7,9,13],"relai":[3,6],"relat":[3,4],"relationship":[0,9,13,14,16],"releas":[0,3,6,7,13,14,16],"relev":[1,3,4,5,7,10,13,14,16],"reli":[3,4,7,9,12,13,14,16],"reliabl":[3,5,7,9,10,13,15,16],"relianc":[3,16],"relief":9,"reload":14,"rem":1,"remain":[0,4,7,9,10,11,13,16],"rememb":[7,9,10,13],"remot":[1,6,10,13],"remote_ident":[1,4,10],"remote_identifi":1,"remote_management_allow":14,"remote_management_en":10,"remote_nod":4,"remote_p":1,"remotesensor":13,"remov":[0,4,10,14,16],"remove_message_handl":10,"remove_ready_callback":10,"render":[4,11,16],"rent":16,"reopen":4,"repeatedli":[0,7],"replac":[0,1,3,9,10,11,13,14,16],"repli":[1,14],"replic":[0,5,13],"reply_data":1,"reply_messag":1,"reply_text":1,"repo":[4,14],"repo_nam":4,"report":[7,10,12,14],"repositori":[0,3,6,13,14,16],"repres":[0,9,13,16],"represent":[1,10,16],"repudi":4,"repurpos":3,"reput":[13,14],"request":[0,4,6,9,10,13,14,15,16],"request_destin":1,"request_fail":1,"request_id":[1,10],"request_packet":1,"request_path":[1,10],"request_receipt":[1,10],"request_receiv":1,"requested_at":[1,10],"requestexampl":1,"requestor":7,"requestreceipt":[6,10],"requir":[0,1,3,4,7,9,10,11,13,14,15,16],"require_shared_inst":10,"required_discovery_valu":[10,14],"rerout":16,"research":0,"resend":10,"reserv":[1,9,10,16],"reset":4,"reshap":0,"resid":[7,16],"resili":[0,3,9,11,14,15,16],"resist":9,"resiz":14,"resolut":[3,4,7,13],"resolv":[4,6,7,9,13,14],"resourc":[1,3,6,7,9,10,12,14,16],"resource_callback":10,"resource_sending_conclud":1,"resource_strategi":10,"respawn":7,"respawn_delai":7,"respawn_interv":7,"respect":[0,4,7,9,15,16],"respond":[0,1,9,10,14],"respond_to_prob":14,"respons":[0,6,7,10,13,15,16],"response_callback":[1,10],"response_gener":[1,10],"rest":[9,15,16],"restart":[7,14],"restartsec":14,"restor":[3,6,7,16],"restrict":[4,7,8,9,14,16],"resubmit":4,"result":[0,1,3,7,13,14],"ret":[6,13],"retain":[0,4,7,10],"retained_ratchet":10,"retibb":6,"reticulum":[0,1,7,9,10],"retipedia":6,"retransmiss":[9,13],"retransmit":[7,13],"retri":[1,4,10,13],"retriev":[0,4,10,14],"return":[0,1,7,10,14,16],"reveal":[3,10,13,15],"revers":14,"review":[0,13],"revis":14,"revok":[4,16],"revolut":16,"revolutionari":0,"rfe":14,"rich":11,"ridicul":13,"right":[0,3,4,7,8,16],"rigid":16,"rington":11,"rippl":16,"risc":6,"risk":[4,13],"riski":16,"rm":4,"rmap":3,"rn":[0,1,3,4,5,6,10,13,14,16],"rncp":6,"rngc":4,"rngit":[0,6],"rngit_config":4,"rnid":[4,6,13],"rnmon":6,"rnode":[3,6,13,14,15],"rnodeconf":[5,6],"rnodef3b9":7,"rnodeinterfac":[3,5,7,14],"rnodemultiinterfac":7,"rnpath":[3,6,7],"rnphone":11,"rnprobe":[3,6],"rns_bin_dir":14,"rns_config":4,"rns_remot":4,"rnsconfig":[4,14],"rnsd":[3,6],"rnsh":[6,11],"rnspure":[3,13],"rnstatu":[3,6,7,13],"rnstransport":[7,14],"rnx":6,"road":[6,16],"roadmap":[12,16],"roam":[6,7,9],"robot":13,"robust":[3,7,16],"role":9,"roll":7,"rom":14,"room":[11,16],"root":[4,13],"rotat":[7,10,13,14],"round":[1,10,14],"rout":[3,5,7,9,10,11,13,14,15,16],"router":[3,7,9,14,16],"rpc":14,"rpc_kei":14,"rprogress":1,"rrc":6,"rrcd":11,"rsg":[0,4],"rsm":[0,4],"rssi":[1,10,14],"rtsct":1,"rtt":[1,10,13],"rttstring":1,"rubber":16,"rule":[0,3,4,6,7,9,14,16],"run":[0,1,3,4,5,7,9,10,11,13,14,15,16],"runtim":[1,13],"rust":3,"rvp":4,"rw":4,"rx":[7,14],"rxb":1,"sa":11,"safe":[4,13,14,16],"safeguard":16,"safeti":0,"sai":[0,16],"said":[4,13],"sake":0,"same":[0,1,3,4,5,7,9,10,11,13,14,16],"sand":16,"satellit":9,"satisfi":13,"satur":0,"save":[4,10,13,14],"save_error":1,"saved_filenam":1,"saw":11,"scalabl":[14,15],"scale":[13,15,16],"scan":[3,7],"scarc":16,"scarciti":6,"scatter":16,"scenario":[3,7,13],"schedul":[4,10],"scheme":[0,4,5,7,14],"scope":[4,7],"scrape":16,"scratch":[5,16],"screen":1,"script":[0,3,4,5,7,14],"scrutini":13,"sd":0,"se":4,"sea":7,"seamless":[7,13],"seamlessli":[7,9],"search":[6,10,11],"second":[1,7,9,10,11,13,14,15,16],"secreci":[7,10,11,13,15],"secret":[9,13],"section":[3,4,5,7,9,11,13,14],"secur":[0,3,4,6,9,11,13,14,15,16],"sed":4,"see":[1,3,4,7,9,10,13,14,15,16],"seek":[0,13,16],"seen":[7,13,16],"segment":[1,3,7,9,10,13,14,15],"select":[1,3,4,7,16],"self":[0,1,3,4,13,15,16],"sell":8,"semi":[4,14],"semtech":5,"send":[1,4,7,9,10,13,14,16],"send_stream_id":10,"sender":[1,3,9,13,14],"sens":[0,9,16],"sensibl":[3,5,7],"sensit":16,"sensor":[9,13],"sent":[1,3,9,10,13,14,16],"sentiment":13,"separ":[0,1,7,9,11,13,14],"seq":4,"sequenc":[1,4,10,13,14,15],"sequenti":[4,15],"serfdom":16,"seri":5,"serial":[1,3,6,9,13,15,16],"serialinterfac":[5,7],"seriou":0,"serv":[0,1,5,6,7,9,11,12,13,14,16],"serve_nomadnet":4,"serve_path":1,"server":[0,1,3,4,5,6,9,10,11,13,14,16],"server_buffer_readi":1,"server_callback":1,"server_client_connect":1,"server_destin":1,"server_fil":1,"server_ident":1,"server_link":1,"server_loop":1,"server_message_receiv":1,"server_packet_receiv":1,"servic":[0,3,4,6,7,9,11,13,16],"session":[3,11,13,14],"set":[0,1,3,4,5,7,9,10,11,13,14,15,16],"set_default_app_data":10,"set_delivery_callback":[1,10],"set_link_closed_callback":[1,10],"set_link_established_callback":[1,10],"set_max_request_s":10,"set_packet_callback":[1,10],"set_proof_requested_callback":10,"set_proof_strategi":[1,10],"set_ratchet_interv":10,"set_remote_identified_callback":[1,10],"set_resource_callback":10,"set_resource_concluded_callback":[1,10],"set_resource_started_callback":[1,10],"set_resource_strategi":[1,10],"set_retained_ratchet":10,"set_timeout":[1,10],"set_timeout_callback":[1,10],"setup":[1,3,4,6,7,14],"sever":[3,4,9,10,14],"sf":14,"sh":7,"sha":[9,10,13],"sha256":[4,13,15],"shall":[8,13],"shape":[9,16],"share":[0,1,3,9,10,11,13,14,16],"share_inst":14,"shared_instance_port":14,"shared_instance_typ":[10,14],"shelf":[13,15],"shell":[0,3,4,6,14,16],"shelter":0,"shift":[0,16],"shim":4,"shine":16,"ship":[13,16],"shop":[1,16],"short":[5,6,7,13,14],"shorter":7,"shorthand":[7,14],"shortlog":4,"shot":3,"should":[0,1,3,4,7,9,10,13,14,16],"should_ingress_limit":1,"should_quit":1,"should_use_implicit_proof":10,"shout":16,"shove":0,"show":[3,4,14,16],"shown":[0,1,12],"shut":[0,16],"side":[0,7,14,15],"sideband":[3,6,14],"sign":[0,6,7,9,10,13,14,16],"signal":[0,1,7,10,13,14,16],"signatur":[0,6,9,10,13,14,15,16],"signer":4,"signifi":13,"signific":9,"significantli":[7,10],"signingkei":4,"silent":14,"similar":[0,1,3,4,5,7,10,11,14,15],"simpl":[1,3,4,5,10,11,13,14,15,16],"simpler":[4,7,9,15],"simplest":[7,13],"simpli":[0,1,3,4,5,7,9,11,12,13,14,16],"simplic":13,"simplifi":1,"simplyequip":5,"simul":13,"simultan":[3,13],"sinc":[1,3,4,7,9,10,13,16],"singl":[0,1,3,4,7,9,10,13,14,15,16],"singular":13,"sit":[3,16],"site":[0,7,9,16],"situat":[3,4,5,7,9,13,14],"size":[1,3,4,7,10,13,14],"size_str":1,"skill":0,"skip":[1,4],"sky":6,"slap":16,"slave":16,"sleep":[1,3,14],"slice":[1,16],"slightli":[1,3,16],"sloppi":16,"slottim":[7,14],"slow":[1,7,9,11,13,16],"slower":[7,10,13],"small":[1,3,7,9,10,13,15,16],"smaller":10,"smallest":3,"snippet":14,"snr":[1,10,14],"so":[0,1,3,4,5,7,8,9,10,11,12,13,14,15,16],"social":[0,3,16],"societi":16,"socket":[7,14,15,16],"soft":16,"softwar":[0,3,4,5,7,8,11,13,14,16],"solar":16,"sole":7,"solid":12,"solo":0,"solut":[0,13,14],"solv":[0,13,16],"some":[0,1,3,5,7,9,10,11,13,14,16],"some_program":4,"some_program_1":4,"some_remot":4,"someon":[0,3,4,7,14,16],"someth":[0,4,7,9,13,16],"somethign":1,"sometim":[5,7,11,16],"somewhat":3,"somewher":16,"soon":[4,10,14],"sort":[7,13,14],"soul":16,"sound":16,"soundmodem":7,"sourc":[0,1,3,4,5,6,9,10,13,15,16],"source_":4,"sovereign":[0,3,13,16],"sovereignli":16,"sovereignti":[6,15],"space":[0,1,3,5,9,13,15,16],"spam":[3,7,13,14],"spammi":14,"span":[7,9,13],"spawn":7,"spe":7,"speak":[0,12,16],"spec":13,"special":[11,13,14],"specif":[0,1,4,5,6,7,9,10,14,15,16],"specifi":[0,1,3,4,5,7,9,10,13,14],"spectrum":[7,13,16],"speed":[1,7,10,13,14,16],"sphere":16,"spinner":16,"spirit":11,"split":1,"sponsor":12,"spoof":[4,16],"spread":[7,14],"spreadingfactor":7,"squelch":7,"ssh":[4,11,14],"ssid":7,"ssl":16,"stabil":[9,16],"stabl":[3,7,9,13,15],"stack":[1,3,9,10,11,13,14,15,16],"stage":13,"stai":[1,7,13,14,16],"stake":0,"stale":[9,10],"stale_grac":10,"stale_tim":10,"stamp":[7,10,14],"stanc":[0,16],"stand":[0,16],"standalon":[6,11],"standard":[0,4,5,7,11,13,14],"start":[1,4,5,6,7,9,10,11,13,14,16],"startup":[1,3,14],"stat":[4,10,14],"state":[0,1,6,9,16],"static":[3,4,7,10,16],"staticmethod":1,"station":7,"stationari":[9,14,16],"statist":[1,6,7,10,12],"stats_ignore_ident":4,"statu":[1,3,4,6,7,10,11,13,14,16],"stderr":14,"stdin":[7,14],"stdio":15,"stdout":[1,4,7,14],"steel":16,"stem":12,"step":[1,3,4,5,16],"stewardship":16,"still":[0,1,3,4,7,9,13,14,16],"stock":1,"stone":16,"stop":[10,15,16],"stopbit":[1,7,14],"storag":[3,6,7,11,13,14,16],"store":[0,1,4,6,10,11,13,14],"store_tru":1,"storm":16,"str":[1,10],"straightforward":7,"strang":16,"stranger":16,"strangl":16,"strateg":9,"strategi":[0,6,7,10],"stream":[0,1,10,11,13,14,16],"stream_id":[1,10],"street":16,"strength":[10,15,16],"strict":[0,16],"strictli":[7,13],"string":[1,10,16],"stringmessag":1,"strip":[0,4,16],"strong":[3,4,13,16],"stronger":3,"strongli":3,"struct":1,"structur":[0,1,6,9,10,13,15,16],"studi":0,"style":[4,7],"sub":[1,7],"subclass":[1,10],"subcommand":4,"subdirectori":4,"subinterfac":7,"subject":[3,8,14,15,16],"sublicens":8,"submit":0,"subnet":[3,9],"subscrib":[3,14,16],"subsequ":[1,4,13],"subset":7,"subsid":7,"substanti":[0,8],"substrat":[0,16],"subtl":16,"subtli":16,"succe":[3,4,14],"succeed":[1,10],"succesfulli":9,"success":[0,3,4,7,10],"successful":10,"successfulli":[1,10,13],"suddenli":16,"sudo":[3,14],"suffer":9,"suffic":13,"suffici":[3,7,13,14,15],"suffix":1,"suit":[0,3,5,11,13,14,16],"suitabl":[1,3,7,9,13,14,15],"sum":16,"super":1,"supersed":7,"superstructur":0,"suppli":[1,7,10,13,14,15],"supplier":3,"support":[0,1,3,4,6,7,9,10,11,13,14,15],"suprem":0,"sure":[1,3,4,5,7,14],"surfac":16,"surrend":0,"surround":[0,16],"surveil":[0,13,16],"surviv":[3,13,14,16],"suspend":0,"sustain":16,"swim":0,"switch":[5,7,9,13,14,16],"sx1262":5,"sx1268":5,"sx1276":5,"sx1278":5,"sx1280":5,"sy":1,"symlink":14,"symmetr":[10,13],"sync":4,"syncer":4,"synchron":[0,6,11,16],"syntax":6,"synthet":16,"system":[0,1,3,4,5,6,7,8,9,10,11,12,13,15],"systemctl":14,"systemd":14,"t":[0,1,3,4,7,9,10,13,14,16],"tabl":[3,7,9,13,14,16],"tablet":3,"tackl":11,"tag":[4,10],"tail":7,"tailor":13,"take":[0,1,3,4,7,11,13,14,15,16],"taken":[1,4,13,14,16],"talk":[11,16],"tamper":[0,4,16],"tangerin":1,"tap":[5,16],"tar":4,"target":[1,3,4,7,11,14,16],"target_hash":10,"target_host":7,"target_port":[7,14],"task":[4,14],"taught":16,"tcp":[3,5,6,9,13,14,15,16],"tcpclientinterfac":[3,5,7],"tcpinterfac":14,"tcpserverinterfac":[3,7],"tdata":1,"teach":[14,16],"teahous":[0,12],"team":4,"teardown":[1,10],"teardown_reason":1,"tech":16,"technic":[0,4],"technician":16,"technolog":0,"technologi":[3,9,16],"teffect":1,"telco":16,"telecom":16,"telemetri":[11,12],"telephon":6,"telephoni":11,"televis":16,"tell":[1,3,7,9,16],"temperatur":13,"templat":6,"temporari":[3,7,14],"temporarili":[7,9],"ten":13,"tenanc":16,"tenant":16,"tend":7,"tenuou":16,"term":[0,3,7,9,13,16],"termin":[1,3,4,10,11,14],"terminologi":[9,13],"termux":3,"terrifi":16,"territori":0,"test":[3,4,11,13,14,16],"testimoni":0,"testnet":14,"tether":16,"text":[1,11,13,14],"textur":16,"tfile":1,"tgz":[4,14],"than":[0,1,3,4,7,9,10,11,12,13,14,15,16],"thank":4,"theatric":0,"thei":[0,1,3,4,7,9,10,11,13,14,16],"them":[0,1,3,4,5,7,9,11,12,13,14,15,16],"theme":4,"themselv":[0,4,7,9,14],"theori":[0,16],"thereaft":13,"therefor":[3,13,15,16],"thi":[0,1,3,4,5,6,7,8,9,10,11,12,13,14,15,16],"thing":[1,3,9,11],"think":[3,5,7,9,12,13,16],"third":[0,4],"those":[0,3,9,13,14,16],"though":[0,3,7,9,13],"thought":16,"thousand":[0,3,7,13,14,16],"thread":[0,1,4],"threat":14,"three":[3,13,16],"threshold":7,"throough":7,"throttl":[7,9],"through":[1,3,4,5,6,7,9,10,11,13,14,15],"throughout":[9,13],"throughput":[0,10,11,13,15],"thrown":[10,16],"thu":[9,10,13],"ti":[1,9,13],"tighten":16,"tightrop":16,"tild":14,"time":[0,1,3,4,5,6,7,9,10,11,12,13,14],"time_since_last":1,"timefram":0,"timeout":[1,10,14,16],"timeoutarg":1,"timer":4,"timescal":0,"timestamp":[1,4,10,13,16],"timestr":1,"tion":7,"titl":4,"tl":16,"tmp":14,"tnc":[5,7,10,11,14,15],"to_fil":10,"todai":[0,13,16],"togeth":[5,9,13,14,16],"toggl":14,"token":[10,13,15],"tokyo":16,"told":16,"toler":7,"tomorrow":16,"too":[1,9,16],"tool":[0,3,4,5,6,7,9,11,13,14,15],"toolkit":[3,9],"top":[0,11,13,14,16],"topic":[3,13],"topograph":7,"topographi":[3,9,13,16],"topologi":[3,7,9,13,15,16],"torn":[10,14],"tort":8,"tortur":16,"torvald":0,"total":[4,10,13,14,15],"total_s":1,"touch":16,"toward":[1,7,13],"tower":16,"toxic":0,"track":[0,4,10,14,16],"track_phy_stat":10,"trade":[9,13],"tradit":[4,9,13,16],"tradition":0,"traffic":[1,3,7,9,10,13,14,16],"train":[8,16],"transceiv":[3,5,7,11,13,15],"transfer":[1,4,10,13,14,15],"transfer_s":1,"transform":15,"transient":16,"transistor":16,"translat":9,"transmiss":[3,7,10,13,16],"transmit":[1,4,7,13,16],"transmitt":16,"transpar":[4,5,7,9,13,14],"transport":[0,1,3,4,6,7,10,11,14,15,16],"transport_en":10,"transport_ident":14,"travel":[0,16],"travers":[0,3,7,10,13,16],"treat":[0,4,9,13,16],"tree":[0,4],"tremend":9,"tri":1,"trigger":7,"trip":[1,10,14],"trivial":[13,15],"troubl":16,"true":[0,1,3,7,10,14,16],"truli":[5,13,16],"truncat":[10,13,14],"truncated_hash":10,"truncated_hashlength":[1,10],"trust":[0,3,4,6,7,9,13,14],"trustless":[6,13,15],"trustworthi":[9,14],"truth":4,"try":[1,4,6,7,9,10,11],"ttime":1,"ttransfer":1,"ttyacm0":7,"ttyusb0":[1,7,14],"ttyusb1":7,"ttyusb2":7,"tun":[5,7],"tun0":7,"tune":7,"tunnel":[3,7,9,13,15,16],"tupl":10,"turn":[0,1,7,14,16],"turnaround":7,"tutori":3,"two":[0,1,4,5,7,9,10,13,14],"tx":[7,14],"txb":1,"txp":14,"txpower":7,"txt":[4,14],"txtail":[7,14],"type":[1,3,6,7,9,10,14],"typeerror":10,"typic":[4,7,9,13],"tyrant":16,"u":[13,14,16],"ubiqu":5,"ubuntu":[6,14],"uci":3,"udp":[6,9,13,14,15],"udpinterfac":7,"uhf":[14,15],"ui":[11,16],"ultim":[3,13,16],"umsgpack":1,"unavail":4,"unblackhol":14,"unblackhole_ident":10,"unblock":14,"unbound":16,"uncar":16,"uncencrypt":1,"uncentraliz":6,"unchang":9,"uncoordin":[3,16],"under":[0,4,7,10,12,13,14,15,16],"underli":[1,7,9,10,13,15],"undermin":16,"underneath":16,"undersea":16,"understand":[0,3,6,7,9,10,14,15,16],"understood":16,"underwai":3,"underwear":16,"unencrypt":[1,10,13],"unequivoc":13,"unexplor":9,"unforeseen":0,"unforg":[9,15],"unfortun":3,"unicod":4,"unicode_icon":4,"unidentifi":1,"unidirect":1,"unifi":[9,13],"uninform":0,"uninterest":1,"uniqu":[1,7,9,10,11,13,14,15,16],"unit":[0,1,3,10,14],"univers":16,"unix":[4,10],"unknown":[1,7,9,10,13,14],"unlearn":16,"unless":[1,3,7,9,10,13],"unlicens":13,"unlik":[0,4,13],"unlock":14,"unmedi":0,"unnecessari":7,"unorganis":9,"unpack":[1,10],"unpackb":1,"unpaid":16,"unplug":[14,16],"unrecover":[1,14],"unreli":7,"unsatur":0,"unset":13,"unsupport":10,"until":[1,3,7,10],"untrust":[4,14],"untrustworthi":16,"unus":14,"unwant":[13,14],"up":[0,1,3,4,5,7,9,10,11,13,14,15,16],"updat":[0,1,3,4,10,13,14,16],"upgrad":[3,10,13],"upkeep":7,"upload":4,"upon":[0,1,7,13],"upset":1,"upstream":[0,4,14],"uptim":16,"urandom":[13,15],"urgenc":0,"urgent":16,"uri":11,"url":[0,4,14,16],"us":[0,1,4,5,6,7,8,9,10,16],"usabl":[4,7,13],"usag":[4,6,7,10,11,12,14],"usb":[0,5,13,14],"useabl":13,"useless":[0,16],"user":[0,1,3,4,7,9,11,13,14,15,16],"user_input":1,"userland":15,"usernam":16,"usernameher":14,"usr":[7,14],"usual":[3,7,9],"utf":1,"util":[1,5,6,7,13,16],"utilis":[1,3,7,10,13,14,15],"ux":11,"v":[4,6,14],"valid":[0,1,3,6,7,9,10,13,14,16],"valu":[0,1,3,4,7,10,13,14,16],"valuabl":[0,4,5,12,16],"valueerror":[1,10],"vanish":16,"vanishingli":16,"var":4,"vari":[3,7,13,14],"variabl":[1,3,4,10],"varieti":[7,11,13,14,15],"variou":[1,3,7,9,11,13,14,15],"vast":[0,3,13,16],"vastli":9,"ve":9,"vehicl":7,"veloc":0,"vendor":[1,5,15],"ventur":16,"venu":0,"verbos":[4,10,14],"veri":[0,3,4,5,7,9,10,11,12,13,14,15,16],"verif":[0,6,9,10,11,13,14,16],"verifi":[0,1,3,6,7,9,10,13,14,15,16],"versa":15,"version":[0,1,3,4,13,14,16],"vhf":[13,15],"vi":4,"via":[0,3,4,5,7,9,11,12,13,14,15,16],"viabl":9,"vice":15,"video":16,"view":[0,3,4,7,12,13,14],"viewabl":4,"vim":4,"violat":7,"virtual":[3,5,7,9,13,14,15],"virtuou":16,"visibl":[0,1,4,7],"visual":[4,13,16],"vital":16,"voic":[0,11,12,16],"voicemail":11,"volunt":3,"voluntari":3,"vouch":16,"vpn":[9,16],"vport":7,"vulner":9,"vv":14,"vvv":3,"w":[4,14],"wa":[0,1,4,5,7,9,10,13,14,15,16],"wai":[0,1,4,6,7,9,10,11,13,14,16],"wait":[1,3,7,11,13,14,16],"walk":16,"wall":16,"wander":16,"want":[0,1,3,4,7,9,11,12,13,14,16],"wantedbi":14,"warn":14,"warrant":15,"warranti":8,"wash":16,"wast":[7,9,12,16],"wave":[5,16],"wb":1,"we":[0,1,3,5,7,9,12,13,16],"weaken":13,"weapon":[0,16],"wear":16,"weather":16,"weav":16,"web":[3,11,16],"websit":[3,7,16],"weigh":16,"weight":[0,4,14,16],"welcom":0,"well":[0,4,5,7,9,10,11,12,13,14,15,16],"went":1,"were":[0,1,4,11,16],"what":[0,1,3,4,6,7,9,10,13,14,16],"whatev":[9,10,13],"wheel":[3,4],"when":[0,1,3,4,5,7,9,10,11,13,14,16],"whenev":[1,10,13],"where":[0,3,4,5,6,7,9,10,11,13,14,16],"whereupon":13,"wherev":16,"whether":[1,3,4,7,8,9,10,13,16],"which":[0,1,3,4,7,8,9,10,11,13,14,15,16],"while":[0,1,3,4,5,7,9,10,11,13,14,15,16],"whim":16,"whitelist":13,"whl":[3,4],"who":[0,3,4,7,9,13,14,16],"whole":[14,16],"whom":[8,16],"whose":[14,16],"why":[0,16],"wide":[3,4,5,7,9,11,13,14,15],"wider":[3,7,9,13,14],"wield":0,"wifi":[3,6,7,9,11,13,14,15,16],"wikipedia":11,"wild":[4,14,16],"wildcard":[1,4],"wilder":16,"willing":14,"window":[6,7,11,14],"windowsil":16,"wipe":14,"wire":[5,6,7,11,15,16],"wireless":[5,9],"wirelessli":7,"wish":[1,7,13,14,16],"within":[0,1,3,4,7,9,10,13,16],"without":[1,3,4,5,6,7,8,9,11,13,14,15,16],"wlan0":7,"wolf":5,"won":1,"word":[0,3,16],"work":[0,3,5,6,7,9,11,13,14,15],"workabl":9,"workflow":6,"world":[0,3,5,7,9,11,14,15,16],"worri":9,"worth":[0,3],"would":[0,4,7,10,11,13,14,16],"wrap":4,"write":[0,1,3,4,5,7,10,11,13,14,16],"write_timeout":1,"writer":[1,10],"written":[1,4,13,15,16],"wrong":[1,3,7,16],"wrote":[1,7],"x":[4,10,13,14],"x25519":[13,15],"x86_64":4,"xenon":1,"xonxoff":1,"y":[1,4],"ye":[3,4,7,13,14,16],"year":[13,16],"yet":[0,1,7,13,16],"yggdrasil":7,"yi":1,"you":[0,1,3,4,5,6,7,9,10,11,12,13,14,15,16],"your":[0,1,4,5,6,7,9,12,13,15,16],"yourself":[0,3,13,14,15,16],"z":1,"zen":[6,15],"zero":[6,7,10,11,15],"zi":1,"zim":11,"zip":4,"zone":[0,13],"zshrc":3},"titles":["Distributed Development","Code Examples","An Explanation of Reticulum for Human Beings","Getting Started Fast","Git Over Reticulum","Communications Hardware","Reticulum Network Stack Manual","Configuring Interfaces","Reticulum License","Building Networks","API Reference","Programs Using Reticulum","Support Reticulum","Understanding Reticulum","Using Reticulum on Your System","What is Reticulum?","Zen of Reticulum"],"titleterms":{"0":5,"1":5,"25":7,"A":16,"Beings":2,"For":16,"In":16,"Not":[9,16],"Of":[0,6,16],"Or":16,"The":[0,4,13,14,16],"To":16,"With":[3,4],"abil":16,"access":[4,13],"ad":3,"address":[9,16],"agenc":16,"alias":4,"an":2,"anchor":16,"android":3,"announc":[1,7,13,16],"api":10,"architectur":[0,16],"archiv":0,"arm64":3,"artifact":0,"attribut":4,"author":4,"auto":7,"autom":[7,14],"automat":4,"ax":7,"backbon":[3,7],"bandwidth":16,"base":[3,5],"basic":13,"beam":5,"bind":4,"blackhol":14,"block":7,"board":5,"bookworm":3,"bootstrap":3,"broadcast":1,"buffer":1,"build":[3,9],"burst":7,"byte":16,"can":15,"carrier":16,"center":[0,16],"channel":1,"chat":11,"client":[7,11],"cloud":16,"code":[1,13],"combin":5,"command":4,"commit":4,"common":7,"commun":5,"compos":0,"concept":9,"conceptu":13,"configur":[4,7,14],"connect":[3,7,9,11],"consider":[7,9],"content":6,"contribut":3,"control":7,"cost":16,"creat":[3,4,5,13],"creation":4,"creator":4,"cryptograph":[4,13],"current":[13,15],"custom":[1,3,4,7],"data":14,"death":16,"debian":3,"decentr":16,"deck":5,"depend":3,"design":16,"destin":[4,7,9,13],"detail":13,"develop":[0,3],"devic":[5,15],"disconnect":16,"discov":14,"discover":7,"discoveri":7,"distribut":[0,3],"document":4,"doe":15,"domain":16,"donat":12,"echo":[1,5],"emerg":16,"empti":4,"enabl":[4,7],"encrypt":16,"entrypoint":3,"environ":16,"establish":13,"ethernet":5,"ethic":16,"exampl":[1,4,7],"exist":16,"explan":2,"fabric":16,"fallaci":16,"fast":3,"featur":16,"feedback":12,"filesync":11,"filetransf":1,"find":3,"finish":16,"fix":14,"flow":16,"fork":4,"form":4,"format":[4,13],"forward":16,"from":16,"function":13,"further":13,"futur":13,"get":[3,13],"git":4,"global":3,"goal":13,"grade":16,"group":4,"hardwar":5,"harm":16,"health":3,"heltec":5,"heterogen":9,"hierarchi":4,"highlight":4,"host":3,"hostil":16,"human":[2,16],"i":[15,16],"i2p":7,"ident":[4,13,16],"identif":1,"illus":16,"implement":15,"implic":13,"improv":14,"includ":[3,14],"independ":16,"indic":6,"infrastructur":[0,3,16],"instal":[3,5],"instanc":[3,9],"interact":[4,11],"interfac":[1,3,7,11,13,14,15,16],"intermediari":0,"internet":3,"interregnum":0,"introduct":13,"introductori":9,"ip":16,"issu":3,"j":11,"kei":13,"kiss":7,"liber":16,"licens":8,"lilygo":5,"limit":[7,16],"line":[4,5],"link":[1,13],"list":14,"listen":7,"local":14,"locat":4,"long":0,"lora":7,"lora32":5,"lunar":3,"lxmf":11,"lxmfy":11,"lxst":11,"maco":3,"manag":[4,14],"manual":[4,6],"mechan":13,"medium":16,"merit":16,"meshchatx":11,"method":4,"micron":11,"minim":1,"mirror":4,"mix":3,"mode":7,"modem":5,"modul":11,"motiv":13,"multi":7,"name":[13,14,16],"network":[3,4,6,9,11,13],"new":7,"node":[4,9,11,13,16],"nomad":[4,11,16],"note":3,"obtain":4,"offer":15,"offlin":4,"open":16,"opencom":5,"openwrt":3,"option":7,"origin":0,"over":[0,3,4],"overview":[9,13],"packet":[5,13],"page":[4,11],"paramet":[4,7],"parser":11,"path":7,"pattern":16,"permiss":4,"person":[3,16],"phone":11,"physic":16,"pi":3,"pipe":7,"platform":[0,3],"port":14,"portabl":16,"post":16,"power":16,"prerequisit":4,"presenc":16,"preserv":16,"primit":[0,13],"principl":16,"prioritis":13,"program":[3,11,14],"propag":13,"propos":4,"protocol":[0,11,13,16],"provid":12,"public":[3,13,16],"publish":14,"pure":3,"python":3,"radio":[3,5],"rak4631":5,"raspberri":3,"rate":7,"rbrowser":11,"reach":13,"refer":[10,13,15],"relai":11,"releas":4,"remot":[4,7,11,14],"repositori":4,"request":[1,7],"resolv":3,"resourc":[11,13],"respons":[1,3],"restor":0,"ret":3,"retibb":11,"reticulum":[2,3,4,5,6,8,11,12,13,14,15,16],"retipedia":11,"risc":3,"rn":11,"rncp":14,"rngit":[4,14],"rnid":14,"rnmon":11,"rnode":[5,7],"rnodeconf":14,"rnpath":14,"rnprobe":14,"rnsd":14,"rnsh":14,"rnstatu":14,"rnx":14,"road":0,"roam":16,"rrc":11,"rule":13,"scarciti":16,"secur":7,"serial":[5,7,14],"serv":4,"server":7,"servic":14,"setup":13,"shell":11,"short":4,"sideband":11,"sign":4,"signatur":4,"sky":16,"sourc":14,"sovereignti":[0,16],"specif":[3,13],"stack":6,"standalon":3,"start":[0,3],"state":4,"statist":4,"statu":15,"storag":4,"store":16,"strategi":3,"structur":4,"support":[5,12],"suprem":5,"synchron":4,"syntax":4,"system":[14,16],"systemwid":14,"t":5,"t114":5,"t3s3":5,"tabl":6,"tcp":7,"telephon":11,"templat":4,"through":[0,16],"time":16,"tool":16,"transport":[9,13],"trust":16,"trustless":9,"try":3,"type":[4,5,13,15],"ubuntu":3,"udp":7,"uncentraliz":16,"understand":13,"unsign":5,"us":[3,11,13,14,15],"usag":[5,13],"userspac":14,"util":[3,4,11,14],"v":3,"v1":5,"v2":5,"v3":5,"v4":5,"valid":4,"verif":4,"verifi":4,"wai":3,"what":15,"where":15,"wifi":5,"window":3,"wire":13,"without":0,"work":[4,16],"workflow":[0,4],"x":5,"xl":5,"your":[3,14],"zen":16,"zero":16}}) \ No newline at end of file diff --git a/docs/manual/software.html b/docs/manual/software.html index 60673be9..c4681a79 100644 --- a/docs/manual/software.html +++ b/docs/manual/software.html @@ -7,7 +7,7 @@ - Programs Using Reticulum - Reticulum Network Stack 1.5.5 documentation + Programs Using Reticulum - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -265,114 +264,96 @@

      Programs Using Reticulum

      -

      This chapter provides a non-exhaustive list of notable programs, systems and application-layer protocols that have been built using Reticulum.

      -

      Many different applications using Reticulum already exist, serving a wide variety of purposes from day-to-day communication and information sharing to systems administration and tackling advanced networking and communications challenges.

      -
      -

      Tip

      -

      For ease of discovery while reading this manual over the web or as an e-book, links provided in this chapter primarily point to repositories and pages located on internet servers. In many cases, these are public web mirrors, with the upstream repositories residing on Reticulum nodes. For the latest source, updates and information regarding any project, be sure to check their Nomad Network or rngit nodes.

      -
      -

      These programs will let you get a feel for how Reticulum works. Most of them have been designed to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast links, such as local WiFi, wired Ethernet, the Internet, or any combination.

      -

      As such, it is easy to get started experimenting, without having to set up any radio transceivers or infrastructure just to try it out. Launching the programs on separate devices connected to the same WiFi network is enough to get started, and physical radio interfaces can then be added later.

      -

      Development of Reticulum-based applications and systems is ongoing, so consider this list a non-exhaustive starting point of some of the options available. With a bit of searching, primarily over Reticulum itself, you will find many more interesting things.

      -
      -

      Browsing & Publishing

      -

      In addition to the programs listed in this section, other Nomad Network clients include:

      - +

      This chapter provides a non-exhaustive list of notable programs, systems and application-layer +protocols that have been built using Reticulum.

      +

      These programs will let you get a feel for how Reticulum works. Most of them have been designed +to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast +links, such as local WiFi, wired Ethernet, the Internet, or any combination.

      +

      As such, it is easy to get started experimenting, without having to set up any radio +transceivers or infrastructure just to try it out. Launching the programs on separate +devices connected to the same WiFi network is enough to get started, and physical +radio interfaces can then be added later.

      +
      +

      Programs & Utilities

      +

      Many different applications using Reticulum already exist, serving a wide variety of purposes +from day-to-day communication and information sharing to systems administration and tackling +advanced networking and communications challenges.

      +

      Development of Reticulum-based applications and systems is ongoing, so consider this list +a non-exhaustive starting point of some of the options available. With a bit of searching, +primarily over Reticulum itself, you will find many more interesting things.

      +
      +

      Remote Shell

      +

      The rnsh program lets you establish fully interactive +remote shell sessions over Reticulum. It also allows you to pipe any program to or from a +remote system, and is similar to how ssh works. The rnsh program is very efficient, and +can facilitate fully interactive shell sessions, even over extremely low-bandwidth links, +such as LoRa or packet radio.

      +

      In addition to the default, fully interactive terminal mode, +for extremely limited links, rnsh offers line-interactive mode, allowing you to interact +with remote systems, even when link throughput is counted in a few hundreds of bits per second.

      +

      Nomad Network

      -

      The terminal-based program Nomad Network provides a complete encrypted communications suite built with Reticulum.

      -_images/nomadnet_6.webp +

      The terminal-based program Nomad Network +provides a complete encrypted communications suite built with Reticulum. It features +encrypted messaging (both direct and delayed-delivery for offline users), file sharing, +and has a built-in text-browser and page server with support for dynamically rendered pages, +user authentication and more.

      +_images/nomadnet_3.png -

      It features encrypted LXMF messaging (both direct and delayed-delivery for offline users), file sharing, and has a built-in text-browser and page server with support for dynamically rendered pages, user authentication and more. It also includes an RRC client and a suite of network utilities.

      -
      -
      -

      Ren Browser

      -

      Ren Browser is a modern, standalone browser for Nomad Network using the Reticulum-Go implementation. The project is actively being developed, and should be considered alpha level software, but much of the functionality is already working. If you want to help expanding the ecosystem, this is an excellent project to take a look at.

      -_images/ren_browser.webp - -

      Ren Browser is great example of an application aiming for true, seamless interoperability with the existing software ecosystem. It can operate fully standalone using it’s own embedded Go implementation of Reticulum, or seamlessly utilise a shared RNS instance already running on the system.

      +

      Nomad Network is a user-facing client +for the messaging and information-sharing protocol LXMF.

      RNS Page Node

      -

      RNS Page Node is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files.

      +

      RNS Page Node is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files.

      Retipedia

      -

      You can host the entirity of Wikipedia, StackExchange or any other .zim file, to Nomad Network clients using Retipedia.

      -_images/retipedia.webp - +

      You can host the entirity of Wikipedia (or any .zim) file to other Nomad Network clients using Retipedia.

      -
      -

      Git Over Reticulum

      -

      The included rngit system provides a fully distributed solution for managing, hosting, collaborating on and interacting with Git repositories over Reticulum.

      -_images/rngit.webp -
      -
      -
      -

      Messaging & Voice

      Sideband

      -

      Sideband is a technical LXMF and LXST client, for Android, Linux, macOS and Windows. It also serves as a multi-purpose Reticulum utility, with features and functionality targeted at advanced users.

      -_images/sideband_devices.webp +

      If you would rather use an LXMF client with a graphical user interface, you can take +a look at Sideband, which is available for Android, +Linux, macOS and Windows. Sideband is an advanced LXMF and LXST client, and a multi-purpose Reticulum +utility, with features and functionality targeted at advanced users.

      +_images/sideband_devices.webp -

      Sideband allows you to communicate with other people or LXMF-compatible systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR Paper Messages, or anything else Reticulum supports.

      -

      It interoperates with all other LXMF clients, and provides advanced features such as PTT and voice messaging, real-time voice calls, file attachments, private telemetry sharing, and a full plugin system for expandability.

      +

      Sideband allows you to communicate with other people or LXMF-compatible +systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR +Paper Messages, or anything else Reticulum supports.

      +

      It also interoperates with all other LXMF clients, and provides advanced features such as voice messaging, +real-time voice calls, file attachments, private telemetry sharing, and a full +plugin system for expandability.

      -
      -

      MeshChatX

      -

      MeshChatX is an all-in-one Reticulum client with messaging, calling, and a NomadNet browser, providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application.

      -_images/meshchatx.webp +
      +

      MeshChatX

      +

      A Reticulum MeshChat fork from the future, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original Reticulum MeshChat project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork.

      +_images/meshchatx.webp -

      Features include full LXST support, custom voicemail, phonebook, contact sharing, ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security.

      +

      Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security.

      -
      -

      Columba

      -

      Columba is a simple and familiar LXMF messaging app Android, built with a native Android interface and Material Design 3. It also includes LXST voice call functionality, and a mobile Nomad Network page browser.

      -_images/columba.webp - -
      -
      -

      Ren TUI

      -

      Ren TUI is an early prototype terminal LXMF client for Reticulum built with Odin on librns (Reticulum-Go). It aims for NomadNet-like messaging/browsing without urwid or ncurses.

      -_images/rentui.webp - -
      -
      -
      -

      Chat

      Reticulum Relay Chat

      Reticulum Relay Chat is a live chat system built on top of the Reticulum Network Stack. It exists to let people talk to each other in real time over Reticulum without dragging in message databases, synchronization engines, or architectural commitments they did not ask for.

      -_images/rrc.webp +

      The rrcd program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include rrc-gui and rrc-web.

      RRC is closer in spirit to IRC than to modern “everything platforms.” You connect, you join a room, you talk, and then you leave. If you were present, you saw the conversation. If you were not, the conversation did not wait for you. This is not an accident. This is the entire design.

      -

      The rrcd program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include:

      -

      RetiBBS

      -

      RetiBBS is an old-school bulletin board system implementation for Reticulum networks.

      +

      RetiBBS is a bulletin board system implementation for Reticulum networks.

      _images/retibbs.webp

      RetiBBS allows users to communicate through message boards in a secure manner.

      +
      +

      RBrowser

      +

      The rBrowser program is a cross-platform, standalone, web-based browser for exploring NomadNetwork Nodes over Reticulum Network. It automatically discovers NomadNet nodes through network announces and provides a user-friendly interface for browsing distributed content with Micron markup support.

      +_images/rbrowser.webp + +

      Includes useful features like automatic listening for announce, adding nodes to favorites, browsing and rendering any kind of NomadNet links, downloading files from remote nodes, a unique local NomadNet Search Engine and more.

      -
      -

      Voice & Telephony

      -

      In addition to the programs listed in this section, other LXST voice clients include:

      -

      Reticulum Network Telephone

      The rnphone program, included as part of the LXST package is a command-line Reticulum telephone utility and daemon, that allows building physical, hardware telephones for LXST and Reticulum, as well as simply performing calls via the command line.

      @@ -380,75 +361,34 @@

      It supports interfacing directly with hardware peripherals such as GPIO keypads and LCD displays, providing a modular system for building secure hardware telephones.

      -
      -

      Partyline

      -

      Partyline is a group voice application for Reticulum, built on LXST, inspired by Mumble.

      -_images/partyline.webp - -

      It provides both realtime group voice over any Reticulum transport capable of >6 kilobits per second, LXST dial-in support, listen-only music/broadcasting rooms, and many other useful functions.

      -

      LXST Phone

      -

      The LXST Phone program is a cross-platform desktop application for performing LXST voice calls over Reticulum. It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management.

      +

      The LXST Phone program is a cross-platform desktop application for performing LXST voice calls over Reticulum.

      _images/lxst_phone.webp -
      -
      -
      -

      System Administration

      -
      -

      Remote Shell

      -

      The included rnsh program lets you establish fully interactive -remote shell sessions over Reticulum. It also allows you to pipe any program to or from a -remote system, and is similar to how ssh works. The rnsh program is very efficient, and -can facilitate fully interactive shell sessions, even over extremely low-bandwidth links, -such as LoRa or packet radio.

      -

      In addition to the default, fully interactive terminal mode, for extremely limited links, -rnsh offers line-interactive mode, allowing you to interact with remote systems, even -when link throughput is counted in a few hundreds of bits per second.

      -
      -
      -

      RNS FileSync

      -

      The RNS FileSync program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing.

      -
      -
      -
      -

      Utilities & Components

      -
      -

      Modem73

      -

      MODEM73 and its companion Modem73Interface is an open source software modem that works with any HF, VHF, or UHF radio capable of 2400 Hz of bandwidth. All you need is a sound card and audio cable for your radio.

      -_images/modem73.webp - +

      It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management.

      LXMFy

      -

      LXMFy is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. Bot implementations exist for Home Assistant control, LLM integrations, and various other purposes.

      +

      LXMFy is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. Bot implementations exist for Home Assistant control, LLM integrations, and various other purposes.

      +
      +
      +

      LXMF Interactive Client

      +

      LXMF Interactive Client is a feature-rich, terminal-based LXMF messaging client with many advanced features and an extensible plugin architecture.

      +
      +
      +

      RNS FileSync

      +

      The RNS FileSync program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing.

      Micron Parser JS

      Micron Parser JS is the JavaScript-based parser for the Micron markup language, that most web-based Nomad Network browsers use. If you want to make utilities or tools that display Micron pages, this library is essential.

      -
      -

      Reticulated

      -

      Reticulated is a Reticulum Network simulator that spawns live, interactive Reticulum instances controllable from a node-based Web UI.

      -_images/reticulated.webp - -

      You can create networks of any topology and set bitrate, MTU, and configure path loss and propagation behavior. It provides a top-down view to a live network. You can connect any Reticulum application to the simulated network. It also has built in utilities like a basic LXMF chat function and Resource transfer included.

      -

      RNMon

      RNMon is a monitoring daemon designed to monitor the status of multiple RNS applications and push the metrics to an InfluxDB instance over the influx line protocol.

      -
      -

      Device Flashers

      -

      In addition to the included command-line rnodeconf flasher, several community-provided RNode device flashers exist:

      - -

      Several programs, such as Sideband and MeshChatX also include built-in device flashers that can be used fully offline.

      -

      Protocols

      A number of standard protocols have emerged through real-world usage and testing in the Reticulum community. While you may sometimes want to use completely custom protocols and implementations when writing Reticulum-based software, using these protocols provides application developers with an easy way to implement advanced functionality quickly and effortlessly. Using them also ensures compatibility and interoperability between many different client applications, creating an open communications ecosystem where users are free to choose the applications that suit their needs, while remaining connected to everyone else.

      @@ -458,8 +398,8 @@ when link throughput is counted in a few hundreds of bits per second.

      LXMF is efficient enough that it can deliver messages over extremely low-bandwidth systems such as packet radio or LoRa. Encrypted LXMF messages can also be encoded as QR-codes or text-based URIs, allowing completely analog paper message transport.

      Using Propagation Nodes, LXMF also offer a way to store and forward messages to users or endpoints that are not directly reachable at the time of message emission.

      -
      -

      LXST

      +
      +

      LXST

      LXST is a simple and flexible real-time streaming format and delivery protocol that allows a wide variety of applications, while using as little bandwidth as possible. It is built on top of Reticulum and offers zero-conf stream routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports. It currently powers real-time voice and telephony applications over Reticulum.

      @@ -472,11 +412,11 @@ when link throughput is counted in a few hundreds of bits per second.

      Interface Modules & Connectivity Resources

      This section provides a list of various community-provided interface modules, guides and resources for creating Reticulum networks over special or challenging mediums.

      @@ -536,49 +476,28 @@ when link throughput is counted in a few hundreds of bits per second.

      -
    + diff --git a/docs/manual/support.html b/docs/manual/support.html index 2eea7c1f..9f7ca70c 100644 --- a/docs/manual/support.html +++ b/docs/manual/support.html @@ -7,7 +7,7 @@ - Support Reticulum - Reticulum Network Stack 1.5.5 documentation + Support Reticulum - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -286,6 +285,8 @@ Ko-Fi: https://ko-fi.com/markqvist
    +

    Are certain features in the development roadmap are important to you or your +organisation? Make them a reality quickly by sponsoring their implementation.

    Provide Feedback

    @@ -382,7 +383,7 @@ circumstances, so we rely on old-fashioned human feedback.

    - + diff --git a/docs/manual/understanding.html b/docs/manual/understanding.html index 13e1188e..9a49172c 100644 --- a/docs/manual/understanding.html +++ b/docs/manual/understanding.html @@ -7,7 +7,7 @@ - Understanding Reticulum - Reticulum Network Stack 1.5.5 documentation + Understanding Reticulum - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -1120,7 +1119,7 @@ those risks are acceptable to you.

    - + diff --git a/docs/manual/using.html b/docs/manual/using.html index 5d07627b..2a2b35d5 100644 --- a/docs/manual/using.html +++ b/docs/manual/using.html @@ -7,7 +7,7 @@ - Using Reticulum on Your System - Reticulum Network Stack 1.5.5 documentation + Using Reticulum on Your System - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
  • The rnstatus Utility

    -

    Using the rnstatus utility, you can view the status of and control configured Reticulum interfaces, similar to the ifconfig program.

    +

    Using the rnstatus utility, you can view the status of configured Reticulum +interfaces, similar to the ifconfig program.

    Usage Examples

    Run rnstatus:

    $ rnstatus
    @@ -558,8 +558,8 @@ Reticulum Transport Instance <5245a8efe1788c6a1cd36144a270e13b> running
     

    All Command-Line Options

    -
    usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A] [-P] [-l]
    -                [-B] [-b] [-t] [-q] [-s SORT] [-r] [-j] [-R hash] [-i path]
    +
    usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A]
    +                [-l] [-t] [-s SORT] [-r] [-j] [-R hash] [-i path]
                     [-w seconds] [-d] [-D] [-m] [-I seconds] [-v] [filter]
     
     Reticulum Network Stack Status
    @@ -571,19 +571,12 @@ options:
       -h, --help            show this help message and exit
       --config CONFIG       path to alternative Reticulum config directory
       --version             show program's version number and exit
    -  --attach name         Attach interface by name
    -  --detach name         Detach interface by name
    -  --reload name         Reload interface by name
       -a, --all             show all interfaces
       -A, --announce-stats  show announce stats
    -  -P, --pr-stats        show path request stats
       -l, --link-stats      show link stats
    -  -B, --burst           only show interfaces with active bursts
    -  -b, --blocked-ips     show blocked IPs per interface
       -t, --totals          display traffic totals
    -  -q, --queues          display queue stats
       -s, --sort SORT       sort interfaces by [rate, traffic, rx, tx, rxs, txs,
    -                        announces, arx, atx, prx, ptx, held]
    +                                            announces, arx, atx, held]
       -r, --reverse         reverse sorting
       -j, --json            output in JSON format
       -R hash               transport identity hash of remote instance to get status from
    @@ -591,8 +584,6 @@ options:
       -w seconds            timeout before giving up on remote queries
       -d, --discovered      list discovered interfaces
       -D                    show details and config entries for discovered interfaces
    -  --show-stale          show stale discovery entries
    -  --show-unknown        show discovery entries without version info
       -m, --monitor         continuously monitor status
       -I, --monitor-interval seconds
                             refresh interval for monitor mode (default: 1)
    @@ -877,7 +868,7 @@ options:
     

    The rnx utility is a basic remote command execution program. It allows you to execute commands on remote systems over Reticulum, and to view returned command output. For a fully interactive remote shell solution, be sure to also take a look -at the rnsh program.

    +at the rnsh program.

    Usage Examples

    Run rnx on the listening system, specifying which identities are allowed to execute commands:

    $ rnx --listen -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
    @@ -932,7 +923,7 @@ optional arguments:
     
    -

    The rnsh Utility

    +

    The rnsh Utility

    The rnsh utility provides a fully interactive remote shell over Reticulum. It allows you to establish encrypted, authenticated shell sessions on remote systems, complete with terminal emulation, pipe support, and window resizing.

    @@ -1316,7 +1307,6 @@ network_identity = ~/.reticulum/storage/identities/my_network
    • The autoconnect_interface_mode options specifies which mode discovered interfaces should be created with when auto-connecting.

    • The autoconnect_announces_to_internal option allows you to specify that auto-connected interfaces should propagate announces to internal mode interfaces, even if the auto-connected interface’s mode would normally not allow for this.

    • -
    • The autoconnect_unverified_implementations option will allow the interface discovery system to auto-connect to any announced interface, even those coming from unverified, experimental or potentially broken implementations. Be very careful with this option.

    @@ -1650,7 +1640,7 @@ systemctl --user enable rnsd.service - + diff --git a/docs/manual/whatis.html b/docs/manual/whatis.html index 88d9d5a3..668be436 100644 --- a/docs/manual/whatis.html +++ b/docs/manual/whatis.html @@ -7,7 +7,7 @@ - What is Reticulum? - Reticulum Network Stack 1.5.5 documentation + What is Reticulum? - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
  • - + diff --git a/docs/manual/zen.html b/docs/manual/zen.html index 66bceb26..70b9aaeb 100644 --- a/docs/manual/zen.html +++ b/docs/manual/zen.html @@ -7,7 +7,7 @@ - Zen of Reticulum - Reticulum Network Stack 1.5.5 documentation + Zen of Reticulum - Reticulum Network Stack 1.4.2 documentation @@ -180,7 +180,7 @@
    @@ -204,7 +204,7 @@
    - Reticulum Network Stack 1.5.5 documentation + Reticulum Network Stack 1.4.2 documentation @@ -227,7 +227,6 @@
  • Support Reticulum
  • Code Examples
  • Reticulum License
  • -
  • Brandolini’s Reference
    • API Reference
    • @@ -678,7 +677,7 @@ Imagine a messaging app. You write it once. It works on a laptop connected to fi
    - + diff --git a/docs/markdown/brandolinis.md b/docs/markdown/brandolinis.md deleted file mode 100644 index 0acd2d61..00000000 --- a/docs/markdown/brandolinis.md +++ /dev/null @@ -1,1386 +0,0 @@ -# Brandolini’s Reference - -> *A little learning is a dang’rous thing;* -> -> *Drink deep, or taste not the Pierian spring.* -> -> *There shallow draughts intoxicate the brain,* -> -> *And drinking largely sobers us again.* -> -> -> - Alexander Pope, 1711 -> - -This chapter serves as a helpful reference for developers, users and community members living in an Age of Bullshit Manufacture equivalent to the Cambrian Explosion. - -As we are surely only at the on-ramp of this trajectory, the chapter will continue to evolve, expand and be updated based on community feedback as events occur. - -## What The Reticulum License Is - -*To understand the legal reality relating to this section, see* [Void Grants](#brandolinis-void-grants). - -The [Reticulum License](license.md#license) is the license under which RNS, the Reticulum reference implementation, has been published since **April 15, 2025**, in versioning effective from **0.9.4** and every release since. In practical terms, it covers all of “modern Reticulum”: The current wire format, the cryptographic machinery as it exists today (including the AES-256-based link encryption, ratchets, et cetera), and all protocol additions that distinguish the modern implementation from the earliest pre-1.0.0 beta formats. If an implementation speaks today’s Reticulum protocol, it is, in all relevant respects, built on Reticulum-licensed work. - -The license is a permissive license. It grants the full range of rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, free of charge; subject to three conditions, which are reproduced here verbatim: - -> * The Software shall not be used in any kind of system which includes amongst its -> functions the ability to purposefully do harm to human beings. -> * The Software shall not be used, directly or indirectly, in the creation of an artificial -> intelligence, machine learning or language model training dataset, including but not -> limited to any use that contributes to the training or development of such a model or -> algorithm. -> * The above copyright notice and this permission notice shall be included in all copies or -> substantial portions of the Software. - -### Why This License Exists - -Reticulum was not moved to a custom license casually or quickly. It was licensed under the MIT license for the greater part of its existence, and that served well for a long time. The change came when it became clear that no existing license expressed what the project needed, and that the two most commonly proposed alternatives would either say the wrong thing or say nothing useful at all. Three factors had primary influence on the situation: - -**The harm problem** - -Reticulum is a tool that was built, deliberately and at great cost, to empower people; to make communication something you own rather than something you are temporarily granted. It has also been, at various times, exactly what certain kinds of organisations would like to use to control people. This is the duality of any powerful tool. A permissive license could not, and cannot, say anything about that; MIT is silent on the question of whether the software may be used to build systems designed to harm human beings. The Reticulum License is not silent. It does not pretend that a license can stop an individual madman; it does assert, however, plainly and in enforceable terms, that corporations and organisations integrated into the legal system cannot consume this software for harm and then claim they never knew. The line is drawn by the creators of the tool, not by an external authority, and it is drawn deliberately. This is covered in more depth in the discussion of the ethics of the tool in the *Zen of Reticulum*. - -**The appropriation problem** - -The second factor was the machine-learning appropriation of the open-source commons. By 2025 that was no longer hypothetical: The commons was already being systematically ingested by corporate training pipelines, and the code, words, and creativity of the people who built it were being converted into synthetic systems designed to replace them. The license responds to this with an explicit condition: The software shall not be used in the creation of an AI, machine-learning, or language-model training dataset, nor in any use that contributes to the training or development of such a model or algorithm. - -It is important to understand what this condition does and does not do, which matters for what follows in this chapter. **The AI-training condition is not the legal basis for protecting the code from machine copying**. That protection *already exists*, in the oldest and most well-tested form of copyright law, and it applies regardless of any license (see [Copyright & The Two Legal Layers](#brandolinis-copyright-and-layers)). - -The AI-training condition addresses something *additional*: the appropriation of the code *into the models themselves*; the ingestion of human-made work as raw material for a synthetic replacement, and the use of that ingestion to fabricate “prior work” minefields, patent-troll material, and a wall of slop meant to bury and devalue the actual ecosystem. That danger was foreseeable; the licensing decision was made with full awareness of it. The events of 2026 have since demonstrated, in public and in detail, that the concern was not theoretical. - -**The sustainability problem** - -Reticulum has been developed and maintained for over a decade by a single person, supported by a small group of contributors and network-building users, without corporate backing, without VC funding, through several serious attempts at commercialization and takeover from both the corporate and the ideological side. The license is part of what makes that sustainable, and possible at all. - -It ensures that the specific, painstaking labor of the reference implementation cannot be hijacked to undermine the intent of the project, while leaving the protocol itself, the mathematical rules of how Reticulum works, dedicated to the public domain, where it belongs to humanity and can never be owned. Anyone can implement the protocol, provided they themselves invest the *actual* work and effort. What the license protects is the work, and the intent embodied in it. - -### On Freedom - -The most persistent mischaracterisation of the Reticulum License is that it is “more restrictive” than the licenses it replaced or than the GPL family. This is backwards, and I’ll state the comparison plainly, because the confusion is the foundation of a great deal of deliberately manufactured controversy examined later in this chapter. - -The GPL and AGPL are constructed around a single, self-referential axiom: that “free” is *defined by the terms of the license itself*, and that any use falling outside those terms is not free. In practice, this means the GPL restricts the freedom of **everyone** - you may not write, distribute, link, or (in some cases) even communicate with GPL-covered software without the entire arrangement being pulled under the same terms, which the licensor then controls. The restriction applies to 100% of users, including the ones building purely constructive things. - -The Reticulum License restricts the freedom of almost nobody. Its restrictions apply only to: - -* Those who wish to use the software in systems that purposefully harm human beings, and -* Those who wish to ingest the software into AI training pipelines. - -That is a restriction on a vanishingly small fraction of would-be users - the 0.0000001% - and it is a restriction they can characterise however they like, forever. The bickering is itself a signal that the licensing decision hit exactly the right target. - -Everyone else retains the full breadth of use, modification, distribution, and sale, with none of the viral encumbrance of copyleft. - -Both licenses contain moral axioms. The difference is that the Reticulum License states its axioms out loud: - -* *Intentionally restricting the way that millions of people use and build upon a foundational - communications technology* - **The mechanism of copyleft** -* *Intentionally restricting a small number of organisations from using the technology to - violently harm human beings* - **The mechanism of the Reticulum License** - -Which of these is more free? The answer is not a matter of committee approval. It is a matter of what the word “free” means to the people *whose freedom is at stake*. The question of whether “open source” requires the blessing of a particular licensing orthodoxy is examined in [Open Source Means Open Source](#brandolinis-opensource). - -### The Prediction, And What Happened - -When the license change was made and publicly discussed in 2025, the following was said, in public, about the machine-learning threat: - -> A generative system could be trained on the Reticulum codebase, and for extremely low cost -> be set to work spewing out millions of source repositories for all kinds of ridiculous -> “Reticulum-based programs”. This is already happening. You can find several examples of this -> on GitHub already. … the point of this kind of trolling is to establish as wide a base for -> “prior work” as possible. By doing that, a patent troll or IP abuser can create an immense -> minefield for real developers. - -This was written when the phenomenon was just beginning. It was not a guess; it was a description of a mechanism already in motion, in the specific context of Reticulum, by people who had already begun to appear around the project. - -Less than a year and a half later, the prediction has now been demonstrated, in public, exhaustively and to the letter. There are now roughly a dozen machine-generated “Reticulum implementations” of the same shape: fluent, marketing-heavy, some without attribution, and - in several cases - actively harmful to the network. The [Prns case](#brandolinis-prns) is an example documented in this chapter. The pattern that produced it (and the people, mechanisms, and the recycling of falsehoods behind the *wider* phenomenon) is examined in [A Movement Of A Dozen](#brandolinis-movement-of-a-dozen). - -The license was written, in part, so that when this happened, the community would not be defenceless. - -### What This Means For Other Implementations - -Two simple things must be kept separate: - -1. **The protocol is public domain.** The mathematical rules of Reticulum - addresses, - signatures, routing, the wire format as an idea - belong to humanity. A *genuine* clean-room - implementation of the protocol, written by human beings who reasoned it out themselves, is - free and welcome, and owes nothing to the Reticulum License. Nothing in this chapter is an - argument against implementing the protocol. On the contrary: More good implementations, - honestly made, are good for the network. -2. **The implementation is licensed.** Copying the reference implementation - by machine or - by silk-screen press - and modifying and redistributing the result, is - exercising rights granted by the license, and is therefore subject to its conditions: The attribution - notice, the harm condition, the AI-training condition. A copy that does not follow those - conditions is **not** licensed, and it is not “open source in spirit”; it is an unauthorized - derivative work, and therefore a copyright violation. - -The distinction is the same one the license itself makes, and it must be held firmly: An implementation that *speaks* Reticulum is exercising the public domain protocol, **if** it was truly created independently. An implementation that was *generated from* RNS, reproduced by a copy process, is a derivative of licensed work. - -### Copyright & The Two Legal Layers - -The exact legal structure is frequently muddied in public discussion; and in several cases deliberately. - -**Layer A - Copyright (license-independent)** - -Copyright does not depend on any license. A primarily machine-generated copy of a copyrighted work - a copy not accompanied by demonstrable, substantial, new human creative input - is a derivative work over which the copier holds no copyright whatsoever, and the reproduction of which, without the original author’s permission, is an infringement of the original author’s copyright. This is standard, well-tested law, and it applies with equal force under the MIT license, the GPL, or any other license, because it is not about the license at all; it is about who created what. A process that copies the reference implementation into a machine, produces a transformed derivative, and then claims “Copyright, [name of the process operator]” - with no human able to account for the creative decisions in the result - **is not** asserting a valid copyright over the result. They are openly asserting the absence of one, and reproducing someone else’s work without authorisation. This is probably the cleanest cut copyright violation one could imagine. - -**Layer B - License Conditions (Reticulum License specific)** - -Where a copy *is* a licensed derivative, the conditions of the Reticulum License apply: The copyright notice and permission notice must be included in all copies and substantial portions; the harm condition must be honoured; the AI-training condition must be honoured. A derivative that follows none of these is unlicensed *a second way over*. - -The two layers are often intentionally confused in public discussion - the AI-training condition is waved around as if the entire question of machine copying rested on it, when in fact machine copying of copyrighted work is a copyright violation that would exist even under the most permissive license ever written. The purpose of keeping the layers separate is not rhetorical flourish; it is that Layer A is old, settled law, and does not rely on any novel legal theory, while Layer B is specific to this license. Both matter. Neither is a substitute for the other. - -Layer B is the required mechanism for closing the loophole where the work is *first* ingested during model training into the weights of a model itself, which would make any resulting violations almost impossible to detect and enforce. - -### The “Clean-Room” Delusion - -I regularly receive mail or LXMF messages from people who proudly and happily proclaim that they have created a “clean-room” implementation of one of the software systems I maintain; RNS, Sideband, LXMF or others. The declaration is delivered with the flat certainty of someone who believes they have found the legal lock-pick that settles everything at once: Their production is independent, uncontaminated, and therefore free of any obligation to the work they copied, its license, or its author. The proclamation usually concludes with a request for blessing, or at least acknowledgment. The proclamation is, usually, also generated by an LLM. - -This is a delusion, and I’ll cut through it surgically. The word they here invoke as magic is attempting to do a great deal of work it was never able to bear. - -**What a clean room actually was** - -The legal “clean room” is not folklore, incantation, witchcraft or a claim a person gets to make about their own output. It is a very specific, institutionalized procedure that grew out of the hardware and software litigations of the 1980s, and it existed for exactly one purpose: **Convincingly proving the absence of copying in a court of law**. - -The canonical case was Phoenix Technologies and its 1984 reimplementation of the IBM PC BIOS. IBM’s BIOS was copyrighted, which meant that the entire PC-compatible industry depended on being able to show that a compatible BIOS had been written independently, and not from IBM’s code. Phoenix’s answer was a physical and procedural firewall: - -* One team, the “dirty room”, read the original code and produced written *functional specifications*: descriptions of what the BIOS did, not how IBM’s code did it. -* A second team, the “clean room”, consisted of engineers who had *never seen the original code at all*, and they wrote the new BIOS from those specifications alone. - -This obsessively documented, legally attested separation was not a preference or a willy-nilly show for a kindergarten; it was the entire legal point, and it had to be *solid*. If even one clean-room engineer had *glanced* at IBM’s code, the clone would have been contaminated, and the defense would shatter. - -The other landmark case was *Computer Associates v. Altai* (1992), in which Altai had copied CA’s scheduler code into its own product, and (upon being caught) hired outside counsel to run a genuine clean-room procedure: A programmer with no exposure to the original was brought in to rewrite the infringing modules from functional specifications prepared by the team that had seen the code. The court credited the good-faith effort precisely because it was done, as the carefully documented record showed, to the letter of the procedure. - -Bear in mind what this procedure entailed in practice, because this is the part the modern claimants never seem to have even *heard* of: Physical separation of teams, legal counsel managing the process, signed and notarised declarations of non-exposure, controlled specification documents that described *behaviour* and never *code*, months of work, and testing against the specification - not against the original, which the implementers were forbidden to consult. - -The people on the clean-room side of the wall did not just *claim* they had not read the code. Their non-exposure was institutionally guaranteed, obsessively documented, and legally attestable through every fart and sneeze of the process, because the entire point was that a court - hostile, adversarial, and entitled to discovery - would believe it. - -*That* is the process. “Clean-room implementation” is the name of a *certified procedure for proving independent authorship*. It is not a mood, a marketing label, or a property a person gets to declare about their own work. - -**The moronic inversion** - -Now consider what is actually being described when someone says: *“I created a clean-room implementation of Reticulum by feeding the source code into a machine.”* - -That sentence is the opposite of a clean room. Feeding a copyrighted work into a machine so that it reproduces and transforms the work is **the definition of copying**, and a clean room is the one procedure whose entire purpose is the demonstrated *absence* of copying. This process cannot demonstrate the absence of copying, because the process is *composed of* copying. The cleanest possible room has been replaced by the dirtiest possible room, and the dirt has been installed as the base feature. What these claimants have actually performed is a photocopy with extra steps, and the extra steps, far from cleaning anything, are simply steps that forge a certificate they believe the output has earned. - -**Why the word is deployed** - -The word “clean-room” is deployed because it sounds like what the speaker wishes were true. It carries the accumulated legal prestige of the 1980s and 1990s - the BIOS clones, the Altai credit - and it sounds like a certificate already issued: *Independent, original, unencumbered*, the work of a person who owes nothing. The claimants want that halo without the procedure: No law firm, no physical separation, no written specifications, no signed declarations, no months of process - just a laptop, a loop, and the word “clean-room” typed into a README. The word must do the work of the process it replaces, and the delusional replacement is the entire point. - -**The irony runs deep** - -The Reticulum protocol is public domain. Any human being can implement it - from the mathematics, from the documentation, from their own reasoning - and owe nothing whatsoever to the license, to the reference implementation, or to me. - -An honestly written implementation needs no incantations, no legal theater, and no special vocabulary; it simply *is* what it claims. It’s plain for everyone to see. **It’s also plain for everyone to see when you’re faking it**. The people who reach for the word “clean-room” are, without exception, the people who need it to create a fantasy - and the people who need that are precisely the people who cannot have it. If what you wrote were genuinely independent work on the public-domain protocol, you would not need to call it anything. The word is not a seal of independence. In this context it is a confession of copying, filed by the copier, in the confident belief that it says the opposite. - -## Open Source Means Open Source - -An objection regarding the “open source status” of RNS appears occasionally in variations, but always reduces to the same syllogism: - -1. “Open source” means *licensed under one of the licenses approved by the Open Source - Initiative (or, in other variants, compatible with GPL orthodoxy)*. -2. The Reticulum License is not on that list. -3. Therefore Reticulum is not open source, and anything built on it is illegitimate. - -The entire argument rests on the first premise, and the first premise is false. - -Open source is not a certification conferred by a committee. It is a description of a fact: The source code is available, and the rights to use, copy, modify, merge, publish, distribute, sublicense, and sell it are granted to everyone, free of charge. - -The Reticulum License does all of those things. It is, in every substantive respect that concerns the person who wants to use, study, modify, or build upon the software, **an open license**. What it adds - and what disqualifies it in the eyes of the orthodoxy - is two conditions on *who may do what with the original work*, not on what the *user* is allowed to do with it. - -The definition being wielded against Reticulum is not even a young one. It is, in essence, the Debian Free Software Guidelines written in 1997, adopted by the OSI in 1998, and treated ever since by certain people as if it had descended from a mountain on tablets. It is an approach that is close to three decades old. Being old is not itself a flaw - but being old is also not a proof of correctness, and the suggestion that the meaning of a word like “open” was settled for all time by a committee in the 1990s, and may never be revisited, is not a position rooted in reason. It is rooted in institutional inertia dressed as principle, maintained by those whose ideologies it serves. - -### What The Argument Is Actually Doing - -I would like to go into some detail about the rhetorical mechanism, because it is roughly the same mechanism that powers the rest of the campaign described later in this chapter. - -The orthodoxy’s argument is self-referential: *“open source means what we say it means, because we are the ones who define it.”* When the Reticulum License is measured against that definition and found wanting, the conclusion is presented as a property of Reticulum - when in fact it is *a property of the definition*. The definition is treated as neutral and authoritative - as some base fact of raw cosmology - and the license is treated as deviant. But the definition is no more neutral than the license is. It encodes a moral position, and that moral position is exactly the one that this project declines to take. - -Consider what the orthodoxy’s definition requires, in the case at hand. The OSI-based definition contains, in its ancestry, the Debian Free Software Guideline of “No discrimination against fields of endeavor”: The software must not restrict who may use it or for what purpose. Taken to its logical end, this means that open source, properly “certified”, must be equally available to those who build hospitals and those who build weapons, those who connect people and those who control them, those who create and those who ingest and replace. - -The orthodoxy presents this as neutrality. It is not neutrality; it is a moral position - one that says, in effect, that *harming human beings is a field of endeavor like any other, and software must not discriminate against it*. - -The Reticulum License takes a different position, and it has the honesty to state it in the license text: this software is for life, not for death; it is for people, not for the systems that would replace them. That is not a restriction on openness, nor on freedom. It is a restriction on *who may consume the work*, and it is a restriction the orthodoxy’s definition cannot express - which is why the orthodoxy must always attack the license as “not open source” rather than engage with what it actually does. - -### Who Is Restricted By Which License? - -The arithmetic is simple, so let’s do it out loud, because the orthodoxy’s framing inverts it every time. - -The GPL family imposes its conditions on **everyone**: You may not distribute, link, or - in the AGPL case - even *communicate with* GPL-covered software without your own work being pulled under the same terms. “Free” in this system is defined (self-referentially) *by the license’s own terms*; everything outside those terms is, by definition, unfree. The restriction applies to 100% of users. It applies to the hospital, the mesh community, the school, and the hobbyist with the same force it applies to anyone else. That is not freedom for everyone; it is a *specific moral architecture applied to everyone*, presented *as if it were the absence of one*. - -The Reticulum License imposes its conditions on a fraction of would-be users so small as to be practically unmeasurable: - -* Those whose intended use is to build systems that purposefully harm human beings, and -* Those whose intended use is to ingest the work into AI training pipelines. - -That is the entire list. Everyone else - the overwhelming majority, the community, the developers, the users, the downstream projects - receives the full, unrestricted rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, with none of the viral encumbrance of copyleft. - -So when the claim is made that the Reticulum License “is more restrictive” than the GPL, the claim is not merely wrong; it is the exact inverse of the truth. The GPL restricts the freedom of everyone, in the name of a specific ideology. - -The Reticulum License restricts the freedom of the harm-doer and the appropriator, and leaves everyone else free. The two approaches do not differ in whether they contain moral axioms - **all licenses do**. They differ in whether the axioms are stated openly, and in *whose* freedom they restrict. - -### The Consequences Of The “Not Open Source” Claim - -This would be a footnote debate if the mischaracterisation had no practical consequences. It has very large ones, and therefore this section exists. - -The claim that “Reticulum is not open source” is the foundation of the takeover narrative it so often accompanies. The sequence is always the same: - -* First, declare the reference implementation illegitimate (“not really open source”, “owned by one man”, “not a real community project”) -* Second, conclude that it is therefore acceptable - even necessary - to replace it. -* Third, present the replacement, which is (conveniently) often the speaker’s own machine-generated clone, as the legitimate heir. - -This is not speculation about motives; it is the documented structure of the campaign examined in [A Movement Of A Dozen](#brandolinis-movement-of-a-dozen). - -The “not open source” claim is the legal-ideological battering ram of that campaign. It is recycled, in article after article, in forum after forum, because it works on people who assume that a committee’s definition is the same thing as the truth. - -It is also worth stating the other obvious fact: None of these critics have ever shown that the Reticulum License restricts *them*. They do not build weapons. They are not training models on the codebase. They are free, in every practical sense, to use Reticulum exactly as they would under MIT or GPL. Their objection is not to a restriction on themselves; it is to the *mere existence* of a license that states a moral position they disagree with, or which they can use as leverage for a different goal entirely, and to the institutional fact that it does so outside *their* system of approval and control. The complaint is not that the license makes them unfree. The complaint is that the license is *free of them*. - -### The Distribution Argument - -A recurring companion claim is that RNS is not properly “distributed”, usually deployed as “it’s not packaged as a .deb” or “it’s not on package manager X”, with the implication that it is somehow unavailable or inaccessible, or hard to install on a given system. - -This is simply false, and it has been false for years. The reference implementation is available on practically every platform and architecture with a single command (`pip install rns`). It is packaged in the Arch User Repository, in NixOS, and in a range of other distribution channels, community-maintained and official. It can be installed, updated, and kept current *over Reticulum itself* - the network transports its own software updates. It is used on more than a hundred thousand devices worldwide, is embedded in over a hundred other packages and applications, and carries terabytes of traffic every day. - -The claim that Reticulum needs a `.deb` to be “really distributed” mistakes the mechanism for the goal. The goal is that people can obtain and update the software easily, everywhere, **on their own terms**, and distribute it themselves, both offline, online and over Reticulum with full cryptographic verification of the install packages, without any central infrastructure. - -That goal is met to an excellence that few software packages can speak of, and met in a way that does not pin the project’s release cadence to the review cycles of distribution maintainers, which is an achievement, not a bug. - -A project like RNS that updates continuously, sometimes daily, and has a track-record of shipping security updates mere hours after issues are discovered, would be crippled by packaging that makes every change wait months for approval. The distribution complaint is a strawman generated for the occasion. It has been answered, in public, many times; it is repeated anyway, because the people repeating it are not counting on facts - they are counting on the reader never having heard the answer. - -### On The Institutions, In Fairness - -None of the above is hostility toward the Open Source Initiative or the Free Software Foundation as institutions, nor toward the individuals who have dedicated decades of work to them. Their contribution to the world of software is **enormous**, and this document does not dispute it. What is disputed is a specific claim of authority: That “open source” is a term whose meaning is reserved to a committee’s approval, and that a license which does not submit itself to that approval is therefore not open. - -That claim of authority is not in Reticulum’s interest, and it is not in the interest of the people the movement claims to serve. The current legal and technological landscape - the machine-appropriation of the commons, the weaponisation of “prior work” generation, the erosion of human agency by synthetic systems - was not anticipated by the DFSG of 1997, and the institutions have not, as of this writing, produced *any* meaningful answer to it. The choice, as it was put at the time of the license change, is whether developers should sit back and wait for a solution from a committee that “may or may not ever materialize, before the entire foundation we stand on has been eaten away”. - -**Reticulum’s answer was to act**. If we had waited, it would have been too late. - -If an OSI-approved license existed that expressed the same constraints - and did not impose the far more severe restrictions of copyleft - the project would have used it. None such exists. That is not Reticulum’s failure. It is the measure of what the definition is missing. - -### What “Open” Means Positively - -For anyone who wants the positive statement rather than the rebuttal, Open Source, in this project’s understanding, means: - -* The source code is available, to everyone, without charge -* Everyone may use, copy, modify, merge, publish, distribute, sublicense, and sell the - software -* No one is discriminated against as a person - developer, user, community, or project -* The protocol itself is dedicated to the public domain, where no one can own it -* And the single, plainly stated condition of the license is that the work shall not be - consumed to harm human beings, or ingested to replace the humans who made it - -If that is not “open” by a definition written in the 1990s, then the definition is the thing -that has failed - not the software, and not the people who use it. - -**Open source means open source**. It was true before the committees existed to certify it, and it will remain true after they are gone. - -## Assistance Versus Machine-Substitution - -### False Dichotomy - -The public discussion around machine learning and software development has been derailed, by the loudest voices on both sides, into a farcical fight between “AI enthusiasts” and “AI haters”. - -> **Both positions are absolute caricatures.** - -> **The fight is beside the point in the ways that matters most.** - -It is not an accident, but a very conscious choice that the initial analysis of the Prns case examined herein, and this very chapter, were themself produced with the assistance of a machine-learning based generative system. - -Specifically, it was generated using `lc` managing a `Q8_K_XL` quant of `DeepSeek v4 Flash` running in the `llama.cpp` inference engine. The main context base used to stabilize, ground and inform the generative environment consisted of approximately 100,000 tokens of text material that I personally thought out and wrote (by hand), then archived and organized, over the last eight years. - -If you know my writing, and have a hard time discerning where this chapter is me directly, typing manually, and where it is the machine output, it is because of two things: Almost every line in this chapter was either directly edited by my hands on the keyboard or typed by me, and the text corpus that informed the process *is* genuinely me - my thoughts, written by me, over a period of years. - -The chapter was produced this way *deliberately and openly*, under a software framework I designed and wrote, with the human directing the inquiry points, flow of investigation, structure of the outputs, contents of every section, making the editorial decisions, reviewing the conclusions, painstakingly adjusting every line, and taking responsibility for the final claims presented, with the goal of reaching a higher level of synthesis and symbolic density than I could have achieved without that assistance. - -The tool was *used as tool*, both to structure and produce a complex chapter, and to inspect a repository so preposterous in size, commit velocity and nonsensical wording that it barred any purely human inquiry. - -As such, the position is hereby, by personal example and demonstration, stated clearly: - -* **Technological assistance is not the problem** - -* **It was never the problem** - -* **It is the opposite of the problem** - -The problems are *substitution* and *usurpation*: The perverse process in which the machine becomes the author of choices, and the human becomes the facade of software, while expecting “reciprocity” or recognition for deeds they never performed. - -### The Line - -The exact distinction can be reduced neither to how many lines of code a machine produced, nor who pressed the keys. The final distinction has to rest on **who, or what, the real agent of the work is**. - -* **Assistance** is when the human retains the full seat of agency: The human decides what to build, judges - what is correct, rejects what is wrong, understands what is shipped, and can answer for every single line of it. - The machine writes, suggests, drafts, accelerates *meaningfully* and within reason - but every choice that survives into the - final work is a choice the human made *consciously*, and understood - or grew to understand - the consequences of. - -* **Substitution** is when the machine *becomes* the agency: the machine decides what to build, - decides what is correct, iterates on its own errors, generates at a scale and speed no single - human can follow - and the human’s role is reduced to providing the name, the account, the - signature, and the marketing launch. - -> The Line can be stated as a single test, and this is the same test that has always separated authorship from couriership: **Can the person whose name is on the work answer for it fully?** Not “can they describe what it does” - can they account for why it is the way it is, line by line and decision by decision, and the history of how it came to be that way, under questioning, without machine assistance, including by people who know more than they do? - -If they cannot, they did not author the work. They wore it, or in more severe cases, they stole it. And a courier who claims authorship is not a writer; they are a fraud. - -### What assistance looks like - -Assisted work is recognisable, and recognisability is itself part of its value. It has the following observable properties: - -1. **The human can explain it.** Under examination, the author can say why this choice was - made, what was rejected, and what they verified. The ability to explain is not a luxury; - it is the very demonstration of authorship. On a large project with many contributors this is partial in - practice - no one can explain every line of a large codebase - but the *full capacity* must - exist between the authors and the *habit* must be visible. -2. **The human can, and does, reject machine output.** An author who has never rejected the machine’s output is not - an author, but an open drain. Rejection is not a stylistic flourish; it is the - mechanism by which any work attains Quality. A record of decisions, including reversals and rejections, - is a sign of agency. A record without rejection is a record of assent-by-default. -3. **The human’s judgment is directional.** The work contains the human’s actual choices; - things the machine would not have decided on its own, which the human can defend on their - merits. This directionality has many names in engineering: Design, architecture, taste, - judgment. Where it is absent, the work has the uniform, self-consistent texture of - something that was never doubted, and doubt is exactly what anything important requires. -4. **Provenance is not concealed.** Disclosure of how the work was made is not a legal - requirement in every jurisdiction; it is not even always a practical requirement. But it - is a *statement* that matters. A project that discloses its process - - including its use of machines - is telling the truth about the relationship between the - human and the work, and that is a property users *are* entitled to know. Concealment of - provenance is not a stylistic choice; it is deception, made by someone who knows what the - record would otherwise show. -5. **The human stands behind the failures.** When assisted work fails, the human can say: Yes, I - chose this, under these assumptions, here is why, here is what I got wrong, here is the fix. - That sentence is the entire contract between an author and the people who depend on their work. - A human who cannot say it - because the choices were never theirs - *has nothing to offer anyone*, and - is by definition entirely replaceable; anyone else could have pressed the same button. - -### What substitution looks like - -Substitution is equally recognisable, once the pattern is known: - -1. **Scope that no human could have authored.** Output that - structurally cannot have been the product of human authorship, review, and judgment, for - example when the volume exceeds any reasonable human capacity within the span of the project’s life. - When the review capacity and the output volume are incommensurable, the output was not - reviewed. It was generated, then forgotten, and the record was signed blindly. -2. **Uniform generation.** The work carries no trace of a human’s changing mind. No style - drift, no contested decisions, no “we tried X and it was wrong, so we did Y”. That is, *no - signs of actual development, growth and learning*. Instead, endless, self-referential - refactoring, renamed abstractions, and the same fluent register in every commit, - every document, every reply. A human’s history shows the wear of judgment. - Pure generation shows the superficial polish of its absence. -3. **Discovery by recursive regeneration.** When errors are found, they are found by the machine’s own - subsequent runs, not by human scrutiny - and the findings are recorded as planned - “reviews” and “alignments” rather than as the corrections they are. This is the tell: A - machine editing its own previous output, while the edit is recorded as if it were a - scheduled milestone. That’s not review. That’s a continuous generation loop revising itself, - with the attempt of an illusion-preserving voice-over. -4. **Laundered provenance.** Theatrical concealment machinery: Commit hooks that refuse - attribution of machine generation, contribution policies that declare machine output - “yours”, processes that scrub personal traces from the history, fake identities in the - contributor list. All of these have one function: To make the record say what the “author” - *wished* had happened, rather than what happened. A project that builds such machinery is - not making a record of its work; it is using automated software to manufacture one. -5. **Authorship by assignment.** The human’s name is on the work, but the human cannot - answer for it - because the human did not choose any of it, and knows it. The name is not a - signature of authorship; it is a *fraudulent claim of ownership* over something the claimant did not - make and cannot explain. In every field that has ever dealt with such claims - art, - literature, invention, journalism - *this has a name*. That **forgery** is now cheap in software - makes it no more forgivable. - -### Infrastructure Matters - -For a hobby project, the distinction between assistance and substitution is a personal matter. For infrastructure - code that other people’s communications, safety, and freedom run on - it is a matter of life and safety, and any statement about this must be entirely unapologetic. - -Infrastructure has one property that distinguishes it from almost all other software: While most of the people who depend on it cannot verify it themselves, **it must be verifiable, auditable and open to human inspection**, and be engineered in such a way that this is as frictionless and straight-forward as possible. - -The reference implementation of Reticulum was designed this way, and continues to make that goal a primary one. While it *is* a complex piece of software, with an intricate array of interlocking and moving parts, the core parts of RNS, the entire Reticulum protocol implementation, the transport logic, the cryptographic primitive interfaces, **is less than 15,000 lines of code**. That line count even includes the rather extensive comments, incredibly detailed logging facilities and other choices that makes the code easy to read and understand for humans, and easy to operate, verify and debug. It took real dedication, and *a lot* of hard work to achieve that. - -Exactly because not *everyone* can do this, it is a fundamental necessity for infrastructure software that it is designed and authored so that *as many as possible can*, and that the *chain of accountability* - who made it, who reviewed it, who answers for it - is held intact at every link. That chain is not decoration. It is the entire mechanism by which normal users decide to trust a system they cannot fully verify. The transparent foundations and the chain combined *is* the verification for most users. - -Substitution breaks the chain both at the foundations, and at its load-bearing link: The human at the top can no longer answer, because they were never the maker. Unfortunately, the chain does not always fail visibly; often it fails silently, at the exact moment a question goes deep, or when routing fails subtly at a critical moment. - -For the user, the result is not a project with a normal and humanly flawed author - it is a project *with no author at all*; a signature with no substance behind it, a name that cannot be held to account, and a body of work that nobody on earth can (or wants to) explain, defend, or fix in a crisis. - -### The Fluency Trap - -It is worth understanding, briefly, why substitution is so attractive and so dangerous at once. Machine-generated output is fluent. It is confident, well-structured, and terminologically *self-consistent*. At the surface, and from the perspective of someone outside a given craft, it is indistinguishable from the output of a knowledgeable human. Fluency of this kind can be a useful property: It makes assisted work fast. But fluency is *not* understanding. It is the *minimal conformal surface* of understanding. - -The danger is that the surface is exclusively where the substituter invests; for the simple reason that the surface is where most of the desired audience looks, and what yields attention to the project. - -A human who can tell the difference uses the tool well. A human who cannot is being deceived through means they cannot detect from inside, namely through their own inability to account for the limits imposed by what they do not know. - -This is why the most important quality for anyone working with systems like Reticulum is not speed, and not vocabulary, but the oldest engineering virtue there is: The honest, uncomfortable, terrorizing knowledge of **what you do not know**. The substitute *never* has it. The substitute’s operator, *if* they ever had it, loses it precisely the moment they accept the fantasy of the substitution process. - -The tool is not the hazard, plain old human nature is. The tool is a mirror, and mirrors - as has been observed for rather longer than software has existed - show the truth only to those willing and yearning to see their own shortcomings. - -### Disclosed Assistance Or Laundered Generation - -The contrast at the centre of this entire discussion can be made clearer with two examples, both of which are publicly accessible, both produced in the same year, by people using the same class of tools. - -The first is this chapter. It was produced with machine assistance, openly: the structure was directed by the human, the material was assembled with the tool’s help, every conclusion, every line, every slight nuance of meaning and semantics was examined, edited, and accepted or rejected by the human, and the process described publicly - including the precise characteristics of the technological assistance framework used. The human can answer for every claim in these documents, because the human made the decisions the claims rest on. - -The second is the project examined in [the Prns case](#brandolinis-prns). It was produced by a machine, at a volume no human could review, and the authorship record was engineered to erase the machine’s role: A commit hook that refuses attribution of machine generation, a contribution policy declaring machine output the human’s own, a fictitious contributor identity, 637,000 lines of unreviewed code. The human cannot answer for the code, because the choices were never made by a human - only the conformal surface of the presentation was. - -The same tool class. The same technology. Two diametrically opposed relationships to truth and responsibility. **The difference is not the tool**. The difference is the presence or absence of the human as the agent of the work - and the honesty or concealment with which that relationship is recorded. - -Assistance leaves the human as the agent and the commons as the source from which the human works and can grow. Substitution removes the human from the loop and converts the commons into fuel for the thing that seeks to replace them, or usurp their work for the profit of the few, human atrophying in the process. - -### The License In This Context - -The Reticulum License, examined in [What The Reticulum License Is](#brandolinis-what-the-license-is), draws the same line, at the level of the law. Its AI condition does not forbid the use of machine-learning technology to *assist* development - no reading of the license could support that, and no one associated with this project has ever claimed so. What the condition forbids is the *appropriation of the work into the machine itself*: The ingestion of the commons as raw material for synthetic replacement, and the concealed fabrication of “prior work” that would bury the real thing. - -As also noted in [Copyright & The Two Legal Layers](#brandolinis-copyright-and-layers), plain old copyright law already protects against the case where a process copies the reference implementation into a machine, produces a transformed derivative, and then claims “Copyright, [name of the process operator]”, with no human able to account for the creative decisions in the result. That is simple, petty theft, as it always has been. - -### The General Principle - -In these regards, the position of this project can be codensed to something relatively concise: - -* Machine assistance in human learning, development, and creation is accepted and useful. - It is not a shameful secret; but it *is* a tool that needs knowledge, training, understanding, - and great care to use. It should not make anything easier or more fun, it should make the - results better, more carefully considered, and arrive at *higher levels of synthesis*, not - produce endless streams of code churn. -* The line is crossed when the human stops being the agent of the work. When *any* output is - accepted that the human cannot explain, produced at a scale the human cannot review, or filed under a record - where the human has arranged to conceal the fact. -* The line is crossed, when a human claims authorship of work they cannot - answer for or has usurped. Credit follows the ability to answer. It has always worked that way, and - machines do not change the rule - they only make it cheaper and more tempting to break. -* The people who depend on infrastructure have the right to know which side of the line - the code they run was made on. That knowledge is the only verification most of - them will ever be able to perform. - -Use assistance, by all means, *and learn*, but don’t substitute. And never sign what you cannot answer for. - -Somewhere, within the general shape of the conceptual framework outlined in this section, or realistically, a more refined variant of it, I do believe there is the *potential* of the *beginning* of a constructive outcome to the immense challenge we are faced with as a species in regards to these problems. - -At the same time, I reserve my right to revert that position to Butlerian Jihad without prior notice. We are on a knife’s edge, and you know it just as well as I do. - -## Void Grants: Legal Foundations of Machine-Generated Code - -A grant of rights is only as real as the rights behind it. A license conveys what its issuer holds, and nothing more. A derivative may be distributed only under whatever license covers what it took. And over a work that a machine originated, no human holds anything at all. - -Each of these statements is a matter of settled statute and case law, and this section sets out the authorities. The conclusions they support are: - -1. A software implementation generated from a licensed work by a machine, published under its operator’s claimed copyright and license, **conveys rights the operator never possessed**. -2. **Its grant is void**. The projects, products, and promises built upon such grants stand on nothing. A field of void grants is a minefield for everyone who crosses it. - -This section is a general examination of copyright law as it applies to machine-generated code. It is not legal advice. - -### I. The Foundational Statute: Translation Is Copying - -The starting point is the statutory definition of the class of works at issue. Title 17, United States Code, section 101 provides the definition of a derivative work: - -> A “derivative work” is a work based upon one or more preexisting works, such as a *translation*, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or *any other form in which a work may be recast, transformed, or adapted*. - -Section 101 further provides that *“the terms ‘including’ and ‘such as’ are illustrative and not limitative”*. As such, the list is a list of *examples*, and the very first example is translation. The definition also reaches, in its own words, *“any other form in which a work may be recast, transformed, or adapted”*. - -The exclusive right at issue is defined in section 106: - -> Subject to sections 107 through 122, the owner of copyright under this title has the exclusive rights to do and to authorize any of the following: -> (1) to reproduce the copyrighted work in copies or phonorecords; -> (2) to prepare derivative works based upon the copyrighted work; -> (3) to distribute copies or phonorecords of the copyrighted work to the public by sale or other transfer of ownership, or by rental, lease, or lending; - -Violation of any of these rights is infringement. 17 U.S.C. § 501(a). - -Taken together, the provisions yield a construction that is difficult to improve upon and impossible to escape: - -1. **A port is a translation.** A work that takes a program written in one language and from it produces a program written in another language, implementing the same behavior, is a translation of the original - whatever mechanism performed it, and however the surface presentation differs. -2. **A translation is a derivative work.** Section 101 says so, explicitly. -3. **Preparing a derivative work is the copyright owner’s exclusive right.** Section 106(2) says so explicitly. A derivative work prepared without authorization, or outside the scope of any authorization, infringes that right. - -The common objection - “the code is in a different language, so it is completely different” - is not a legal argument; it is the definition of what the statute already classifies. - -Translation *is the paradigm* of derivative authorship, not an exception to it. If translating a work without permission was a defense, the derivative-work right would be meaningless; every infringing adaptation would simply need to be “different enough” to count as a translation of its source. - -Nor does the identity of the translator change the analysis. The statute asks whether the work is “based upon” a preexisting work. It does not ask who or what performed the translation. A machine that ingests a licensed work and reproduces a translated transformation of it has performed mechanical translation; the output is a derivative of the ingested work, and the law governing derivative works applies to it exactly as it applies to a translation made by hand. - -### II. Ideas Are Free, Expressions Protected - -The second foundation is the distinction between what a work *is* - its ideas, its processes, its systems - and the *expression* in which those ideas are embodied. The distinction is codified in section 102(b): - -> In no case does copyright protection for an original work of authorship extend to any idea, procedure, process, system, method of operation, concept, principle, or discovery, regardless of the form in which it is described, explained, illustrated, or embodied in such work. - -The principle is older than the statute, and its application to software is settled. An author may own the particular code in which a method is expressed; the method itself, the process, the algorithm, belongs to no one - because *“the sine qua non of copyright is originality”* (*Feist Publications, Inc. v. Rural Telephone Service Co.*, 499 U.S. 340, 345. 1991). And ideas are not original to anyone in particular, *Baker v. Selden*, 101 U.S. 99 (1879), established the line: Copyright in a book describing a system does not prevent others from practicing the system. - -Applied to a communications protocol, the structure is clean: - ---- - -**An implementation of a public-domain protocol, honestly reasoned and written by humans, is welcome and unencumbered, and owes nothing to any license. An implementation generated from a licensed implementation is a derivative of licensed work, subject to its conditions and to the ordinary copyright of its source.** - ---- - -The public-domain status of the protocol’s ideas changes nothing about the copyright in an implementation’s expression. A person who copies an implementation - by machine or by hand - cannot defend the copying by pointing to the protocol, because the protocol covers the ideas, not the symbols in which someone expressed them. - -A further misconception can be dismissed here; the recently popularized “clean-room” claim about the absence of copying. A [clean-room](#brandolinis-cleanroom-delusion) is a not a magic label achievable by LLM, but a specific, institutionalized procedure for proving to a court that copying did not occur: Physically separated teams, written functional specifications, attestable non-exposure, and no access to the original by the implementers (*Altai*, 982 F.2d at 709–11). - - *“I created a clean-room implementation by feeding the original source code into a machine”* attempts to *claim* the certified, legally established procedure for proving the absence of copying by describing, in the same breath, a process **composed entirely of copying**. The word is deployed because the procedure it names is what the claimant would *need to have happened*. - -### III. Machine Output Has No Author - -The third foundation is the human authorship requirement. It is settled at both levels of the federal courts and in the Copyright Office’s administering practice. - -In *Thaler v. Perlmutter*, the district court held that *“copyright law protects only works of human creation,”* and that *“human authorship is a bedrock requirement of copyright”* (687 F. Supp. 3d 140, 146, D.D.C. 2023). On appeal, the United States Court of Appeals for the District of Columbia Circuit affirmed, holding that the Copyright Act *“requires all eligible works to be authored in the first instance by a human being”* (No. 23-5233, D.C. Cir. Mar. 18, 2025). - -The requirement traces to the ordinary meaning of the statutory term. In *Burrow-Giles Lithographic Co. v. Sarony*, the Supreme Court defined an “author” as *“he to whom anything owes its origin; originator; maker; one who completes a work of science or literature”* (111 U.S. 53, 57–58, 1884), and more recently described the author as *“the person who translates an idea into a fixed, tangible expression entitled to copyright protection”* (*Community for Creative Non-Violence v. Reid*, 490 U.S. 730, 737, 1989). - ---- - -**A machine does not owe expression its origin; it reproduces it. No court has recognized copyright in material created by non-humans, and the courts that have addressed the question have rejected it. If you create by machine generation, you do not own the output.** - ---- - -The Copyright Office’s registration guidance applies the requirement to generative systems in terms that are directly on point. The Office asks: - -> whether the “work” is basically one of human authorship, with the computer [or other device] merely being an assisting instrument, or whether the traditional elements of authorship in the work (literary, artistic, or musical expression or elements of selection, arrangement, etc.) were actually conceived and executed not by man but by a machine. -* *Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence*, 88 Fed. Reg. 16190, 16192 (Mar. 16, 2023) - -And it states the consequence: *“If a work’s traditional elements of authorship were produced by a machine, the work lacks human authorship and the Office will not register it”* (*Id.*). - -On the role of prompts, the Office is equally explicit: when a system receives a prompt and produces a work in response, *“these prompts function more like instructions to a commissioned artist - they identify what the prompter wishes to have depicted, but the machine determines how those instructions are implemented in its output”* (*Id.*). - -The agency applied this standard and excluded machine-originated material from registration in the leading administrative case. In *Zarya of the Dawn* (Cancellation Decision, Feb. 21, 2023, Registration No. VAu001480196), the Office cancelled registration of images generated by the Midjourney service, holding that *“it was Midjourney-not [the author]-that originated the ‘traditional elements of authorship’ in the images”*, while preserving protection for the human-authored text and the human selection and arrangement of the work. - -The Copyright Office’s 2025 report on AI copyrightability reaches the same conclusion: - -> Based on the functioning of current generally available technology, prompts do not alone provide sufficient control. -* U.S. Copyright Office, *Copyright and Artificial Intelligence, Part 2: Copyrightability*, at iii (Jan. 29, 2025) - -The report’s analysis is explicit: *“The Office concludes that, given current generally available technology, prompts alone do not provide sufficient human control to make users of an AI system the authors of the output. Prompts essentially function as instructions that convey unprotectible ideas”* (*Id.* at 18). - -Its guiding distinction is stated plainly: *“The use of AI tools to assist rather than stand in for human creativity does not affect the availability of copyright protection for the output”* while *“Copyright does not extend to purely AI-generated material, or material where there is insufficient human control over the expressive elements”* (*Id.* at iii). - -The boundary is therefore not “AI or no AI”. It is *assistance or substitution*: Whether a human exercised sufficient creative control over the expressive elements and can answer for them, or whether the machine originated them and the human merely supplied instructions and a signature. That is a question of fact in each case, determined from the record of how the work was made. - -Two facts follow, and stated together, they form the base legal reality: - -1. **A work whose expressive elements were originated by a machine contains no valid new copyright: It has no human author. To the extent that the same work reproduces a preexisting copyrighted work without explicit license, it is at the same time an unauthorized derivative. It is unowned for anything new it contains, and unlicensed for everything taken.** -2. **Its operator therefore holds no copyright in it and no grant over it - and a person who holds neither copyright nor grant can convey neither.** - -The first fact makes the work **unownable** by its operator. The second makes it infringing against its source. Neither replaces the other: - -* The absence of copyright in machine output does not make the output free for anyone to take; it makes it unowned. -* This is a different thing entirely, especially when what it contains is someone else’s expression. - -### IV. License Conditions Bound the Grant - -The fourth foundation governs what a license *is*. A copyright owner may grant permission to use the work on conditions. The conditions define the scope of the permission. Use outside the scope is use that was never authorized. - -In *Jacobsen v. Katzer*, 535 F.3d 1373 (Fed. Cir. 2008), the Federal Circuit held that the conditions of an open-source license are *conditions of the copyright license itself*, not mere contractual promises, and that a licensee operating outside them is an infringer: - -> Copyright holders who engage in open source licensing have the right to control the modification and distribution of copyrighted material. - -> The choice to exact consideration in the form of compliance with the open source requirements of disclosure and explanation of changes, rather than as a dollar-denominated fee, is entitled to no less legal recognition. - -> It is outside the scope of the [license] to modify and distribute the copyrighted materials without copyright notices and a tracking of modifications from the original computer files. -* *Id.* at 1381–82 - -The doctrinal mechanism is settled: *“A copyright owner who grants a nonexclusive license to use his copyrighted material waives his right to sue the licensee for copyright infringement”* within the scope of the license. If, however *“the licensee acts outside the scope, the licensor can bring an action for copyright infringement”*: An act outside the granted scope **is infringement** (*Sun Microsystems, Inc. v. Microsoft Corp.*, 188 F.3d 1115, 1121, 9th Cir. 1999; *Graham v. James*, 144 F.3d 229, 236, 2d Cir. 1998; *S.O.S., Inc. v. Payday, Inc.*, 886 F.2d 1081, 1087, 9th Cir. 1989). - -Whether a given license term is a condition of the grant or a mere covenant is determined from the license’s own language; conditions that limit what may be copied, modified, and distributed - the attribution terms, the notice terms, the modification-tracking terms, the field-of-use terms - are the boundaries of the copyright grant (*MDY Industries, LLC v. Blizzard Entertainment, Inc.*, 629 F.3d 928, 940, 9th Cir. 2010). - -Three consequences follow for the analysis at hand. They are not hypothetical. - -**First, the conditions are measured at the moment of copying** - -A work is licensed under whatever conditions applied when the reproduction, adaptation, and distribution occurred. There is **no** period in which copying a licensed work is “free” because the copy was made by a machine; the machine is the *licensee’s tool*, and the licensee is bound by the conditions regardless. A derivative generated without satisfying the conditions - without the attribution, without authorization for the manner of use - was never within the scope of the license, from the first instant of its existence. - -**Second, breach does not restore rights that never attached** - -The district court in *Artifex Software, Inc. v. Hancom, Inc.*, No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017), analyzing the GPL’s termination provisions, observed that *“the language of the GPL suggests that [the licensee’s] obligations persisted beyond termination of its rights to propagate software using Ghostscript,”* because the source-code obligation recurs each time a covered work is conveyed. The same structure operates a fortiori where a derivative was produced *outside* the license entirely: No grant ever attached, and no subsequent event creates one. - -**Third, the settled conditions are the enforceable core** - -The attribution and notice conditions rest on the settled doctrine above. Novel conditions (restrictions on field of use, restrictions on how the work may be consumed) are enforceable as conditions of the grant when clearly drafted, but their enforcement history is thinner. The argument that a derivative is unlicensed does *not* depend on the novel conditions being vindicated; it needs **only** the settled ones, and the ordinary law of derivative works, to stand. - -### V. The Void Grant - -The preceding sections assemble into a single structure. Let us apply it to an example situation: - -* A project presented as an independent implementation of a protocol; “ground-up”, “from scratch” or “clean-room” work. -* The actual history shows continuous, machine-mediated ingestion of a licensed reference implementation. -* The output is generated at a scale no human could produce or review, with the provenance of the process obscured. - -**Step One: The derivative** - -The project’s output is a translation of the licensed implementation: A derivative work, produced by a copy process. 17 U.S.C. §§ 101, 106(2). Preparing it without authorization infringed from the first moment of its existence. *Id.* § 501(a). - -**Step Two: The license** - -The derivative was produced outside the conditions of the license; without the notices, without the attribution. The license never attached (*Jacobsen*, 535 F.3d at 1381–82). - -**Step Three: Authorship** - -The expression was originated by a machine. No human can account for its creative decisions, and no human authored it. It contains no valid new copyright (*Thaler*, 687 F. Supp. 3d at 146; No. 23-5233, D.C. Cir. 2025; 88 Fed. Reg. at 16192). - -**Consequence: Void Grant** - -The statute supplies the consequence. Section 103(a) provides: - -> The subject matter of copyright as specified by section 102 includes compilations and derivative works, but protection for a work employing preexisting material in which copyright subsists does not extend to any part of the work in which such material has been used unlawfully. - -And section 103(b) provides that copyright in a derivative work *“extends only to the material contributed by the author of such work.”* The legislative history states the point without ambiguity: *“An unauthorized translation of a novel could not be copyrighted at all”* (H.R. Rep. No. 94-1476, at 57, 1976). - -In our example, the operator of the derivative’s copy process decides to assert copyright over the output, and licenses it to the world under terms of the operator’s choosing, for example the Apache License. - -But the operator holds no copyright in machine-originated content (section III) and no license over the source from which it was derived (sections I and IV). - -For the derived material, the operator’s copyright claim **is an assertion of a right that never existed**. The material was used unlawfully, and section 103(a) withholds protection from *every* part of the work in which it appears. For the machine-originated remainder, the operator holds no copyright either, for lack of any human author. - -The operator’s license to the world is a promise the operator had no power to make. Every person who receives the work believing they received a valid grant has received **a void grant**, and stands, as a matter of settled law, in the position of a downstream user of an unauthorized derivative, whatever they were told at the time. - -This is a cascading consequence, and it is why the pattern is a hazard. No combination of self-granted licenses - “MIT,” “Apache,” “public domain,” anything at all - can transfer rights the operator never held. Every downstream project, product, or deployment built on the derivative is built on unlicensed reproduction of a third party’s work, whether or not it knew, whether or not it was told, and whether or not the marketing was plausible. - -The longer the derivative circulates unexamined, the more it becomes embedded, forked, vendored, depended upon, and the more expensive its correction becomes for everyone who trusted it. A field of superficially real projects, repositories, and licenses that **in reality confer nothing on anyone who relies on them is a minefield**, in the most precise sense the law allows: The rights they purport to grant *were never possessed*. - -The law defines what may be asserted, not what will be. The copyright owner may choose whether and how to assert these rights, and at what point in time. The exposure exists regardless, and for *every* downstream that trusted the void grant. This is the scope of the hazard that the conduct exemplified here produces. - -### VI. Fair Use Does Not Rescue This Pattern - -Every defendant raises fair use. It is an essential and important limitation in the service of free speech, accountability, criticism, education and other important domains. Therefore, it must be considered, and in the case exemplified here, it fails. - -Fair use is a limitation on the exclusive rights, evaluated case by case under four factors: The purpose and character of the use; the nature of the copyrighted work; the amount and substantiality of the portion used; and the effect of the use on the potential market for or value of the work (17 U.S.C. § 107). - -On the first factor, the governing standard is *Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith*, 598 U.S. 508 (2023), in which the Supreme Court held that: - -> In sum, the first fair use factor considers whether the use of a copyrighted work has a further purpose or different character, which is a matter of degree, and the degree of difference must be balanced against the commercial nature of the use - -The question is whether the use serves a further purpose, not whether the surface changed. A derivative that exists to serve as the same thing as the original, marketed to the people who would otherwise use the original, has not acquired a further purpose by being translated; the surface changed, the function did not, and the use is commercial. - -The remaining factors resolve the same way. Source code is expressive creative work, not bare fact. The pattern under examination copies the whole; a complete implementation, ingested and translated in its entirety. And the market effect is the decisive factor, because it is engineered: A derivative marketed as a drop-in replacement for the work it was copied from is not a complement to the original’s market, it is a substitute for it. The Supreme Court has long held that use of a work to satisfy the market demand for the original is the paradigm of an *unfair* use (*Harper & Row, Publishers, Inc. v. Nation Enterprises*, 471 U.S. 539, 566–67, 1985). - -The one case that is always raised in response, *Google LLC v. Oracle America, Inc.*, 593 U.S. 1 (2021), holds less than it is believed to. The Court there assumed *“for argument’s sake, that the material was copyrightable”* and held *“that the copying here at issue nonetheless constituted a fair use”* (*Id.* at 11). - -The material in question was a thin slice of *declaring code*, which the Court found sits, *“if copyrightable at all, further than are most computer programs (such as the implementing code) from the core of copyright”* (*id.*), in a use that built a new platform for a new environment, with the copier **having written the vast majority of its own code**. By comparison: A wholesale translation of an entire implementation, marketed as a substitute for it, is **not** that case; it is the other end of every factor. - -The valuable zones of fair use remain, and they are important. **Commentary and criticism**, quoting from a work to review, discuss or criticize it. **Teaching and scholarship**, explaining concepts and quoting excerpts, is its traditional beneficiary. - -### Conclusion - -The law is neither obscure nor new. The conclusion it yields can now be stated: - -* A machine translation of a protected implementation **is a derivative work**, and an unauthorized derivative **is an infringement**, whatever language it speaks and whatever machinery produced it. -* A work whose expression a machine originated has no human author, no valid new copyright, and without valid copyright **no license can be granted on it**. -* § 103 withholds protection from every part of a derivative in which protected material was used unlawfully. A license is a grant with conditions, and a derivative made outside the conditions **never received a grant**. -* A person who holds no copyright and no grant **can convey neither**, and *every* downstream consumer who received from them received a **void grant**. - -This is a *catastrophic* failure state for all who want to create things good and useful: The projects that inherit only void grants and liability believing they have inherited foundations, the users who depend on code that no one on earth can answer for, the honest builders whose work is buried under a field of plausible-looking impostors. - -### Sources and Authorities - -Statutory references are to Title 17, United States Code. Case citations are to the official or generally cited reporters. Online links are for reference only. Section numbers refer to the sections in which each authority is principally relied upon. - -**Statutes** - -* [17 U.S.C. § 101 (definitions: “derivative work,” “including,” “publication”) - I, II](https://www.law.cornell.edu/uscode/text/17/101) -* [17 U.S.C. § 102(b) (idea/expression) - II](https://www.law.cornell.edu/uscode/text/17/102) -* [17 U.S.C. § 103(a)–(b) (derivative works employing unlawfully used preexisting material) - V](https://www.law.cornell.edu/uscode/text/17/103) -* [17 U.S.C. § 106(1)–(3) (exclusive rights: reproduction, derivative works, distribution) - I](https://www.law.cornell.edu/uscode/text/17/106) -* [17 U.S.C. § 107 (fair use) - VI](https://www.law.cornell.edu/uscode/text/17/107) -* [17 U.S.C. § 501(a) (infringement) - I, V](https://www.law.cornell.edu/uscode/text/17/501) - -**Judicial decisions** - -* [\*Jacobsen v. Katzer\*, 535 F.3d 1373 (Fed. Cir. 2008) - IV, V](https://www.casemine.com/judgement/us/5914b290add7b04934760a54) -* [\*Sun Microsystems, Inc. v. Microsoft Corp.\*, 188 F.3d 1115 (9th Cir. 1999) - IV](https://law.justia.com/cases/federal/appellate-courts/F3/188/1115/629501/) -* [\*Graham v. James\*, 144 F.3d 229 (2d Cir. 1998) - IV](https://caselaw.findlaw.com/court/us-2nd-circuit/1011522.html) -* [\*S.O.S., Inc. v. Payday, Inc.\*, 886 F.2d 1081 (9th Cir. 1989) - IV](https://law.justia.com/cases/federal/appellate-courts/F2/886/1081/19091/) -* [\*MDY Industries, LLC v. Blizzard Entertainment, Inc.\*, 629 F.3d 928 (9th Cir. 2010) - IV](https://law.justia.com/cases/federal/appellate-courts/ca9/09-15932/09-15932-2011-02-25.html) -* [\*Artifex Software, Inc. v. Hancom, Inc.\*, No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017) (settled; dismissed with prejudice Jan. 2018) - IV](https://storage.courtlistener.com/recap/gov.uscourts.cand.305835/gov.uscourts.cand.305835.54.0_1.pdf) -* [\*Thaler v. Perlmutter\*, 687 F. Supp. 3d 140 (D.D.C. 2023), aff’d, No. 23-5233 (D.C. Cir. Mar. 18, 2025) - III, V](https://www.bitlaw.com/source/cases/copyright/Thaler-Perlmutter-Dct.html) -* [\*Burrow-Giles Lithographic Co. v. Sarony\*, 111 U.S. 53 (1884) - III](https://supreme.justia.com/cases/federal/us/111/53/) -* [\*Community for Creative Non-Violence v. Reid\*, 490 U.S. 730 (1989) - III](https://supreme.justia.com/cases/federal/us/490/730/) -* [\*Feist Publications, Inc. v. Rural Telephone Service Co.\*, 499 U.S. 340 (1991) - II](https://supreme.justia.com/cases/federal/us/499/340/) -* [\*Baker v. Selden\*, 101 U.S. 99 (1879) - II](https://supreme.justia.com/cases/federal/us/101/99/) -* [\*Computer Associates International, Inc. v. Altai, Inc.\*, 982 F.2d 693 (2d Cir. 1992) - II](https://law.justia.com/cases/federal/appellate-courts/F2/982/693/137252/) -* [\*Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith\*, 598 U.S. 508 (2023) - VI](https://www.supremecourt.gov/opinions/22pdf/21-869_87ad.pdf) -* [\*Google LLC v. Oracle America, Inc.\*, 593 U.S. 1 (2021) - VI](https://supreme.justia.com/cases/federal/us/593/18-956/) -* [\*Harper & Row, Publishers, Inc. v. Nation Enterprises\*, 471 U.S. 539 (1985) - VI](https://supreme.justia.com/cases/federal/us/471/539/) - -**Administrative and policy authorities** - -* [\*Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence\*, 88 Fed. Reg. 16190 (Mar. 16, 2023) - III](https://www.federalregister.gov/documents/2023/03/16/2023-05321/copyright-registration-guidance-works-containing-material-generated-by-artificial-intelligence) -* [\*Zarya of the Dawn\*, U.S. Copyright Office (Cancellation Decision, Feb. 21, 2023), Registration No. VAu001480196 - III](https://www.copyright.gov/docs/zarya-of-the-dawn.pdf) -* [U.S. Copyright Office, \*Copyright and Artificial Intelligence, Part 2: Copyrightability\* (Jan. 29, 2025) - III](https://www.copyright.gov/ai/Copyright-and-Artificial-Intelligence-Part-2-Copyrightability-Report.pdf) -* [H.R. Rep. No. 94-1476 (1976) - V](https://law.resource.org/pub/us/works/aba/ibr/H.Rep.94-1476.pdf) - -## Evaluating a Reticulum Implementation - -### The Intention Of This Guide - -Very few user of network infrastructure can verify the software they run. This is true of the reference implementation as well, and it is precisely why it was engineered the way it was: the entire Reticulum protocol implementation - the transport logic, the cryptographic interfaces, the routing - is less than 15,000 lines of code, including its extensive comments and detailed logging. The reference is *designed* to be readable by a human in a reasonable sitting. That is a deliberate decision, and it is the standard against which every other implementation should be measured: An implementation that asks for trust while being structurally unreadable is asking for the absurd. - -Because most users cannot verify the code directly, the evaluation of the *project* is the evaluation of the software. The framework below exists so that this evaluation can be performed in minutes, by anyone, without expertise - and so that each new “implementation” does not require the kind of full investigation that would otherwise be if not impossible, prohibitevly expensive. - -### Two Questions That Resolve Most Cases - -Other considerations in this guide is further corroboration. These two questions allow you to decide the case quickly in most instances, and they should be asked first. - -**Question 1: Does this speak the protocol, or was it just generated from the reference?** - -The copyright and license realities, described in [What The Reticulum License Is](#brandolinis-what-the-license-is), is the legal foundation of this distinction. The protocol is public domain: A *genuine* [clean-room](#brandolinis-cleanroom-delusion) implementation, reasoned out and written by humans, is free and welcome, and owes nothing to any license. But an implementation that was *directly generated from* the reference - reproduced by a copy process, whether manually or through a machine or software - is a derivative of the licensed work, and is subject to both the license’s conditions, and to the ordinary copyright law examined in [Copyright & The Two Legal Layers](#brandolinis-copyright-and-layers). A primarily machine-generated copy without substantial human creative input is not merely unlicensed; it is a copyright violation that would exist under any license at all. - -**Ask**: Does the project say where its protocol knowledge came from? Does it credit the reference implementation, or claim “ground-up” status while its own history shows continuous ingestion of the reference’s source? Does its license carry the original copyright notice? The answers tell you, within minutes, which of two very different kinds of project you are dealing with. - -**Question 2: Who, or what, is the agent of the work?** - -This distinction, established in [Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution), is the trust foundation of everything else. Is there a human who decided what to build, judged what was correct, rejected what was wrong, understands what was shipped - and can answer for it, line by line and decision by decision, *without machine assistance*? Or is the machine the agent, with the human’s role reduced to the name, the account, and the signature? - -> This test has a sharper form: **would the project be meaningfully different if someone else had pressed the button?** If the answer is no - if any person could have been the signature and nothing would change - then the “author” is not the agent of the work. They are its facade, and the project is, in the most literal sense, worthless and unowned. - -A project with no owner is a project no one can answer for, and infrastructure with no one to answer for it has no place carrying other people’s traffic. - -### Other Signals To Look For - -Once the first two questions are answered, the following signals determine the verdict. None is alone decisive; together they form a pattern that is difficult to fake in the directions that matter. - -#### Provenance and attribution - -* **Credit to the reference.** Does the project name Reticulum’s reference implementation - in its README, its website, its licenses, and the code? Does it present itself as a port - or as a “ground-up” origin story? Absence of credit, in a field where every implementation - owes its existence to the reference, is not an oversight; it is a decision. - -* **License compliance.** For a derivative, the license conditions apply: the copyright - notice and permission notice in all copies and substantial portions, the harm condition, - the AI-training condition. A project whose license file claims a fresh copyright in the - operator’s name, over work that was generated from the reference, has failed the minimum - bar of honesty before any code is read. - -* **Disclosed provenance.** Is the use of machine assistance stated openly, or does the - project maintain machinery to conceal it - commit hooks that refuse attribution of - generation, contribution policies that declare machine output the human’s own, scrubbers - for personal traces, fictitious contributor identities? The first is the mark of a human - using a tool. The second is the mark of a record being manufactured. - -* **The author’s history.** Is there a prior record - projects, contributions, a body of - work that a person can look at and reasonably assess? A sudden, fully-formed “decade of experience” - appearing from nowhere, attached to a project of unprecedented scope, is a story that - contradicts itself. - -#### Agency and answerability - -* **Who answers the questions?** When technical questions are asked, does a human answer - with substance - going deeper, explaining trade-offs, admitting uncertainty - or do - answers deflect to process (“the CI verifies it”, “the proofs prove it”, “the audit - covered it”)? Are the answers themself machine generated? Deflection to process is - the substitute’s answer pattern: It names the machinery instead of the reasoning. - -* **Velocity commensurate with authorship.** Is the output volume compatible with human - authorship, review, and judgment? An implementation ten (or 42!) times the size of the reference, - produced in a fraction of the time by a single person, is not evidence of brilliance; it - is evidence of unchecked generation. Churn is part of the same signal: A project that - deletes half of everything it adds is not building; it is looping. - -* **Signs of development, growth and learning.** Do reversals exist? Contested decisions? - “We tried X and it was wrong, so we did Y”? Humans change their minds, admit mistakes, and - leave the wear of judgment in their history. Pure generation shows “the polish of its - absence”: uniform register, endless renaming, and errors discovered by the generator’s own - later runs, recorded as if they were planned milestones. - -* **Standing behind failures.** When something is wrong, does the human say “I chose this, - here is what I got wrong, here is the fix”? Or do corrections arrive as process language, - with nobody accepting that a decision was even made? A project whose history contains no - first-person acceptance of error contains no author. - -#### Engineering reality - -* **Honest benchmarks, or none.** If performance is claimed, is the baseline real - stock, - current, interpreted RNS, on the modern interface, at equal policy, with methodology - published in advance and reproducible by anyone? - -* **Interop with reality.** Is compatibility verified against the *current* reference - implementation - on real hardware, including the radio and serial interfaces this - protocol family lives on - in both directions, over long transfers, uptimes and edge cases? - -* **The readability standard.** Can a competent human read the core of this implementation - in a reasonable sitting, the way the 15,000-line reference can be read? Was human - readability a stated design goal, or an afterthought? A project that cannot be inspected - is a project asking for trust it has not earned and will never attend to. - -* **Nonstandard inventions.** Does the project “improve” on the protocol with its own - modes - new channel behaviour on shared spectrum, new rendezvous mechanisms, new crypto, - “post-quantum” replacements, “turbo” variants? Inventions that change behaviour on a - shared network are not features; they are forks of the airwaves, imposed on every other - node that shares the medium. A project that invents on top of a protocol it has not yet - correctly implemented is not advanced; it is dangerous to its neighbours. - -* **The public record.** The commit history is public. Read it. - -#### Network citizenship - -* **Honest presence.** Does the project identify itself honestly on the network, or does - it masquerade as something it is not? - -* **Coexistence.** Does it respect the shared medium and network conventions? A node that - is a bad neighbour on the spectrum is a hazard regardless of how well its code works in isolation. - -* **Community participation.** Does the author participate in the community as a member - - answering questions, contributing, learning - or only appear to advertise? The second is - not participation; it is exploiting the community as a broadcasting target. - -#### Monetary and influence signals - -* **Money under the claim.** Donations, funding, or commercial offers premised on claims - like “89× faster” or “formally proven” convert false marketing into a different category - of problem. When money changes hands on the strength of fabricated measurements, the - stakes are no longer theoretical. - -* **Tokens and “investment” vehicles.** Any implementation that surrounds itself with a - crypto token, a “coin”, or an investment pitch should be treated as what it is: A project - whose primary purpose is to earn the controller a profit, not open networking. - -* **Marketing-first ordering.** A polished website on day three, a twelve-language marketing - presence, a “benchmarks” page and a “get started” funnel existing before the protocol works - is a massive red flag. Real builders ship, then talk. The inverse order is the signature of - a project whose goal is attention. - -### The Conformal Surface - -One warning before the synthesis. When you evaluate any project, assume - as a working hypothesis - that its surface is an investment. The marketing, the design, the badge collection, the “proof records”, the fluent and confident prose: This is exactly where substitution invests, for the simple reason that it is where the audience looks first. Fluency is the *minimal conformal surface* of understanding, and the surface is not evidence of anything except that the author *wanted to look convincing*. If you find yourself impressed by the surface before you have asked Question 1 and Question 2, you are doing the evaluation in the order the substitute operator hoped you would. - -### Possible Verdicts - -The framework proposed here can yield three different verdicts: - -1. **Meets the standard.** A named human who can answer for the work and demonstrably does; - provenance disclosed; attribution given; honest benchmarks; interop - demonstrated against current, real RNS on real media; readable code; a maintenance - history that shows growth and learning. This standard is not high. It is what the - reference implementation has met, in public, for over a decade, and it is what any - project that wants other people’s traffic should be willing to meet in turn. -2. **Experimental, but honest.** A project that clearly labels itself as in-progress and - experimental (“do not use this yet”, in plain words, as the first thing a user sees) - discloses its provenance, credits the reference, and is being developed in the open by - humans who answer questions. Such projects should be encouraged. They are how the - ecosystem grows, new ideas are tested, and the framework above is designed to make an - honest experimental project *easy* to recognize, not hard. -3. **Does not meet the standard.** Unanswerable authorship, concealed provenance, fabricated - measurements, invented protocol behaviour, absent interop, marketing ahead of substance. - Such projects should be treated as unproven regardless of their surface, and as hazards - if they invite users onto shared networks. The community owes them exactly the - skepticism they have earned, and not a moment of engagement beyond that; the network’s - time is better spent elsewhere. - -### These Standards Are Reasonable - -I’ll close this guide by stating what these standards are *not*: They are neither a demand for perfection, nor a ban on machine assistance. Assistance is useful, when it is used to arrive at *higher levels of synthesis* than unaided work. It is not a requirement that an implementation be a clone of the reference; neither is it a requirement that it invents everything from scratch or aspires to be a [“clean-room”](#brandolinis-cleanroom-delusion) implementation. Good implementations of the protocol are very welcome, *however they come*, but working on one requires *serious* work, considerable skill, and **deep competence**. - -The reference implementation has met this standard for years, and its users know what that looks like. - -## The Movement Of A Dozen - -### Presentation, Elongated - -Whenever Reticulum defends itself, its license, or its community - or simply continues to exist - a familiar chorus appears. The register is always similar: *concern*. Concern about the direction of the project. Concern about its license. Concern about its governance. Concern about its “bullying” of “good-faith contributors”. Concern about the future of the ecosystem at the hands of a single maintainer. The chorus speaks in the plural, presents itself as a body of independent voices, and insists, with some frequency, that it is… *not alone*. - -The implicit claim under examination in this section is that there exists a *movement* of concerned individuals, spread across the ecosystem, all worried about Reticulum, and therefore (for some reason) deserving of a place at the table of decisions. - -A resolution of this claim requires one step: Count them. - -### Headcount - -This “movement” does have a headcount. It is not in the hundreds, and it is not in the dozens. It is a small number of people who appear, in various combinations and under various names, in the following roles: - -* Creators of the machine-generated “Reticulum implementations” examined in this chapter. -* Creators of articles praising those implementations and lamenting the state of the - reference, its moral shortcomings, its unavailability or lack of something it already has. -* Commenters “defending” the implementations when they are evaluated reasonably and critically, - most often by attempting simply to derail the discussion. -* Critics of the Reticulum License on “open source” grounds. -* “Concerned community members” objecting to the maintainer’s conduct. -* Campaigners on online platforms; and -* In some cases, a single individual performing *several* of these roles at once, under - different names, in the same week. - -The overlap is well documented at this point, across time and different platforms. The same phrasing, the same arguments, the same links, the same reactive reflexes, the same handful of accounts appearing in the same threads at the same moments. When the sockpuppets are unmasked (and they frequently unmask themselves) the cast does not grow, but rapidly shrinks. - -This is the most interesting single fact about the “movement”: **It isn’t one. It is a synthetic cast**, held together mostly by LLM-glue, reddit handles and hysteria. A cast is not a constituency. It performs; it does not represent. The difference matters quite a bit, because a great deal of this “movement’s” imagined power comes from the assumption that the *plural form* is actual evidence of plurality. If that sentence seemed opaque, notice how many times a statement or article uses “we” instead of “I”, and it will be less so. - -### Recycled Wine, Fermenting - -The cast is not only small, it is also, by any evidentiary standard, *forgetful*. Every major claim this “movement” advances has been advanced before, answered in public, with evidence, and often years ago - and is then advanced again, unchanged and completely ignoring any rationale or evidence presented previously (to the same people), as if the answer had never been written. - -* **The license argument.** Reticulum is “not really open source” because its license is not - approved by a particular orthodoxy. This is answered (now in a new and updated version, for the - fifth time) in full in [Open Source Means Open Source](#brandolinis-opensource). It was also - answered in public, at length, at the time of the license change - including the - arithmetic of who is actually restricted by which license. It is repeated *ad infinitum* anyway. - Quite a conversation starter, apparently. - -* **The distribution argument.** Reticulum is “not properly distributed” because it is not - packaged as a `.deb` or on some favoured catalogue. Reality: It is available on practically every - platform with a single command, packaged by the community in a wide range of channels, and - capable of updating itself over its own network. The argument has been answered; it is repeated. - -* **The “stepped back” narrative.** When the primary mirror of the project moved away from - a public internet platform, it was spun as the founder leaving for good, or now taking on dictatorial - powers, or abandoning his creation, or any variation or nonsensical combination thereof. - Those spins always center around justifying that *a vaccuum now needs to be filled*. - There was no such vaccuum. The narrative is repeated in various forms whenever convenient. - -* **The enclosure narrative.** The license is reframed as a “power grab” or an “enclosure” - of the commons - as if a license whose protocol is public domain and whose conditions - restrict only harm and appropriation were a form of enclosure. The [most recent](https://gaggl.com/blogs/2026-09-05-enclosure-by-good-intentions/) (and mostly incoherent) - expression of this narrative is, once again, fluent, polite, and factually hollow. The - bizarre arguments supporting the “article” include: Defunct and abandoned forks presented - as living, community forks bearing the very Reticulum License mislabeled as AGPL license vehicles - (created by “concerned users”), companion app forks correctly retaining the Creative Commons - licensing used as further evidence of the same and completely abandoned projects propped up - as being “the ecosystem”. Another case of LLM generated nonsense that the “author” didn’t bother checking. - The intent is clear here, simply produce more *mass* of the bullshit, so it can be further - referenced, regurgitated and linked to. - -* **The “bullying” narrative.** Whenever the community declines to treat manufactured - controversies as legitimate, or calmly documents the falsity of claims, or (Lord forbid) - moves a forum post for violating clearly stated forum rules, - the response is the same: The documentation is “bullying”, the community is “excluding” people, the - maintainer is “unprofessional”. In the most recent example, those accusations were made by someone who, in the - same week, publically called for the Reticulum maintainer’s “removal from the equation”, forged a moderator’s - username, attempted to drag unrelated individuals into the campaign, and presented a new - round of outright falsehoods. Contrary to popular belief, the accusation of incivility - is not the last resort of the wronged; it is the standard reflex of the exposed. - -None of the “arguments” engages the answers given repeatedly, and extensively. That is the point, and the strategy. The arguments are not produced for the purpose of being engaged; they are produced for the purpose of *being repeated* - the same wine, poured into new bottles, on a cycle. Each cycle is short, cheap to produce, and (thanks to freely available fluent text generation) now nearly effortless. The intended audience is not the community, which has heard it all before. The intended audience is the newcomer, who has not. - -### The Method of Madness - -The method behind the pattern is consistent and simple enough to be described precisely: - -**Bullshit asymmetry** - -Producing a fluent, plausibly-sourced smear is now the work of minutes. Refuting it to the standard this manual demands is the work of days, and the refuted smear is replaced by the next one within the week. This asymmetry isn’t just a fine point of the campaign; **it is the campaign**. The goal is exhaustion - the slow conversion of the community’s time and attention from building to defending. - -**Weaponized concern** - -The register of the campaign is almost always the register of solicitude: “we are worried about the ecosystem”, “we just want what is best for the community”, “this is about the future of the project”. The concern is directed, without exception, at the things the campaign wants changed, and the change it wants is always the same: **The replacement of the reference’s authority with its own**. Concern is the *costume*; capture is the choreography. It is the oldest form of cheap power grab in the book. - -**Event seizure** - -Any event - a license change, a platform move, a forum moderation decision, a subreddit closure, a security notice, even the most banal messages from maintainers or known contributors - are seized, stripped of their actual context, and re-narrated as evidence of the same pre-written story. The subreddit that was closed after its moderator vanished and ceased moderating (according to *prior agreement with said moderator*) becomes “the maintainer shutting down dissent”. Moving away from a highly centralized online platform becomes “the founder retreating”. The license becomes “the enclosure”. The pattern is so reliable that the story could be pre-written by a lightly mechanized quill; only the date changes. The actual real-world context of these events is documented, in public, in each case; the re-narration proceeds regardless. - -**The legitimacy circuit** - -The campaign invests heavily in the places where legitimacy is minted: Conference talks, blog platforms, social threads, and forums - presented as if these appearances constituted some sort of ecosystem-wide groundswell rather than the same dozen people moving through the same circuit. The LLM-generated articles cite the LLM-generated ports; the ports are praised in the articles; the managers of the ports spread the articles, and echo them on the forums and internet platforms; the conference presentations cite the forums. Random bypassers with the need for a cause watch the conference presentations and take on the holy duty of Internet Soldier. The circuit is closed, self-referential, and (to a reader encountering it fresh) convincing. It is also, entirely, **a loop of bullshit**. - -**The prior-work minefield** - -The machine-generated implementations have a consequence beyond attention: They flood the space with fake “prior work”. The effect, as the license discussion predicted in the beginning of 2025, is the creation of a minefield of sloppy repositories that can later be matched against any genuine project, and attempt to claim prior art, or even worse: Dissuade actual, honest developers or builders from taking up a real project they might have succeeded with, because they now believe it has already been done. Just as importantly, it also buries real efforts under machine-generated noise. The machine-ports are not failed attempts to contribute. They are (disregarding intentions, but effectively) the campaign’s long game, deployed as inventory. - -### A Persistent, But Fragile Triad - -This chapter as a whole has documented three fronts: The legal-ideological attack on the license ([Open Source Means Open Source](#brandolinis-opensource)), the substitution and usurpation pattern in machine-generated implementations ([Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution)), and the takeover narratives examined in this section. They are three fronts of the same operation, run by the same cast. - -* The license attack and other urgency-engineered narratives are the *cover*: They supply - the ideological justification for treating the reference as illegitimate, and needing replacement. -* The ports are the *inventory*: they supply the “replacements” that the narrative claims - the ecosystem needs. -* The takeover narrative is the *objective function*: the repeated, persistent, escalating claim that - the reference should be replaced, and that the speakers themself are the natural heirs. - -Each front depends on the others, and all three depend on a single, fragile assumption: That their audience cannot count. Restore the count, and the charade collapses into what it is: - -**A small group of people, obsessively orbiting a technology they did not build, synthetically producing the -appearance of a constituency they do not have, in service of a takeover they cannot justify.** - -### What The Pattern Is Not - -It is important, to also say what this pattern is *not*, exactly because the campaign depends on being mistaken for such things. - -**It is not criticism** - -The Reticulum community has always had lively, rigorous criticism - of decisions, of priorities, of direction - and it has always engaged that criticism on the merits, and changed course and development priorities many times. The license discussions at the time of the change were extensive, public, and answered in full, including with the critics themselves. Core protocol decisions were always discussed and evaluated in the community. The complex coordination and execution of several protocol upgrades between a loosely organised group of developers, node operators and users have succeeded surprisingly fluidly. All of these are success stories that the “critics” **always** ignore. Real criticism engages the content and accepts evidence. The campaign engages nothing. - -**It is not a community** - -Communities *build things*, maintain them, and have skin in the game; long-term users, operators of nodes, authors of real applications, people whose traffic flows through the network every day. A community has a stake. The “movement” only has an agenda. - -**It is not a debate** - -A debate has parties who evaluate arguments and evidence, and as a function of that, the debate moves forward, perpetually. The campaign’s arguments, as shown above, are not designed to survive contact with answers, but to be repeated past them. You cannot debate a loop. You can decline to perform for it, though. - -### Intent & Consequence - -While it would provide for interesting speculation, we do not need to know what the individuals in the cast *intend*, and this document will not speculate about their inner lives. People may believe their own narratives entirely, or partly, or not at all; **the pattern is the same in any case**, and the pattern is what matters here, since it is the exact thing that has been so insidiously destructive and resource wasting for the actual Reticulum project and community, ever since Reticulum was more widely discovered, popularized, and became a prime target for all sorts of grift known to man. - -The consequences are very real: - -* **Time**. The community’s scarcest resource is the attention of skilled people who actually build - and maintain the network. The campaign converts that attention into reactive defence, on a cycle - with no end. - -* **Harm**. Hastily machine-generated implementations, marketed as drop-in replacements and carrying - traffic onto shared networks, mislead users and can damage the network and its neighbours. - We, the actual reticulum community, all rely on these networks every day, and it is my belief - that we need to defend them. The fabricated measurements and laundered authorship examined in this chapter are not - editorial embellishments; they are the mechanism by which real users are induced to run - unverified, and often destructive software. - -* **Erosion of the currency of trust**. Every manufactured scandal, every fluently polite smear, every - fabricated “movement” debases the currency of trust and degrades it for **everyone**; - including, most of all, the honest projects that come after. This is deep harm, and - it is the primary reason this chapter exists. - -Whatever the intent - profit, control, attention, or a simple, obsessive inability to stop - the mechanism is the same, and the response should be *calibrated to the mechanism as a whole*, not to the individual person acting as the current avatar for it. - -### Recognition & Response - -For anyone who encounters the pattern fresh, here is a proposal on how to recognise it, and what to do about it. - -**Recognition:** The pattern is recognisable in minutes, once the signs are known: - -* Claims that have been answered before, repeated as if never answered. -* The register of solemn concern applied, without exception, in one direction. -* A severe allergic reaction to answering any questions posed. -* A cast that is small, overlapping, and frequently anonymous, with the *plural form* doing the - work of plurality. -* Sockpuppet theater: The same person or footsoldiers thereof appearing as several “supporters” - (and occasionally slipping up mid-performance). -* Articles, ports, and forum threads that cite and reference each other in a closed loop. -* The immediate, reflexive turn to “bullying”, “exclusion” and “toxicity” whenever a claim is refuted. -* A complete absence of engagement with the substance of the answers given, and a complete - absence of stake in the network itself. - -**Response:** A constructive response is one that is proportionate, and does not cost the -community its resources, sanity and dignity: - -* Engage the content once, on its merits (if any), with evidence, in public, and then refer to the - existing records. The answers already exist; they do not need to be re-derived for every new - bottle of piss-sour burgundy served from a man behind the curtains. I will do my best to keep this - chapter up to date with such answers and records, for easy reference, but other great resources - exist. -* Do not become part of the theater performance. The “movement” feeds on engagement in *any* form; - attention is its fuel, and outrage is as good as endorsement for them, if not better. -* Do the headcount. When the audience is reminded that a “movement” is a dozen people - recycling answered falsehoods, its projected power dissipates. The power was never real; - only the audience’s own assumption of scale can make the illusion effective. A polished-looking - “ground up” implementation might look like the work of many people, but is most likely - just the result of an LLM looping on a laptop for a couple of months (this is weird, but it is - the reality we now live in). Four different users suddenly singing in unison may look like the - representatives of a broader sentiment, but is most likely the same person hiding behind the same keyboard. -* Then, spend the saved time on the networks: On building real infrastructure, putting up nodes - and antennas, on real development, real maintenance, real coexistence, and on the people - *who actually use Reticulum*. The things that we can only do *together*, as humans. That is - what the fake “movement” cannot do, no matter how fluent and smooth its productions become. - -## Network Health & Coexistence - -### Substance Of The Networks - -Before examining health, we should consider what is being protected. The networks are not a repository. They are not a website, a social presence, or a set of marketing pages. They are a habitat: A living commons of communication, carried by, from a catious estimate, an intermesh now consisting of over a hundred thousand physical devices, embedded in a plethora of applications and packages, transporting terabytes of traffic every single day - real messages, real coordination, real people - across radios, serial lines, phones, laptops, and backbone servers, in cities, places and ways that no other network reaches. - -The protocol belongs to humanity; no one owns it. The network belongs to everyone on it, and its membership is defined by nothing other than participation: A node on a mast, a phone in a pocket, a server routing for its neighbourhood, a developer building on the commons. The network has no gatekeepers because it needs none; it is held together by the same physics and mathematics everywhere, and by a shared expectation of how members behave. That expectation - unwritten, unenforced by any central authority, and just as important as any code - is the subject of this section. The *Zen of Reticulum* calls the software a habitat. A habitat is healthy or unhealthy as a whole, and the health of this one is decided, every day, by the people who live in it. - -### Health Is A Property Of The Whole - -A network is not made healthy by the excellence of any single part. It is made healthy by the behaviour of every part on the shared medium, and how they interact. And the shared medium is the point. When your node transmits, it transmits into the same spectrum, the same line, the same time, the same mathematical address space, the same bandwidth that its neighbours depend on. - -The network cannot be certified by any single project’s claims. A node that is a bad neighbour, that invents its own channel behaviour on shared spectrum, that announces without discipline, that spawns tens of thousands of links to scrape the network, that floods and retries without mercy, is a hazard to every node that shares its medium, regardless of how well its “creator” imagines its own code to work in isolation. - -On a shared medium, “my software works” is not a complete sentence. The complete sentence is “my software works *and coexists*”. A network’s health deteriorates with its worst-behaved member, and every member has a responsibility to not be *that* member. There is no final, technical solution for this; the solutions are human. - -### A History Of What Works - -Let’s recall, because it is the ground truth this entire thing rests on, that the coexistence model has *worked* - is working, right now, **at scale**. Reticulum’s protocol decisions have been discussed and evaluated in the community, openly, for years. Protocol upgrades have been designed, coordinated, and executed between a loosely organised group of independent developers, node operators, and users - and have succeeded, repeatedly, without any corporate board, without any central command, and without the collapse that the skeptics predict every second week (while screaming for *GOVERNANCE*). The reference implementation has been developed in the open for over a decade, by a maintainer who answers for it, fixes bugs or issues within hours of their discovery, and is set down in code deliberately kept small enough (the entire protocol implementation under 15,000 lines, including its comments) for a human to read, verify, *and challenge*. - -This is the empirical baseline that every narrative examined in [A Movement Of A Dozen](#brandolinis-movement-of-a-dozen) conveniently ignores, in order to justify: - -* The claim that the project is a one-man dictatorship -* The claim that the community is excluded -* The claim that the networks need saving from their own founders - -The **reality**, documented and reproducible, is that a loose community of humans coordinates complex technical evolution *without* the apparatus the narratives insist is missing. The health of the networks is not a promise for the future; it is a demonstrated property of the past and present, under the leadership and community model that brought it into existence and navigated it through difficult decisions and situations over a decade. **My job is to keep it that way**, and you can be damned certain that I take it seriously. - -### The Role Of The Individual - -The networks are built by physical acts and human coordination. You cannot prompt an antenna into existence. No model can string up a dipole, or stand in the rain aligning a radio, or coordinate with a neighbour three valleys away about who covers which repeater frequency, or sit down with a newcomer and explain why their first node didn’t announce. These are the acts that make a network *organic* - present in the physical world, resilient because it is *real*, valuable because it is *human*. The machines and technology we use are tools aimed at a well-defined, openly described goal. But the networks themself are composed of intentional human actions, and every node operator is infrastructure for other humans. - -This has a corollary for the individual: You do not need permission, and you do not need to be an expert, to participate. Run a node. Help a neighbour. Ask questions in the community, and answer the questions you can. Build something small at first. The network grows by these acts, and they are the most meaningful ones - the things that can only be done together, as humans, in the physical world. The parasitic “movement” that haunts the peripheries of this ecosystem cannot do any of this. It cannot build a node because it does not exist, in any real connected and human sense; only people can, and people are precisely what a self-recurrent loop lacks. - -### The Place Of New Implementations - -The future of the network includes more implementations - and they are welcome. This chapter is not an argument against implementing Reticulum. The protocol is public domain, by design, and the network is made stronger by genuine diversity: More *good* implementations, honestly made, mean more resilience, more experimentation, more people who understand the protocol deeply *because they have built it themselves*. That is the goal: **For people to understand and own the technology they rely on**. Not to spew out 600,000 line rust ports that fills no meaningful gap or purpose. - -What the ecosystem needs, from every implementation, is the ordinary floor that [Evaluating A Reticulum Implementation](#brandolinis-evaluating) establishes, and that the reference has met for over a decade: A human who made it and answers for it; provenance disclosed; attribution given; benchmarks honest or absent; interop demonstrated against the real network; code a human can read. And it needs the one thing that costs nothing and changes everything: **Honest labeling**. - -An experimental implementation that says, plainly, as the first thing a user sees: “This is in-progress, experimental, do not depend on it yet, but help if you can”, that is a gift to the ecosystem. It contributes, it invites participation, and it protects users. - -The opposite, the polished “drop-in replacement”, does damage far beyond its own users: It dissuades the honest developers who might have built *the real thing*, because the polished slop persuades them the work is already done. That dissuasion is one of the quietest and most expensive harms in this entire affair, and the antidote is honesty at every project’s door. - -### Expanding The Commons - -Coexistence, reduced to its essentials, is a short formula; the duties of membership in a network no one owns: - -* **Be honest.** Say what you are, what you built, how you built it, and what you used to - build it. Label experimental work as experimental. Credit the work you build on, including - the reference implementation, in the licenses and in the prose. Concealment is not a - defence; it is a very clear diagnosis marker. - -* **Be owned.** Have a human who made the work and can answer for it. Use assistance, don’t - substitute. Never sign what you cannot answer for. The chain of accountability is the - community’s only verification at scale, and every link in it is a *person*, not a facade. - -* **Be a good neighbour.** Respect the shared mediums. Do not invent new behaviour in our - shared space and call it progress. The network belongs to its other members as much as - to you. - -* **Verify against the network.** Compatibility is a property demonstrated with actual nodes, - on actual media, against the actual current protocol, over a period of *years*, not 48 hours. - And not a claim pinned to an old snapshot over loopback. - -* **Participate as a member.** Ask, answer, help, build. *Advertising at* the community is not - the same as being part of it, and it is **not** the first thing you should do. - -None of this is a high bar. It is the bar the reference implementation has met, in public, for over a decade, and the bar the ecosystem has adopted organically itself, successfully, through every protocol upgrade it has ever coordinated, and everything running on Reticulum today, that people actually use. It is not a new requirement; it is the existing, working culture of the networks, now written down so that newcomers can find it and so that those who would exploit us cannot claim they never saw it. - -### Defence Of The Commons - -The networks need defending - not because they are under severe physical attacks (yet), but because attention, talent and resources is being stolen from them, trust is being mined against them, and its newcomers are being harvested by the machinery documented in this chapter. Defence, for a commons, has specific shapes, and it is not what the theater would like it to be. - -* Defence is **documentation**: Records kept public, of what was claimed, what was - answered, and what turned out true. -* Defence is **a headcount**: Calm, unexcited reminders that a “movement” might just be a dozen - people having a tantrum. -* Defence is **honesty in action**: The visible, ordinary practice of the good behaviour - by the people who actually run the networks. -* And defence is **building**: Every node raised, every page of *real* documentation, every - honest implementation, every new user helped into the community with the knowledge to know - what they are doing is more durable than any rebuttal, because it adds to the thing being - defended. - ---- - -**The theatre cannot keep up with a network.** - -**It can only orbit it, while it recycles its own sour piss.** - ---- - -A lot of us rely on these networks every day. They carry our messages, our coordination, our friendships, and in some places, our safety. That is exactly why they are a target: - -They are valuable, free, and very hard to shut down. The response is not to become aggressive. It is to be *more* of what the network already is: Real, open, owned, and shared. - -*Mark* - -## Addendum A: The Prns Case - -### Purpose & Scope - -This section provides a concrete example, which future evaluations can use as a template. Its purpose is not to be the last word on this particular subject, but to be an example of how the ideas in this chapter can be applied, and to make the next such examination easier. - -The subject of this case study is **Prns**, a project published at the repository `github.com/KenAKAFrosty/Prns`, presented as a “ground-up implementation of Reticulum, written in Rust”, marketed as faster, safer, and more strictly tested than the reference, widely promoted with the claim of “up to 89× the throughput” of the reference implementation, and an “author” actively engaged in the tactics described in [A Movement Of A Dozen](#brandolinis-recycled-wine) on online platforms. The record described below is the project’s own, and sourced from its GitHub repository, analyzed locally. - -### The Project In Brief - -* **Time span.** 3,374 commits between 2026-05-26 and 2026-09-12: approximately 110 days, - at a mean rate of ~31 commits per day. Monthly: 132 / 1,137 / 1,097 / 805 / 203. - -* **Authorship.** 3,252 of 3,374 commits (96.4%) are authored by a single account. A - second identity, “Prns Tests [tests@example.test](mailto:tests@example.test)” - a fictitious identity on a reserved - domain - contributed 35 commits, every one of them website and marketing content. - -* **Size.** 3,519 tracked files; 1,983 Rust files; 636,226 lines of Rust; approximately - 22.7 MB of Rust source; 69.5 MB of total repository content. - -* **Scope claimed.** A daemon (“drop-in replacement” for the reference daemon), embedded - firmware for three microcontroller families (including six ESP32-S3 board variants and - nRF52840), SDKs and bindings for nine languages, a browser-based node and web flasher, a - twelve-language website, a benchmark apparatus with its own publication and “proof” - pipeline, and a validation hub with oracles, fuzzing, mutation testing, and formal-proof - claims. - -* **Marketing claims.** “Ground-up implementation of Reticulum”; “up to 89× the - throughput”; “48× smaller peak-memory footprint”; “33× the energy efficiency”; - “Measured, not just claimed”; “Enforced, then audited”; “byte-for-byte wire parity with - the reference implementation”; “works with Sideband, NomadNet, MeshChat, etc.”; - “no_std (no alloc required either)”. - -* **Status at the time of writing.** Version 0.3.7; published version history begins at - 0.3.0. The repository continues to receive commits. - -### Question 1: Is this a clean implementation, or was it directly generated from the reference? - -The first of the two deciding questions from [Evaluating A Reticulum Implementation](#brandolinis-evaluating) is settled by the project’s own record. An actual [clean-room](#brandolinis-cleanroom-delusion) implementation, reasoned out and written by humans, would owe nothing to any license. The data available shows clearly that Prns is not that. - -* **The founding state.** The first commit, “Scaffold Personal Reticulum suite”, contains - the scaffolding of not one but two ports - `personal-rns` and `personal-lxmf`, the latter - being the reference’s companion messaging protocol - together with a daemon scaffold and - a 233-line founding document, `docs/build-ethos.md`. That document declares the project’s - goal as a “performance-focused drop-in-replacement for rnsd” and its governing principle - as: “Port the contract, not the implementation.” It further states directly to the - machine-loop, that **“You owe the reference fidelity at exactly two boundaries: - The wire… The behavior. … Between those two boundaries, the model is yours”**. - At this point, the very first commit, effectively the whole of the implementation design - and any relevant decisions are handed over to the LLM. - -* **Continuous ingestion from the reference.** 506 commits reference RNS or Reticulum - source behaviour; the word “parity” appears in 49 commit messages. Commits cite the reference - implementation’s own source locations (for example `Transport.py:1367`) when correcting - their own divergences and mistakes. - -* **Byte-level fidelity as an explicit goal.** Within 48 hours of the first commit, the - record shows a “crypto adapter - vetted primitives, byte-exact vs RNS 1.3.1”, and a wire - implementation matching the reference’s packet header layout. The project’s own - validation apparatus decodes wire vectors using the reference implementation’s own - packet parser, and the interop suite consists of real, stock-RNS peer nodes - that is, - the reference source itself, used as the ground truth against which this LLM output is - checked, and committed as accepted and verified after 48 hours of the project’s recorded start. - -* **The marketing mismatch.** The same record that documents all of the above describes the - project, publicly, as “a ground-up implementation of Reticulum” - while declining, on its - website, to link to the reference implementation at all: the entire tree mentions the - reference’s author in six files, and the website in none. - -Conclusion: the project’s own history and commit record establishes that it was generated directly from the reference implementation’s source code, not reasoned out from the public-domain protocol. It is a derivative of licensed work within the meaning of [the Reticulum License](#brandolinis-what-the-license-is) and [general copyright law](#brandolinis-copyright-and-layers). The “ground-up” claim, which carries its entire legal and marketing posture, is disproven by the project’s own history. - -### Question 2: Who, or what, is the agent of the work? - -The second deciding question is answered by the application of [Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution) criteria to the project. - -**Scope that no human could have authored** - -636,226 lines of Rust in 110 days is approximately 5,800 lines per day, every day, including weekends - with 149 commits recorded between midnight and 06:00, and Saturday the single busiest weekday. No human review or decision capacity is commensurable with this volume; the review capacity and the output volume are structurally incommensurable, which is the definition of unauthored scope. - -**Uniform generation** - -The record shows no sign of human development, growth, or learning: No contested decisions, no style drift. Instead: endless renaming (in the first 48 hours: `State → EngineState → TickInput → TickOutput`), uniform fluent register in every commit, and the exact same, synthetic voice in code, documentation, and website copy. - -**Discovery by regeneration** - -Corrections arrived by the machine’s own subsequent runs, recorded as planned milestones. In early July 2026, a series of “core review” commits documented, in the project’s own words: - -* announce-ID history “was unbounded per-slot growth” (`54cc8d601`); -* interface announce limiting could “leak a sustained flood” (`9d27af75c`); -* a “phantom-airtime leak” where frames “used to vanish silently” (`4ed75ffdd`); -* Group-packet deduplication “which RNS does not do” - a behaviour the reference never had, - implemented and later removed (`edd8e72be`); -* “both `unwrap_or(&[])` arms were unreachable dead defaults” (`74bea355e`); -* a benchmark crate “hasn’t compiled since” a prior rename, undetected for weeks because it - was outside the CI’s workspace (`86240b027`). - -Earlier still: the record shows a “>1 MiB RNS-to-Prns interop stall” closed three weeks in -(`cc4d524a3`), and a known send-path hang shipped with the note “typed fast-fail a -candidate for later” (`a010e6613`). - -**Laundered provenance** - -The repository contains a committed commit-message hook (`.githooks/commit-msg`) that *blocks* any commit containing attribution for machine generation (“generated with claude/codex/copilot/cursor/gemini”, “co-authored-by”), with the explanation “The committer owns the commit”. The contributing guide is explicit: “AI tools … are welcome … what you submit is *yours*”, and describes the contributing population as “both human and automated contributors”. The fictitious “Prns Tests” identity completes the picture: A manufactured, unknowable contributor, used for marketing content. - -**Authorship by assignment** - -The human whose name is on the work cannot answer for it, because the choices were never made by a human, and only the general shape and constraints of the outward presentation was. Nothing in the project’s history shows an individual explaining a design decision, accepting a mistake in the first person, or going deeper under technical questioning; the record shows process language and marketing instead. Per the earlier mentioned test: any other person could have pressed the same button, and nothing would be different. The project is effectively unowned, and void of value. - -By the criteria of [Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution), this is substitution. The machine was the agent of the work; the “author” was a facade. - -### The performance claims - -The project’s central marketing claim - “up to 89× the throughput” of the reference - is also its most completely documented failure, because the project published the evidence of its own measurement. The apparatus, the results, and the problems are all on the record. - -**The claim** - -The README states “up to 89× the throughput”; the on-network marketing page repeats “up to 89× the throughput, 48× smaller peak-memory footprint, and 33× the energy efficiency of stock RNS 1.4.2”; the website’s front page carries the tagline “Measured, not just claimed”. - -**The apparatus** - -The reference is loaded through `benchmarks/reference/compiled_reference.py`, which installs `pyximport` and then asserts two things: that `RNS.compiled == True`, and that at least *one* RNS module was loaded from a native extension. The project’s own published “proof” records show exactly what those checks captured, on every platform: - -```default -aarch64-apple-darwin: .../RNS.cpython-313-darwin.so, Cython 3.2.8, Python 3.13.13 -x86_64-unknown-linux-gnu: .../RNS.cpython-313-x86_64-linux-gnu.so, gcc 11.4.0 -x86_64-pc-windows-msvc: ~/.prns-oc/lib.win-amd64-cpython-313/RNS.cp313-win_amd64.pyd -``` - -In every case the recorded native module is the **top-level package only** - `RNS.so`, nothing below it. Nothing named `RNS.Transport`, `RNS.Destination`, `RNS.Packet`, `RNS.Link`, or `RNS.Resource` ever appears, because *nothing below the top level was ever compiled*. Nobody bothered to check or verify *anything* here. - -The machine output was simply taken for truth, and a full marketing strategy was spun and produced on it. “RNS 1.4.2 (compiled)” - the label used in every table, chart, and marketing page - *is not a mode of RNS that exists*, but the hallucinations of a machine, which the humans created a marketing strategy on. In reality, it is the old `CRNS` development shim, whose top-level-only, debug-oriented “compilation” is slower than plain interpreted RNS, and which was *defunct for over a year* before these benchmarks were published. - -**Interface choices** - -The harness writes the reference’s configuration itself (`benchmarks/reference/participant_node.py`): `TCPClientInterface` for the initiator, `TCPServerInterface` for the responder and both relay sides, with `UDPInterface` as the abstract wire. The reference was never given its modern `BackboneInterface` on any platform, forcing the slowest possible interrfaces. The interface selection is never mentioned in any methodology text. - -**The policy** - -The published default-policy tables record the asymmetry directly: Prns at 500 Mbps / 128 KiB TCP policy against the reference at 10 Mbps / 8 KiB - slyly footnoted as “preserves each implementation’s normal TCP policy” (in the case of RNS, a conservative default policy that was selected to prioritize coexistence on slower, shared links, requiring the user to dial it up to full speed if they knew what they were doing). Raw transport “throughput” ratios of 20.65× (Linux) and 38.84× (Windows) are therefore mostly policy, not engine. - -**The cross-host evidence** - -The published results show the same scenario producing wildly different ratios by host, and the variance is entirely in the reference artifact: - -| Host | Prns, single-packet | “Reference”, single-packet | Ratio | -|-------------------------|-----------------------|------------------------------|---------| -| macOS (Apple M4) | 37.4k/s | 420/s | 89.09× | -| Linux (i7-1260P) | 25.3k/s | 3.5k/s | 7.19× | -| Windows (Ryzen 5 5600X) | 30.8k/s | 3.9k/s | 8.00× | - -Prns is consistent (25–37k/s). The reference artifact collapses to different depths on different hosts, and the single worst collapse - 420 packets per second on macOS - is the cell selected as the global headline. The “89×” is not a measure of Prns; it is a measure of how badly the broken baseline happened to fail on one host. - -For reality calibration: Stock, interpreted RNS 1.5.4 on an ordinary mid-range laptop, single core, sustains roughly 30,000 single-packet deliveries per second (including ephemeral-key decryption), ~175,000–280,000 packets per second in transit relay, and on the order of 10 Gbps at 16 KiB payloads. The project’s “compiled” artifact measured 420–3,900 packets per second for its single-packet scenario: An order of magnitude or more *below* stock interpreted RNS. The comparison being marketed was not “Rust vs Python”; **it was “an implementation running under optimal conditions versus a mislabeled, broken shim on a slow legacy interface at a severe policy disadvantage”**. - -**The equalized rows** - -The project’s own “1 Gbps policy” rows, in which both sides are explicitly configured identically, publish ratios of roughly 2.4–8.4× across all hosts and scenarios - still against the broken baseline, but nowhere near 89×, and consistent with the ordinary, uncontroversial observation that a native implementation tends to outperform an interpreted one in raw throughput. That point needed no fudging to make, and is banal. - -Interestingly, though, it seems that Prns is really only *marginally* faster than a Python implementation, running in interpreted mode, on a single CPU core. - -**Assessment** - -The front page says “Measured, not just claimed.” The record shows the opposite ordering: claimed, and arranged to look measured. No methodology section anywhere states what “compiled” means, that it is a deprecated development shim, that it builds debug targets, or which interface the reference was given; the one statement of the policy asymmetry appears in a footnote of the results tables themselves. If they did not need to fudge it, they would have published the method clearly. - -### Overall Evaluation - -Evaluated against the conditions of [Evaluating A Reticulum Implementation](#brandolinis-evaluating): - -* **Provenance and attribution.** Fails: The fake “ground-up” origin story; zero reference links - on the website; a single well-hidden link in the README; a license file reading - `Copyright (c) 2026 The Prns Authors` over work directly generated from the licensed reference; - no original copyright or permission notice anywhere. - -* **Agency and answerability.** Fails on every criterion of [Assistance Versus Machine Substitution](#brandolinis-assistance-versus-substitution): - Unauthored scope, uniform generation, discovery by regeneration, laundered provenance, - authorship by assignment. - -* **Engineering.** Fails: The benchmark apparatus above; interop demonstrated only - against a pinned, outdated reference version over loopback while the marketing claims current - parity, interop failures with stock RNS; vendored prebuilt binaries and a republished fork - of an upstream crate (`nrf-softdevice 0.1.0-prns.1`) with no verifiable provenance; - committed WASM bundles. - -* **Network behaviour.** Fails: Invented spectrum behaviour and compatibility claims - about third-party applications of the reference ecosystem (Sideband, NomadNet, MeshChat) - that are not established by any evidence. - -* **Monetary and influence signals.** The marketing apparatus - website before protocol, - day-three twelve-language marketing, false benchmark headlines, “audited” and “formal proof” - claims - the conformal surface described in [Evaluating A Reticulum Implementation](#brandolinis-evaluating): - is where this project invested. - -### Licensing Analysis - -Applying the two legal layers established in [Copyright & The Two Legal Layers](#brandolinis-copyright-and-layers): - -**Layer A - copyright infringement (license-independent)** - -The record shows a direct copy roundtrip: the licensed reference implementation fed into a machine, transformed, at a scale and cadence no human could review, with a license file asserting a fresh copyright in the operator’s name and no acknowledgment of the work the project was generated from. No substantial, new human creative input is demonstrated anywhere in the record - the founding document explicitly frames the design as an instructed port of the reference’s contract. A primarily machine-generated copy of a copyrighted work, not accompanied by demonstrable substantial human creative input, is a derivative work over which the copier holds no copyright, and a reproduction of which without the original author’s permission infringes the original author’s copyright. This holds under any license, MIT included. The operator’s copyright claim over the result is, on this evidence, not merely unsubstantiated; it is the absence of one, directly asserted in the project’s own writing. - -**Layer B - license conditions (Reticulum License specific)** - -Modern Reticulum - the current wire format, the AES-256-based link encryption, ratchets, the cryptographic machinery as it exists today - has been published under the Reticulum License since April 15, 2025, effective from release 0.9.4 and every release since. As a derivative of that work, the license’s conditions attach: The copyright and permission notice must be included in all copies and substantial portions; the AI-training condition must be honoured; the harm condition must be honoured. The record shows none of the notice requirements met, and a production process that consists of the licensed work being ingested into a machine and regurgitated, at industrial scale and cadence. - -**The cascading consequence** - -Prns is distributed claiming dual MIT/Apache-2.0 licensing. It is, however, a direct derivative of Reticulum-licensed work, and the claimed grant is therefore void. Every downstream project that builds on Prns believing it is MIT/Apache-licensed is building on a grant that does not exist. The users of Prns, and all software developers using it, have in effect, been placed under conditions they were never told about. The “ground-up” claim is not merely false; it is the sole load-bearing element of an entire licensing posture, and it fails spectacularly. - -### Consequences - -The consequences of this pattern are examined in the following sections: The harm to users and to the network in [A Movement Of A Dozen](#brandolinis-movement-of-a-dozen), and the human dimension in [Network Health & Coexistence](#brandolinis-health-and-coexistence). The contribution of this addendum is narrower and intended as an instructive example: This is what a substitution project looks like, in full, on its own record. It is a reference example for [Evaluating A Reticulum Implementation](#brandolinis-evaluating), and it demonstrates, in one case, most of the elements this chapter describes. - -### Verdict - -Applying the three verdicts of [Evaluating A Reticulum Implementation](#brandolinis-evaluating), this project does not meet the standard. It is not “experimental but honest” - it is not labeled as experimental anywhere; the first thing users saw was a polished marketing presence promising a finished, tested, drop-in replacement. It is a project whose central claims are fabricated (the performance apparatus), whose provenance is laundered (the authorship machinery), whose foundation is misrepresented (the “ground-up” story), whose core semantics were learned late and by regeneration, and whose operator cannot possibly answer for any of it. It is also a blatant copyright violation of the reference implementation. - -The proposed conclusion, for users and for the network: Treat the Prns project as hostile regardless of its polished surface, and as a hazard insofar as it invites users onto shared networks. Nothing in this section is a judgment about the people involved; it is a reading of the results produced, their history and how they came to be, which in the regrettable lack of any human authorship is the only thing that can be expected to be held to account. - -### Dating - -This examination reflects the publicly available material as of September 2026; the repository in question continued to receive commits at the time of writing, so readers should treat citations to commits and files as point-in-time evidence. A [forensics snapshot](https://github.com/markqvist/forensics_prns) is publicly available on GitHub. \ No newline at end of file diff --git a/docs/markdown/git.md b/docs/markdown/git.md index 253c1f36..a3dd6d61 100644 --- a/docs/markdown/git.md +++ b/docs/markdown/git.md @@ -639,7 +639,6 @@ The following template files are supported: - `commit.mu` - Individual commit detail page - `refs.mu` - Branches and tags listing - `stats.mu` - Statistics page -- `no_ident.mu` - Displayed to non-identified initiators if blocked Templates can include the following variables: diff --git a/docs/markdown/index.md b/docs/markdown/index.md index 78262bc8..e3335ae4 100644 --- a/docs/markdown/index.md +++ b/docs/markdown/index.md @@ -75,37 +75,26 @@ to participate in the development of Reticulum itself. * [The Work Is Finished](zen.md#the-work-is-finished) * [Open Sky](zen.md#open-sky) * [Programs Using Reticulum](software.md) - * [Browsing & Publishing](software.md#browsing-publishing) + * [Programs & Utilities](software.md#programs-utilities) + * [Remote Shell](software.md#remote-shell) * [Nomad Network](software.md#nomad-network) - * [Ren Browser](software.md#ren-browser) * [RNS Page Node](software.md#rns-page-node) * [Retipedia](software.md#retipedia) - * [Git Over Reticulum](software.md#git-over-reticulum) - * [Messaging & Voice](software.md#messaging-voice) * [Sideband](software.md#sideband) - * [MeshChatX](software.md#id6) - * [Columba](software.md#id8) - * [Ren TUI](software.md#ren-tui) - * [Chat](software.md#chat) + * [MeshChatX](software.md#meshchatx) * [Reticulum Relay Chat](software.md#reticulum-relay-chat) * [RetiBBS](software.md#retibbs) - * [Voice & Telephony](software.md#voice-telephony) + * [RBrowser](software.md#rbrowser) * [Reticulum Network Telephone](software.md#reticulum-network-telephone) - * [Partyline](software.md#partyline) * [LXST Phone](software.md#lxst-phone) - * [System Administration](software.md#system-administration) - * [Remote Shell](software.md#remote-shell) - * [RNS FileSync](software.md#rns-filesync) - * [Utilities & Components](software.md#utilities-components) - * [Modem73](software.md#modem73) * [LXMFy](software.md#lxmfy) + * [LXMF Interactive Client](software.md#lxmf-interactive-client) + * [RNS FileSync](software.md#rns-filesync) * [Micron Parser JS](software.md#micron-parser-js) - * [Reticulated](software.md#reticulated) * [RNMon](software.md#rnmon) - * [Device Flashers](software.md#device-flashers) * [Protocols](software.md#protocols) * [LXMF](software.md#lxmf) - * [LXST](software.md#id30) + * [LXST](software.md#id16) * [RRC](software.md#rrc) * [Interface Modules & Connectivity Resources](software.md#interface-modules-connectivity-resources) * [Using Reticulum on Your System](using.md) @@ -194,7 +183,6 @@ to participate in the development of Reticulum itself. * [Announce Rate Control](interfaces.md#announce-rate-control) * [New Destination Rate Limiting](interfaces.md#new-destination-rate-limiting) * [Path Request Burst Control](interfaces.md#path-request-burst-control) - * [Tuning Ingress Queues](interfaces.md#tuning-ingress-queues) * [Building Networks](networks.md) * [Concepts & Overview](networks.md#concepts-overview) * [Introductory Considerations](networks.md#introductory-considerations) @@ -281,75 +269,6 @@ to participate in the development of Reticulum itself. * [Filetransfer](examples.md#filetransfer) * [Custom Interfaces](examples.md#custom-interfaces) * [Reticulum License](license.md) -* [Brandolini’s Reference](brandolinis.md) - * [What The Reticulum License Is](brandolinis.md#what-the-reticulum-license-is) - * [Why This License Exists](brandolinis.md#why-this-license-exists) - * [On Freedom](brandolinis.md#on-freedom) - * [The Prediction, And What Happened](brandolinis.md#the-prediction-and-what-happened) - * [What This Means For Other Implementations](brandolinis.md#what-this-means-for-other-implementations) - * [Copyright & The Two Legal Layers](brandolinis.md#copyright-the-two-legal-layers) - * [The “Clean-Room” Delusion](brandolinis.md#the-clean-room-delusion) - * [Open Source Means Open Source](brandolinis.md#open-source-means-open-source) - * [What The Argument Is Actually Doing](brandolinis.md#what-the-argument-is-actually-doing) - * [Who Is Restricted By Which License?](brandolinis.md#who-is-restricted-by-which-license) - * [The Consequences Of The “Not Open Source” Claim](brandolinis.md#the-consequences-of-the-not-open-source-claim) - * [The Distribution Argument](brandolinis.md#the-distribution-argument) - * [On The Institutions, In Fairness](brandolinis.md#on-the-institutions-in-fairness) - * [What “Open” Means Positively](brandolinis.md#what-open-means-positively) - * [Assistance Versus Machine-Substitution](brandolinis.md#assistance-versus-machine-substitution) - * [False Dichotomy](brandolinis.md#false-dichotomy) - * [The Line](brandolinis.md#the-line) - * [What assistance looks like](brandolinis.md#what-assistance-looks-like) - * [What substitution looks like](brandolinis.md#what-substitution-looks-like) - * [Infrastructure Matters](brandolinis.md#infrastructure-matters) - * [The Fluency Trap](brandolinis.md#the-fluency-trap) - * [Disclosed Assistance Or Laundered Generation](brandolinis.md#disclosed-assistance-or-laundered-generation) - * [The License In This Context](brandolinis.md#the-license-in-this-context) - * [The General Principle](brandolinis.md#the-general-principle) - * [Void Grants: Legal Foundations of Machine-Generated Code](brandolinis.md#void-grants-legal-foundations-of-machine-generated-code) - * [I. The Foundational Statute: Translation Is Copying](brandolinis.md#i-the-foundational-statute-translation-is-copying) - * [II. Ideas Are Free, Expressions Protected](brandolinis.md#ii-ideas-are-free-expressions-protected) - * [III. Machine Output Has No Author](brandolinis.md#iii-machine-output-has-no-author) - * [IV. License Conditions Bound the Grant](brandolinis.md#iv-license-conditions-bound-the-grant) - * [V. The Void Grant](brandolinis.md#v-the-void-grant) - * [VI. Fair Use Does Not Rescue This Pattern](brandolinis.md#vi-fair-use-does-not-rescue-this-pattern) - * [Conclusion](brandolinis.md#conclusion) - * [Sources and Authorities](brandolinis.md#sources-and-authorities) - * [Evaluating a Reticulum Implementation](brandolinis.md#evaluating-a-reticulum-implementation) - * [The Intention Of This Guide](brandolinis.md#the-intention-of-this-guide) - * [Two Questions That Resolve Most Cases](brandolinis.md#two-questions-that-resolve-most-cases) - * [Other Signals To Look For](brandolinis.md#other-signals-to-look-for) - * [The Conformal Surface](brandolinis.md#the-conformal-surface) - * [Possible Verdicts](brandolinis.md#possible-verdicts) - * [These Standards Are Reasonable](brandolinis.md#these-standards-are-reasonable) - * [The Movement Of A Dozen](brandolinis.md#the-movement-of-a-dozen) - * [Presentation, Elongated](brandolinis.md#presentation-elongated) - * [Headcount](brandolinis.md#headcount) - * [Recycled Wine, Fermenting](brandolinis.md#recycled-wine-fermenting) - * [The Method of Madness](brandolinis.md#the-method-of-madness) - * [A Persistent, But Fragile Triad](brandolinis.md#a-persistent-but-fragile-triad) - * [What The Pattern Is Not](brandolinis.md#what-the-pattern-is-not) - * [Intent & Consequence](brandolinis.md#intent-consequence) - * [Recognition & Response](brandolinis.md#recognition-response) - * [Network Health & Coexistence](brandolinis.md#network-health-coexistence) - * [Substance Of The Networks](brandolinis.md#substance-of-the-networks) - * [Health Is A Property Of The Whole](brandolinis.md#health-is-a-property-of-the-whole) - * [A History Of What Works](brandolinis.md#a-history-of-what-works) - * [The Role Of The Individual](brandolinis.md#the-role-of-the-individual) - * [The Place Of New Implementations](brandolinis.md#the-place-of-new-implementations) - * [Expanding The Commons](brandolinis.md#expanding-the-commons) - * [Defence Of The Commons](brandolinis.md#defence-of-the-commons) - * [Addendum A: The Prns Case](brandolinis.md#addendum-a-the-prns-case) - * [Purpose & Scope](brandolinis.md#purpose-scope) - * [The Project In Brief](brandolinis.md#the-project-in-brief) - * [Question 1: Is this a clean implementation, or was it directly generated from the reference?](brandolinis.md#question-1-is-this-a-clean-implementation-or-was-it-directly-generated-from-the-reference) - * [Question 2: Who, or what, is the agent of the work?](brandolinis.md#question-2-who-or-what-is-the-agent-of-the-work) - * [The performance claims](brandolinis.md#the-performance-claims) - * [Overall Evaluation](brandolinis.md#overall-evaluation) - * [Licensing Analysis](brandolinis.md#licensing-analysis) - * [Consequences](brandolinis.md#consequences) - * [Verdict](brandolinis.md#verdict) - * [Dating](brandolinis.md#dating) * [API Reference](reference.md) * [`Reticulum`](reference.md#RNS.Reticulum) diff --git a/docs/markdown/interfaces.md b/docs/markdown/interfaces.md index 6a4d974a..b11111f1 100644 --- a/docs/markdown/interfaces.md +++ b/docs/markdown/interfaces.md @@ -939,9 +939,6 @@ When `discoverable` is enabled, a variety of additional options become available `discovery_name` : A human-readable name for the interface. This name will be displayed to users on remote systems when they list discovered interfaces. If not specified, the interface name (the section header) will be used. -`discovery_lxmf_address` -: An optional LXMF address for contacting the operator of this interface. The address will be displayed to users on remote systems when they view details for discovered interfaces. Useful for getting in touch with other operators to interconnect networks with and coordinating network coverage. - `announce_interval` : The interval in minutes between successive discovery announces for this interface. Default is 360 minutes (6 hours). For stable, long-running infrastructure, higher intervals (12 to 22 hours) are usually sufficient and reduce network load. Minimum allowed value is 5 minutes (but expect to have your announces throttled if using intervals below one hour). @@ -1487,30 +1484,4 @@ but all the parameters are exposed for configuration if needed. All of the above settings can be configured both as instance-wide defaults under the `[reticulum]` section of the configuration file, or on a per- -interface basis under the relevant interface configuration section. - -## Tuning Ingress Queues - -Reticulum uses separate, prioritized queues for processing inbound packets. The default four queues are drained in the following order: - -> 1. Data and general traffic -> 2. Announces -> 3. Path requests -> 4. Ingress-limited traffic - -You can tune the queue sizes from the defaults, for example if you’re running a setup where you want to simply drop most traffic from ingress-limited interfaces. The queue sizes are configured in the `[reticulum]` section of the config - -> * The `qlen_in_data` option sets the maximum size of the data -> queue. Defaults to `1024`. -> -> * The `qlen_in_announce` option sets the maximum size of the announce -> queue. Defaults to `128`. -> -> * The `qlen_in_pr` option sets the maximum size of the path request -> queue. Defaults to `128`. -> -> * The `qlen_in_il` option sets the maximum size of the ingress limiter -> queue. Defaults to `8`. -> - -If an inbound packet arrives and its target queue is full, the packet is dropped. Reticulum will always completely drain higher-priority queues before starting to drain a lower-priority one. As an example, the ingress limiter queue will only start draining once all other queues are empty. \ No newline at end of file +interface basis under the relevant interface configuration section. \ No newline at end of file diff --git a/docs/markdown/reference.md b/docs/markdown/reference.md index 8c6c8668..acc04c2a 100644 --- a/docs/markdown/reference.md +++ b/docs/markdown/reference.md @@ -69,44 +69,9 @@ alterations should be made on a per-interface basis instead. Minimum bitrate required across a medium for Reticulum to be able to successfully establish links. Currently 5 bits per second. -#### `DEFAULT_PER_HOP_TIMEOUT = 6` - -The default per-hop timeout value used in various timeout calculations. - #### `static get_instance()` -Returns the currently running Reticulum instance. - -#### `get_first_hop_timeout(destination)` - -Returns a best-effort estimate of a reasonable minimum -*first-hop* timeout value for a given destination hash. -If a path is known, this calculation takes the next-hop -interface’s bitrate into account. If no path is currently -known, returns `DEFAULT_PER_HOP_TIMEOUT`. - -* **Parameters:** - **destination** – A destination hash, as *bytes*. - -* **Returns:** - First-hop timeout, in seconds. - -#### `get_lowest_interface_bitrate()` - -Returns the bitrate of the slowest currently online -interface, or None if no online interface bitrate - -* **Returns:** - Lowest online interface bitrate in bits per second, or `None`. - -#### `get_medium_path_timeout()` - -Returns an estimate of a reasonable minimum path request timeout covering -a full round trip for an MTU on the slowest currently online interface -plus per hop grace - -* **Returns:** - Timeout in seconds or 0 if it’s unknown. +Return the currently running Reticulum instance #### `static should_use_implicit_proof()` @@ -860,7 +825,7 @@ statistics, you will be able to retrieve stats such as *RSSI*, *SNR* and physica #### `get_expected_rate()` * **Returns:** - The expected in-flight data rate of an established link. + The packet expected in-flight data rate of an established link. #### `get_mode()` diff --git a/docs/markdown/software.md b/docs/markdown/software.md index 024a5511..26060e0c 100644 --- a/docs/markdown/software.md +++ b/docs/markdown/software.md @@ -1,99 +1,100 @@ # Programs Using Reticulum -This chapter provides a non-exhaustive list of notable programs, systems and application-layer protocols that have been built using Reticulum. +This chapter provides a non-exhaustive list of notable programs, systems and application-layer +protocols that have been built using Reticulum. -Many different applications using Reticulum already exist, serving a wide variety of purposes from day-to-day communication and information sharing to systems administration and tackling advanced networking and communications challenges. +These programs will let you get a feel for how Reticulum works. Most of them have been designed +to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast +links, such as local WiFi, wired Ethernet, the Internet, or any combination. -These programs will let you get a feel for how Reticulum works. Most of them have been designed to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast links, such as local WiFi, wired Ethernet, the Internet, or any combination. +As such, it is easy to get started experimenting, without having to set up any radio +transceivers or infrastructure just to try it out. Launching the programs on separate +devices connected to the same WiFi network is enough to get started, and physical +radio interfaces can then be added later. -As such, it is easy to get started experimenting, without having to set up any radio transceivers or infrastructure just to try it out. Launching the programs on separate devices connected to the same WiFi network is enough to get started, and physical radio interfaces can then be added later. +## Programs & Utilities -Development of Reticulum-based applications and systems is ongoing, so consider this list a non-exhaustive starting point of *some* of the options available. With a bit of searching, primarily over Reticulum itself, you will find many more interesting things. +Many different applications using Reticulum already exist, serving a wide variety of purposes +from day-to-day communication and information sharing to systems administration and tackling +advanced networking and communications challenges. -## Browsing & Publishing +Development of Reticulum-based applications and systems is ongoing, so consider this list +a non-exhaustive starting point of *some* of the options available. With a bit of searching, +primarily over Reticulum itself, you will find many more interesting things. -In addition to the programs listed in this section, other Nomad Network clients include: +### Remote Shell -- [MeshChatX](https://meshchatx.com/) -- [Columba](https://github.com/torlando-tech/columba/) +The [rnsh](https://github.com/acehoss/rnsh) program lets you establish fully interactive +remote shell sessions over Reticulum. It also allows you to pipe any program to or from a +remote system, and is similar to how `ssh` works. The `rnsh` program is very efficient, and +can facilitate fully interactive shell sessions, even over extremely low-bandwidth links, +such as LoRa or packet radio. + +In addition to the default, fully interactive terminal mode, +for extremely limited links, `rnsh` offers line-interactive mode, allowing you to interact +with remote systems, even when link throughput is counted in a few hundreds of bits per second. ### Nomad Network -The terminal-based program [Nomad Network](https://github.com/markqvist/nomadnet) provides a complete encrypted communications suite built with Reticulum. +The terminal-based program [Nomad Network](https://github.com/markqvist/nomadnet) +provides a complete encrypted communications suite built with Reticulum. It features +encrypted messaging (both direct and delayed-delivery for offline users), file sharing, +and has a built-in text-browser and page server with support for dynamically rendered pages, +user authentication and more. -It features encrypted LXMF messaging (both direct and delayed-delivery for offline users), file sharing, and has a built-in text-browser and page server with support for dynamically rendered pages, user authentication and more. It also includes an RRC client and a suite of network utilities. +[![image](screenshots/nomadnet_3.png)](https://github.com/markqvist/nomadnet) -### Ren Browser - -[Ren Browser](https://github.com/Quad4-Software/Ren-Browser) is a modern, standalone browser for Nomad Network using the [Reticulum-Go](https://github.com/Quad4-Software/Reticulum-Go) implementation. The project is actively being developed, and should be considered alpha level software, but much of the functionality is already working. If you want to help expanding the ecosystem, this is an excellent project to take a look at. - -Ren Browser is great example of an application aiming for true, seamless interoperability with the existing software ecosystem. It can operate fully standalone using it’s own embedded [Go implementation](https://github.com/Quad4-Software/Reticulum-Go) of Reticulum, or seamlessly utilise a shared RNS instance already running on the system. +[Nomad Network](https://github.com/markqvist/nomadnet) is a user-facing client +for the messaging and information-sharing protocol LXMF. ### RNS Page Node -[RNS Page Node](https://github.com/Quad4-Software/rns-page-node) is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files. +[RNS Page Node](https://git.quad4.io/RNS-Things/rns-page-node) is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files. ### Retipedia -You can host the entirity of Wikipedia, StackExchange or any other `.zim` file, to Nomad Network clients using [Retipedia](https://github.com/RFnexus/Retipedia). - -### Git Over Reticulum - -The included [rngit](git.md#git-main) system provides a fully distributed solution for managing, hosting, collaborating on and interacting with Git repositories over Reticulum. - -## Messaging & Voice +You can host the entirity of Wikipedia (or any `.zim`) file to other Nomad Network clients using [Retipedia](https://github.com/RFnexus/Retipedia). ### Sideband -[Sideband](https://github.com/markqvist/Sideband) is a technical LXMF and LXST client, for Android, Linux, macOS and Windows. It also serves as a multi-purpose Reticulum utility, with features and functionality targeted at advanced users. +If you would rather use an LXMF client with a graphical user interface, you can take +a look at [Sideband](https://unsigned.io/sideband), which is available for Android, +Linux, macOS and Windows. Sideband is an advanced LXMF and LXST client, and a multi-purpose Reticulum +utility, with features and functionality targeted at advanced users. -Sideband allows you to communicate with other people or LXMF-compatible systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR Paper Messages, or anything else Reticulum supports. +Sideband allows you to communicate with other people or LXMF-compatible +systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR +Paper Messages, or anything else Reticulum supports. -It interoperates with all other LXMF clients, and provides advanced features such as PTT and voice messaging, real-time voice calls, file attachments, private telemetry sharing, and a full plugin system for expandability. +It also interoperates with all other LXMF clients, and provides advanced features such as voice messaging, +real-time voice calls, file attachments, private telemetry sharing, and a full +plugin system for expandability. ### MeshChatX -[MeshChatX](https://meshchatx.com/) is an all-in-one Reticulum client with messaging, calling, and a NomadNet browser, providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. +A [Reticulum MeshChat fork from the future](https://git.quad4.io/RNS-Things/MeshChatX), with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original [Reticulum MeshChat](https://github.com/liamcottle/reticulum-meshchat) project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork. -Features include full LXST support, custom voicemail, phonebook, contact sharing, ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security. - -### Columba - -[Columba](https://github.com/torlando-tech/columba/) is a simple and familiar LXMF messaging app Android, built with a native Android interface and Material Design 3. It also includes LXST voice call functionality, and a mobile Nomad Network page browser. - -### Ren TUI - -[Ren TUI](https://github.com/Quad4-Software/Ren-TUI) is an early prototype terminal LXMF client for Reticulum built with Odin on librns (Reticulum-Go). It aims for NomadNet-like messaging/browsing without urwid or ncurses. - -## Chat +Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security. ### Reticulum Relay Chat [Reticulum Relay Chat](https://rrc.kc1awv.net/) is a live chat system built on top of the Reticulum Network Stack. It exists to let people talk to each other in real time over Reticulum without dragging in message databases, synchronization engines, or architectural commitments they did not ask for. +The [rrcd](https://github.com/kc1awv/rrcd) program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include [rrc-gui](https://github.com/kc1awv/rrc-gui) and [rrc-web](https://github.com/kc1awv/rrc-web). + RRC is closer in spirit to IRC than to modern “everything platforms.” You connect, you join a room, you talk, and then you leave. If you were present, you saw the conversation. If you were not, the conversation did not wait for you. This is not an accident. This is the entire design. -The [rrcd](https://github.com/kc1awv/rrcd) program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include: - -- [Nomad Network](https://github.com/markqvist/NomadNet/) -- [MeshChatX](https://meshchatx.com/) -- [Eridanus](https://github.com/torlando-tech/eridanus) -- [rrc-gui](https://github.com/kc1awv/rrc-gui) -- [rrc-web](https://github.com/kc1awv/rrc-web) - ### RetiBBS -[RetiBBS](https://github.com/kc1awv/RetiBBS) is an old-school bulletin board system implementation for Reticulum networks. +[RetiBBS](https://github.com/kc1awv/RetiBBS) is a bulletin board system implementation for Reticulum networks. RetiBBS allows users to communicate through message boards in a secure manner. -## Voice & Telephony +### RBrowser -In addition to the programs listed in this section, other LXST voice clients include: +The [rBrowser](https://github.com/fr33n0w/rBrowser) program is a cross-platform, standalone, web-based browser for exploring NomadNetwork Nodes over Reticulum Network. It automatically discovers NomadNet nodes through network announces and provides a user-friendly interface for browsing distributed content with Micron markup support. -- [Sideband](https://github.com/markqvist/Sideband) -- [MeshChatX](https://meshchatx.com/) -- [Columba](https://github.com/torlando-tech/columba/) +Includes useful features like automatic listening for announce, adding nodes to favorites, browsing and rendering any kind of NomadNet links, downloading files from remote nodes, a unique local NomadNet Search Engine and more. ### Reticulum Network Telephone @@ -101,67 +102,32 @@ The `rnphone` program, included as part of the [LXST](https://github.com/markqvi It supports interfacing directly with hardware peripherals such as GPIO keypads and LCD displays, providing a modular system for building secure hardware telephones. -### Partyline - -[Partyline](https://github.com/RFnexus/partyline) is a group voice application for Reticulum, built on [LXST](https://github.com/markqvist/LXST), inspired by [Mumble](https://github.com/mumble-voip/mumble). - -It provides both realtime group voice over any Reticulum transport capable of >6 kilobits per second, LXST dial-in support, listen-only music/broadcasting rooms, and many other useful functions. - ### LXST Phone -The [LXST Phone](https://github.com/kc1awv/lxst_phone) program is a cross-platform desktop application for performing LXST voice calls over Reticulum. It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management. +The [LXST Phone](https://github.com/kc1awv/lxst_phone) program is a cross-platform desktop application for performing LXST voice calls over Reticulum. -## System Administration - -### Remote Shell - -The included [rnsh](using.md#using-rnsh) program lets you establish fully interactive -remote shell sessions over Reticulum. It also allows you to pipe any program to or from a -remote system, and is similar to how `ssh` works. The `rnsh` program is very efficient, and -can facilitate fully interactive shell sessions, even over extremely low-bandwidth links, -such as LoRa or packet radio. - -In addition to the default, fully interactive terminal mode, for extremely limited links, -`rnsh` offers line-interactive mode, allowing you to interact with remote systems, even -when link throughput is counted in a few hundreds of bits per second. - -### RNS FileSync - -The [RNS FileSync](https://github.com/Quad4-Software/RNS-Filesync) program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing. - -## Utilities & Components - -### Modem73 - -[MODEM73](https://github.com/RFnexus/modem73) and its companion [Modem73Interface](https://github.com/RFnexus/modem73interface) is an open source software modem that works with any HF, VHF, or UHF radio capable of 2400 Hz of bandwidth. All you need is a sound card and audio cable for your radio. +It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management. ### LXMFy -[LXMFy](https://lxmfy.quad4.io/) is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. [Bot implementations exist](https://lxmfy.quad4.io/latest/creating-bots/) for Home Assistant control, LLM integrations, and various other purposes. +[LXMFy](https://lxmfy.quad4.io/) is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. [Bot implementations exist](https://github.com/lxmfy/awesome-lxmfy-bots) for Home Assistant control, LLM integrations, and various other purposes. + +### LXMF Interactive Client + +[LXMF Interactive Client](https://github.com/fr33n0w/lxmf-cli) is a feature-rich, terminal-based LXMF messaging client with many advanced features and an extensible plugin architecture. + +### RNS FileSync + +The [RNS FileSync](https://git.quad4.io/RNS-Things/RNS-Filesync) program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing. ### Micron Parser JS [Micron Parser JS](https://github.com/RFnexus/micron-parser-js) is the JavaScript-based parser for the Micron markup language, that most web-based Nomad Network browsers use. If you want to make utilities or tools that display Micron pages, this library is essential. -### Reticulated - -[Reticulated](https://github.com/RFnexus/Reticulated) is a Reticulum Network simulator that spawns live, interactive Reticulum instances controllable from a node-based Web UI. - -You can create networks of any topology and set bitrate, MTU, and configure path loss and propagation behavior. It provides a top-down view to a live network. You can connect any Reticulum application to the simulated network. It also has built in utilities like a basic LXMF chat function and Resource transfer included. - ### RNMon [RNMon](https://github.com/lbatalha/rnmon) is a monitoring daemon designed to monitor the status of multiple RNS applications and push the metrics to an InfluxDB instance over the influx line protocol. -## Device Flashers - -In addition to the included command-line `rnodeconf` flasher, several community-provided RNode device flashers exist: - -- [RNS Moscow](https://flasher.rns.moscow/) -- [Aetherlab](https://flasher.aetherlab.org/) - -Several programs, such as [Sideband](https://github.com/markqvist/Sideband) and [MeshChatX](https://meshchatx.com/) also include built-in device flashers that can be used fully offline. - ## Protocols A number of standard protocols have emerged through real-world usage and testing in the Reticulum community. While you may sometimes want to use completely custom protocols and implementations when writing Reticulum-based software, using these protocols provides application developers with an easy way to implement advanced functionality quickly and effortlessly. Using them also ensures compatibility and interoperability between many different client applications, creating an open communications ecosystem where users are free to choose the applications that suit their needs, while remaining connected to everyone else. @@ -188,8 +154,8 @@ RRC is intentionally simple. It does not pretend to be email, a mailbox, or a di This section provides a list of various community-provided interface modules, guides and resources for creating Reticulum networks over special or challenging mediums. -* [Modem73](https://github.com/RFnexus/modem73) is an efficient software modem that can be used with Reticulum -* Custom interface module for running [RNS over HTTP](https://github.com/Quad4-Software/RNS-over-HTTP) +* Custom interface module for running [RNS over HTTP](https://git.quad4.io/RNS-Things/RNS-over-HTTP) * Guide for running [Reticulum over ICMP](https://github.com/matvik22000/rns-over-icmp) using `PipeInterface` * Guide for running [Reticulum over DNS](https://github.com/markqvist/Reticulum/discussions/1002) with Iodine -* Guide for running [Reticulum over HF radio](https://github.com/RFnexus/reticulum-over-hf) \ No newline at end of file +* Guide for running [Reticulum over HF radio](https://github.com/RFnexus/reticulum-over-hf) +* [Modem73](https://github.com/RFnexus/modem73) is a KISS TNC OFDM modem frontend that can be used with Reticulum \ No newline at end of file diff --git a/docs/markdown/support.md b/docs/markdown/support.md index 46ec7440..69a9aefe 100644 --- a/docs/markdown/support.md +++ b/docs/markdown/support.md @@ -24,6 +24,9 @@ Ko-Fi: https://ko-fi.com/markqvist ``` +Are certain features in the development roadmap are important to you or your +organisation? Make them a reality quickly by sponsoring their implementation. + ## Provide Feedback Feedback on the usage, functioning and potential dysfunctioning of any and diff --git a/docs/markdown/using.md b/docs/markdown/using.md index f930caf8..36f509c4 100644 --- a/docs/markdown/using.md +++ b/docs/markdown/using.md @@ -256,7 +256,8 @@ You can easily add `rnsd` as an always-on service by [configuring a service](#us ### The rnstatus Utility -Using the `rnstatus` utility, you can view the status of and control configured Reticulum interfaces, similar to the `ifconfig` program. +Using the `rnstatus` utility, you can view the status of configured Reticulum +interfaces, similar to the `ifconfig` program. **Usage Examples** @@ -317,8 +318,8 @@ Reticulum Transport Instance <5245a8efe1788c6a1cd36144a270e13b> running **All Command-Line Options** ```text -usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A] [-P] [-l] - [-B] [-b] [-t] [-q] [-s SORT] [-r] [-j] [-R hash] [-i path] +usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A] + [-l] [-t] [-s SORT] [-r] [-j] [-R hash] [-i path] [-w seconds] [-d] [-D] [-m] [-I seconds] [-v] [filter] Reticulum Network Stack Status @@ -330,19 +331,12 @@ options: -h, --help show this help message and exit --config CONFIG path to alternative Reticulum config directory --version show program's version number and exit - --attach name Attach interface by name - --detach name Detach interface by name - --reload name Reload interface by name -a, --all show all interfaces -A, --announce-stats show announce stats - -P, --pr-stats show path request stats -l, --link-stats show link stats - -B, --burst only show interfaces with active bursts - -b, --blocked-ips show blocked IPs per interface -t, --totals display traffic totals - -q, --queues display queue stats -s, --sort SORT sort interfaces by [rate, traffic, rx, tx, rxs, txs, - announces, arx, atx, prx, ptx, held] + announces, arx, atx, held] -r, --reverse reverse sorting -j, --json output in JSON format -R hash transport identity hash of remote instance to get status from @@ -350,8 +344,6 @@ options: -w seconds timeout before giving up on remote queries -d, --discovered list discovered interfaces -D show details and config entries for discovered interfaces - --show-stale show stale discovery entries - --show-unknown show discovery entries without version info -m, --monitor continuously monitor status -I, --monitor-interval seconds refresh interval for monitor mode (default: 1) @@ -678,7 +670,7 @@ For the full documentation on the rngit system, see the [Git Over Reticulum](git The `rnx` utility is a basic remote command execution program. It allows you to execute commands on remote systems over Reticulum, and to view returned command output. For a fully interactive remote shell solution, be sure to also take a look -at the [rnsh](#using-rnsh) program. +at the [rnsh](https://github.com/acehoss/rnsh) program. **Usage Examples** @@ -1187,7 +1179,6 @@ For more fine-grained control over how discovered interfaces are auto-connected, * The `autoconnect_interface_mode` options specifies which mode discovered interfaces should be created with when auto-connecting. * The `autoconnect_announces_to_internal` option allows you to specify that auto-connected interfaces should propagate announces to `internal` mode interfaces, even if the auto-connected interface’s mode would normally not allow for this. -* The `autoconnect_unverified_implementations` option will allow the interface discovery system to auto-connect to *any* announced interface, even those coming from unverified, experimental or potentially broken implementations. Be very careful with this option. ## Remote Management diff --git a/docs/source/brandolinis.rst b/docs/source/brandolinis.rst deleted file mode 100644 index 334dc662..00000000 --- a/docs/source/brandolinis.rst +++ /dev/null @@ -1,1625 +0,0 @@ -.. _brandolinis-main: - -Brandolini's Reference -====================== - - | *A little learning is a dang’rous thing;* - | *Drink deep, or taste not the Pierian spring.* - | *There shallow draughts intoxicate the brain,* - | *And drinking largely sobers us again.* - | - | - Alexander Pope, 1711 - -This chapter serves as a helpful reference for developers, users and community members living in an Age of Bullshit Manufacture equivalent to the Cambrian Explosion. - -As we are surely only at the on-ramp of this trajectory, the chapter will continue to evolve, expand and be updated based on community feedback as events occur. - -.. _brandolinis-what-the-license-is: - -What The Reticulum License Is ------------------------------ - -*To understand the legal reality relating to this section, see* :ref:`Void Grants`. - -The :ref:`Reticulum License` is the license under which RNS, the Reticulum reference implementation, has been published since **April 15, 2025**\ , in versioning effective from **0.9.4** and every release since. In practical terms, it covers all of "modern Reticulum": The current wire format, the cryptographic machinery as it exists today (including the AES-256-based link encryption, ratchets, et cetera), and all protocol additions that distinguish the modern implementation from the earliest pre-1.0.0 beta formats. If an implementation speaks today's Reticulum protocol, it is, in all relevant respects, built on Reticulum-licensed work. - -The license is a permissive license. It grants the full range of rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, free of charge; subject to three conditions, which are reproduced here verbatim: - -.. - - * - The Software shall not be used in any kind of system which includes amongst its - functions the ability to purposefully do harm to human beings. - - * - The Software shall not be used, directly or indirectly, in the creation of an artificial - intelligence, machine learning or language model training dataset, including but not - limited to any use that contributes to the training or development of such a model or - algorithm. - - * - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - -.. _brandolinis-why-the-license-exists: - -Why This License Exists -^^^^^^^^^^^^^^^^^^^^^^^ - -Reticulum was not moved to a custom license casually or quickly. It was licensed under the MIT license for the greater part of its existence, and that served well for a long time. The change came when it became clear that no existing license expressed what the project needed, and that the two most commonly proposed alternatives would either say the wrong thing or say nothing useful at all. Three factors had primary influence on the situation: - -**The harm problem** - -Reticulum is a tool that was built, deliberately and at great cost, to empower people; to make communication something you own rather than something you are temporarily granted. It has also been, at various times, exactly what certain kinds of organisations would like to use to control people. This is the duality of any powerful tool. A permissive license could not, and cannot, say anything about that; MIT is silent on the question of whether the software may be used to build systems designed to harm human beings. The Reticulum License is not silent. It does not pretend that a license can stop an individual madman; it does assert, however, plainly and in enforceable terms, that corporations and organisations integrated into the legal system cannot consume this software for harm and then claim they never knew. The line is drawn by the creators of the tool, not by an external authority, and it is drawn deliberately. This is covered in more depth in the discussion of the ethics of the tool in the *Zen of Reticulum*. - -**The appropriation problem** - -The second factor was the machine-learning appropriation of the open-source commons. By 2025 that was no longer hypothetical: The commons was already being systematically ingested by corporate training pipelines, and the code, words, and creativity of the people who built it were being converted into synthetic systems designed to replace them. The license responds to this with an explicit condition: The software shall not be used in the creation of an AI, machine-learning, or language-model training dataset, nor in any use that contributes to the training or development of such a model or algorithm. - -It is important to understand what this condition does and does not do, which matters for what follows in this chapter. **The AI-training condition is not the legal basis for protecting the code from machine copying**. That protection *already exists*\ , in the oldest and most well-tested form of copyright law, and it applies regardless of any license (see :ref:`Copyright & The Two Legal Layers`). - -The AI-training condition addresses something *additional*\ : the appropriation of the code *into the models themselves*\ ; the ingestion of human-made work as raw material for a synthetic replacement, and the use of that ingestion to fabricate "prior work" minefields, patent-troll material, and a wall of slop meant to bury and devalue the actual ecosystem. That danger was foreseeable; the licensing decision was made with full awareness of it. The events of 2026 have since demonstrated, in public and in detail, that the concern was not theoretical. - -**The sustainability problem** - -Reticulum has been developed and maintained for over a decade by a single person, supported by a small group of contributors and network-building users, without corporate backing, without VC funding, through several serious attempts at commercialization and takeover from both the corporate and the ideological side. The license is part of what makes that sustainable, and possible at all. - -It ensures that the specific, painstaking labor of the reference implementation cannot be hijacked to undermine the intent of the project, while leaving the protocol itself, the mathematical rules of how Reticulum works, dedicated to the public domain, where it belongs to humanity and can never be owned. Anyone can implement the protocol, provided they themselves invest the *actual* work and effort. What the license protects is the work, and the intent embodied in it. - -.. _brandolinis-on-freedom: - -On Freedom -^^^^^^^^^^ - -The most persistent mischaracterisation of the Reticulum License is that it is "more restrictive" than the licenses it replaced or than the GPL family. This is backwards, and I'll state the comparison plainly, because the confusion is the foundation of a great deal of deliberately manufactured controversy examined later in this chapter. - -The GPL and AGPL are constructed around a single, self-referential axiom: that "free" is *defined by the terms of the license itself*\ , and that any use falling outside those terms is not free. In practice, this means the GPL restricts the freedom of **everyone** - you may not write, distribute, link, or (in some cases) even communicate with GPL-covered software without the entire arrangement being pulled under the same terms, which the licensor then controls. The restriction applies to 100% of users, including the ones building purely constructive things. - -The Reticulum License restricts the freedom of almost nobody. Its restrictions apply only to: - -* Those who wish to use the software in systems that purposefully harm human beings, and -* Those who wish to ingest the software into AI training pipelines. - -That is a restriction on a vanishingly small fraction of would-be users - the 0.0000001% - and it is a restriction they can characterise however they like, forever. The bickering is itself a signal that the licensing decision hit exactly the right target. - -Everyone else retains the full breadth of use, modification, distribution, and sale, with none of the viral encumbrance of copyleft. - -Both licenses contain moral axioms. The difference is that the Reticulum License states its axioms out loud: - -* *Intentionally restricting the way that millions of people use and build upon a foundational - communications technology* - **The mechanism of copyleft** -* *Intentionally restricting a small number of organisations from using the technology to - violently harm human beings* - **The mechanism of the Reticulum License** - -Which of these is more free? The answer is not a matter of committee approval. It is a matter of what the word "free" means to the people *whose freedom is at stake*. The question of whether "open source" requires the blessing of a particular licensing orthodoxy is examined in :ref:`Open Source Means Open Source`. - -.. _brandolinis-prediction: - -The Prediction, And What Happened -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When the license change was made and publicly discussed in 2025, the following was said, in public, about the machine-learning threat: - -.. - - A generative system could be trained on the Reticulum codebase, and for extremely low cost - be set to work spewing out millions of source repositories for all kinds of ridiculous - "Reticulum-based programs". This is already happening. You can find several examples of this - on GitHub already. … the point of this kind of trolling is to establish as wide a base for - "prior work" as possible. By doing that, a patent troll or IP abuser can create an immense - minefield for real developers. - - -This was written when the phenomenon was just beginning. It was not a guess; it was a description of a mechanism already in motion, in the specific context of Reticulum, by people who had already begun to appear around the project. - -Less than a year and a half later, the prediction has now been demonstrated, in public, exhaustively and to the letter. There are now roughly a dozen machine-generated "Reticulum implementations" of the same shape: fluent, marketing-heavy, some without attribution, and - in several cases - actively harmful to the network. The :ref:`Prns case` is an example documented in this chapter. The pattern that produced it (and the people, mechanisms, and the recycling of falsehoods behind the *wider* phenomenon) is examined in :ref:`A Movement Of A Dozen`. - -The license was written, in part, so that when this happened, the community would not be defenceless. - -.. _brandolinis-means-for-other-implementations: - -What This Means For Other Implementations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Two simple things must be kept separate: - -#. - **The protocol is public domain.** The mathematical rules of Reticulum - addresses, - signatures, routing, the wire format as an idea - belong to humanity. A *genuine* clean-room - implementation of the protocol, written by human beings who reasoned it out themselves, is - free and welcome, and owes nothing to the Reticulum License. Nothing in this chapter is an - argument against implementing the protocol. On the contrary: More good implementations, - honestly made, are good for the network. - -#. - **The implementation is licensed.** Copying the reference implementation - by machine or - by silk-screen press - and modifying and redistributing the result, is - exercising rights granted by the license, and is therefore subject to its conditions: The attribution - notice, the harm condition, the AI-training condition. A copy that does not follow those - conditions is **not** licensed, and it is not "open source in spirit"; it is an unauthorized - derivative work, and therefore a copyright violation. - -The distinction is the same one the license itself makes, and it must be held firmly: An implementation that *speaks* Reticulum is exercising the public domain protocol, **if** it was truly created independently. An implementation that was *generated from* RNS, reproduced by a copy process, is a derivative of licensed work. - -.. _brandolinis-copyright-and-layers: - -Copyright & The Two Legal Layers -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The exact legal structure is frequently muddied in public discussion; and in several cases deliberately. - -**Layer A - Copyright (license-independent)** - -Copyright does not depend on any license. A primarily machine-generated copy of a copyrighted work - a copy not accompanied by demonstrable, substantial, new human creative input - is a derivative work over which the copier holds no copyright whatsoever, and the reproduction of which, without the original author's permission, is an infringement of the original author's copyright. This is standard, well-tested law, and it applies with equal force under the MIT license, the GPL, or any other license, because it is not about the license at all; it is about who created what. A process that copies the reference implementation into a machine, produces a transformed derivative, and then claims "Copyright, [name of the process operator]" - with no human able to account for the creative decisions in the result - **is not** asserting a valid copyright over the result. They are openly asserting the absence of one, and reproducing someone else's work without authorisation. This is probably the cleanest cut copyright violation one could imagine. - -**Layer B - License Conditions (Reticulum License specific)** - -Where a copy *is* a licensed derivative, the conditions of the Reticulum License apply: The copyright notice and permission notice must be included in all copies and substantial portions; the harm condition must be honoured; the AI-training condition must be honoured. A derivative that follows none of these is unlicensed *a second way over*. - -The two layers are often intentionally confused in public discussion - the AI-training condition is waved around as if the entire question of machine copying rested on it, when in fact machine copying of copyrighted work is a copyright violation that would exist even under the most permissive license ever written. The purpose of keeping the layers separate is not rhetorical flourish; it is that Layer A is old, settled law, and does not rely on any novel legal theory, while Layer B is specific to this license. Both matter. Neither is a substitute for the other. - -Layer B is the required mechanism for closing the loophole where the work is *first* ingested during model training into the weights of a model itself, which would make any resulting violations almost impossible to detect and enforce. - -.. _brandolinis-cleanroom-delusion: - -The "Clean-Room" Delusion -^^^^^^^^^^^^^^^^^^^^^^^^^ - -I regularly receive mail or LXMF messages from people who proudly and happily proclaim that they have created a "clean-room" implementation of one of the software systems I maintain; RNS, Sideband, LXMF or others. The declaration is delivered with the flat certainty of someone who believes they have found the legal lock-pick that settles everything at once: Their production is independent, uncontaminated, and therefore free of any obligation to the work they copied, its license, or its author. The proclamation usually concludes with a request for blessing, or at least acknowledgment. The proclamation is, usually, also generated by an LLM. - -This is a delusion, and I'll cut through it surgically. The word they here invoke as magic is attempting to do a great deal of work it was never able to bear. - -**What a clean room actually was** - -The legal "clean room" is not folklore, incantation, witchcraft or a claim a person gets to make about their own output. It is a very specific, institutionalized procedure that grew out of the hardware and software litigations of the 1980s, and it existed for exactly one purpose: **Convincingly proving the absence of copying in a court of law**. - -The canonical case was Phoenix Technologies and its 1984 reimplementation of the IBM PC BIOS. IBM's BIOS was copyrighted, which meant that the entire PC-compatible industry depended on being able to show that a compatible BIOS had been written independently, and not from IBM's code. Phoenix's answer was a physical and procedural firewall: - -* One team, the "dirty room", read the original code and produced written *functional specifications*\ : descriptions of what the BIOS did, not how IBM's code did it. -* A second team, the "clean room", consisted of engineers who had *never seen the original code at all*\ , and they wrote the new BIOS from those specifications alone. - -This obsessively documented, legally attested separation was not a preference or a willy-nilly show for a kindergarten; it was the entire legal point, and it had to be *solid*. If even one clean-room engineer had *glanced* at IBM's code, the clone would have been contaminated, and the defense would shatter. - -The other landmark case was *Computer Associates v. Altai* (1992), in which Altai had copied CA's scheduler code into its own product, and (upon being caught) hired outside counsel to run a genuine clean-room procedure: A programmer with no exposure to the original was brought in to rewrite the infringing modules from functional specifications prepared by the team that had seen the code. The court credited the good-faith effort precisely because it was done, as the carefully documented record showed, to the letter of the procedure. - -Bear in mind what this procedure entailed in practice, because this is the part the modern claimants never seem to have even *heard* of: Physical separation of teams, legal counsel managing the process, signed and notarised declarations of non-exposure, controlled specification documents that described *behaviour* and never *code*, months of work, and testing against the specification - not against the original, which the implementers were forbidden to consult. - -The people on the clean-room side of the wall did not just *claim* they had not read the code. Their non-exposure was institutionally guaranteed, obsessively documented, and legally attestable through every fart and sneeze of the process, because the entire point was that a court - hostile, adversarial, and entitled to discovery - would believe it. - -*That* is the process. "Clean-room implementation" is the name of a *certified procedure for proving independent authorship*. It is not a mood, a marketing label, or a property a person gets to declare about their own work. - -**The moronic inversion** - -Now consider what is actually being described when someone says: *"I created a clean-room implementation of Reticulum by feeding the source code into a machine."* - -That sentence is the opposite of a clean room. Feeding a copyrighted work into a machine so that it reproduces and transforms the work is **the definition of copying**\ , and a clean room is the one procedure whose entire purpose is the demonstrated *absence* of copying. This process cannot demonstrate the absence of copying, because the process is *composed of* copying. The cleanest possible room has been replaced by the dirtiest possible room, and the dirt has been installed as the base feature. What these claimants have actually performed is a photocopy with extra steps, and the extra steps, far from cleaning anything, are simply steps that forge a certificate they believe the output has earned. - -**Why the word is deployed** - -The word "clean-room" is deployed because it sounds like what the speaker wishes were true. It carries the accumulated legal prestige of the 1980s and 1990s - the BIOS clones, the Altai credit - and it sounds like a certificate already issued: *Independent, original, unencumbered*\ , the work of a person who owes nothing. The claimants want that halo without the procedure: No law firm, no physical separation, no written specifications, no signed declarations, no months of process - just a laptop, a loop, and the word "clean-room" typed into a README. The word must do the work of the process it replaces, and the delusional replacement is the entire point. - -**The irony runs deep** - -The Reticulum protocol is public domain. Any human being can implement it - from the mathematics, from the documentation, from their own reasoning - and owe nothing whatsoever to the license, to the reference implementation, or to me. - -An honestly written implementation needs no incantations, no legal theater, and no special vocabulary; it simply *is* what it claims. It's plain for everyone to see. **It's also plain for everyone to see when you're faking it**. The people who reach for the word "clean-room" are, without exception, the people who need it to create a fantasy - and the people who need that are precisely the people who cannot have it. If what you wrote were genuinely independent work on the public-domain protocol, you would not need to call it anything. The word is not a seal of independence. In this context it is a confession of copying, filed by the copier, in the confident belief that it says the opposite. - -.. _brandolinis-opensource: - -Open Source Means Open Source ------------------------------ - -An objection regarding the "open source status" of RNS appears occasionally in variations, but always reduces to the same syllogism: - - -#. "Open source" means *licensed under one of the licenses approved by the Open Source - Initiative (or, in other variants, compatible with GPL orthodoxy)*. -#. The Reticulum License is not on that list. -#. Therefore Reticulum is not open source, and anything built on it is illegitimate. - -The entire argument rests on the first premise, and the first premise is false. - -Open source is not a certification conferred by a committee. It is a description of a fact: The source code is available, and the rights to use, copy, modify, merge, publish, distribute, sublicense, and sell it are granted to everyone, free of charge. - -The Reticulum License does all of those things. It is, in every substantive respect that concerns the person who wants to use, study, modify, or build upon the software, **an open license**. What it adds - and what disqualifies it in the eyes of the orthodoxy - is two conditions on *who may do what with the original work*\ , not on what the *user* is allowed to do with it. - -The definition being wielded against Reticulum is not even a young one. It is, in essence, the Debian Free Software Guidelines written in 1997, adopted by the OSI in 1998, and treated ever since by certain people as if it had descended from a mountain on tablets. It is an approach that is close to three decades old. Being old is not itself a flaw - but being old is also not a proof of correctness, and the suggestion that the meaning of a word like "open" was settled for all time by a committee in the 1990s, and may never be revisited, is not a position rooted in reason. It is rooted in institutional inertia dressed as principle, maintained by those whose ideologies it serves. - -.. _brandolinis-what-the-argument-is-doing: - -What The Argument Is Actually Doing -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -I would like to go into some detail about the rhetorical mechanism, because it is roughly the same mechanism that powers the rest of the campaign described later in this chapter. - -The orthodoxy's argument is self-referential: *"open source means what we say it means, because we are the ones who define it."* When the Reticulum License is measured against that definition and found wanting, the conclusion is presented as a property of Reticulum - when in fact it is *a property of the definition*. The definition is treated as neutral and authoritative - as some base fact of raw cosmology - and the license is treated as deviant. But the definition is no more neutral than the license is. It encodes a moral position, and that moral position is exactly the one that this project declines to take. - -Consider what the orthodoxy's definition requires, in the case at hand. The OSI-based definition contains, in its ancestry, the Debian Free Software Guideline of "No discrimination against fields of endeavor": The software must not restrict who may use it or for what purpose. Taken to its logical end, this means that open source, properly "certified", must be equally available to those who build hospitals and those who build weapons, those who connect people and those who control them, those who create and those who ingest and replace. - -The orthodoxy presents this as neutrality. It is not neutrality; it is a moral position - one that says, in effect, that *harming human beings is a field of endeavor like any other, and software must not discriminate against it*. - -The Reticulum License takes a different position, and it has the honesty to state it in the license text: this software is for life, not for death; it is for people, not for the systems that would replace them. That is not a restriction on openness, nor on freedom. It is a restriction on *who may consume the work*\ , and it is a restriction the orthodoxy's definition cannot express - which is why the orthodoxy must always attack the license as "not open source" rather than engage with what it actually does. - -.. _brandolinis-who-is-restricted: - -Who Is Restricted By Which License? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The arithmetic is simple, so let's do it out loud, because the orthodoxy's framing inverts it every time. - -The GPL family imposes its conditions on **everyone**\ : You may not distribute, link, or - in the AGPL case - even *communicate with* GPL-covered software without your own work being pulled under the same terms. "Free" in this system is defined (self-referentially) *by the license's own terms*\ ; everything outside those terms is, by definition, unfree. The restriction applies to 100% of users. It applies to the hospital, the mesh community, the school, and the hobbyist with the same force it applies to anyone else. That is not freedom for everyone; it is a *specific moral architecture applied to everyone*\ , presented *as if it were the absence of one*. - -The Reticulum License imposes its conditions on a fraction of would-be users so small as to be practically unmeasurable: - -* Those whose intended use is to build systems that purposefully harm human beings, and -* Those whose intended use is to ingest the work into AI training pipelines. - -That is the entire list. Everyone else - the overwhelming majority, the community, the developers, the users, the downstream projects - receives the full, unrestricted rights to use, copy, modify, merge, publish, distribute, sublicense, and sell the software, with none of the viral encumbrance of copyleft. - -So when the claim is made that the Reticulum License "is more restrictive" than the GPL, the claim is not merely wrong; it is the exact inverse of the truth. The GPL restricts the freedom of everyone, in the name of a specific ideology. - -The Reticulum License restricts the freedom of the harm-doer and the appropriator, and leaves everyone else free. The two approaches do not differ in whether they contain moral axioms - **all licenses do**. They differ in whether the axioms are stated openly, and in *whose* freedom they restrict. - -.. _brandolinis-consequences-not-open: - -The Consequences Of The "Not Open Source" Claim -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This would be a footnote debate if the mischaracterisation had no practical consequences. It has very large ones, and therefore this section exists. - -The claim that "Reticulum is not open source" is the foundation of the takeover narrative it so often accompanies. The sequence is always the same: - -* First, declare the reference implementation illegitimate ("not really open source", "owned by one man", "not a real community project") -* Second, conclude that it is therefore acceptable - even necessary - to replace it. -* Third, present the replacement, which is (conveniently) often the speaker's own machine-generated clone, as the legitimate heir. - -This is not speculation about motives; it is the documented structure of the campaign examined in :ref:`A Movement Of A Dozen`. - -The "not open source" claim is the legal-ideological battering ram of that campaign. It is recycled, in article after article, in forum after forum, because it works on people who assume that a committee's definition is the same thing as the truth. - -It is also worth stating the other obvious fact: None of these critics have ever shown that the Reticulum License restricts *them*. They do not build weapons. They are not training models on the codebase. They are free, in every practical sense, to use Reticulum exactly as they would under MIT or GPL. Their objection is not to a restriction on themselves; it is to the *mere existence* of a license that states a moral position they disagree with, or which they can use as leverage for a different goal entirely, and to the institutional fact that it does so outside *their* system of approval and control. The complaint is not that the license makes them unfree. The complaint is that the license is *free of them*. - -.. _brandolinis-distribution-argument: - -The Distribution Argument -^^^^^^^^^^^^^^^^^^^^^^^^^ - -A recurring companion claim is that RNS is not properly "distributed", usually deployed as "it's not packaged as a .deb" or "it's not on package manager X", with the implication that it is somehow unavailable or inaccessible, or hard to install on a given system. - -This is simply false, and it has been false for years. The reference implementation is available on practically every platform and architecture with a single command (\ ``pip install rns``\ ). It is packaged in the Arch User Repository, in NixOS, and in a range of other distribution channels, community-maintained and official. It can be installed, updated, and kept current *over Reticulum itself* - the network transports its own software updates. It is used on more than a hundred thousand devices worldwide, is embedded in over a hundred other packages and applications, and carries terabytes of traffic every day. - -The claim that Reticulum needs a ``.deb`` to be "really distributed" mistakes the mechanism for the goal. The goal is that people can obtain and update the software easily, everywhere, **on their own terms**, and distribute it themselves, both offline, online and over Reticulum with full cryptographic verification of the install packages, without any central infrastructure. - -That goal is met to an excellence that few software packages can speak of, and met in a way that does not pin the project's release cadence to the review cycles of distribution maintainers, which is an achievement, not a bug. - -A project like RNS that updates continuously, sometimes daily, and has a track-record of shipping security updates mere hours after issues are discovered, would be crippled by packaging that makes every change wait months for approval. The distribution complaint is a strawman generated for the occasion. It has been answered, in public, many times; it is repeated anyway, because the people repeating it are not counting on facts - they are counting on the reader never having heard the answer. - -.. _brandolinis-on-institutions: - -On The Institutions, In Fairness -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -None of the above is hostility toward the Open Source Initiative or the Free Software Foundation as institutions, nor toward the individuals who have dedicated decades of work to them. Their contribution to the world of software is **enormous**\ , and this document does not dispute it. What is disputed is a specific claim of authority: That "open source" is a term whose meaning is reserved to a committee's approval, and that a license which does not submit itself to that approval is therefore not open. - -That claim of authority is not in Reticulum's interest, and it is not in the interest of the people the movement claims to serve. The current legal and technological landscape - the machine-appropriation of the commons, the weaponisation of "prior work" generation, the erosion of human agency by synthetic systems - was not anticipated by the DFSG of 1997, and the institutions have not, as of this writing, produced *any* meaningful answer to it. The choice, as it was put at the time of the license change, is whether developers should sit back and wait for a solution from a committee that "may or may not ever materialize, before the entire foundation we stand on has been eaten away". - -**Reticulum's answer was to act**. If we had waited, it would have been too late. - -If an OSI-approved license existed that expressed the same constraints - and did not impose the far more severe restrictions of copyleft - the project would have used it. None such exists. That is not Reticulum's failure. It is the measure of what the definition is missing. - -.. _brandolinis-what-open-means: - -What "Open" Means Positively -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For anyone who wants the positive statement rather than the rebuttal, Open Source, in this project's understanding, means: - -* The source code is available, to everyone, without charge -* Everyone may use, copy, modify, merge, publish, distribute, sublicense, and sell the - software -* No one is discriminated against as a person - developer, user, community, or project -* The protocol itself is dedicated to the public domain, where no one can own it -* And the single, plainly stated condition of the license is that the work shall not be - consumed to harm human beings, or ingested to replace the humans who made it - -If that is not "open" by a definition written in the 1990s, then the definition is the thing -that has failed - not the software, and not the people who use it. - -**Open source means open source**. It was true before the committees existed to certify it, and it will remain true after they are gone. - -.. _brandolinis-assistance-versus-substitution: - -Assistance Versus Machine-Substitution --------------------------------------- - -.. _brandolinis-false-dichotomy: - -False Dichotomy -^^^^^^^^^^^^^^^ - -The public discussion around machine learning and software development has been derailed, by the loudest voices on both sides, into a farcical fight between "AI enthusiasts" and "AI haters". - -.. - - **Both positions are absolute caricatures.** - - **The fight is beside the point in the ways that matters most.** - -It is not an accident, but a very conscious choice that the initial analysis of the Prns case examined herein, and this very chapter, were themself produced with the assistance of a machine-learning based generative system. - -Specifically, it was generated using ``lc`` managing a ``Q8_K_XL`` quant of ``DeepSeek v4 Flash`` running in the ``llama.cpp`` inference engine. The main context base used to stabilize, ground and inform the generative environment consisted of approximately 100,000 tokens of text material that I personally thought out and wrote (by hand), then archived and organized, over the last eight years. - -If you know my writing, and have a hard time discerning where this chapter is me directly, typing manually, and where it is the machine output, it is because of two things: Almost every line in this chapter was either directly edited by my hands on the keyboard or typed by me, and the text corpus that informed the process *is* genuinely me - my thoughts, written by me, over a period of years. - -The chapter was produced this way *deliberately and openly*, under a software framework I designed and wrote, with the human directing the inquiry points, flow of investigation, structure of the outputs, contents of every section, making the editorial decisions, reviewing the conclusions, painstakingly adjusting every line, and taking responsibility for the final claims presented, with the goal of reaching a higher level of synthesis and symbolic density than I could have achieved without that assistance. - -The tool was *used as tool*, both to structure and produce a complex chapter, and to inspect a repository so preposterous in size, commit velocity and nonsensical wording that it barred any purely human inquiry. - -As such, the position is hereby, by personal example and demonstration, stated clearly: - -* **Technological assistance is not the problem** -* **It was never the problem** -* **It is the opposite of the problem** - -The problems are *substitution* and *usurpation*\ : The perverse process in which the machine becomes the author of choices, and the human becomes the facade of software, while expecting "reciprocity" or recognition for deeds they never performed. - -.. _brandolinis-the-line: - -The Line -^^^^^^^^ - -The exact distinction can be reduced neither to how many lines of code a machine produced, nor who pressed the keys. The final distinction has to rest on **who, or what, the real agent of the work is**. - - -* **Assistance** is when the human retains the full seat of agency: The human decides what to build, judges - what is correct, rejects what is wrong, understands what is shipped, and can answer for every single line of it. - The machine writes, suggests, drafts, accelerates *meaningfully* and within reason - but every choice that survives into the - final work is a choice the human made *consciously*\ , and understood - or grew to understand - the consequences of. -* **Substitution** is when the machine *becomes* the agency: the machine decides what to build, - decides what is correct, iterates on its own errors, generates at a scale and speed no single - human can follow - and the human's role is reduced to providing the name, the account, the - signature, and the marketing launch. - -.. - - The Line can be stated as a single test, and this is the same test that has always separated authorship from couriership: **Can the person whose name is on the work answer for it fully?** Not "can they describe what it does" - can they account for why it is the way it is, line by line and decision by decision, and the history of how it came to be that way, under questioning, without machine assistance, including by people who know more than they do? - -If they cannot, they did not author the work. They wore it, or in more severe cases, they stole it. And a courier who claims authorship is not a writer; they are a fraud. - -.. _brandolinis-what-assistance-looks-like: - -What assistance looks like -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Assisted work is recognisable, and recognisability is itself part of its value. It has the following observable properties: - -#. **The human can explain it.** Under examination, the author can say why this choice was - made, what was rejected, and what they verified. The ability to explain is not a luxury; - it is the very demonstration of authorship. On a large project with many contributors this is partial in - practice - no one can explain every line of a large codebase - but the *full capacity* must - exist between the authors and the *habit* must be visible. -#. **The human can, and does, reject machine output.** An author who has never rejected the machine's output is not - an author, but an open drain. Rejection is not a stylistic flourish; it is the - mechanism by which any work attains Quality. A record of decisions, including reversals and rejections, - is a sign of agency. A record without rejection is a record of assent-by-default. -#. **The human's judgment is directional.** The work contains the human's actual choices; - things the machine would not have decided on its own, which the human can defend on their - merits. This directionality has many names in engineering: Design, architecture, taste, - judgment. Where it is absent, the work has the uniform, self-consistent texture of - something that was never doubted, and doubt is exactly what anything important requires. -#. **Provenance is not concealed.** Disclosure of how the work was made is not a legal - requirement in every jurisdiction; it is not even always a practical requirement. But it - is a *statement* that matters. A project that discloses its process - - including its use of machines - is telling the truth about the relationship between the - human and the work, and that is a property users *are* entitled to know. Concealment of - provenance is not a stylistic choice; it is deception, made by someone who knows what the - record would otherwise show. -#. **The human stands behind the failures.** When assisted work fails, the human can say: Yes, I - chose this, under these assumptions, here is why, here is what I got wrong, here is the fix. - That sentence is the entire contract between an author and the people who depend on their work. - A human who cannot say it - because the choices were never theirs - *has nothing to offer anyone*\ , and - is by definition entirely replaceable; anyone else could have pressed the same button. - -.. _brandolinis-what-substitution-looks-like: - -What substitution looks like -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Substitution is equally recognisable, once the pattern is known: - -#. **Scope that no human could have authored.** Output that - structurally cannot have been the product of human authorship, review, and judgment, for - example when the volume exceeds any reasonable human capacity within the span of the project's life. - When the review capacity and the output volume are incommensurable, the output was not - reviewed. It was generated, then forgotten, and the record was signed blindly. -#. **Uniform generation.** The work carries no trace of a human's changing mind. No style - drift, no contested decisions, no "we tried X and it was wrong, so we did Y". That is, *no - signs of actual development, growth and learning*. Instead, endless, self-referential - refactoring, renamed abstractions, and the same fluent register in every commit, - every document, every reply. A human's history shows the wear of judgment. - Pure generation shows the superficial polish of its absence. -#. **Discovery by recursive regeneration.** When errors are found, they are found by the machine's own - subsequent runs, not by human scrutiny - and the findings are recorded as planned - "reviews" and "alignments" rather than as the corrections they are. This is the tell: A - machine editing its own previous output, while the edit is recorded as if it were a - scheduled milestone. That's not review. That's a continuous generation loop revising itself, - with the attempt of an illusion-preserving voice-over. -#. **Laundered provenance.** Theatrical concealment machinery: Commit hooks that refuse - attribution of machine generation, contribution policies that declare machine output - "yours", processes that scrub personal traces from the history, fake identities in the - contributor list. All of these have one function: To make the record say what the "author" - *wished* had happened, rather than what happened. A project that builds such machinery is - not making a record of its work; it is using automated software to manufacture one. -#. **Authorship by assignment.** The human's name is on the work, but the human cannot - answer for it - because the human did not choose any of it, and knows it. The name is not a - signature of authorship; it is a *fraudulent claim of ownership* over something the claimant did not - make and cannot explain. In every field that has ever dealt with such claims - art, - literature, invention, journalism - *this has a name*. That **forgery** is now cheap in software - makes it no more forgivable. - -.. _brandolinis-infrastructure-matters: - -Infrastructure Matters -^^^^^^^^^^^^^^^^^^^^^^ - -For a hobby project, the distinction between assistance and substitution is a personal matter. For infrastructure - code that other people's communications, safety, and freedom run on - it is a matter of life and safety, and any statement about this must be entirely unapologetic. - -Infrastructure has one property that distinguishes it from almost all other software: While most of the people who depend on it cannot verify it themselves, **it must be verifiable, auditable and open to human inspection**\ , and be engineered in such a way that this is as frictionless and straight-forward as possible. - -The reference implementation of Reticulum was designed this way, and continues to make that goal a primary one. While it *is* a complex piece of software, with an intricate array of interlocking and moving parts, the core parts of RNS, the entire Reticulum protocol implementation, the transport logic, the cryptographic primitive interfaces, **is less than 15,000 lines of code**. That line count even includes the rather extensive comments, incredibly detailed logging facilities and other choices that makes the code easy to read and understand for humans, and easy to operate, verify and debug. It took real dedication, and *a lot* of hard work to achieve that. - -Exactly because not *everyone* can do this, it is a fundamental necessity for infrastructure software that it is designed and authored so that *as many as possible can*\ , and that the *chain of accountability* - who made it, who reviewed it, who answers for it - is held intact at every link. That chain is not decoration. It is the entire mechanism by which normal users decide to trust a system they cannot fully verify. The transparent foundations and the chain combined *is* the verification for most users. - -Substitution breaks the chain both at the foundations, and at its load-bearing link: The human at the top can no longer answer, because they were never the maker. Unfortunately, the chain does not always fail visibly; often it fails silently, at the exact moment a question goes deep, or when routing fails subtly at a critical moment. - -For the user, the result is not a project with a normal and humanly flawed author - it is a project *with no author at all*\ ; a signature with no substance behind it, a name that cannot be held to account, and a body of work that nobody on earth can (or wants to) explain, defend, or fix in a crisis. - -.. _brandolinis-the-fluency-trap: - -The Fluency Trap -^^^^^^^^^^^^^^^^ - -It is worth understanding, briefly, why substitution is so attractive and so dangerous at once. Machine-generated output is fluent. It is confident, well-structured, and terminologically *self-consistent*. At the surface, and from the perspective of someone outside a given craft, it is indistinguishable from the output of a knowledgeable human. Fluency of this kind can be a useful property: It makes assisted work fast. But fluency is *not* understanding. It is the *minimal conformal surface* of understanding. - -The danger is that the surface is exclusively where the substituter invests; for the simple reason that the surface is where most of the desired audience looks, and what yields attention to the project. - -A human who can tell the difference uses the tool well. A human who cannot is being deceived through means they cannot detect from inside, namely through their own inability to account for the limits imposed by what they do not know. - -This is why the most important quality for anyone working with systems like Reticulum is not speed, and not vocabulary, but the oldest engineering virtue there is: The honest, uncomfortable, terrorizing knowledge of **what you do not know**. The substitute *never* has it. The substitute's operator, *if* they ever had it, loses it precisely the moment they accept the fantasy of the substitution process. - -The tool is not the hazard, plain old human nature is. The tool is a mirror, and mirrors - as has been observed for rather longer than software has existed - show the truth only to those willing and yearning to see their own shortcomings. - -.. _brandolinis-disclosed-assistance: - -Disclosed Assistance Or Laundered Generation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The contrast at the centre of this entire discussion can be made clearer with two examples, both of which are publicly accessible, both produced in the same year, by people using the same class of tools. - -The first is this chapter. It was produced with machine assistance, openly: the structure was directed by the human, the material was assembled with the tool's help, every conclusion, every line, every slight nuance of meaning and semantics was examined, edited, and accepted or rejected by the human, and the process described publicly - including the precise characteristics of the technological assistance framework used. The human can answer for every claim in these documents, because the human made the decisions the claims rest on. - -The second is the project examined in :ref:`the Prns case`. It was produced by a machine, at a volume no human could review, and the authorship record was engineered to erase the machine's role: A commit hook that refuses attribution of machine generation, a contribution policy declaring machine output the human's own, a fictitious contributor identity, 637,000 lines of unreviewed code. The human cannot answer for the code, because the choices were never made by a human - only the conformal surface of the presentation was. - -The same tool class. The same technology. Two diametrically opposed relationships to truth and responsibility. **The difference is not the tool**. The difference is the presence or absence of the human as the agent of the work - and the honesty or concealment with which that relationship is recorded. - -Assistance leaves the human as the agent and the commons as the source from which the human works and can grow. Substitution removes the human from the loop and converts the commons into fuel for the thing that seeks to replace them, or usurp their work for the profit of the few, human atrophying in the process. - -.. _brandolinis-license-in-context: - -The License In This Context -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The Reticulum License, examined in :ref:`What The Reticulum License Is`, draws the same line, at the level of the law. Its AI condition does not forbid the use of machine-learning technology to *assist* development - no reading of the license could support that, and no one associated with this project has ever claimed so. What the condition forbids is the *appropriation of the work into the machine itself*\ : The ingestion of the commons as raw material for synthetic replacement, and the concealed fabrication of "prior work" that would bury the real thing. - -As also noted in :ref:`Copyright & The Two Legal Layers`, plain old copyright law already protects against the case where a process copies the reference implementation into a machine, produces a transformed derivative, and then claims "Copyright, [name of the process operator]", with no human able to account for the creative decisions in the result. That is simple, petty theft, as it always has been. - -.. _brandolinis-general-principle: - -The General Principle -^^^^^^^^^^^^^^^^^^^^^ - -In these regards, the position of this project can be codensed to something relatively concise: - -* Machine assistance in human learning, development, and creation is accepted and useful. - It is not a shameful secret; but it *is* a tool that needs knowledge, training, understanding, - and great care to use. It should not make anything easier or more fun, it should make the - results better, more carefully considered, and arrive at *higher levels of synthesis*\ , not - produce endless streams of code churn. -* The line is crossed when the human stops being the agent of the work. When *any* output is - accepted that the human cannot explain, produced at a scale the human cannot review, or filed under a record - where the human has arranged to conceal the fact. -* The line is crossed, when a human claims authorship of work they cannot - answer for or has usurped. Credit follows the ability to answer. It has always worked that way, and - machines do not change the rule - they only make it cheaper and more tempting to break. -* The people who depend on infrastructure have the right to know which side of the line - the code they run was made on. That knowledge is the only verification most of - them will ever be able to perform. - -Use assistance, by all means, *and learn*\ , but don't substitute. And never sign what you cannot answer for. - -Somewhere, within the general shape of the conceptual framework outlined in this section, or realistically, a more refined variant of it, I do believe there is the *potential* of the *beginning* of a constructive outcome to the immense challenge we are faced with as a species in regards to these problems. - -At the same time, I reserve my right to revert that position to Butlerian Jihad without prior notice. We are on a knife's edge, and you know it just as well as I do. - -.. _brandolinis-void-grants: - -Void Grants: Legal Foundations of Machine-Generated Code --------------------------------------------------------- - -A grant of rights is only as real as the rights behind it. A license conveys what its issuer holds, and nothing more. A derivative may be distributed only under whatever license covers what it took. And over a work that a machine originated, no human holds anything at all. - -Each of these statements is a matter of settled statute and case law, and this section sets out the authorities. The conclusions they support are: - -#. A software implementation generated from a licensed work by a machine, published under its operator's claimed copyright and license, **conveys rights the operator never possessed**. -#. **Its grant is void**. The projects, products, and promises built upon such grants stand on nothing. A field of void grants is a minefield for everyone who crosses it. - -This section is a general examination of copyright law as it applies to machine-generated code. It is not legal advice. - -I. The Foundational Statute: Translation Is Copying -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The starting point is the statutory definition of the class of works at issue. Title 17, United States Code, section 101 provides the definition of a derivative work: - -.. - - A "derivative work" is a work based upon one or more preexisting works, such as a *translation*\ , musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or *any other form in which a work may be recast, transformed, or adapted*. - - -Section 101 further provides that *"the terms 'including' and 'such as' are illustrative and not limitative"*. As such, the list is a list of *examples*\ , and the very first example is translation. The definition also reaches, in its own words, *"any other form in which a work may be recast, transformed, or adapted"*. - -The exclusive right at issue is defined in section 106: - -.. - - Subject to sections 107 through 122, the owner of copyright under this title has the exclusive rights to do and to authorize any of the following: - (1) to reproduce the copyrighted work in copies or phonorecords; - (2) to prepare derivative works based upon the copyrighted work; - (3) to distribute copies or phonorecords of the copyrighted work to the public by sale or other transfer of ownership, or by rental, lease, or lending; - - -Violation of any of these rights is infringement. 17 U.S.C. § 501(a). - -Taken together, the provisions yield a construction that is difficult to improve upon and impossible to escape: - -#. **A port is a translation.** A work that takes a program written in one language and from it produces a program written in another language, implementing the same behavior, is a translation of the original - whatever mechanism performed it, and however the surface presentation differs. -#. **A translation is a derivative work.** Section 101 says so, explicitly. -#. **Preparing a derivative work is the copyright owner's exclusive right.** Section 106(2) says so explicitly. A derivative work prepared without authorization, or outside the scope of any authorization, infringes that right. - -The common objection - "the code is in a different language, so it is completely different" - is not a legal argument; it is the definition of what the statute already classifies. - -Translation *is the paradigm* of derivative authorship, not an exception to it. If translating a work without permission was a defense, the derivative-work right would be meaningless; every infringing adaptation would simply need to be "different enough" to count as a translation of its source. - -Nor does the identity of the translator change the analysis. The statute asks whether the work is "based upon" a preexisting work. It does not ask who or what performed the translation. A machine that ingests a licensed work and reproduces a translated transformation of it has performed mechanical translation; the output is a derivative of the ingested work, and the law governing derivative works applies to it exactly as it applies to a translation made by hand. - -II. Ideas Are Free, Expressions Protected -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The second foundation is the distinction between what a work *is* - its ideas, its processes, its systems - and the *expression* in which those ideas are embodied. The distinction is codified in section 102(b): - -.. - - In no case does copyright protection for an original work of authorship extend to any idea, procedure, process, system, method of operation, concept, principle, or discovery, regardless of the form in which it is described, explained, illustrated, or embodied in such work. - - -The principle is older than the statute, and its application to software is settled. An author may own the particular code in which a method is expressed; the method itself, the process, the algorithm, belongs to no one - because *"the sine qua non of copyright is originality"* (\ *Feist Publications, Inc. v. Rural Telephone Service Co.*\ , 499 U.S. 340, 345. 1991). And ideas are not original to anyone in particular, *Baker v. Selden*\ , 101 U.S. 99 (1879), established the line: Copyright in a book describing a system does not prevent others from practicing the system. - -Applied to a communications protocol, the structure is clean: - ----- - -**An implementation of a public-domain protocol, honestly reasoned and written by humans, is welcome and unencumbered, and owes nothing to any license. An implementation generated from a licensed implementation is a derivative of licensed work, subject to its conditions and to the ordinary copyright of its source.** - ----- - -The public-domain status of the protocol's ideas changes nothing about the copyright in an implementation's expression. A person who copies an implementation - by machine or by hand - cannot defend the copying by pointing to the protocol, because the protocol covers the ideas, not the symbols in which someone expressed them. - -A further misconception can be dismissed here; the recently popularized "clean-room" claim about the absence of copying. A :ref:`clean-room` is a not a magic label achievable by LLM, but a specific, institutionalized procedure for proving to a court that copying did not occur: Physically separated teams, written functional specifications, attestable non-exposure, and no access to the original by the implementers (\ *Altai*\ , 982 F.2d at 709–11). - -*"I created a clean-room implementation by feeding the original source code into a machine"* attempts to *claim* the certified, legally established procedure for proving the absence of copying by describing, in the same breath, a process **composed entirely of copying**. The word is deployed because the procedure it names is what the claimant would *need to have happened*. - -III. Machine Output Has No Author -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The third foundation is the human authorship requirement. It is settled at both levels of the federal courts and in the Copyright Office's administering practice. - -In *Thaler v. Perlmutter*\ , the district court held that *"copyright law protects only works of human creation,"* and that *"human authorship is a bedrock requirement of copyright"* (687 F. Supp. 3d 140, 146, D.D.C. 2023). On appeal, the United States Court of Appeals for the District of Columbia Circuit affirmed, holding that the Copyright Act *"requires all eligible works to be authored in the first instance by a human being"* (No. 23-5233, D.C. Cir. Mar. 18, 2025). - -The requirement traces to the ordinary meaning of the statutory term. In *Burrow-Giles Lithographic Co. v. Sarony*\ , the Supreme Court defined an "author" as *"he to whom anything owes its origin; originator; maker; one who completes a work of science or literature"* (111 U.S. 53, 57–58, 1884), and more recently described the author as *"the person who translates an idea into a fixed, tangible expression entitled to copyright protection"* (\ *Community for Creative Non-Violence v. Reid*\ , 490 U.S. 730, 737, 1989). - ----- - -**A machine does not owe expression its origin; it reproduces it. No court has recognized copyright in material created by non-humans, and the courts that have addressed the question have rejected it. If you create by machine generation, you do not own the output.** - ----- - -The Copyright Office's registration guidance applies the requirement to generative systems in terms that are directly on point. The Office asks: - -.. - - whether the "work" is basically one of human authorship, with the computer [or other device] merely being an assisting instrument, or whether the traditional elements of authorship in the work (literary, artistic, or musical expression or elements of selection, arrangement, etc.) were actually conceived and executed not by man but by a machine. - - - -* *Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence*\ , 88 Fed. Reg. 16190, 16192 (Mar. 16, 2023) - -And it states the consequence: *"If a work's traditional elements of authorship were produced by a machine, the work lacks human authorship and the Office will not register it"* (\ *Id.*\ ). - -On the role of prompts, the Office is equally explicit: when a system receives a prompt and produces a work in response, *"these prompts function more like instructions to a commissioned artist - they identify what the prompter wishes to have depicted, but the machine determines how those instructions are implemented in its output"* (\ *Id.*\ ). - -The agency applied this standard and excluded machine-originated material from registration in the leading administrative case. In *Zarya of the Dawn* (Cancellation Decision, Feb. 21, 2023, Registration No. VAu001480196), the Office cancelled registration of images generated by the Midjourney service, holding that *"it was Midjourney-not [the author]-that originated the 'traditional elements of authorship' in the images"*\ , while preserving protection for the human-authored text and the human selection and arrangement of the work. - -The Copyright Office's 2025 report on AI copyrightability reaches the same conclusion: - -.. - - Based on the functioning of current generally available technology, prompts do not alone provide sufficient control. - - - -* U.S. Copyright Office, *Copyright and Artificial Intelligence, Part 2: Copyrightability*\ , at iii (Jan. 29, 2025) - -The report's analysis is explicit: *"The Office concludes that, given current generally available technology, prompts alone do not provide sufficient human control to make users of an AI system the authors of the output. Prompts essentially function as instructions that convey unprotectible ideas"* (\ *Id.* at 18). - -Its guiding distinction is stated plainly: *"The use of AI tools to assist rather than stand in for human creativity does not affect the availability of copyright protection for the output"* while *"Copyright does not extend to purely AI-generated material, or material where there is insufficient human control over the expressive elements"* (\ *Id.* at iii). - -The boundary is therefore not "AI or no AI". It is *assistance or substitution*\ : Whether a human exercised sufficient creative control over the expressive elements and can answer for them, or whether the machine originated them and the human merely supplied instructions and a signature. That is a question of fact in each case, determined from the record of how the work was made. - -Two facts follow, and stated together, they form the base legal reality: - - -#. **A work whose expressive elements were originated by a machine contains no valid new copyright: It has no human author. To the extent that the same work reproduces a preexisting copyrighted work without explicit license, it is at the same time an unauthorized derivative. It is unowned for anything new it contains, and unlicensed for everything taken.** -#. **Its operator therefore holds no copyright in it and no grant over it - and a person who holds neither copyright nor grant can convey neither.** - -The first fact makes the work **unownable** by its operator. The second makes it infringing against its source. Neither replaces the other: - - -* The absence of copyright in machine output does not make the output free for anyone to take; it makes it unowned. -* This is a different thing entirely, especially when what it contains is someone else's expression. - -IV. License Conditions Bound the Grant -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The fourth foundation governs what a license *is*. A copyright owner may grant permission to use the work on conditions. The conditions define the scope of the permission. Use outside the scope is use that was never authorized. - -In *Jacobsen v. Katzer*\ , 535 F.3d 1373 (Fed. Cir. 2008), the Federal Circuit held that the conditions of an open-source license are *conditions of the copyright license itself*\ , not mere contractual promises, and that a licensee operating outside them is an infringer: - -.. - - Copyright holders who engage in open source licensing have the right to control the modification and distribution of copyrighted material. - - The choice to exact consideration in the form of compliance with the open source requirements of disclosure and explanation of changes, rather than as a dollar-denominated fee, is entitled to no less legal recognition. - - It is outside the scope of the [license] to modify and distribute the copyrighted materials without copyright notices and a tracking of modifications from the original computer files. - - - -* *Id.* at 1381–82 - -The doctrinal mechanism is settled: *"A copyright owner who grants a nonexclusive license to use his copyrighted material waives his right to sue the licensee for copyright infringement"* within the scope of the license. If, however *"the licensee acts outside the scope, the licensor can bring an action for copyright infringement"*\ : An act outside the granted scope **is infringement** (\ *Sun Microsystems, Inc. v. Microsoft Corp.*\ , 188 F.3d 1115, 1121, 9th Cir. 1999; *Graham v. James*\ , 144 F.3d 229, 236, 2d Cir. 1998; *S.O.S., Inc. v. Payday, Inc.*\ , 886 F.2d 1081, 1087, 9th Cir. 1989). - -Whether a given license term is a condition of the grant or a mere covenant is determined from the license's own language; conditions that limit what may be copied, modified, and distributed - the attribution terms, the notice terms, the modification-tracking terms, the field-of-use terms - are the boundaries of the copyright grant (\ *MDY Industries, LLC v. Blizzard Entertainment, Inc.*\ , 629 F.3d 928, 940, 9th Cir. 2010). - -Three consequences follow for the analysis at hand. They are not hypothetical. - -**First, the conditions are measured at the moment of copying** - -A work is licensed under whatever conditions applied when the reproduction, adaptation, and distribution occurred. There is **no** period in which copying a licensed work is "free" because the copy was made by a machine; the machine is the *licensee's tool*\ , and the licensee is bound by the conditions regardless. A derivative generated without satisfying the conditions - without the attribution, without authorization for the manner of use - was never within the scope of the license, from the first instant of its existence. - -**Second, breach does not restore rights that never attached** - -The district court in *Artifex Software, Inc. v. Hancom, Inc.*\ , No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017), analyzing the GPL's termination provisions, observed that *"the language of the GPL suggests that [the licensee's] obligations persisted beyond termination of its rights to propagate software using Ghostscript,"* because the source-code obligation recurs each time a covered work is conveyed. The same structure operates a fortiori where a derivative was produced *outside* the license entirely: No grant ever attached, and no subsequent event creates one. - -**Third, the settled conditions are the enforceable core** - -The attribution and notice conditions rest on the settled doctrine above. Novel conditions (restrictions on field of use, restrictions on how the work may be consumed) are enforceable as conditions of the grant when clearly drafted, but their enforcement history is thinner. The argument that a derivative is unlicensed does *not* depend on the novel conditions being vindicated; it needs **only** the settled ones, and the ordinary law of derivative works, to stand. - -V. The Void Grant -^^^^^^^^^^^^^^^^^ - -The preceding sections assemble into a single structure. Let us apply it to an example situation: - - -* A project presented as an independent implementation of a protocol; "ground-up", "from scratch" or "clean-room" work. -* The actual history shows continuous, machine-mediated ingestion of a licensed reference implementation. -* The output is generated at a scale no human could produce or review, with the provenance of the process obscured. - -**Step One: The derivative** - -The project's output is a translation of the licensed implementation: A derivative work, produced by a copy process. 17 U.S.C. §§ 101, 106(2). Preparing it without authorization infringed from the first moment of its existence. *Id.* § 501(a). - -**Step Two: The license** - -The derivative was produced outside the conditions of the license; without the notices, without the attribution. The license never attached (\ *Jacobsen*\ , 535 F.3d at 1381–82). - -**Step Three: Authorship** - -The expression was originated by a machine. No human can account for its creative decisions, and no human authored it. It contains no valid new copyright (\ *Thaler*\ , 687 F. Supp. 3d at 146; No. 23-5233, D.C. Cir. 2025; 88 Fed. Reg. at 16192). - -**Consequence: Void Grant** - -The statute supplies the consequence. Section 103(a) provides: - -.. - - The subject matter of copyright as specified by section 102 includes compilations and derivative works, but protection for a work employing preexisting material in which copyright subsists does not extend to any part of the work in which such material has been used unlawfully. - - -And section 103(b) provides that copyright in a derivative work *"extends only to the material contributed by the author of such work."* The legislative history states the point without ambiguity: *"An unauthorized translation of a novel could not be copyrighted at all"* (H.R. Rep. No. 94-1476, at 57, 1976). - -In our example, the operator of the derivative's copy process decides to assert copyright over the output, and licenses it to the world under terms of the operator's choosing, for example the Apache License. - -But the operator holds no copyright in machine-originated content (section III) and no license over the source from which it was derived (sections I and IV). - -For the derived material, the operator's copyright claim **is an assertion of a right that never existed**. The material was used unlawfully, and section 103(a) withholds protection from *every* part of the work in which it appears. For the machine-originated remainder, the operator holds no copyright either, for lack of any human author. - -The operator's license to the world is a promise the operator had no power to make. Every person who receives the work believing they received a valid grant has received **a void grant**\ , and stands, as a matter of settled law, in the position of a downstream user of an unauthorized derivative, whatever they were told at the time. - -This is a cascading consequence, and it is why the pattern is a hazard. No combination of self-granted licenses - "MIT," "Apache," "public domain," anything at all - can transfer rights the operator never held. Every downstream project, product, or deployment built on the derivative is built on unlicensed reproduction of a third party's work, whether or not it knew, whether or not it was told, and whether or not the marketing was plausible. - -The longer the derivative circulates unexamined, the more it becomes embedded, forked, vendored, depended upon, and the more expensive its correction becomes for everyone who trusted it. A field of superficially real projects, repositories, and licenses that **in reality confer nothing on anyone who relies on them is a minefield**\ , in the most precise sense the law allows: The rights they purport to grant *were never possessed*. - -The law defines what may be asserted, not what will be. The copyright owner may choose whether and how to assert these rights, and at what point in time. The exposure exists regardless, and for *every* downstream that trusted the void grant. This is the scope of the hazard that the conduct exemplified here produces. - -VI. Fair Use Does Not Rescue This Pattern -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Every defendant raises fair use. It is an essential and important limitation in the service of free speech, accountability, criticism, education and other important domains. Therefore, it must be considered, and in the case exemplified here, it fails. - -Fair use is a limitation on the exclusive rights, evaluated case by case under four factors: The purpose and character of the use; the nature of the copyrighted work; the amount and substantiality of the portion used; and the effect of the use on the potential market for or value of the work (17 U.S.C. § 107). - -On the first factor, the governing standard is *Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith*\ , 598 U.S. 508 (2023), in which the Supreme Court held that: - -.. - - In sum, the first fair use factor considers whether the use of a copyrighted work has a further purpose or different character, which is a matter of degree, and the degree of difference must be balanced against the commercial nature of the use - - -The question is whether the use serves a further purpose, not whether the surface changed. A derivative that exists to serve as the same thing as the original, marketed to the people who would otherwise use the original, has not acquired a further purpose by being translated; the surface changed, the function did not, and the use is commercial. - -The remaining factors resolve the same way. Source code is expressive creative work, not bare fact. The pattern under examination copies the whole; a complete implementation, ingested and translated in its entirety. And the market effect is the decisive factor, because it is engineered: A derivative marketed as a drop-in replacement for the work it was copied from is not a complement to the original's market, it is a substitute for it. The Supreme Court has long held that use of a work to satisfy the market demand for the original is the paradigm of an *unfair* use (\ *Harper & Row, Publishers, Inc. v. Nation Enterprises*\ , 471 U.S. 539, 566–67, 1985). - -The one case that is always raised in response, *Google LLC v. Oracle America, Inc.*\ , 593 U.S. 1 (2021), holds less than it is believed to. The Court there assumed *"for argument's sake, that the material was copyrightable"* and held *"that the copying here at issue nonetheless constituted a fair use"* (\ *Id.* at 11). - -The material in question was a thin slice of *declaring code*\ , which the Court found sits, *"if copyrightable at all, further than are most computer programs (such as the implementing code) from the core of copyright"* (\ *id.*\ ), in a use that built a new platform for a new environment, with the copier **having written the vast majority of its own code**. By comparison: A wholesale translation of an entire implementation, marketed as a substitute for it, is **not** that case; it is the other end of every factor. - -The valuable zones of fair use remain, and they are important. **Commentary and criticism**\ , quoting from a work to review, discuss or criticize it. **Teaching and scholarship**\ , explaining concepts and quoting excerpts, is its traditional beneficiary. - -Conclusion -^^^^^^^^^^ - -The law is neither obscure nor new. The conclusion it yields can now be stated: - - -* A machine translation of a protected implementation **is a derivative work**, and an unauthorized derivative **is an infringement**, whatever language it speaks and whatever machinery produced it. -* A work whose expression a machine originated has no human author, no valid new copyright, and without valid copyright **no license can be granted on it**. -* § 103 withholds protection from every part of a derivative in which protected material was used unlawfully. A license is a grant with conditions, and a derivative made outside the conditions **never received a grant**. -* A person who holds no copyright and no grant **can convey neither**, and *every* downstream consumer who received from them received a **void grant**. - -This is a *catastrophic* failure state for all who want to create things good and useful: The projects that inherit only void grants and liability believing they have inherited foundations, the users who depend on code that no one on earth can answer for, the honest builders whose work is buried under a field of plausible-looking impostors. - -Sources and Authorities -^^^^^^^^^^^^^^^^^^^^^^^ - -Statutory references are to Title 17, United States Code. Case citations are to the official or generally cited reporters. Online links are for reference only. Section numbers refer to the sections in which each authority is principally relied upon. - -**Statutes** - - -* `17 U.S.C. § 101 (definitions: "derivative work," "including," "publication") - I, II `_ -* `17 U.S.C. § 102(b) (idea/expression) - II `_ -* `17 U.S.C. § 103(a)–(b) (derivative works employing unlawfully used preexisting material) - V `_ -* `17 U.S.C. § 106(1)–(3) (exclusive rights: reproduction, derivative works, distribution) - I `_ -* `17 U.S.C. § 107 (fair use) - VI `_ -* `17 U.S.C. § 501(a) (infringement) - I, V `_ - -**Judicial decisions** - - -* `\ *Jacobsen v. Katzer*\ , 535 F.3d 1373 (Fed. Cir. 2008) - IV, V `_ -* `\ *Sun Microsystems, Inc. v. Microsoft Corp.*\ , 188 F.3d 1115 (9th Cir. 1999) - IV `_ -* `\ *Graham v. James*\ , 144 F.3d 229 (2d Cir. 1998) - IV `_ -* `\ *S.O.S., Inc. v. Payday, Inc.*\ , 886 F.2d 1081 (9th Cir. 1989) - IV `_ -* `\ *MDY Industries, LLC v. Blizzard Entertainment, Inc.*\ , 629 F.3d 928 (9th Cir. 2010) - IV `_ -* `\ *Artifex Software, Inc. v. Hancom, Inc.*\ , No. 16-cv-06982-JSC (N.D. Cal. Sept. 12, 2017) (settled; dismissed with prejudice Jan. 2018) - IV `_ -* `\ *Thaler v. Perlmutter*\ , 687 F. Supp. 3d 140 (D.D.C. 2023), aff'd, No. 23-5233 (D.C. Cir. Mar. 18, 2025) - III, V `_ -* `\ *Burrow-Giles Lithographic Co. v. Sarony*\ , 111 U.S. 53 (1884) - III `_ -* `\ *Community for Creative Non-Violence v. Reid*\ , 490 U.S. 730 (1989) - III `_ -* `\ *Feist Publications, Inc. v. Rural Telephone Service Co.*\ , 499 U.S. 340 (1991) - II `_ -* `\ *Baker v. Selden*\ , 101 U.S. 99 (1879) - II `_ -* `\ *Computer Associates International, Inc. v. Altai, Inc.*\ , 982 F.2d 693 (2d Cir. 1992) - II `_ -* `\ *Andy Warhol Foundation for the Visual Arts, Inc. v. Goldsmith*\ , 598 U.S. 508 (2023) - VI `_ -* `\ *Google LLC v. Oracle America, Inc.*\ , 593 U.S. 1 (2021) - VI `_ -* `\ *Harper & Row, Publishers, Inc. v. Nation Enterprises*\ , 471 U.S. 539 (1985) - VI `_ - -**Administrative and policy authorities** - - -* `\ *Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence*\ , 88 Fed. Reg. 16190 (Mar. 16, 2023) - III `_ -* `\ *Zarya of the Dawn*\ , U.S. Copyright Office (Cancellation Decision, Feb. 21, 2023), Registration No. VAu001480196 - III `_ -* `U.S. Copyright Office, *Copyright and Artificial Intelligence, Part 2: Copyrightability* (Jan. 29, 2025) - III `_ -* `H.R. Rep. No. 94-1476 (1976) - V `_ - -.. _brandolinis-evaluating: - -Evaluating a Reticulum Implementation -------------------------------------- - -.. _brandolinis-intention-of-guide: - -The Intention Of This Guide -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Very few user of network infrastructure can verify the software they run. This is true of the reference implementation as well, and it is precisely why it was engineered the way it was: the entire Reticulum protocol implementation - the transport logic, the cryptographic interfaces, the routing - is less than 15,000 lines of code, including its extensive comments and detailed logging. The reference is *designed* to be readable by a human in a reasonable sitting. That is a deliberate decision, and it is the standard against which every other implementation should be measured: An implementation that asks for trust while being structurally unreadable is asking for the absurd. - -Because most users cannot verify the code directly, the evaluation of the *project* is the evaluation of the software. The framework below exists so that this evaluation can be performed in minutes, by anyone, without expertise - and so that each new "implementation" does not require the kind of full investigation that would otherwise be if not impossible, prohibitevly expensive. - -.. _brandolinis-two-questions: - -Two Questions That Resolve Most Cases -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Other considerations in this guide is further corroboration. These two questions allow you to decide the case quickly in most instances, and they should be asked first. - -**Question 1: Does this speak the protocol, or was it just generated from the reference?** - -The copyright and license realities, described in :ref:`What The Reticulum License Is`, is the legal foundation of this distinction. The protocol is public domain: A *genuine* :ref:`clean-room` implementation, reasoned out and written by humans, is free and welcome, and owes nothing to any license. But an implementation that was *directly generated from* the reference - reproduced by a copy process, whether manually or through a machine or software - is a derivative of the licensed work, and is subject to both the license's conditions, and to the ordinary copyright law examined in :ref:`Copyright & The Two Legal Layers`. A primarily machine-generated copy without substantial human creative input is not merely unlicensed; it is a copyright violation that would exist under any license at all. - -**Ask**: Does the project say where its protocol knowledge came from? Does it credit the reference implementation, or claim "ground-up" status while its own history shows continuous ingestion of the reference's source? Does its license carry the original copyright notice? The answers tell you, within minutes, which of two very different kinds of project you are dealing with. - -**Question 2: Who, or what, is the agent of the work?** - -This distinction, established in :ref:`Assistance Versus Machine Substitution`, is the trust foundation of everything else. Is there a human who decided what to build, judged what was correct, rejected what was wrong, understands what was shipped - and can answer for it, line by line and decision by decision, *without machine assistance*\ ? Or is the machine the agent, with the human's role reduced to the name, the account, and the signature? - -.. - - This test has a sharper form: **would the project be meaningfully different if someone else had pressed the button?** If the answer is no - if any person could have been the signature and nothing would change - then the "author" is not the agent of the work. They are its facade, and the project is, in the most literal sense, worthless and unowned. - -A project with no owner is a project no one can answer for, and infrastructure with no one to answer for it has no place carrying other people's traffic. - -.. _brandolinis-other-signals: - -Other Signals To Look For -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Once the first two questions are answered, the following signals determine the verdict. None is alone decisive; together they form a pattern that is difficult to fake in the directions that matter. - -.. _brandolinis-provenance: - -Provenance and attribution -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* **Credit to the reference.** Does the project name Reticulum's reference implementation - in its README, its website, its licenses, and the code? Does it present itself as a port - or as a "ground-up" origin story? Absence of credit, in a field where every implementation - owes its existence to the reference, is not an oversight; it is a decision. -* **License compliance.** For a derivative, the license conditions apply: the copyright - notice and permission notice in all copies and substantial portions, the harm condition, - the AI-training condition. A project whose license file claims a fresh copyright in the - operator's name, over work that was generated from the reference, has failed the minimum - bar of honesty before any code is read. -* **Disclosed provenance.** Is the use of machine assistance stated openly, or does the - project maintain machinery to conceal it - commit hooks that refuse attribution of - generation, contribution policies that declare machine output the human's own, scrubbers - for personal traces, fictitious contributor identities? The first is the mark of a human - using a tool. The second is the mark of a record being manufactured. -* **The author's history.** Is there a prior record - projects, contributions, a body of - work that a person can look at and reasonably assess? A sudden, fully-formed "decade of experience" - appearing from nowhere, attached to a project of unprecedented scope, is a story that - contradicts itself. - -.. _brandolinis-agency: - -Agency and answerability -~~~~~~~~~~~~~~~~~~~~~~~~ - -* **Who answers the questions?** When technical questions are asked, does a human answer - with substance - going deeper, explaining trade-offs, admitting uncertainty - or do - answers deflect to process ("the CI verifies it", "the proofs prove it", "the audit - covered it")? Are the answers themself machine generated? Deflection to process is - the substitute's answer pattern: It names the machinery instead of the reasoning. -* **Velocity commensurate with authorship.** Is the output volume compatible with human - authorship, review, and judgment? An implementation ten (or 42!) times the size of the reference, - produced in a fraction of the time by a single person, is not evidence of brilliance; it - is evidence of unchecked generation. Churn is part of the same signal: A project that - deletes half of everything it adds is not building; it is looping. -* **Signs of development, growth and learning.** Do reversals exist? Contested decisions? - "We tried X and it was wrong, so we did Y"? Humans change their minds, admit mistakes, and - leave the wear of judgment in their history. Pure generation shows "the polish of its - absence": uniform register, endless renaming, and errors discovered by the generator's own - later runs, recorded as if they were planned milestones. -* **Standing behind failures.** When something is wrong, does the human say "I chose this, - here is what I got wrong, here is the fix"? Or do corrections arrive as process language, - with nobody accepting that a decision was even made? A project whose history contains no - first-person acceptance of error contains no author. - -.. _brandolinis-engineering: - -Engineering reality -~~~~~~~~~~~~~~~~~~~ - -* **Honest benchmarks, or none.** If performance is claimed, is the baseline real - stock, - current, interpreted RNS, on the modern interface, at equal policy, with methodology - published in advance and reproducible by anyone? -* **Interop with reality.** Is compatibility verified against the *current* reference - implementation - on real hardware, including the radio and serial interfaces this - protocol family lives on - in both directions, over long transfers, uptimes and edge cases? -* **The readability standard.** Can a competent human read the core of this implementation - in a reasonable sitting, the way the 15,000-line reference can be read? Was human - readability a stated design goal, or an afterthought? A project that cannot be inspected - is a project asking for trust it has not earned and will never attend to. -* **Nonstandard inventions.** Does the project "improve" on the protocol with its own - modes - new channel behaviour on shared spectrum, new rendezvous mechanisms, new crypto, - "post-quantum" replacements, "turbo" variants? Inventions that change behaviour on a - shared network are not features; they are forks of the airwaves, imposed on every other - node that shares the medium. A project that invents on top of a protocol it has not yet - correctly implemented is not advanced; it is dangerous to its neighbours. -* **The public record.** The commit history is public. Read it. - -.. _brandolinis-network-citizenship: - -Network citizenship -~~~~~~~~~~~~~~~~~~~ - -* **Honest presence.** Does the project identify itself honestly on the network, or does - it masquerade as something it is not? -* **Coexistence.** Does it respect the shared medium and network conventions? A node that - is a bad neighbour on the spectrum is a hazard regardless of how well its code works in isolation. -* **Community participation.** Does the author participate in the community as a member - - answering questions, contributing, learning - or only appear to advertise? The second is - not participation; it is exploiting the community as a broadcasting target. - -.. _brandolinis-signals: - -Monetary and influence signals -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* **Money under the claim.** Donations, funding, or commercial offers premised on claims - like "89× faster" or "formally proven" convert false marketing into a different category - of problem. When money changes hands on the strength of fabricated measurements, the - stakes are no longer theoretical. -* **Tokens and "investment" vehicles.** Any implementation that surrounds itself with a - crypto token, a "coin", or an investment pitch should be treated as what it is: A project - whose primary purpose is to earn the controller a profit, not open networking. -* **Marketing-first ordering.** A polished website on day three, a twelve-language marketing - presence, a "benchmarks" page and a "get started" funnel existing before the protocol works - is a massive red flag. Real builders ship, then talk. The inverse order is the signature of - a project whose goal is attention. - -.. _brandolinis-conformal-surface: - -The Conformal Surface -^^^^^^^^^^^^^^^^^^^^^ - -One warning before the synthesis. When you evaluate any project, assume - as a working hypothesis - that its surface is an investment. The marketing, the design, the badge collection, the "proof records", the fluent and confident prose: This is exactly where substitution invests, for the simple reason that it is where the audience looks first. Fluency is the *minimal conformal surface* of understanding, and the surface is not evidence of anything except that the author *wanted to look convincing*. If you find yourself impressed by the surface before you have asked Question 1 and Question 2, you are doing the evaluation in the order the substitute operator hoped you would. - -.. _brandolinis-possible-verdicts: - -Possible Verdicts -^^^^^^^^^^^^^^^^^ - -The framework proposed here can yield three different verdicts: - -#. - **Meets the standard.** A named human who can answer for the work and demonstrably does; - provenance disclosed; attribution given; honest benchmarks; interop - demonstrated against current, real RNS on real media; readable code; a maintenance - history that shows growth and learning. This standard is not high. It is what the - reference implementation has met, in public, for over a decade, and it is what any - project that wants other people's traffic should be willing to meet in turn. - -#. - **Experimental, but honest.** A project that clearly labels itself as in-progress and - experimental ("do not use this yet", in plain words, as the first thing a user sees) - discloses its provenance, credits the reference, and is being developed in the open by - humans who answer questions. Such projects should be encouraged. They are how the - ecosystem grows, new ideas are tested, and the framework above is designed to make an - honest experimental project *easy* to recognize, not hard. - -#. - **Does not meet the standard.** Unanswerable authorship, concealed provenance, fabricated - measurements, invented protocol behaviour, absent interop, marketing ahead of substance. - Such projects should be treated as unproven regardless of their surface, and as hazards - if they invite users onto shared networks. The community owes them exactly the - skepticism they have earned, and not a moment of engagement beyond that; the network's - time is better spent elsewhere. - -.. _brandolinis-standards-reasonable: - -These Standards Are Reasonable -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -I'll close this guide by stating what these standards are *not*\ : They are neither a demand for perfection, nor a ban on machine assistance. Assistance is useful, when it is used to arrive at *higher levels of synthesis* than unaided work. It is not a requirement that an implementation be a clone of the reference; neither is it a requirement that it invents everything from scratch or aspires to be a :ref:`"clean-room"` implementation. Good implementations of the protocol are very welcome, *however they come*, but working on one requires *serious* work, considerable skill, and **deep competence**. - -The reference implementation has met this standard for years, and its users know what that looks like. - -.. _brandolinis-movement-of-a-dozen: - -The Movement Of A Dozen ------------------------ - -.. _brandolinis-presentation-elongated: - -Presentation, Elongated -^^^^^^^^^^^^^^^^^^^^^^^ - -Whenever Reticulum defends itself, its license, or its community - or simply continues to exist - a familiar chorus appears. The register is always similar: *concern*. Concern about the direction of the project. Concern about its license. Concern about its governance. Concern about its "bullying" of "good-faith contributors". Concern about the future of the ecosystem at the hands of a single maintainer. The chorus speaks in the plural, presents itself as a body of independent voices, and insists, with some frequency, that it is... *not alone*. - -The implicit claim under examination in this section is that there exists a *movement* of concerned individuals, spread across the ecosystem, all worried about Reticulum, and therefore (for some reason) deserving of a place at the table of decisions. - -A resolution of this claim requires one step: Count them. - -.. _brandolinis-headcount: - -Headcount -^^^^^^^^^ - -This "movement" does have a headcount. It is not in the hundreds, and it is not in the dozens. It is a small number of people who appear, in various combinations and under various names, in the following roles: - -* Creators of the machine-generated "Reticulum implementations" examined in this chapter. -* Creators of articles praising those implementations and lamenting the state of the - reference, its moral shortcomings, its unavailability or lack of something it already has. -* Commenters "defending" the implementations when they are evaluated reasonably and critically, - most often by attempting simply to derail the discussion. -* Critics of the Reticulum License on "open source" grounds. -* "Concerned community members" objecting to the maintainer's conduct. -* Campaigners on online platforms; and -* In some cases, a single individual performing *several* of these roles at once, under - different names, in the same week. - -The overlap is well documented at this point, across time and different platforms. The same phrasing, the same arguments, the same links, the same reactive reflexes, the same handful of accounts appearing in the same threads at the same moments. When the sockpuppets are unmasked (and they frequently unmask themselves) the cast does not grow, but rapidly shrinks. - -This is the most interesting single fact about the "movement": **It isn't one. It is a synthetic cast**, held together mostly by LLM-glue, reddit handles and hysteria. A cast is not a constituency. It performs; it does not represent. The difference matters quite a bit, because a great deal of this "movement's" imagined power comes from the assumption that the *plural form* is actual evidence of plurality. If that sentence seemed opaque, notice how many times a statement or article uses "we" instead of "I", and it will be less so. - -.. _brandolinis-recycled-wine: - -Recycled Wine, Fermenting -^^^^^^^^^^^^^^^^^^^^^^^^^ - -The cast is not only small, it is also, by any evidentiary standard, *forgetful*. Every major claim this "movement" advances has been advanced before, answered in public, with evidence, and often years ago - and is then advanced again, unchanged and completely ignoring any rationale or evidence presented previously (to the same people), as if the answer had never been written. - -* **The license argument.** Reticulum is "not really open source" because its license is not - approved by a particular orthodoxy. This is answered (now in a new and updated version, for the - fifth time) in full in :ref:`Open Source Means Open Source`. It was also - answered in public, at length, at the time of the license change - including the - arithmetic of who is actually restricted by which license. It is repeated *ad infinitum* anyway. - Quite a conversation starter, apparently. -* **The distribution argument.** Reticulum is "not properly distributed" because it is not - packaged as a ``.deb`` or on some favoured catalogue. Reality: It is available on practically every - platform with a single command, packaged by the community in a wide range of channels, and - capable of updating itself over its own network. The argument has been answered; it is repeated. -* **The "stepped back" narrative.** When the primary mirror of the project moved away from - a public internet platform, it was spun as the founder leaving for good, or now taking on dictatorial - powers, or abandoning his creation, or any variation or nonsensical combination thereof. - Those spins always center around justifying that *a vaccuum now needs to be filled*. - There was no such vaccuum. The narrative is repeated in various forms whenever convenient. -* **The enclosure narrative.** The license is reframed as a "power grab" or an "enclosure" - of the commons - as if a license whose protocol is public domain and whose conditions - restrict only harm and appropriation were a form of enclosure. The `most recent `_ (and mostly incoherent) - expression of this narrative is, once again, fluent, polite, and factually hollow. The - bizarre arguments supporting the "article" include: Defunct and abandoned forks presented - as living, community forks bearing the very Reticulum License mislabeled as AGPL license vehicles - (created by "concerned users"), companion app forks correctly retaining the Creative Commons - licensing used as further evidence of the same and completely abandoned projects propped up - as being "the ecosystem". Another case of LLM generated nonsense that the "author" didn't bother checking. - The intent is clear here, simply produce more *mass* of the bullshit, so it can be further - referenced, regurgitated and linked to. -* **The "bullying" narrative.** Whenever the community declines to treat manufactured - controversies as legitimate, or calmly documents the falsity of claims, or (Lord forbid) - moves a forum post for violating clearly stated forum rules, - the response is the same: The documentation is "bullying", the community is "excluding" people, the - maintainer is "unprofessional". In the most recent example, those accusations were made by someone who, in the - same week, publically called for the Reticulum maintainer's "removal from the equation", forged a moderator's - username, attempted to drag unrelated individuals into the campaign, and presented a new - round of outright falsehoods. Contrary to popular belief, the accusation of incivility - is not the last resort of the wronged; it is the standard reflex of the exposed. - -None of the "arguments" engages the answers given repeatedly, and extensively. That is the point, and the strategy. The arguments are not produced for the purpose of being engaged; they are produced for the purpose of *being repeated* - the same wine, poured into new bottles, on a cycle. Each cycle is short, cheap to produce, and (thanks to freely available fluent text generation) now nearly effortless. The intended audience is not the community, which has heard it all before. The intended audience is the newcomer, who has not. - -.. _brandolinis-the-method: - -The Method of Madness -^^^^^^^^^^^^^^^^^^^^^ - -The method behind the pattern is consistent and simple enough to be described precisely: - -**Bullshit asymmetry** - -Producing a fluent, plausibly-sourced smear is now the work of minutes. Refuting it to the standard this manual demands is the work of days, and the refuted smear is replaced by the next one within the week. This asymmetry isn't just a fine point of the campaign; **it is the campaign**. The goal is exhaustion - the slow conversion of the community's time and attention from building to defending. - -**Weaponized concern** - -The register of the campaign is almost always the register of solicitude: "we are worried about the ecosystem", "we just want what is best for the community", "this is about the future of the project". The concern is directed, without exception, at the things the campaign wants changed, and the change it wants is always the same: **The replacement of the reference's authority with its own**. Concern is the *costume*\ ; capture is the choreography. It is the oldest form of cheap power grab in the book. - -**Event seizure** - -Any event - a license change, a platform move, a forum moderation decision, a subreddit closure, a security notice, even the most banal messages from maintainers or known contributors - are seized, stripped of their actual context, and re-narrated as evidence of the same pre-written story. The subreddit that was closed after its moderator vanished and ceased moderating (according to *prior agreement with said moderator*) becomes "the maintainer shutting down dissent". Moving away from a highly centralized online platform becomes "the founder retreating". The license becomes "the enclosure". The pattern is so reliable that the story could be pre-written by a lightly mechanized quill; only the date changes. The actual real-world context of these events is documented, in public, in each case; the re-narration proceeds regardless. - -**The legitimacy circuit** - -The campaign invests heavily in the places where legitimacy is minted: Conference talks, blog platforms, social threads, and forums - presented as if these appearances constituted some sort of ecosystem-wide groundswell rather than the same dozen people moving through the same circuit. The LLM-generated articles cite the LLM-generated ports; the ports are praised in the articles; the managers of the ports spread the articles, and echo them on the forums and internet platforms; the conference presentations cite the forums. Random bypassers with the need for a cause watch the conference presentations and take on the holy duty of Internet Soldier. The circuit is closed, self-referential, and (to a reader encountering it fresh) convincing. It is also, entirely, **a loop of bullshit**. - -**The prior-work minefield** - -The machine-generated implementations have a consequence beyond attention: They flood the space with fake "prior work". The effect, as the license discussion predicted in the beginning of 2025, is the creation of a minefield of sloppy repositories that can later be matched against any genuine project, and attempt to claim prior art, or even worse: Dissuade actual, honest developers or builders from taking up a real project they might have succeeded with, because they now believe it has already been done. Just as importantly, it also buries real efforts under machine-generated noise. The machine-ports are not failed attempts to contribute. They are (disregarding intentions, but effectively) the campaign's long game, deployed as inventory. - -.. _brandolinis-fragile-triad: - -A Persistent, But Fragile Triad -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This chapter as a whole has documented three fronts: The legal-ideological attack on the license (:ref:`Open Source Means Open Source`), the substitution and usurpation pattern in machine-generated implementations (:ref:`Assistance Versus Machine Substitution`), and the takeover narratives examined in this section. They are three fronts of the same operation, run by the same cast. - -* The license attack and other urgency-engineered narratives are the *cover*: They supply - the ideological justification for treating the reference as illegitimate, and needing replacement. -* The ports are the *inventory*: they supply the "replacements" that the narrative claims - the ecosystem needs. -* The takeover narrative is the *objective function*: the repeated, persistent, escalating claim that - the reference should be replaced, and that the speakers themself are the natural heirs. - -Each front depends on the others, and all three depend on a single, fragile assumption: That their audience cannot count. Restore the count, and the charade collapses into what it is: - -**A small group of people, obsessively orbiting a technology they did not build, synthetically producing the -appearance of a constituency they do not have, in service of a takeover they cannot justify.** - -.. _brandolinis-pattern-is-not: - -What The Pattern Is Not -^^^^^^^^^^^^^^^^^^^^^^^ - -It is important, to also say what this pattern is *not*, exactly because the campaign depends on being mistaken for such things. - -**It is not criticism** - -The Reticulum community has always had lively, rigorous criticism - of decisions, of priorities, of direction - and it has always engaged that criticism on the merits, and changed course and development priorities many times. The license discussions at the time of the change were extensive, public, and answered in full, including with the critics themselves. Core protocol decisions were always discussed and evaluated in the community. The complex coordination and execution of several protocol upgrades between a loosely organised group of developers, node operators and users have succeeded surprisingly fluidly. All of these are success stories that the "critics" **always** ignore. Real criticism engages the content and accepts evidence. The campaign engages nothing. - -**It is not a community** - -Communities *build things*, maintain them, and have skin in the game; long-term users, operators of nodes, authors of real applications, people whose traffic flows through the network every day. A community has a stake. The "movement" only has an agenda. - -**It is not a debate** - -A debate has parties who evaluate arguments and evidence, and as a function of that, the debate moves forward, perpetually. The campaign's arguments, as shown above, are not designed to survive contact with answers, but to be repeated past them. You cannot debate a loop. You can decline to perform for it, though. - -.. _brandolinis-intent-and-consequence: - -Intent & Consequence -^^^^^^^^^^^^^^^^^^^^ - -While it would provide for interesting speculation, we do not need to know what the individuals in the cast *intend*, and this document will not speculate about their inner lives. People may believe their own narratives entirely, or partly, or not at all; **the pattern is the same in any case**, and the pattern is what matters here, since it is the exact thing that has been so insidiously destructive and resource wasting for the actual Reticulum project and community, ever since Reticulum was more widely discovered, popularized, and became a prime target for all sorts of grift known to man. - -The consequences are very real: - -* **Time**. The community's scarcest resource is the attention of skilled people who actually build - and maintain the network. The campaign converts that attention into reactive defence, on a cycle - with no end. -* **Harm**. Hastily machine-generated implementations, marketed as drop-in replacements and carrying - traffic onto shared networks, mislead users and can damage the network and its neighbours. - We, the actual reticulum community, all rely on these networks every day, and it is my belief - that we need to defend them. The fabricated measurements and laundered authorship examined in this chapter are not - editorial embellishments; they are the mechanism by which real users are induced to run - unverified, and often destructive software. -* **Erosion of the currency of trust**. Every manufactured scandal, every fluently polite smear, every - fabricated "movement" debases the currency of trust and degrades it for **everyone**; - including, most of all, the honest projects that come after. This is deep harm, and - it is the primary reason this chapter exists. - -Whatever the intent - profit, control, attention, or a simple, obsessive inability to stop - the mechanism is the same, and the response should be *calibrated to the mechanism as a whole*, not to the individual person acting as the current avatar for it. - -.. _brandolinis-recognition-and-response: - -Recognition & Response -^^^^^^^^^^^^^^^^^^^^^^ - -For anyone who encounters the pattern fresh, here is a proposal on how to recognise it, and what to do about it. - -**Recognition:** The pattern is recognisable in minutes, once the signs are known: - -* Claims that have been answered before, repeated as if never answered. -* The register of solemn concern applied, without exception, in one direction. -* A severe allergic reaction to answering any questions posed. -* A cast that is small, overlapping, and frequently anonymous, with the *plural form* doing the - work of plurality. -* Sockpuppet theater: The same person or footsoldiers thereof appearing as several "supporters" - (and occasionally slipping up mid-performance). -* Articles, ports, and forum threads that cite and reference each other in a closed loop. -* The immediate, reflexive turn to "bullying", "exclusion" and "toxicity" whenever a claim is refuted. -* A complete absence of engagement with the substance of the answers given, and a complete - absence of stake in the network itself. - -**Response:** A constructive response is one that is proportionate, and does not cost the -community its resources, sanity and dignity: - -* Engage the content once, on its merits (if any), with evidence, in public, and then refer to the - existing records. The answers already exist; they do not need to be re-derived for every new - bottle of piss-sour burgundy served from a man behind the curtains. I will do my best to keep this - chapter up to date with such answers and records, for easy reference, but other great resources - exist. -* Do not become part of the theater performance. The "movement" feeds on engagement in *any* form; - attention is its fuel, and outrage is as good as endorsement for them, if not better. -* Do the headcount. When the audience is reminded that a "movement" is a dozen people - recycling answered falsehoods, its projected power dissipates. The power was never real; - only the audience's own assumption of scale can make the illusion effective. A polished-looking - "ground up" implementation might look like the work of many people, but is most likely - just the result of an LLM looping on a laptop for a couple of months (this is weird, but it is - the reality we now live in). Four different users suddenly singing in unison may look like the - representatives of a broader sentiment, but is most likely the same person hiding behind the same keyboard. -* Then, spend the saved time on the networks: On building real infrastructure, putting up nodes - and antennas, on real development, real maintenance, real coexistence, and on the people - *who actually use Reticulum*. The things that we can only do *together*, as humans. That is - what the fake "movement" cannot do, no matter how fluent and smooth its productions become. - -.. _brandolinis-health-and-coexistence: - -Network Health & Coexistence ----------------------------- - -.. _brandolinis-substance: - -Substance Of The Networks -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Before examining health, we should consider what is being protected. The networks are not a repository. They are not a website, a social presence, or a set of marketing pages. They are a habitat: A living commons of communication, carried by, from a catious estimate, an intermesh now consisting of over a hundred thousand physical devices, embedded in a plethora of applications and packages, transporting terabytes of traffic every single day - real messages, real coordination, real people - across radios, serial lines, phones, laptops, and backbone servers, in cities, places and ways that no other network reaches. - -The protocol belongs to humanity; no one owns it. The network belongs to everyone on it, and its membership is defined by nothing other than participation: A node on a mast, a phone in a pocket, a server routing for its neighbourhood, a developer building on the commons. The network has no gatekeepers because it needs none; it is held together by the same physics and mathematics everywhere, and by a shared expectation of how members behave. That expectation - unwritten, unenforced by any central authority, and just as important as any code - is the subject of this section. The *Zen of Reticulum* calls the software a habitat. A habitat is healthy or unhealthy as a whole, and the health of this one is decided, every day, by the people who live in it. - -.. _brandolinis-property-of-the-whole: - -Health Is A Property Of The Whole -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A network is not made healthy by the excellence of any single part. It is made healthy by the behaviour of every part on the shared medium, and how they interact. And the shared medium is the point. When your node transmits, it transmits into the same spectrum, the same line, the same time, the same mathematical address space, the same bandwidth that its neighbours depend on. - -The network cannot be certified by any single project's claims. A node that is a bad neighbour, that invents its own channel behaviour on shared spectrum, that announces without discipline, that spawns tens of thousands of links to scrape the network, that floods and retries without mercy, is a hazard to every node that shares its medium, regardless of how well its "creator" imagines its own code to work in isolation. - -On a shared medium, "my software works" is not a complete sentence. The complete sentence is "my software works *and coexists*". A network's health deteriorates with its worst-behaved member, and every member has a responsibility to not be *that* member. There is no final, technical solution for this; the solutions are human. - -.. _brandolinis-history-of-what-works: - -A History Of What Works -^^^^^^^^^^^^^^^^^^^^^^^ - -Let's recall, because it is the ground truth this entire thing rests on, that the coexistence model has *worked* - is working, right now, **at scale**. Reticulum's protocol decisions have been discussed and evaluated in the community, openly, for years. Protocol upgrades have been designed, coordinated, and executed between a loosely organised group of independent developers, node operators, and users - and have succeeded, repeatedly, without any corporate board, without any central command, and without the collapse that the skeptics predict every second week (while screaming for *GOVERNANCE*). The reference implementation has been developed in the open for over a decade, by a maintainer who answers for it, fixes bugs or issues within hours of their discovery, and is set down in code deliberately kept small enough (the entire protocol implementation under 15,000 lines, including its comments) for a human to read, verify, *and challenge*. - -This is the empirical baseline that every narrative examined in :ref:`A Movement Of A Dozen` conveniently ignores, in order to justify: - -* The claim that the project is a one-man dictatorship -* The claim that the community is excluded -* The claim that the networks need saving from their own founders - -The **reality**, documented and reproducible, is that a loose community of humans coordinates complex technical evolution *without* the apparatus the narratives insist is missing. The health of the networks is not a promise for the future; it is a demonstrated property of the past and present, under the leadership and community model that brought it into existence and navigated it through difficult decisions and situations over a decade. **My job is to keep it that way**, and you can be damned certain that I take it seriously. - -.. _brandolinis-role-of-the-individual: - -The Role Of The Individual -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The networks are built by physical acts and human coordination. You cannot prompt an antenna into existence. No model can string up a dipole, or stand in the rain aligning a radio, or coordinate with a neighbour three valleys away about who covers which repeater frequency, or sit down with a newcomer and explain why their first node didn't announce. These are the acts that make a network *organic* - present in the physical world, resilient because it is *real*, valuable because it is *human*. The machines and technology we use are tools aimed at a well-defined, openly described goal. But the networks themself are composed of intentional human actions, and every node operator is infrastructure for other humans. - -This has a corollary for the individual: You do not need permission, and you do not need to be an expert, to participate. Run a node. Help a neighbour. Ask questions in the community, and answer the questions you can. Build something small at first. The network grows by these acts, and they are the most meaningful ones - the things that can only be done together, as humans, in the physical world. The parasitic "movement" that haunts the peripheries of this ecosystem cannot do any of this. It cannot build a node because it does not exist, in any real connected and human sense; only people can, and people are precisely what a self-recurrent loop lacks. - -.. _brandolinis-place-of-new: - -The Place Of New Implementations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The future of the network includes more implementations - and they are welcome. This chapter is not an argument against implementing Reticulum. The protocol is public domain, by design, and the network is made stronger by genuine diversity: More *good* implementations, honestly made, mean more resilience, more experimentation, more people who understand the protocol deeply *because they have built it themselves*. That is the goal: **For people to understand and own the technology they rely on**. Not to spew out 600,000 line rust ports that fills no meaningful gap or purpose. - -What the ecosystem needs, from every implementation, is the ordinary floor that :ref:`Evaluating A Reticulum Implementation` establishes, and that the reference has met for over a decade: A human who made it and answers for it; provenance disclosed; attribution given; benchmarks honest or absent; interop demonstrated against the real network; code a human can read. And it needs the one thing that costs nothing and changes everything: **Honest labeling**. - -An experimental implementation that says, plainly, as the first thing a user sees: "This is in-progress, experimental, do not depend on it yet, but help if you can", that is a gift to the ecosystem. It contributes, it invites participation, and it protects users. - -The opposite, the polished "drop-in replacement", does damage far beyond its own users: It dissuades the honest developers who might have built *the real thing*, because the polished slop persuades them the work is already done. That dissuasion is one of the quietest and most expensive harms in this entire affair, and the antidote is honesty at every project's door. - -.. _brandolinis-expanding-commons: - -Expanding The Commons -^^^^^^^^^^^^^^^^^^^^^ - -Coexistence, reduced to its essentials, is a short formula; the duties of membership in a network no one owns: - -* **Be honest.** Say what you are, what you built, how you built it, and what you used to - build it. Label experimental work as experimental. Credit the work you build on, including - the reference implementation, in the licenses and in the prose. Concealment is not a - defence; it is a very clear diagnosis marker. -* **Be owned.** Have a human who made the work and can answer for it. Use assistance, don't - substitute. Never sign what you cannot answer for. The chain of accountability is the - community's only verification at scale, and every link in it is a *person*, not a facade. -* **Be a good neighbour.** Respect the shared mediums. Do not invent new behaviour in our - shared space and call it progress. The network belongs to its other members as much as - to you. -* **Verify against the network.** Compatibility is a property demonstrated with actual nodes, - on actual media, against the actual current protocol, over a period of *years*, not 48 hours. - And not a claim pinned to an old snapshot over loopback. -* **Participate as a member.** Ask, answer, help, build. *Advertising at* the community is not - the same as being part of it, and it is **not** the first thing you should do. - -None of this is a high bar. It is the bar the reference implementation has met, in public, for over a decade, and the bar the ecosystem has adopted organically itself, successfully, through every protocol upgrade it has ever coordinated, and everything running on Reticulum today, that people actually use. It is not a new requirement; it is the existing, working culture of the networks, now written down so that newcomers can find it and so that those who would exploit us cannot claim they never saw it. - -.. _brandolinis-defence-of-commons: - -Defence Of The Commons -^^^^^^^^^^^^^^^^^^^^^^ - -The networks need defending - not because they are under severe physical attacks (yet), but because attention, talent and resources is being stolen from them, trust is being mined against them, and its newcomers are being harvested by the machinery documented in this chapter. Defence, for a commons, has specific shapes, and it is not what the theater would like it to be. - -* Defence is **documentation**: Records kept public, of what was claimed, what was - answered, and what turned out true. -* Defence is **a headcount**: Calm, unexcited reminders that a "movement" might just be a dozen - people having a tantrum. -* Defence is **honesty in action**: The visible, ordinary practice of the good behaviour - by the people who actually run the networks. -* And defence is **building**: Every node raised, every page of *real* documentation, every - honest implementation, every new user helped into the community with the knowledge to know - what they are doing is more durable than any rebuttal, because it adds to the thing being - defended. - ----- - -**The theatre cannot keep up with a network.** - -**It can only orbit it, while it recycles its own sour piss.** - ----- - -A lot of us rely on these networks every day. They carry our messages, our coordination, our friendships, and in some places, our safety. That is exactly why they are a target: - -They are valuable, free, and very hard to shut down. The response is not to become aggressive. It is to be *more* of what the network already is: Real, open, owned, and shared. - -*Mark* - -.. _brandolinis-prns: - -Addendum A: The Prns Case -------------------------- - -.. _brandolinis-purpose-and-scope: - -Purpose & Scope -^^^^^^^^^^^^^^^ - -This section provides a concrete example, which future evaluations can use as a template. Its purpose is not to be the last word on this particular subject, but to be an example of how the ideas in this chapter can be applied, and to make the next such examination easier. - -The subject of this case study is **Prns**, a project published at the repository ``github.com/KenAKAFrosty/Prns``, presented as a "ground-up implementation of Reticulum, written in Rust", marketed as faster, safer, and more strictly tested than the reference, widely promoted with the claim of "up to 89× the throughput" of the reference implementation, and an "author" actively engaged in the tactics described in :ref:`A Movement Of A Dozen` on online platforms. The record described below is the project's own, and sourced from its GitHub repository, analyzed locally. - -.. _brandolinis-project-in-brief: - -The Project In Brief -^^^^^^^^^^^^^^^^^^^^ - -* **Time span.** 3,374 commits between 2026-05-26 and 2026-09-12: approximately 110 days, - at a mean rate of ~31 commits per day. Monthly: 132 / 1,137 / 1,097 / 805 / 203. -* **Authorship.** 3,252 of 3,374 commits (96.4%) are authored by a single account. A - second identity, "Prns Tests tests@example.test" - a fictitious identity on a reserved - domain - contributed 35 commits, every one of them website and marketing content. -* **Size.** 3,519 tracked files; 1,983 Rust files; 636,226 lines of Rust; approximately - 22.7 MB of Rust source; 69.5 MB of total repository content. -* **Scope claimed.** A daemon ("drop-in replacement" for the reference daemon), embedded - firmware for three microcontroller families (including six ESP32-S3 board variants and - nRF52840), SDKs and bindings for nine languages, a browser-based node and web flasher, a - twelve-language website, a benchmark apparatus with its own publication and "proof" - pipeline, and a validation hub with oracles, fuzzing, mutation testing, and formal-proof - claims. -* **Marketing claims.** "Ground-up implementation of Reticulum"; "up to 89× the - throughput"; "48× smaller peak-memory footprint"; "33× the energy efficiency"; - "Measured, not just claimed"; "Enforced, then audited"; "byte-for-byte wire parity with - the reference implementation"; "works with Sideband, NomadNet, MeshChat, etc."; - "no_std (no alloc required either)". -* **Status at the time of writing.** Version 0.3.7; published version history begins at - 0.3.0. The repository continues to receive commits. - -.. _brandolinis-prns-question-1: - -Question 1: Is this a clean implementation, or was it directly generated from the reference? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The first of the two deciding questions from :ref:`Evaluating A Reticulum Implementation` is settled by the project's own record. An actual :ref:`clean-room` implementation, reasoned out and written by humans, would owe nothing to any license. The data available shows clearly that Prns is not that. - -* **The founding state.** The first commit, "Scaffold Personal Reticulum suite", contains - the scaffolding of not one but two ports - ``personal-rns`` and ``personal-lxmf``\ , the latter - being the reference's companion messaging protocol - together with a daemon scaffold and - a 233-line founding document, ``docs/build-ethos.md``. That document declares the project's - goal as a "performance-focused drop-in-replacement for rnsd" and its governing principle - as: "Port the contract, not the implementation." It further states directly to the - machine-loop, that **"You owe the reference fidelity at exactly two boundaries: - The wire... The behavior. ... Between those two boundaries, the model is yours"**. - At this point, the very first commit, effectively the whole of the implementation design - and any relevant decisions are handed over to the LLM. -* **Continuous ingestion from the reference.** 506 commits reference RNS or Reticulum - source behaviour; the word "parity" appears in 49 commit messages. Commits cite the reference - implementation's own source locations (for example ``Transport.py:1367``) when correcting - their own divergences and mistakes. -* **Byte-level fidelity as an explicit goal.** Within 48 hours of the first commit, the - record shows a "crypto adapter - vetted primitives, byte-exact vs RNS 1.3.1", and a wire - implementation matching the reference's packet header layout. The project's own - validation apparatus decodes wire vectors using the reference implementation's own - packet parser, and the interop suite consists of real, stock-RNS peer nodes - that is, - the reference source itself, used as the ground truth against which this LLM output is - checked, and committed as accepted and verified after 48 hours of the project's recorded start. -* **The marketing mismatch.** The same record that documents all of the above describes the - project, publicly, as "a ground-up implementation of Reticulum" - while declining, on its - website, to link to the reference implementation at all: the entire tree mentions the - reference's author in six files, and the website in none. - -Conclusion: the project's own history and commit record establishes that it was generated directly from the reference implementation's source code, not reasoned out from the public-domain protocol. It is a derivative of licensed work within the meaning of :ref:`the Reticulum License` and :ref:`general copyright law`. The "ground-up" claim, which carries its entire legal and marketing posture, is disproven by the project's own history. - -.. _brandolinis-prns-question-2: - -Question 2: Who, or what, is the agent of the work? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The second deciding question is answered by the application of :ref:`Assistance Versus Machine Substitution` criteria to the project. - -**Scope that no human could have authored** - -636,226 lines of Rust in 110 days is approximately 5,800 lines per day, every day, including weekends - with 149 commits recorded between midnight and 06:00, and Saturday the single busiest weekday. No human review or decision capacity is commensurable with this volume; the review capacity and the output volume are structurally incommensurable, which is the definition of unauthored scope. - -**Uniform generation** - -The record shows no sign of human development, growth, or learning: No contested decisions, no style drift. Instead: endless renaming (in the first 48 hours: ``State → EngineState → TickInput → TickOutput``\ ), uniform fluent register in every commit, and the exact same, synthetic voice in code, documentation, and website copy. - -**Discovery by regeneration** - -Corrections arrived by the machine's own subsequent runs, recorded as planned milestones. In early July 2026, a series of "core review" commits documented, in the project's own words: - -* announce-ID history "was unbounded per-slot growth" (\ ``54cc8d601``\ ); -* interface announce limiting could "leak a sustained flood" (\ ``9d27af75c``\ ); -* a "phantom-airtime leak" where frames "used to vanish silently" (\ ``4ed75ffdd``\ ); -* Group-packet deduplication "which RNS does not do" - a behaviour the reference never had, - implemented and later removed (\ ``edd8e72be``\ ); -* "both ``unwrap_or(&[])`` arms were unreachable dead defaults" (\ ``74bea355e``\ ); -* a benchmark crate "hasn't compiled since" a prior rename, undetected for weeks because it - was outside the CI's workspace (\ ``86240b027``\ ). - -Earlier still: the record shows a ">1 MiB RNS-to-Prns interop stall" closed three weeks in -(\ ``cc4d524a3``\ ), and a known send-path hang shipped with the note "typed fast-fail a -candidate for later" (\ ``a010e6613``\ ). - -**Laundered provenance** - -The repository contains a committed commit-message hook (\ ``.githooks/commit-msg``\ ) that *blocks* any commit containing attribution for machine generation ("generated with claude/codex/copilot/cursor/gemini", "co-authored-by"), with the explanation "The committer owns the commit". The contributing guide is explicit: "AI tools ... are welcome ... what you submit is *yours*", and describes the contributing population as "both human and automated contributors". The fictitious "Prns Tests" identity completes the picture: A manufactured, unknowable contributor, used for marketing content. - -**Authorship by assignment** - -The human whose name is on the work cannot answer for it, because the choices were never made by a human, and only the general shape and constraints of the outward presentation was. Nothing in the project's history shows an individual explaining a design decision, accepting a mistake in the first person, or going deeper under technical questioning; the record shows process language and marketing instead. Per the earlier mentioned test: any other person could have pressed the same button, and nothing would be different. The project is effectively unowned, and void of value. - -By the criteria of :ref:`Assistance Versus Machine Substitution`, this is substitution. The machine was the agent of the work; the "author" was a facade. - -.. _brandolinis-prns-performance: - -The performance claims -^^^^^^^^^^^^^^^^^^^^^^ - -The project's central marketing claim - "up to 89× the throughput" of the reference - is also its most completely documented failure, because the project published the evidence of its own measurement. The apparatus, the results, and the problems are all on the record. - -**The claim** - -The README states "up to 89× the throughput"; the on-network marketing page repeats "up to 89× the throughput, 48× smaller peak-memory footprint, and 33× the energy efficiency of stock RNS 1.4.2"; the website's front page carries the tagline "Measured, not just claimed". - -**The apparatus** - -The reference is loaded through ``benchmarks/reference/compiled_reference.py``\ , which installs ``pyximport`` and then asserts two things: that ``RNS.compiled == True``\ , and that at least *one* RNS module was loaded from a native extension. The project's own published "proof" records show exactly what those checks captured, on every platform: - -.. code-block:: - - aarch64-apple-darwin: .../RNS.cpython-313-darwin.so, Cython 3.2.8, Python 3.13.13 - x86_64-unknown-linux-gnu: .../RNS.cpython-313-x86_64-linux-gnu.so, gcc 11.4.0 - x86_64-pc-windows-msvc: ~/.prns-oc/lib.win-amd64-cpython-313/RNS.cp313-win_amd64.pyd - -In every case the recorded native module is the **top-level package only** - ``RNS.so``\ , nothing below it. Nothing named ``RNS.Transport``\ , ``RNS.Destination``\ , ``RNS.Packet``\ , ``RNS.Link``\ , or ``RNS.Resource`` ever appears, because *nothing below the top level was ever compiled*. Nobody bothered to check or verify *anything* here. - -The machine output was simply taken for truth, and a full marketing strategy was spun and produced on it. "RNS 1.4.2 (compiled)" - the label used in every table, chart, and marketing page - *is not a mode of RNS that exists*, but the hallucinations of a machine, which the humans created a marketing strategy on. In reality, it is the old ``CRNS`` development shim, whose top-level-only, debug-oriented "compilation" is slower than plain interpreted RNS, and which was *defunct for over a year* before these benchmarks were published. - -**Interface choices** - -The harness writes the reference's configuration itself (\ ``benchmarks/reference/participant_node.py``\ ): ``TCPClientInterface`` for the initiator, ``TCPServerInterface`` for the responder and both relay sides, with ``UDPInterface`` as the abstract wire. The reference was never given its modern ``BackboneInterface`` on any platform, forcing the slowest possible interrfaces. The interface selection is never mentioned in any methodology text. - -**The policy** - -The published default-policy tables record the asymmetry directly: Prns at 500 Mbps / 128 KiB TCP policy against the reference at 10 Mbps / 8 KiB - slyly footnoted as "preserves each implementation's normal TCP policy" (in the case of RNS, a conservative default policy that was selected to prioritize coexistence on slower, shared links, requiring the user to dial it up to full speed if they knew what they were doing). Raw transport "throughput" ratios of 20.65× (Linux) and 38.84× (Windows) are therefore mostly policy, not engine. - -**The cross-host evidence** - -The published results show the same scenario producing wildly different ratios by host, and the variance is entirely in the reference artifact: - -.. list-table:: - :header-rows: 1 - - * - Host - - Prns, single-packet - - "Reference", single-packet - - Ratio - * - macOS (Apple M4) - - 37.4k/s - - 420/s - - 89.09× - * - Linux (i7-1260P) - - 25.3k/s - - 3.5k/s - - 7.19× - * - Windows (Ryzen 5 5600X) - - 30.8k/s - - 3.9k/s - - 8.00× - -Prns is consistent (25–37k/s). The reference artifact collapses to different depths on different hosts, and the single worst collapse - 420 packets per second on macOS - is the cell selected as the global headline. The "89×" is not a measure of Prns; it is a measure of how badly the broken baseline happened to fail on one host. - -For reality calibration: Stock, interpreted RNS 1.5.4 on an ordinary mid-range laptop, single core, sustains roughly 30,000 single-packet deliveries per second (including ephemeral-key decryption), ~175,000–280,000 packets per second in transit relay, and on the order of 10 Gbps at 16 KiB payloads. The project's "compiled" artifact measured 420–3,900 packets per second for its single-packet scenario: An order of magnitude or more *below* stock interpreted RNS. The comparison being marketed was not "Rust vs Python"; **it was "an implementation running under optimal conditions versus a mislabeled, broken shim on a slow legacy interface at a severe policy disadvantage"**. - -**The equalized rows** - -The project's own "1 Gbps policy" rows, in which both sides are explicitly configured identically, publish ratios of roughly 2.4–8.4× across all hosts and scenarios - still against the broken baseline, but nowhere near 89×, and consistent with the ordinary, uncontroversial observation that a native implementation tends to outperform an interpreted one in raw throughput. That point needed no fudging to make, and is banal. - -Interestingly, though, it seems that Prns is really only *marginally* faster than a Python implementation, running in interpreted mode, on a single CPU core. - -**Assessment** - -The front page says "Measured, not just claimed." The record shows the opposite ordering: claimed, and arranged to look measured. No methodology section anywhere states what "compiled" means, that it is a deprecated development shim, that it builds debug targets, or which interface the reference was given; the one statement of the policy asymmetry appears in a footnote of the results tables themselves. If they did not need to fudge it, they would have published the method clearly. - -.. _brandolinis-prns-overall: - -Overall Evaluation -^^^^^^^^^^^^^^^^^^ - -Evaluated against the conditions of :ref:`Evaluating A Reticulum Implementation`: - -* **Provenance and attribution.** Fails: The fake "ground-up" origin story; zero reference links - on the website; a single well-hidden link in the README; a license file reading - ``Copyright (c) 2026 The Prns Authors`` over work directly generated from the licensed reference; - no original copyright or permission notice anywhere. -* **Agency and answerability.** Fails on every criterion of :ref:`Assistance Versus Machine Substitution`: - Unauthored scope, uniform generation, discovery by regeneration, laundered provenance, - authorship by assignment. -* **Engineering.** Fails: The benchmark apparatus above; interop demonstrated only - against a pinned, outdated reference version over loopback while the marketing claims current - parity, interop failures with stock RNS; vendored prebuilt binaries and a republished fork - of an upstream crate (``nrf-softdevice 0.1.0-prns.1``) with no verifiable provenance; - committed WASM bundles. -* **Network behaviour.** Fails: Invented spectrum behaviour and compatibility claims - about third-party applications of the reference ecosystem (Sideband, NomadNet, MeshChat) - that are not established by any evidence. -* **Monetary and influence signals.** The marketing apparatus - website before protocol, - day-three twelve-language marketing, false benchmark headlines, "audited" and "formal proof" - claims - the conformal surface described in :ref:`Evaluating A Reticulum Implementation`: - is where this project invested. - -.. _brandolinis-prns-licensing: - -Licensing Analysis -^^^^^^^^^^^^^^^^^^ - -Applying the two legal layers established in :ref:`Copyright & The Two Legal Layers`: - -**Layer A - copyright infringement (license-independent)** - -The record shows a direct copy roundtrip: the licensed reference implementation fed into a machine, transformed, at a scale and cadence no human could review, with a license file asserting a fresh copyright in the operator's name and no acknowledgment of the work the project was generated from. No substantial, new human creative input is demonstrated anywhere in the record - the founding document explicitly frames the design as an instructed port of the reference's contract. A primarily machine-generated copy of a copyrighted work, not accompanied by demonstrable substantial human creative input, is a derivative work over which the copier holds no copyright, and a reproduction of which without the original author's permission infringes the original author's copyright. This holds under any license, MIT included. The operator's copyright claim over the result is, on this evidence, not merely unsubstantiated; it is the absence of one, directly asserted in the project's own writing. - -**Layer B - license conditions (Reticulum License specific)** - -Modern Reticulum - the current wire format, the AES-256-based link encryption, ratchets, the cryptographic machinery as it exists today - has been published under the Reticulum License since April 15, 2025, effective from release 0.9.4 and every release since. As a derivative of that work, the license's conditions attach: The copyright and permission notice must be included in all copies and substantial portions; the AI-training condition must be honoured; the harm condition must be honoured. The record shows none of the notice requirements met, and a production process that consists of the licensed work being ingested into a machine and regurgitated, at industrial scale and cadence. - -**The cascading consequence** - -Prns is distributed claiming dual MIT/Apache-2.0 licensing. It is, however, a direct derivative of Reticulum-licensed work, and the claimed grant is therefore void. Every downstream project that builds on Prns believing it is MIT/Apache-licensed is building on a grant that does not exist. The users of Prns, and all software developers using it, have in effect, been placed under conditions they were never told about. The "ground-up" claim is not merely false; it is the sole load-bearing element of an entire licensing posture, and it fails spectacularly. - -.. _brandolinis-prns-consequences: - -Consequences -^^^^^^^^^^^^ - -The consequences of this pattern are examined in the following sections: The harm to users and to the network in :ref:`A Movement Of A Dozen`, and the human dimension in :ref:`Network Health & Coexistence`. The contribution of this addendum is narrower and intended as an instructive example: This is what a substitution project looks like, in full, on its own record. It is a reference example for :ref:`Evaluating A Reticulum Implementation`, and it demonstrates, in one case, most of the elements this chapter describes. - -.. _brandolinis-prns-verdict: - -Verdict -^^^^^^^ - -Applying the three verdicts of :ref:`Evaluating A Reticulum Implementation`, this project does not meet the standard. It is not "experimental but honest" - it is not labeled as experimental anywhere; the first thing users saw was a polished marketing presence promising a finished, tested, drop-in replacement. It is a project whose central claims are fabricated (the performance apparatus), whose provenance is laundered (the authorship machinery), whose foundation is misrepresented (the "ground-up" story), whose core semantics were learned late and by regeneration, and whose operator cannot possibly answer for any of it. It is also a blatant copyright violation of the reference implementation. - -The proposed conclusion, for users and for the network: Treat the Prns project as hostile regardless of its polished surface, and as a hazard insofar as it invites users onto shared networks. Nothing in this section is a judgment about the people involved; it is a reading of the results produced, their history and how they came to be, which in the regrettable lack of any human authorship is the only thing that can be expected to be held to account. - -.. _brandolinis-prns-dating: - -Dating -^^^^^^ - -This examination reflects the publicly available material as of September 2026; the repository in question continued to receive commits at the time of writing, so readers should treat citations to commits and files as point-in-time evidence. A `forensics snapshot `_ is publicly available on GitHub. diff --git a/docs/source/git.rst b/docs/source/git.rst index 3699be39..6cedd1b0 100644 --- a/docs/source/git.rst +++ b/docs/source/git.rst @@ -8,16 +8,6 @@ This chapter of the manual serves as the technical reference for the distributed A set of utilities for distributed collaborative software development and publishing are included in RNS. -.. only:: html - - .. image:: screenshots/rngit.webp - :align: center - -.. only:: latex - - .. image:: screenshots/rngit.png - :align: center - The system consists of two parts: The ``rngit`` node that hosts repositories, and the ``git-remote-rns`` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: ``rns://DESTINATION_HASH/group/repo``. If you set a branch to track a Reticulum remote as the default upstream, you can simply use ``git`` as you normally would; all commands work transparently and as expected. @@ -143,9 +133,6 @@ To create a new empty repository on a remote node: This creates a bare Git repository at the specified path. You must have ``create`` permission for the target group. When a repository is created, the creator automatically receives ``adm`` (admin) permissions on the repository through an auto-generated ``.allowed`` file. -.. tip:: - If you want to use a different primary branch name than ``master``, you can change it inside the bare git repository directory with ``git symbolic-ref HEAD refs/heads/name_of_other_branch``. - **All Command-Line Options (rngit create)** .. code:: text @@ -696,7 +683,6 @@ The following template files are supported: - ``commit.mu`` - Individual commit detail page - ``refs.mu`` - Branches and tags listing - ``stats.mu`` - Statistics page -- ``no_ident.mu`` - Displayed to non-identified initiators if blocked Templates can include the following variables: diff --git a/docs/source/graphics/rns_logo_256.webp b/docs/source/graphics/rns_logo_256.webp deleted file mode 100644 index 8c2e9196..00000000 Binary files a/docs/source/graphics/rns_logo_256.webp and /dev/null differ diff --git a/docs/source/graphics/rns_logo_512.webp b/docs/source/graphics/rns_logo_512.webp deleted file mode 100644 index 566a2ff1..00000000 Binary files a/docs/source/graphics/rns_logo_512.webp and /dev/null differ diff --git a/docs/source/index.rst b/docs/source/index.rst index fd305241..d00efa5c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -32,7 +32,6 @@ to participate in the development of Reticulum itself. support examples license - brandolinis .. toctree:: :maxdepth: 2 diff --git a/docs/source/interfaces.rst b/docs/source/interfaces.rst index 9a9374a8..61625592 100644 --- a/docs/source/interfaces.rst +++ b/docs/source/interfaces.rst @@ -998,9 +998,6 @@ When ``discoverable`` is enabled, a variety of additional options become availab ``discovery_name`` A human-readable name for the interface. This name will be displayed to users on remote systems when they list discovered interfaces. If not specified, the interface name (the section header) will be used. -``discovery_lxmf_address`` - An optional LXMF address for contacting the operator of this interface. The address will be displayed to users on remote systems when they view details for discovered interfaces. Useful for getting in touch with other operators to interconnect networks with and coordinating network coverage. - ``announce_interval`` The interval in minutes between successive discovery announces for this interface. Default is 360 minutes (6 hours). For stable, long-running infrastructure, higher intervals (12 to 22 hours) are usually sufficient and reduce network load. Minimum allowed value is 5 minutes (but expect to have your announces throttled if using intervals below one hour). @@ -1570,34 +1567,4 @@ but all the parameters are exposed for configuration if needed. All of the above settings can be configured both as instance-wide defaults under the ``[reticulum]`` section of the configuration file, or on a per- -interface basis under the relevant interface configuration section. - - -Tuning Ingress Queues -===================== - -Reticulum uses separate, prioritized queues for processing inbound packets. The default four queues are drained in the following order: - - 1. Data and general traffic - - 2. Announces - - 3. Path requests - - 4. Ingress-limited traffic - -You can tune the queue sizes from the defaults, for example if you're running a setup where you want to simply drop most traffic from ingress-limited interfaces. The queue sizes are configured in the ``[reticulum]`` section of the config - - * | The ``qlen_in_data`` option sets the maximum size of the data - queue. Defaults to ``1024``. - - * | The ``qlen_in_announce`` option sets the maximum size of the announce - queue. Defaults to ``128``. - - * | The ``qlen_in_pr`` option sets the maximum size of the path request - queue. Defaults to ``128``. - - * | The ``qlen_in_il`` option sets the maximum size of the ingress limiter - queue. Defaults to ``8``. - -If an inbound packet arrives and its target queue is full, the packet is dropped. Reticulum will always completely drain higher-priority queues before starting to drain a lower-priority one. As an example, the ingress limiter queue will only start draining once all other queues are empty. \ No newline at end of file +interface basis under the relevant interface configuration section. \ No newline at end of file diff --git a/docs/source/screenshots/columba.png b/docs/source/screenshots/columba.png index b464ca07..89d85443 100644 Binary files a/docs/source/screenshots/columba.png and b/docs/source/screenshots/columba.png differ diff --git a/docs/source/screenshots/columba.webp b/docs/source/screenshots/columba.webp index 07ea65ed..956ea8e4 100644 Binary files a/docs/source/screenshots/columba.webp and b/docs/source/screenshots/columba.webp differ diff --git a/docs/source/screenshots/meshchatx.png b/docs/source/screenshots/meshchatx.png index 6677a0a6..018ec871 100644 Binary files a/docs/source/screenshots/meshchatx.png and b/docs/source/screenshots/meshchatx.png differ diff --git a/docs/source/screenshots/meshchatx.webp b/docs/source/screenshots/meshchatx.webp index 94edb72a..24fd7ba0 100644 Binary files a/docs/source/screenshots/meshchatx.webp and b/docs/source/screenshots/meshchatx.webp differ diff --git a/docs/source/screenshots/modem73.jpg b/docs/source/screenshots/modem73.jpg deleted file mode 100644 index ca3ebcb5..00000000 Binary files a/docs/source/screenshots/modem73.jpg and /dev/null differ diff --git a/docs/source/screenshots/modem73.png b/docs/source/screenshots/modem73.png deleted file mode 100644 index 2cec4d1d..00000000 Binary files a/docs/source/screenshots/modem73.png and /dev/null differ diff --git a/docs/source/screenshots/modem73.webp b/docs/source/screenshots/modem73.webp deleted file mode 100644 index 5adadc76..00000000 Binary files a/docs/source/screenshots/modem73.webp and /dev/null differ diff --git a/docs/source/screenshots/nomadnet_6.png b/docs/source/screenshots/nomadnet_6.png deleted file mode 100644 index 3df866ef..00000000 Binary files a/docs/source/screenshots/nomadnet_6.png and /dev/null differ diff --git a/docs/source/screenshots/nomadnet_6.webp b/docs/source/screenshots/nomadnet_6.webp deleted file mode 100644 index 3f973658..00000000 Binary files a/docs/source/screenshots/nomadnet_6.webp and /dev/null differ diff --git a/docs/source/screenshots/partyline.png b/docs/source/screenshots/partyline.png deleted file mode 100644 index 16982c6d..00000000 Binary files a/docs/source/screenshots/partyline.png and /dev/null differ diff --git a/docs/source/screenshots/partyline.webp b/docs/source/screenshots/partyline.webp deleted file mode 100644 index ac7d96c1..00000000 Binary files a/docs/source/screenshots/partyline.webp and /dev/null differ diff --git a/docs/source/screenshots/ren_browser.png b/docs/source/screenshots/ren_browser.png deleted file mode 100644 index 7737e50a..00000000 Binary files a/docs/source/screenshots/ren_browser.png and /dev/null differ diff --git a/docs/source/screenshots/ren_browser.webp b/docs/source/screenshots/ren_browser.webp deleted file mode 100644 index 6a6223ec..00000000 Binary files a/docs/source/screenshots/ren_browser.webp and /dev/null differ diff --git a/docs/source/screenshots/rentui.png b/docs/source/screenshots/rentui.png deleted file mode 100644 index 6272b4f5..00000000 Binary files a/docs/source/screenshots/rentui.png and /dev/null differ diff --git a/docs/source/screenshots/rentui.webp b/docs/source/screenshots/rentui.webp deleted file mode 100644 index 0c40fad6..00000000 Binary files a/docs/source/screenshots/rentui.webp and /dev/null differ diff --git a/docs/source/screenshots/reticulated.jpg b/docs/source/screenshots/reticulated.jpg deleted file mode 100644 index 785622b8..00000000 Binary files a/docs/source/screenshots/reticulated.jpg and /dev/null differ diff --git a/docs/source/screenshots/reticulated.webp b/docs/source/screenshots/reticulated.webp deleted file mode 100644 index ebd2a67e..00000000 Binary files a/docs/source/screenshots/reticulated.webp and /dev/null differ diff --git a/docs/source/screenshots/retipedia.png b/docs/source/screenshots/retipedia.png deleted file mode 100644 index fae27ff6..00000000 Binary files a/docs/source/screenshots/retipedia.png and /dev/null differ diff --git a/docs/source/screenshots/retipedia.webp b/docs/source/screenshots/retipedia.webp deleted file mode 100644 index 8ffe1a0f..00000000 Binary files a/docs/source/screenshots/retipedia.webp and /dev/null differ diff --git a/docs/source/screenshots/rngit.png b/docs/source/screenshots/rngit.png deleted file mode 100644 index 037b16fc..00000000 Binary files a/docs/source/screenshots/rngit.png and /dev/null differ diff --git a/docs/source/screenshots/rngit.webp b/docs/source/screenshots/rngit.webp deleted file mode 100644 index 5e1f54e4..00000000 Binary files a/docs/source/screenshots/rngit.webp and /dev/null differ diff --git a/docs/source/screenshots/rrc.png b/docs/source/screenshots/rrc.png deleted file mode 100644 index 21ab31b0..00000000 Binary files a/docs/source/screenshots/rrc.png and /dev/null differ diff --git a/docs/source/screenshots/rrc.webp b/docs/source/screenshots/rrc.webp deleted file mode 100644 index 56889713..00000000 Binary files a/docs/source/screenshots/rrc.webp and /dev/null differ diff --git a/docs/source/software.rst b/docs/source/software.rst index 66315fea..c7314aa1 100644 --- a/docs/source/software.rst +++ b/docs/source/software.rst @@ -4,225 +4,146 @@ Programs Using Reticulum ************************ -This chapter provides a non-exhaustive list of notable programs, systems and application-layer protocols that have been built using Reticulum. +This chapter provides a non-exhaustive list of notable programs, systems and application-layer +protocols that have been built using Reticulum. -Many different applications using Reticulum already exist, serving a wide variety of purposes from day-to-day communication and information sharing to systems administration and tackling advanced networking and communications challenges. +These programs will let you get a feel for how Reticulum works. Most of them have been designed +to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast +links, such as local WiFi, wired Ethernet, the Internet, or any combination. -.. tip:: - For ease of discovery while reading this manual over the web or as an e-book, links provided in this chapter primarily point to repositories and pages located on internet servers. In many cases, these are public web mirrors, with the upstream repositories residing on Reticulum nodes. For the latest source, updates and information regarding any project, be sure to check their Nomad Network or ``rngit`` nodes. +As such, it is easy to get started experimenting, without having to set up any radio +transceivers or infrastructure just to try it out. Launching the programs on separate +devices connected to the same WiFi network is enough to get started, and physical +radio interfaces can then be added later. -These programs will let you get a feel for how Reticulum works. Most of them have been designed to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast links, such as local WiFi, wired Ethernet, the Internet, or any combination. +Programs & Utilities +==================== -As such, it is easy to get started experimenting, without having to set up any radio transceivers or infrastructure just to try it out. Launching the programs on separate devices connected to the same WiFi network is enough to get started, and physical radio interfaces can then be added later. +Many different applications using Reticulum already exist, serving a wide variety of purposes +from day-to-day communication and information sharing to systems administration and tackling +advanced networking and communications challenges. -Development of Reticulum-based applications and systems is ongoing, so consider this list a non-exhaustive starting point of *some* of the options available. With a bit of searching, primarily over Reticulum itself, you will find many more interesting things. +Development of Reticulum-based applications and systems is ongoing, so consider this list +a non-exhaustive starting point of *some* of the options available. With a bit of searching, +primarily over Reticulum itself, you will find many more interesting things. -Browsing & Publishing -===================== +Remote Shell +^^^^^^^^^^^^ -In addition to the programs listed in this section, other Nomad Network clients include: +The `rnsh `_ program lets you establish fully interactive +remote shell sessions over Reticulum. It also allows you to pipe any program to or from a +remote system, and is similar to how ``ssh`` works. The ``rnsh`` program is very efficient, and +can facilitate fully interactive shell sessions, even over extremely low-bandwidth links, +such as LoRa or packet radio. -- `MeshChatX `_ -- `Columba `_ +In addition to the default, fully interactive terminal mode, +for extremely limited links, ``rnsh`` offers line-interactive mode, allowing you to interact +with remote systems, even when link throughput is counted in a few hundreds of bits per second. + +.. raw:: latex + + \newpage Nomad Network ^^^^^^^^^^^^^ -The terminal-based program `Nomad Network `_ provides a complete encrypted communications suite built with Reticulum. +The terminal-based program `Nomad Network `_ +provides a complete encrypted communications suite built with Reticulum. It features +encrypted messaging (both direct and delayed-delivery for offline users), file sharing, +and has a built-in text-browser and page server with support for dynamically rendered pages, +user authentication and more. -.. only:: html +.. image:: screenshots/nomadnet_3.png + :target: https://github.com/markqvist/nomadnet - .. image:: screenshots/nomadnet_6.webp - :align: center - :target: https://github.com/markqvist/nomadnet - -.. only:: latex - - .. image:: screenshots/nomadnet_6.png - :align: center - :target: https://github.com/markqvist/nomadnet - -It features encrypted LXMF messaging (both direct and delayed-delivery for offline users), file sharing, and has a built-in text-browser and page server with support for dynamically rendered pages, user authentication and more. It also includes an RRC client and a suite of network utilities. - - -Ren Browser -^^^^^^^^^^^ - -`Ren Browser `_ is a modern, standalone browser for Nomad Network using the `Reticulum-Go `_ implementation. The project is actively being developed, and should be considered alpha level software, but much of the functionality is already working. If you want to help expanding the ecosystem, this is an excellent project to take a look at. - -.. only:: html - - .. image:: screenshots/ren_browser.webp - :align: center - :target: https://github.com/fr33n0w/rBrowser - -.. only:: latex - - .. image:: screenshots/ren_browser.png - :align: center - :target: https://github.com/fr33n0w/rBrowser - -Ren Browser is great example of an application aiming for true, seamless interoperability with the existing software ecosystem. It can operate fully standalone using it's own embedded `Go implementation `_ of Reticulum, or seamlessly utilise a shared RNS instance already running on the system. +`Nomad Network `_ is a user-facing client +for the messaging and information-sharing protocol LXMF. RNS Page Node ^^^^^^^^^^^^^ -`RNS Page Node `_ is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files. +`RNS Page Node `_ is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files. Retipedia ^^^^^^^^^ -You can host the entirity of Wikipedia, StackExchange or any other ``.zim`` file, to Nomad Network clients using `Retipedia `_. - -.. only:: html - - .. image:: screenshots/retipedia.webp - :align: center - :target: https://github.com/RFnexus/Retipedia - -.. only:: latex - - .. image:: screenshots/retipedia.png - :align: center - :target: https://github.com/RFnexus/Retipedia - - -Git Over Reticulum -^^^^^^^^^^^^^^^^^^ - -The included :ref:`rngit` system provides a fully distributed solution for managing, hosting, collaborating on and interacting with Git repositories over Reticulum. - -.. only:: html - - .. image:: screenshots/rngit.webp - :align: center - -.. only:: latex - - .. image:: screenshots/rngit.png - :align: center +You can host the entirity of Wikipedia (or any ``.zim``) file to other Nomad Network clients using `Retipedia `_. .. raw:: latex \newpage -Messaging & Voice -================= - Sideband ^^^^^^^^ -`Sideband `_ is a technical LXMF and LXST client, for Android, Linux, macOS and Windows. It also serves as a multi-purpose Reticulum utility, with features and functionality targeted at advanced users. +If you would rather use an LXMF client with a graphical user interface, you can take +a look at `Sideband `_, which is available for Android, +Linux, macOS and Windows. Sideband is an advanced LXMF and LXST client, and a multi-purpose Reticulum +utility, with features and functionality targeted at advanced users. .. only:: html .. image:: screenshots/sideband_devices.webp :align: center - :target: https://github.com/markqvist/Sideband + :target: https://unsigned.io/sideband .. only:: latex .. image:: screenshots/sideband_devices.png :align: center - :target: https://github.com/markqvist/Sideband + :target: https://unsigned.io/sideband -Sideband allows you to communicate with other people or LXMF-compatible systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR Paper Messages, or anything else Reticulum supports. +Sideband allows you to communicate with other people or LXMF-compatible +systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR +Paper Messages, or anything else Reticulum supports. -It interoperates with all other LXMF clients, and provides advanced features such as PTT and voice messaging, real-time voice calls, file attachments, private telemetry sharing, and a full plugin system for expandability. +It also interoperates with all other LXMF clients, and provides advanced features such as voice messaging, +real-time voice calls, file attachments, private telemetry sharing, and a full +plugin system for expandability. .. raw:: latex \newpage MeshChatX -^^^^^^^^^ +^^^^^^^^ -`MeshChatX `_ is an all-in-one Reticulum client with messaging, calling, and a NomadNet browser, providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. +A `Reticulum MeshChat fork from the future `_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original `Reticulum MeshChat `_ project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork. .. only:: html .. image:: screenshots/meshchatx.webp :align: center - :target: https://meshchatx.com/ + :target: https://git.quad4.io/RNS-Things/MeshChatX .. only:: latex .. image:: screenshots/meshchatx.png :align: center - :target: https://meshchatx.com/ + :target: https://git.quad4.io/RNS-Things/MeshChatX -Features include full LXST support, custom voicemail, phonebook, contact sharing, ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security. +Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security. -Columba -^^^^^^^ +.. raw:: latex -`Columba `_ is a simple and familiar LXMF messaging app Android, built with a native Android interface and Material Design 3. It also includes LXST voice call functionality, and a mobile Nomad Network page browser. - -.. only:: html - - .. image:: screenshots/columba.webp - :align: center - :target: https://github.com/torlando-tech/columba/ - -.. only:: latex - - .. image:: screenshots/columba.png - :align: center - :target: https://github.com/torlando-tech/columba/ - - -Ren TUI -^^^^^^^ - -`Ren TUI `_ is an early prototype terminal LXMF client for Reticulum built with Odin on librns (Reticulum-Go). It aims for NomadNet-like messaging/browsing without urwid or ncurses. - -.. only:: html - - .. image:: screenshots/rentui.webp - :align: center - :target: https://github.com/Quad4-Software/Ren-TUI - -.. only:: latex - - .. image:: screenshots/rentui.png - :align: center - :target: https://github.com/Quad4-Software/Ren-TUI - -Chat -==== + \newpage Reticulum Relay Chat ^^^^^^^^^^^^^^^^^^^^ `Reticulum Relay Chat `_ is a live chat system built on top of the Reticulum Network Stack. It exists to let people talk to each other in real time over Reticulum without dragging in message databases, synchronization engines, or architectural commitments they did not ask for. -.. only:: html - - .. image:: screenshots/rrc.webp - :align: center - -.. only:: latex - - .. image:: screenshots/rrc.png - :align: center +The `rrcd `_ program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include `rrc-gui `_ and `rrc-web `_. RRC is closer in spirit to IRC than to modern “everything platforms.” You connect, you join a room, you talk, and then you leave. If you were present, you saw the conversation. If you were not, the conversation did not wait for you. This is not an accident. This is the entire design. -The `rrcd `_ program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include: - -- `Nomad Network `_ -- `MeshChatX `_ -- `Eridanus `_ -- `rrc-gui `_ -- `rrc-web `_ - RetiBBS ^^^^^^^ -`RetiBBS `_ is an old-school bulletin board system implementation for Reticulum networks. +`RetiBBS `_ is a bulletin board system implementation for Reticulum networks. .. only:: html @@ -242,14 +163,29 @@ RetiBBS allows users to communicate through message boards in a secure manner. \newpage -Voice & Telephony -================= +RBrowser +^^^^^^^^ -In addition to the programs listed in this section, other LXST voice clients include: +The `rBrowser `_ program is a cross-platform, standalone, web-based browser for exploring NomadNetwork Nodes over Reticulum Network. It automatically discovers NomadNet nodes through network announces and provides a user-friendly interface for browsing distributed content with Micron markup support. -- `Sideband `_ -- `MeshChatX `_ -- `Columba `_ +.. only:: html + + .. image:: screenshots/rbrowser.webp + :align: center + :target: https://github.com/fr33n0w/rBrowser + +.. only:: latex + + .. image:: screenshots/rbrowser.png + :align: center + :target: https://github.com/fr33n0w/rBrowser + +Includes useful features like automatic listening for announce, adding nodes to favorites, browsing and rendering any kind of NomadNet links, downloading files from remote nodes, a unique local NomadNet Search Engine and more. + + +.. raw:: latex + + \newpage Reticulum Network Telephone ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -270,31 +206,14 @@ The ``rnphone`` program, included as part of the `LXST `_ is a group voice application for Reticulum, built on `LXST `_, inspired by `Mumble `_. - -.. only:: html - - .. image:: screenshots/partyline.webp - :align: center - :target: https://github.com/markqvist/LXST - -.. only:: latex - - .. image:: screenshots/partyline.png - :align: center - :target: https://github.com/markqvist/LXST - -It provides both realtime group voice over any Reticulum transport capable of >6 kilobits per second, LXST dial-in support, listen-only music/broadcasting rooms, and many other useful functions. - + \newpage LXST Phone ^^^^^^^^^^ -The `LXST Phone `_ program is a cross-platform desktop application for performing LXST voice calls over Reticulum. It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management. +The `LXST Phone `_ program is a cross-platform desktop application for performing LXST voice calls over Reticulum. .. only:: html @@ -308,60 +227,28 @@ The `LXST Phone `_ program is a cross-plat :align: center :target: https://github.com/kc1awv/lxst_phone +It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management. + .. raw:: latex \newpage -System Administration -===================== - -Remote Shell -^^^^^^^^^^^^ - -The included :ref:`rnsh` program lets you establish fully interactive -remote shell sessions over Reticulum. It also allows you to pipe any program to or from a -remote system, and is similar to how ``ssh`` works. The ``rnsh`` program is very efficient, and -can facilitate fully interactive shell sessions, even over extremely low-bandwidth links, -such as LoRa or packet radio. - -In addition to the default, fully interactive terminal mode, for extremely limited links, -``rnsh`` offers line-interactive mode, allowing you to interact with remote systems, even -when link throughput is counted in a few hundreds of bits per second. - -RNS FileSync -^^^^^^^^^^^^ - -The `RNS FileSync `_ program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing. - -.. raw:: latex - - \newpage - -Utilities & Components -====================== - -Modem73 -^^^^^^^ - -`MODEM73 `_ and its companion `Modem73Interface `_ is an open source software modem that works with any HF, VHF, or UHF radio capable of 2400 Hz of bandwidth. All you need is a sound card and audio cable for your radio. - -.. only:: html - - .. image:: screenshots/modem73.webp - :align: center - :target: https://github.com/RFnexus/modem73 - -.. only:: latex - - .. image:: screenshots/modem73.jpg - :align: center - :target: https://github.com/RFnexus/modem73 - LXMFy ^^^^^ -`LXMFy `_ is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. `Bot implementations exist `_ for Home Assistant control, LLM integrations, and various other purposes. +`LXMFy `_ is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. `Bot implementations exist `_ for Home Assistant control, LLM integrations, and various other purposes. + + +LXMF Interactive Client +^^^^^^^^^^^^^^^^^^^^^^^ + +`LXMF Interactive Client `_ is a feature-rich, terminal-based LXMF messaging client with many advanced features and an extensible plugin architecture. + +RNS FileSync +^^^^^^^^^^^^ + +The `RNS FileSync `_ program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing. Micron Parser JS @@ -369,25 +256,6 @@ Micron Parser JS `Micron Parser JS `_ is the JavaScript-based parser for the Micron markup language, that most web-based Nomad Network browsers use. If you want to make utilities or tools that display Micron pages, this library is essential. -Reticulated -^^^^^^^^^^^ - -`Reticulated `_ is a Reticulum Network simulator that spawns live, interactive Reticulum instances controllable from a node-based Web UI. - -.. only:: html - - .. image:: screenshots/reticulated.webp - :align: center - :target: https://github.com/RFnexus/Reticulated - -.. only:: latex - - .. image:: screenshots/reticulated.jpg - :align: center - :target: https://github.com/RFnexus/Reticulated - -You can create networks of any topology and set bitrate, MTU, and configure path loss and propagation behavior. It provides a top-down view to a live network. You can connect any Reticulum application to the simulated network. It also has built in utilities like a basic LXMF chat function and Resource transfer included. - RNMon ^^^^^ @@ -399,16 +267,6 @@ RNMon \newpage -Device Flashers -=============== - -In addition to the included command-line ``rnodeconf`` flasher, several community-provided RNode device flashers exist: - -- `RNS Moscow `_ -- `Aetherlab `_ - -Several programs, such as `Sideband `_ and `MeshChatX `_ also include built-in device flashers that can be used fully offline. - Protocols ========= @@ -440,8 +298,8 @@ Interface Modules & Connectivity Resources This section provides a list of various community-provided interface modules, guides and resources for creating Reticulum networks over special or challenging mediums. -* `Modem73 `_ is an efficient software modem that can be used with Reticulum -* Custom interface module for running `RNS over HTTP `_ +* Custom interface module for running `RNS over HTTP `_ * Guide for running `Reticulum over ICMP `_ using ``PipeInterface`` * Guide for running `Reticulum over DNS `_ with Iodine -* Guide for running `Reticulum over HF radio `_ \ No newline at end of file +* Guide for running `Reticulum over HF radio `_ +* `Modem73 `_ is a KISS TNC OFDM modem frontend that can be used with Reticulum \ No newline at end of file diff --git a/docs/source/support.rst b/docs/source/support.rst index 011f1231..39bcf8f3 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -28,6 +28,9 @@ Donations are gratefully accepted via the following channels: Ko-Fi: https://ko-fi.com/markqvist +Are certain features in the development roadmap are important to you or your +organisation? Make them a reality quickly by sponsoring their implementation. + .. raw:: latex \newpage diff --git a/docs/source/using.rst b/docs/source/using.rst index c31ce7dd..d87eefaa 100644 --- a/docs/source/using.rst +++ b/docs/source/using.rst @@ -274,7 +274,8 @@ You can easily add ``rnsd`` as an always-on service by :ref:`configuring a servi The rnstatus Utility ==================== -Using the ``rnstatus`` utility, you can view the status of and control configured Reticulum interfaces, similar to the ``ifconfig`` program. +Using the ``rnstatus`` utility, you can view the status of configured Reticulum +interfaces, similar to the ``ifconfig`` program. **Usage Examples** @@ -336,8 +337,8 @@ Filter output to only show some interfaces: .. code:: text - usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A] [-P] [-l] - [-B] [-b] [-t] [-q] [-s SORT] [-r] [-j] [-R hash] [-i path] + usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A] + [-l] [-t] [-s SORT] [-r] [-j] [-R hash] [-i path] [-w seconds] [-d] [-D] [-m] [-I seconds] [-v] [filter] Reticulum Network Stack Status @@ -349,19 +350,12 @@ Filter output to only show some interfaces: -h, --help show this help message and exit --config CONFIG path to alternative Reticulum config directory --version show program's version number and exit - --attach name Attach interface by name - --detach name Detach interface by name - --reload name Reload interface by name -a, --all show all interfaces -A, --announce-stats show announce stats - -P, --pr-stats show path request stats -l, --link-stats show link stats - -B, --burst only show interfaces with active bursts - -b, --blocked-ips show blocked IPs per interface -t, --totals display traffic totals - -q, --queues display queue stats -s, --sort SORT sort interfaces by [rate, traffic, rx, tx, rxs, txs, - announces, arx, atx, prx, ptx, held] + announces, arx, atx, held] -r, --reverse reverse sorting -j, --json output in JSON format -R hash transport identity hash of remote instance to get status from @@ -369,8 +363,6 @@ Filter output to only show some interfaces: -w seconds timeout before giving up on remote queries -d, --discovered list discovered interfaces -D show details and config entries for discovered interfaces - --show-stale show stale discovery entries - --show-unknown show discovery entries without version info -m, --monitor continuously monitor status -I, --monitor-interval seconds refresh interval for monitor mode (default: 1) @@ -709,7 +701,7 @@ The rnx Utility The ``rnx`` utility is a basic remote command execution program. It allows you to execute commands on remote systems over Reticulum, and to view returned command output. For a fully interactive remote shell solution, be sure to also take a look -at the :ref:`rnsh` program. +at the `rnsh `_ program. **Usage Examples** @@ -775,8 +767,6 @@ another one, which will be created if it does not already exist --version show program's version number and exit -.. _using-rnsh: - The rnsh Utility ================ @@ -1236,8 +1226,6 @@ For more fine-grained control over how discovered interfaces are auto-connected, * The ``autoconnect_announces_to_internal`` option allows you to specify that auto-connected interfaces should propagate announces to ``internal`` mode interfaces, even if the auto-connected interface's mode would normally not allow for this. -* The ``autoconnect_unverified_implementations`` option will allow the interface discovery system to auto-connect to *any* announced interface, even those coming from unverified, experimental or potentially broken implementations. Be very careful with this option. - Remote Management ----------------- diff --git a/setup.py b/setup.py index 68556b3c..46126b19 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,7 @@ import setuptools import sys -import os -import shutil -import subprocess pure_python = False -native_build = False pure_notice = "\n\n**Warning!** *This package is the zero-dependency version of Reticulum. You should almost certainly use the [normal package](https://pypi.org/project/rns) instead. Do NOT install this package unless you know exactly why you are doing it!*" if '--pure' in sys.argv: @@ -13,11 +9,6 @@ if '--pure' in sys.argv: sys.argv.remove('--pure') print("Building pure-python wheel") -if '--native' in sys.argv: - native_build = True - sys.argv.remove('--native') - print("Building native wheel") - exec(open("RNS/_version.py", "r").read()) with open("README.md", "r") as fh: long_description = fh.read() @@ -35,127 +26,6 @@ else: requirements = ['cryptography>=3.4.7', 'pyserial>=3.5'] excluded_modules = ["tests.*", "tests"] - -DIRECTIVES = { "annotation_typing": False } -NATIVE_EXCLUDES = { "RNS.Interfaces.AX25KISSInterface", - "RNS.Interfaces.I2PInterface", - "RNS.Interfaces.KISSInterface", - "RNS.Interfaces.RNodeInterface", - "RNS.Interfaces.RNodeMultiInterface", - "RNS.Interfaces.SerialInterface", - "RNS.Interfaces.WeaveInterface", - "RNS.Utilities.rngit.client", - "RNS.Utilities.rngit.commitsigs", - "RNS.Utilities.rngit.highlight", - "RNS.Utilities.rngit.main", - "RNS.Utilities.rngit.pages", - "RNS.Utilities.rngit.server", - "RNS.Utilities.rngit.media", - "RNS.Utilities.rngit.util", - "RNS.Utilities.rnsh._version", - "RNS.Utilities.rnsh.args", - "RNS.Utilities.rnsh.exception", - "RNS.Utilities.rnsh.helpers", - "RNS.Utilities.rnsh.initiator", - "RNS.Utilities.rnsh.listener", - "RNS.Utilities.rnsh.loop", - "RNS.Utilities.rnsh.process", - "RNS.Utilities.rnsh.protocol", - "RNS.Utilities.rnsh.retry", - "RNS.Utilities.rnsh.rnsh", - "RNS.Utilities.rnsh.session", - "RNS.Utilities.rnsd", - "RNS.Utilities.rncp", - "RNS.Utilities.rnid", - "RNS.Utilities.rnir", - "RNS.Utilities.rnodeconf", - "RNS.Utilities.rnpath", - "RNS.Utilities.rnprobe", - "RNS.Utilities.rnpkg", - "RNS.Utilities.rnstatus", - "RNS.Utilities.rnx", - "RNS._version" } - -ext_modules = [] -compiled_modules = set() -build_cmdclass = {} - -from setuptools.command.build_ext import build_ext as _build_ext -from setuptools.command.build_py import build_py as _build_py -if native_build: - from setuptools import Extension - from Cython.Build import cythonize - - native_opt = os.environ.get("RNS_NATIVE_OPT", "-O3") - if sys.platform == "win32": native_cargs = [] - else: native_cargs = [native_opt, "-g0", "-fno-lto"] - - rns_root = "RNS" - for root, dirs, files in os.walk(rns_root): - dirs[:] = [d for d in dirs if d != "__pycache__"] - for fn in files: - if not fn.endswith(".py"): continue - if fn == "__init__.py": continue - path = os.path.join(root, fn).replace(os.sep, "/") - if path.startswith("RNS/vendor/i2plib/"): continue - if path == "RNS/vendor/configobj.py": continue - modname = path[:-3].replace("/", ".") - if modname in NATIVE_EXCLUDES: continue - compiled_modules.add(modname) - ext_modules.append(Extension(modname, [path], extra_compile_args=list(native_cargs))) - - ext_modules = cythonize(ext_modules, language_level=3, compiler_directives=DIRECTIVES) - - if pure_python: print("Warning: --pure is ignored when building --native") - - # Exclude the CRNS development shim from built wheels - excluded_modules += ["CRNS", "CRNS.*"] - -# Trim superfluous symbol tables -class NativeBuildExt(_build_ext): - def run(self): - super().run() - if sys.platform == "win32": return - strip = shutil.which("strip") - if strip is None: return - for output in self.get_outputs(): - if output.endswith((".so", ".pyd")): - try: - if sys.platform == "darwin": subprocess.run([strip, "-x", output], check=True) - else: subprocess.run([strip, "--strip-unneeded", output], check=True) - except Exception: pass - -# Exclude sources of files that were compiled -# to an so, and inject build info. -class NativeBuildPy(_build_py): - def run(self): - super().run() - if native_build: self._write_buildinfo() - - def find_package_modules(self, package, package_dir): - modules = super().find_package_modules(package, package_dir) - if native_build: - modules = [ (pkg, mod, fname) for (pkg, mod, fname) in modules - if (pkg + "." + mod) not in compiled_modules ] - return modules - - def get_outputs(self, include_bytecode=True): - outputs = super().get_outputs(include_bytecode) - if native_build: outputs.append(self._buildinfo_path()) - return outputs - - def _buildinfo_path(self): - return os.path.join(self.build_lib, "RNS", "_buildinfo.py") - - def _write_buildinfo(self): - path = self._buildinfo_path() - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, "w") as fh: - fh.write("# Generated build information\n") - fh.write("compiled = True\n") - -if native_build: build_cmdclass = { "build_ext": NativeBuildExt, "build_py": NativeBuildPy } - packages = setuptools.find_packages(exclude=excluded_modules) setuptools.setup( @@ -170,27 +40,29 @@ setuptools.setup( packages=packages, license="Reticulum License", license_files = ("LICENSE"), - classifiers=[ "Programming Language :: Python :: 3", - "Operating System :: OS Independent", - "Development Status :: 5 - Production/Stable" ], + classifiers=[ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + "Development Status :: 5 - Production/Stable", + ], entry_points= { - 'console_scripts': [ 'rnsd=RNS.Utilities.rnsd:main', - 'rnstatus=RNS.Utilities.rnstatus:main', - 'rnprobe=RNS.Utilities.rnprobe:main', - 'rnpath=RNS.Utilities.rnpath:main', - 'rnid=RNS.Utilities.rnid:main', - 'rncp=RNS.Utilities.rncp:main', - 'rnx=RNS.Utilities.rnx:main', - 'rnir=RNS.Utilities.rnir:main', - 'rnpkg=RNS.Utilities.rnpkg:main', - 'rnsh=RNS.Utilities.rnsh.rnsh:main', - 'rngit=RNS.Utilities.rngit.server:main', - 'rngcs=RNS.Utilities.rngit.commitsigs:main', - 'git-remote-rns=RNS.Utilities.rngit.client:main', - 'rnodeconf=RNS.Utilities.rnodeconf:main' ] + 'console_scripts': [ + 'rnsd=RNS.Utilities.rnsd:main', + 'rnstatus=RNS.Utilities.rnstatus:main', + 'rnprobe=RNS.Utilities.rnprobe:main', + 'rnpath=RNS.Utilities.rnpath:main', + 'rnid=RNS.Utilities.rnid:main', + 'rncp=RNS.Utilities.rncp:main', + 'rnx=RNS.Utilities.rnx:main', + 'rnir=RNS.Utilities.rnir:main', + 'rnpkg=RNS.Utilities.rnpkg:main', + 'rnsh=RNS.Utilities.rnsh.rnsh:main', + 'rngit=RNS.Utilities.rngit.server:main', + 'rngcs=RNS.Utilities.rngit.commitsigs:main', + 'git-remote-rns=RNS.Utilities.rngit.client:main', + 'rnodeconf=RNS.Utilities.rnodeconf:main', + ] }, install_requires=requirements, python_requires='>=3.7', - ext_modules=ext_modules, - cmdclass=build_cmdclass, ) diff --git a/tests/__init__.py b/tests/__init__.py index 589827eb..49340625 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,26 @@ -import os -if os.environ.get("RNS_COMPILE"): from CRNS import RNS -else: import RNS +compiling = False +noticed = False +notice_delay = 0.3 +import time +import sys +import threading +from importlib.util import find_spec +if find_spec("pyximport") and find_spec("cython"): + import pyximport; pyxloader = pyximport.install(pyimport=True, language_level=3)[1] -notice = f"Starting tests in {'compiled' if RNS.compiled else 'interpreted'} mode" -print(f"\n{notice}\n"+"="*len(notice)+"\n\n") +def notice_job(): + global noticed + started = time.time() + while compiling: + if time.time() > started+notice_delay and compiling: + noticed = True + print("Compiling RNS object code... ", end="") + sys.stdout.flush() + break + time.sleep(0.1) + + +compiling = True +threading.Thread(target=notice_job, daemon=True).start() +import RNS; compiling = False +if noticed: print("Done."); sys.stdout.flush() \ No newline at end of file diff --git a/tests/all.py b/tests/all.py index 17332612..17556b19 100644 --- a/tests/all.py +++ b/tests/all.py @@ -5,10 +5,6 @@ from .hashes import TestSHA512 from .identity import TestIdentity from .link import TestLink from .channel import TestChannel -from .hkdf import TestHKDF -from .ifac import TestIFAC -from .coalesced_transmit import TestTransmitBuffer -from .hdlc import TestReceiveBuffer -from .egress import TestHWMLimiter -if __name__ == '__main__': unittest.main(verbosity=2) \ No newline at end of file +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/tests/coalesced_transmit.py b/tests/coalesced_transmit.py deleted file mode 100644 index b1e026e4..00000000 --- a/tests/coalesced_transmit.py +++ /dev/null @@ -1,460 +0,0 @@ -# Partially generated with lc/kimi_k2.6 -# Reviewed and approved by Mark Qvist -import unittest - -import os -import time -import random -import socket -import threading - -from RNS.Interfaces.util.TransmitBuffer import TransmitBuffer -from RNS.Interfaces.util.HDLC import HDLC - -TARGET = TransmitBuffer.COALESCE_TARGET -FRAME_SEED = 0x7842AB1E - -def framed(payload): return bytes([HDLC.FLAG]) + HDLC.escape(payload) + bytes([HDLC.FLAG]) - -def unframe(stream): - frames = [] - fb = stream - while True: - frame_start = fb.find(bytes([HDLC.FLAG])) - if frame_start != -1: - frame_end = fb.find(bytes([HDLC.FLAG]), frame_start + 1) - if frame_end != -1: - frame = fb[frame_start + 1:frame_end] - frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG])) - frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC])) - if len(frame) > 0: - frames.append(bytes(frame)) - fb = fb[frame_end:] - else: break - else: break - return frames - -def rng_payload(rng, size): - raw = bytearray(os.urandom(size)) - if size > 0: - for off in range(0, size, max(size // 32, 1)): - raw[off] = 0x7E if (off % 2) else 0x7D - return bytes(raw) - -def assert_accounting(test, tb, expect_len, expect_sendable, expect_frames): - test.assertEqual(len(tb), expect_len) - test.assertEqual(tb.sendable, expect_sendable) - test.assertEqual(tb.frames_buffered, expect_frames) - test.assertTrue(0 <= tb.sendable <= len(tb)) - -class TestTransmitBuffer(unittest.TestCase): - - def test_01_empty_state(self): - print("") - tb = TransmitBuffer() - assert_accounting(self, tb, 0, 0, 0) - self.assertEqual(tb.chunks_buffered, 0) - self.assertEqual(tb.drain_to(None), 0) # no chunks -> nothing to do - - # Test that frames appended to an idle queue become sendable - # at once, so no coalescing latency is incurred for sparse traffic. - def test_02_sparse_visibility(self): - print("") - tb = TransmitBuffer() - for payload_size in (200, 1024, 16384, 65534): - tb.append(framed(rng_payload(random.Random(FRAME_SEED), payload_size))) - self.assertGreater(tb.sendable, 0) - self.assertGreater(len(tb), 0) - - # Test that small frames coalesce into chunks bounded by - # the configured coalescing target. - def test_03_coalescing(self): - print("") - tb = TransmitBuffer() - rng = random.Random(FRAME_SEED) - payloads = [rng_payload(rng, 200) for _ in range(2000)] - for p in payloads: tb.append(framed(p)) - - self.assertEqual(len(tb), sum(len(framed(p)) for p in payloads)) - self.assertEqual(tb.frames_buffered, len(payloads)) - for chunk, frames in tb._chunks: - self.assertLessEqual(len(chunk), TARGET) - self.assertGreater(len(chunk), 0) - self.assertGreater(frames, 0) - - # Visible accounting must match the chunk queue; buffered accounting - # must additionally include the producer's in-progress coalescing - # chunk, which is not visible to the consumer yet. - in_cur = len(tb._cur) if tb._cur is not None else 0 - in_cur_frames = tb._cur_frames if tb._cur is not None else 0 - visible = sum(len(c) for c, _ in tb._chunks) - visible_frames = sum(frames for _, frames in tb._chunks) - self.assertEqual(tb.sendable, visible) - self.assertEqual(tb.sendable + in_cur, len(tb)) - self.assertEqual(visible_frames + in_cur_frames, len(payloads)) - self.assertEqual(tb.frames_buffered, len(payloads)) - - # Test that frames whose on-wire size is at or above the coalescing - # target are queued as their own chunk. Frames just below must coalesce. - # Must apply to final, framed and escape on-wire stream. - def test_04_large_frame_granularity(self): - print("") - tb = TransmitBuffer() - below = bytes([HDLC.FLAG]) + b"A" * (TARGET - 10) + bytes([HDLC.FLAG]) - at = bytes([HDLC.FLAG]) + b"B" * (TARGET - 2) + bytes([HDLC.FLAG]) - above = bytes([HDLC.FLAG]) + b"C" * TARGET + bytes([HDLC.FLAG]) - big = bytes([HDLC.FLAG]) + b"D" * 262144 + bytes([HDLC.FLAG]) - - self.assertLess(len(below), TARGET) - self.assertEqual(len(at), TARGET) - self.assertGreater(len(above), TARGET) - - tb.append(below) - tb.append(at) - tb.append(above) - tb.append(big) - - chunks = list(tb._chunks) - self.assertEqual(len(chunks), 4) # 1 coalesced chunk + 3 own chunks - self.assertEqual(chunks[0][1], 1) - self.assertEqual(chunks[0][0], below) - self.assertEqual(chunks[1][1], 1) - self.assertEqual(chunks[1][0], at) - self.assertEqual(chunks[2][1], 1) - self.assertEqual(chunks[2][0], above) - self.assertEqual(chunks[3][1], 1) - self.assertEqual(chunks[3][0], big) - self.assertEqual(tb.frames_buffered, 4) - - # Test that a partially written head chunk resumes - # correctly on the next drain. - def test_05_partial_head_resume(self): - print("") - a, b = socket.socketpair() - a.setblocking(False) - b.setblocking(False) - a.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4096) - - tb = TransmitBuffer() - rng = random.Random(FRAME_SEED) - payloads = [rng_payload(rng, 262144) for _ in range(8)] - frames = [framed(p) for p in payloads] - for f in frames: tb.append(f) - expected = b"".join(frames) - - # Drain without any receiver: Small kernel buffer forces partial - # writes, and the drain must stop gracefully on BlockingIOError. - written = 0 - w = tb.drain_to(a) - self.assertGreater(w, 0) - self.assertLess(w, len(frames[0])) - written += w - self.assertLess(tb.sendable, len(expected)) - w = tb.drain_to(a) # window still full, must not raise or corrupt - self.assertGreaterEqual(w, 0) - written += w - self.assertGreaterEqual(len(tb), 0) - - # Drain the kernel side, then run the drain to completion. - got = bytearray() - def recv_loop(): - while len(got) < len(expected): - try: d = b.recv(1 << 20) - except BlockingIOError: - time.sleep(0.001) - continue - if not d: - break - got.extend(d) - rt = threading.Thread(target=recv_loop, daemon=True) - rt.start() - while tb.sendable > 0: - w = tb.drain_to(a) - if w == 0: time.sleep(0.001) - rt.join(timeout=10) - self.assertFalse(rt.is_alive()) - self.assertEqual(bytes(got), expected) - assert_accounting(self, tb, 0, 0, 0) - a.close(); b.close() - - # Test that a full kernel window stops the drain without - # any exceptions, and that a later drain delivers the rest. - def test_06_graceful_blocking_io(self): - print("") - a, b = socket.socketpair() - a.setblocking(False) - b.setblocking(False) - a.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 2048) - - tb = TransmitBuffer() - rng = random.Random(FRAME_SEED) - frames = [framed(rng_payload(rng, 200)) for _ in range(100000)] - for f in frames: - tb.append(f) - expected = b"".join(frames) - - # Fill the kernel window. Repeated drains must never raise, and must - # eventually make no progress while the window is full. - for _ in range(5_000_000): - w = tb.drain_to(a) - if w == 0: break - if w < 0: self.fail("drain_to returned a negative count") - self.assertGreater(len(tb), 0) - - # Production for the burst has ended, release the coalescing tail. - tb.flush() - - got = bytearray() - def recv_loop(): - while len(got) < len(expected): - try: d = b.recv(1 << 20) - except BlockingIOError: - time.sleep(0.001) - continue - if not d: - break - got.extend(d) - rt = threading.Thread(target=recv_loop, daemon=True) - rt.start() - while tb.sendable > 0: - w = tb.drain_to(a) - if w == 0: time.sleep(0.001) - rt.join(timeout=10) - self.assertFalse(rt.is_alive()) - self.assertEqual(bytes(got), expected) - assert_accounting(self, tb, 0, 0, 0) - a.close(); b.close() - - # Exactness test of end-to-end, in-order roundtrip delivery. - def test_07_roundtrip_and_frame_integrity(self): - print("") - a, b = socket.socketpair() - a.setblocking(True) - b.setblocking(True) - - tb = TransmitBuffer() - rng = random.Random(FRAME_SEED) - sizes = [200, 512, 1024, 4096, 8192, 16384, 65534, 65536, 65537, 262144] - payloads = [rng_payload(rng, size) for size in sizes] * 8 - frames = [framed(p) for p in payloads] - for f in frames: tb.append(f) - expected = b"".join(frames) - - # Non-blocking socket, pipelined drain and receive - a.setblocking(False) - b.setblocking(False) - - got = bytearray() - wrote = 0 - deadline = time.time() + 30 - while tb.sendable > 0 or len(got) < len(expected): - self.assertLess(time.time(), deadline, "test timed out") - if tb.sendable > 0: wrote += tb.drain_to(a) - try: - d = b.recv(1 << 20) - if d: got.extend(d) - except BlockingIOError: - time.sleep(0.0002) - - self.assertEqual(wrote, len(expected)) - assert_accounting(self, tb, 0, 0, 0) - self.assertEqual(bytes(got), expected) - self.assertEqual(unframe(bytes(got)), payloads) - a.close(); b.close() - - # Test that a producer flush makes the in-progress coalescing - # tail visible, and that the buffer is fully drainable afterwards. - def test_08_flush_releases_tail(self): - print("") - tb = TransmitBuffer() - rng = random.Random(FRAME_SEED) - f1 = framed(rng_payload(rng, 200)) - f2 = framed(rng_payload(rng, 200)) - f3 = framed(rng_payload(rng, 200)) - tb.append(f1) # Queue idle -> visible immediately - tb.append(f2) # Queue busy -> coalesced into tail - tb.append(f3) # Queue busy -> coalesced into tail - - self.assertEqual(tb.sendable, len(f1)) - self.assertEqual(len(tb), len(f1) + len(f2) + len(f3)) - self.assertEqual(tb.chunks_buffered, 1) - - tb.flush() - self.assertEqual(tb.sendable, len(f1) + len(f2) + len(f3)) - self.assertEqual(tb.chunks_buffered, 2) - self.assertEqual(tb.frames_buffered, 3) - - # Ensure frames appended while the queue is busy are not - # stranded in the coalescing tail. - def test_09_stranded_tail_delivery(self): - print("") - a, b = socket.socketpair() - a.setblocking(False) - b.setblocking(False) - - tb = TransmitBuffer() - rng = random.Random(FRAME_SEED) - f1 = framed(rng_payload(rng, 200)) - f2 = framed(rng_payload(rng, 200)) - f3 = framed(rng_payload(rng, 200)) - expected = f1 + f2 + f3 - - tb.append(f1) # queue idle -> own chunk, visible - tb.append(f2) # queue busy -> coalescing tail, invisible - tb.append(f3) # queue busy -> coalescing tail, invisible - self.assertEqual(tb.sendable, len(f1)) - - # No further appends. Draining alone without a flush must - # release and deliver the coalescing tail. - got = bytearray() - deadline = time.time() + 10 - while tb.sendable > 0 or len(got) < len(expected): - self.assertLess(time.time(), deadline, "test timed out") - tb.drain_to(a) - try: - d = b.recv(1 << 20) - if d: got.extend(d) - except BlockingIOError: - time.sleep(0.0002) - self.assertEqual(bytes(got), expected) - assert_accounting(self, tb, 0, 0, 0) - a.close(); b.close() - - # Test that the coalescing tail is released when the queue - # drains, and that len() is the total buffered and senable - # is the transmittable part. - def test_10_sendable_gating(self): - print("") - a, b = socket.socketpair() - a.setblocking(False) - b.setblocking(False) - b.settimeout(5.0) - - tb = TransmitBuffer() - rng = random.Random(FRAME_SEED) - frames = [framed(rng_payload(rng, 200)) for _ in range(400)] - expected = b"".join(frames) - for f in frames: - tb.append(f) - self.assertEqual(tb.sendable + (len(tb._cur) if tb._cur is not None else 0), len(tb)) - - # Drain to completion. The drain must release the tail by itself. - got = bytearray() - deadline = time.time() + 10 - while tb.sendable > 0 or len(got) < len(expected): - self.assertLess(time.time(), deadline, "test timed out") - tb.drain_to(a) - try: - d = b.recv(1 << 20) - if d: got.extend(d) - except BlockingIOError: - time.sleep(0.0002) - self.assertEqual(bytes(got), expected) - assert_accounting(self, tb, 0, 0, 0) - a.close(); b.close() - - # Exactness test of concurrent producer append while consumer drains. - def test_11_concurrent_producer_consumer(self): - print("") - a, b = socket.socketpair() - a.setblocking(False) - b.setblocking(False) - b.settimeout(5.0) - - tb = TransmitBuffer() - rng = random.Random(FRAME_SEED) - sizes = [200, 1024, 8192, 65536, 262144] - payloads = [rng_payload(rng, rng.choice(sizes)) for _ in range(2500)] - frames = [framed(p) for p in payloads] - total = sum(len(f) for f in frames) - expected = b"".join(frames) - - producer_errors = [] - def producer(): - try: - for f in frames: tb.append(f) - except Exception as e: - producer_errors.append(e) - pt = threading.Thread(target=producer, daemon=True) - pt.start() - - got = bytearray() - def recv_loop(): - while len(got) < total: - try: d = b.recv(1 << 20) - except BlockingIOError: - time.sleep(0.001) - continue - except socket.timeout: break - if not d: break - got.extend(d) - rt = threading.Thread(target=recv_loop, daemon=True) - rt.start() - - ct0 = time.perf_counter() - while pt.is_alive() or tb.sendable > 0: - tb.drain_to(a) - time.sleep(0.0002) - - # Final drain once the producer has exited. - w = 1 - while w > 0: w = tb.drain_to(a) - t_elapsed = time.perf_counter() - ct0 - - pt.join(timeout=10) - self.assertFalse(pt.is_alive()) - self.assertEqual(producer_errors, []) - # Production for the burst has ended. Release the coalescing tail so - # every appended byte is transmittable, then drain to completion. - tb.flush() - w = 1 - while w > 0: w = tb.drain_to(a) - rt.join(timeout=10) - self.assertFalse(rt.is_alive()) - self.assertEqual(len(got), total) - self.assertEqual(bytes(got), expected) - assert_accounting(self, tb, 0, 0, 0) - a.close(); b.close() - print(f"Concurrent {len(frames)}-frame burst ({total/1e6:.1f} MB) drained in {t_elapsed*1000:.1f} ms") - - # Test small-frame flood and execution time. - def test_12_small_frame_burst(self): - print("") - a, b = socket.socketpair() - a.setblocking(False) - b.setblocking(False) - - tb = TransmitBuffer() - rng = random.Random(FRAME_SEED) - frames = [framed(rng_payload(rng, 200)) for _ in range(50000)] - total = sum(len(f) for f in frames) - expected = b"".join(frames) - - got = bytearray() - def recv_loop(): - while len(got) < total: - try: d = b.recv(1 << 20) - except BlockingIOError: - time.sleep(0.001) - continue - if not d: break - got.extend(d) - rt = threading.Thread(target=recv_loop, daemon=True) - rt.start() - - t0 = time.perf_counter() - for f in frames: tb.append(f) - tb.flush() - while tb.sendable > 0: tb.drain_to(a) - rt.join(timeout=15) - t_elapsed = time.perf_counter() - t0 - - self.assertFalse(rt.is_alive()) - self.assertEqual(len(got), total) - self.assertEqual(bytes(got), expected) - assert_accounting(self, tb, 0, 0, 0) - a.close(); b.close() - print(f"50k x 200B flood ({total/1e6:.1f} MB) appended+drained in {t_elapsed*1000:.1f} ms") - self.assertLess(t_elapsed, 10.0) - -if __name__ == "__main__": unittest.main(verbosity=2) \ No newline at end of file diff --git a/tests/egress.py b/tests/egress.py deleted file mode 100644 index 86c86824..00000000 --- a/tests/egress.py +++ /dev/null @@ -1,206 +0,0 @@ -# Partially generated with lc/kimi_k2.6 -# Reviewed and approved by Mark Qvist -import unittest - -import time - -from RNS.Interfaces.util.TransmitBuffer import TransmitBuffer -from RNS.Interfaces.BackboneInterface import BackboneInterface, BackboneClientInterface - -def framed(payload): return b"\x7e" + payload + b"\x7e" - -class FakeSock: - def send(self, data): return len(data) - def fileno(self): return 999999 - def close(self): return - -class FakeIF: - def __init__(self, tb=None): - self.transmit_buffer = tb if tb is not None else TransmitBuffer() - self._dp_ec_prev_sent = 0 - self._dp_ec_zero_ticks = 0 - self._dp_ec_last_drain = 10.0 - self.tx_stalled = False - self.detached = False - self.socket = FakeSock() - self.torn_down = False - - def receive(self, data): self.torn_down = True - -class TestHWMLimiter(unittest.TestCase): - - def test_01_hard_valve_bounds_memory(self): - print("") - tb = TransmitBuffer() - limit = 4096 - accepted_count = 0 - total_accepted = 0 - rejected = 0 - for _ in range(200): - for size in (500, 700): - frame = framed(b"x" * size) - accepted = tb.append(frame, limit) - if accepted: - accepted_count += 1 - total_accepted += len(frame) - self.assertLessEqual(len(tb), limit) - else: - rejected += 1 - # Rejection implies the frame would breach the limit. - self.assertGreater(total_accepted + len(frame), limit) - - # The limit must never be exceeded, some frames must have been - # rejected, and the buffer must still be fully drainable. - self.assertGreater(rejected, 0) - self.assertLessEqual(len(tb), limit) - drained = tb.drain_to(FakeSock()) - self.assertEqual(drained, total_accepted) - self.assertEqual(len(tb), 0) - - def test_02_oversized_frame_rejected(self): - print("") - tb = TransmitBuffer() - limit = 2048 - self.assertTrue(tb.append(framed(b"x" * 500), limit)) - # A single frame that would exceed the limit is rejected outright, - # even against an empty buffer. - self.assertFalse(tb.append(framed(b"x" * 3000), limit)) - self.assertEqual(len(tb), len(framed(b"x" * 500))) - # The buffer remains fully functional after rejection. - self.assertTrue(tb.append(framed(b"x" * 1000), limit)) - self.assertEqual(len(tb), len(framed(b"x" * 500)) + len(framed(b"x" * 1000))) - drained = tb.drain_to(FakeSock()) - self.assertEqual(drained, len(framed(b"x" * 500)) + len(framed(b"x" * 1000))) - - def test_03_valve_accepts_after_drain(self): - print("") - tb = TransmitBuffer() - limit = 2048 - for _ in range(10): self.assertTrue(tb.append(framed(b"x" * 200), limit)) - self.assertEqual(len(tb), len(framed(b"x" * 200)) * 10) - # Drain most of the buffer; appends are accepted again up to the - # limit, and the total stays bounded. - tb.drain_to(FakeSock()) - for _ in range(10): self.assertTrue(tb.append(framed(b"x" * 200), limit)) - self.assertLessEqual(len(tb), limit) - - def test_04_append_without_limit_unbounded(self): - print("") - tb = TransmitBuffer() - for _ in range(100): self.assertTrue(tb.append(framed(b"x" * 200))) - self.assertEqual(len(tb), len(framed(b"x" * 200)) * 100) - - def test_05_stall_engages_on_zero_drain(self): - print("") - tb = TransmitBuffer() - iface = FakeIF(tb) - # Fill beyond the mid watermark. - while len(tb) <= BackboneInterface.DP_EC_MID_WM: tb.append(framed(b"y" * 4096)) - # No drain progress, fewer than STALL_TICKS evaluations must not stall. - for tick in range(BackboneInterface.DP_EC_STALL_TICKS - 1): - BackboneInterface._dp_ec_evaluate(iface, 11.0 + tick) - self.assertFalse(iface.tx_stalled) - # The STALL_TICKSth zero-drain tick at/above the - # mid watermark engages the gate. - BackboneInterface._dp_ec_evaluate(iface, 15.0) - self.assertTrue(iface.tx_stalled) - - def test_06_stall_releases_below_mid(self): - print("") - tb = TransmitBuffer() - iface = FakeIF(tb) - while len(tb) <= BackboneInterface.DP_EC_MID_WM: tb.append(framed(b"y" * 4096)) - for t in range(BackboneInterface.DP_EC_STALL_TICKS + 1): BackboneInterface._dp_ec_evaluate(iface, 11.0 + t) - self.assertTrue(iface.tx_stalled) - # Drain the buffer completely; the next tick releases the gate. - tb.drain_to(FakeSock()) - self.assertEqual(len(tb), 0) - BackboneInterface._dp_ec_evaluate(iface, 20.0) - self.assertFalse(iface.tx_stalled) - self.assertEqual(iface._dp_ec_zero_ticks, 0) - - def test_07_stall_engages_on_drain_eta(self): - print("") - tb = TransmitBuffer() - iface = FakeIF(tb) - while len(tb) <= BackboneInterface.DP_EC_MID_WM: tb.append(framed(b"y" * 4096)) - # Pretend 512 bytes drained since the last tick, the queue would - # take far longer than DP_EC_MAX_ETA to clear at that rate. - iface._dp_ec_prev_sent = tb._tx_sent - 512 - BackboneInterface._dp_ec_evaluate(iface, 11.0) - self.assertTrue(iface.tx_stalled) - - def test_08_stall_hysteresis_no_flap(self): - print("") - tb = TransmitBuffer() - iface = FakeIF(tb) - while len(tb) <= BackboneInterface.DP_EC_MID_WM: tb.append(framed(b"y" * 4096)) - - # Engage the gate with an ETA above max. - buffered = len(tb) - iface._dp_ec_prev_sent = tb._tx_sent - 128 - BackboneInterface._dp_ec_evaluate(iface, 11.0) - self.assertTrue(iface.tx_stalled) - - # A drain ETA between RELEASE and MAX is the hysteresis band, - # the gate must stay engaged without flapping. - mid_eta = (BackboneInterface.DP_EC_MAX_ETA + BackboneInterface.DP_EC_RELEASE_ETA) / 2 - mid_drain = buffered / mid_eta - iface._dp_ec_prev_sent = tb._tx_sent - mid_drain - BackboneInterface._dp_ec_evaluate(iface, 12.0) - self.assertTrue(iface.tx_stalled) - - # Only when drainage clears the queue well within the release - # ETA is the gate released. - fast_drain = buffered / (BackboneInterface.DP_EC_RELEASE_ETA / 2) - iface._dp_ec_prev_sent = tb._tx_sent - fast_drain - BackboneInterface._dp_ec_evaluate(iface, 13.0) - self.assertFalse(iface.tx_stalled) - - def test_09_sendable_zero_no_stall(self): - print("") - tb = TransmitBuffer() - iface = FakeIF(tb) - while len(tb) <= BackboneInterface.DP_EC_MID_WM: tb.append(framed(b"y" * 4096)) - # A tick with nothing sendable. All data still unsent but none in - # the drainer's visible set is impossible with a full buffer, so - # simulate the empty state instead. An empty buffer must reset - # stall state and the dead-drain clock. - tb.drain_to(FakeSock()) - BackboneInterface._dp_ec_evaluate(iface, 11.0) - self.assertFalse(iface.tx_stalled) - self.assertEqual(iface._dp_ec_last_drain, 11.0) - - def test_10_dead_peer_escalation(self): - print("") - tb = TransmitBuffer() - iface = FakeIF(tb) - while len(tb) <= BackboneInterface.DP_EC_MID_WM: tb.append(framed(b"y" * 4096)) - # Last drain progress a long time ago, no drain since. - iface._dp_ec_last_drain = 10.0 - iface._dp_ec_zero_ticks = BackboneInterface.DP_EC_STALL_TICKS - escalated = BackboneInterface._dp_ec_evaluate(iface, 100.0) - self.assertTrue(escalated) - self.assertTrue(iface.torn_down) - - def test_11_process_outgoing_gate(self): - print("") - # Construct a shell client interface object without full - # initialisation. Process_outgoing only requires the - # egress-control state and the transmit buffer gating. - iface = object.__new__(BackboneClientInterface) - iface.online = True - iface.detached = False - iface.tx_stalled = True - iface.tx_hwm = 1024 - iface.tx_drops = 0 - iface.tx_dropped_bytes = 0 - iface.transmit_buffer = TransmitBuffer() - iface.socket = None - - iface.process_outgoing(b"gated-frame") - self.assertEqual(iface.tx_drops, 1) - self.assertEqual(iface.tx_dropped_bytes, len(framed(b"gated-frame"))) - self.assertEqual(len(iface.transmit_buffer), 0) - -if __name__ == "__main__": unittest.main(verbosity=2) \ No newline at end of file diff --git a/tests/hdlc.py b/tests/hdlc.py deleted file mode 100644 index 585b1dff..00000000 --- a/tests/hdlc.py +++ /dev/null @@ -1,268 +0,0 @@ -# Partially generated with lc/kimi_k2.6 -# Reviewed and approved by Mark Qvist -import unittest - -import os -import time -import random - -from RNS.Interfaces.util.HDLC import HDLC, ReceiveBuffer - -SEED = 0x52EC0FF -MIN_LEN = 19 # HEADER_MINSIZE -MTU = 1048576 - -def framed(payload): return HDLC.FLAG_B + HDLC.escape(payload) + HDLC.FLAG_B - -def rand_payload(rng, size): - p = bytearray(os.urandom(size)) - for off in range(0, max(size, 1), max(size // 16, 1)): - p[off] = 0x7E if (off % 2) else 0x7D - return bytes(p) - -# Original, pre-refactor HDLC handler algorithm -def legacy_feed(state, data, mtu, min_len, max_len): - if len(data) > 0: - state["buf"] += data - flags_remaining = True - while flags_remaining: - frame_start = state["buf"].find(HDLC.FLAG) - if frame_start != -1: - frame_end = state["buf"].find(HDLC.FLAG, frame_start+1) - if frame_end != -1: - frame = state["buf"][frame_start+1:frame_end] - frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG])) - frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC])) - frame_len = len(frame) - if frame_len != 0: - if check_frame_len(frame_len, min_len, max_len): state["frames"].append(frame) - else: state["invalids"].append(frame_len) - - state["buf"] = state["buf"][frame_end:] - - else: - if len(state["buf"]) > mtu*2: state["buf"] = b"" - flags_remaining = False - else: - state["buf"] = b"" - flags_remaining = False - -def check_frame_len(frame_len, min_len, max_len): - if frame_len <= min_len: return False - elif max_len == None: return True - elif frame_len > max_len: return False - else: return True - -class Harness: - def __init__(self, mtu=MTU, min_len=MIN_LEN, max_len=None, track_invalid=True): - self.frames = [] - self.invalids = [] - self.rb = ReceiveBuffer(mtu, min_len, max_len, on_frame=self.frames.append, - on_invalid=self.invalids.append if track_invalid else None) - def unconsumed(self): - return bytes(self.rb._buf[self.rb._off:]) - -class TestReceiveBuffer(unittest.TestCase): - # Legacy parity, also checks that the closing flag of - # the last consumed frame is retained as the start - # marker for the next unframing. - def test_01_single_frame_roundtrip(self): - print("") - rng = random.Random(SEED) - for size in (20, 200, 4096, 262144): - p = rand_payload(rng, size) - h = Harness() - h.rb.feed(framed(p)) - self.assertEqual(h.frames, [p]) - self.assertEqual(h.invalids, []) - self.assertEqual(h.unconsumed(), HDLC.FLAG_B) - self.assertEqual(len(h.rb), 1) - - # Test frames split across arbitrary feed boundaries - def test_02_split_chunks_straddle(self): - print("") - rng = random.Random(SEED) - payloads = [rand_payload(rng, rng.choice((20, 200, 1024, 16384))) for _ in range(200)] - stream = b"".join(framed(p) for p in payloads) - - h = Harness() - # One-byte-per-feed pathological case - for byte in stream: h.rb.feed(bytes([byte])) - - self.assertEqual(h.frames, payloads) - self.assertEqual(len(h.rb), 1) # trailing flag - - # Test exactness of escape-heavy payloads - def test_03_escape_roundtrip(self): - print("") - rng = random.Random(SEED ^ 0xE5C) - payloads = [] - for _ in range(100): - size = rng.choice((64, 512, 8192)) - p = bytearray(os.urandom(size)) - # Dense 0x7E/0x7D injection - for i in range(0, size, 3): p[i] = 0x7E if (i % 2) else 0x7D - payloads.append(bytes(p)) - - h = Harness() - for p in payloads: h.rb.feed(framed(p)) - self.assertEqual(h.frames, payloads) - - # Empty frames must skip silently - def test_04_consecutive_flags_skipped(self): - print("") - rng = random.Random(SEED) - p1 = rand_payload(rng, 100) - p2 = rand_payload(rng, 100) - h = Harness() - h.rb.feed(framed(p1) + HDLC.FLAG_B + HDLC.FLAG_B + framed(p2)) - self.assertEqual(h.frames, [p1, p2]) - self.assertEqual(h.invalids, []) - - # Test that invalid sized frames report correctly - def test_05_size_range(self): - print("") - h = Harness(min_len=19, max_len=100) - h.rb.feed(framed(b"x" * 5)) # frame len 5 -> too short - h.rb.feed(framed(b"x" * 19)) # frame len 19 -> not > 19, which is bad - h.rb.feed(framed(b"x" * 100)) # frame len 100 -> within range - h.rb.feed(framed(b"x" * 101)) # frame len 101 -> too long - self.assertEqual(h.frames, [b"x" * 100]) - self.assertEqual(h.invalids, [5, 19, 101]) - - # Without an on_invalid hook, out-of-range frames are dropped - h2 = Harness(min_len=19, max_len=100, track_invalid=False) - h2.rb.feed(framed(b"x" * 5)) - h2.rb.feed(framed(b"x" * 50)) - h2.rb.feed(framed(b"x" * 101)) - self.assertEqual(h2.frames, [b"x" * 50]) - self.assertEqual(h2.invalids, []) - - # A flag without a closing flag is retained up to the - # overflow bound, beyond which the buffer must reset - def test_06_partial_and_overflow_reset(self): - print("") - h = Harness(mtu=1024) - # Partial frame within bound must retained - h.rb.feed(b"\x7e" + b"Y" * 100) - self.assertEqual(len(h.rb), 101) - self.assertEqual(h.frames, []) - # ... and completing it delivers the frame - h.rb.feed(b"Z" * 40 + b"\x7e") - self.assertEqual(h.frames, [b"Y" * 100 + b"Z" * 40]) - self.assertEqual(len(h.rb), 1) - - # Partial frame past the overflow bound must drop and reset - h2 = Harness(mtu=1024) - h2.rb.feed(b"\x7e" + b"Y" * 5000) - self.assertEqual(len(h2.rb), 0) - # Buffer must be usable after the reset - h2.rb.feed(framed(b"x" * 100)) - self.assertEqual(h2.frames, [b"x" * 100]) - - # Test that input without any flag is discarded immediately - def test_07_garbage_reset(self): - print("") - h = Harness() - h.rb.feed(b"\x00" * 100000 + b"\x01" * 100000) - self.assertEqual(len(h.rb), 0) - self.assertEqual(h.frames, []) - h.rb.feed(framed(b"x" * 100)) - self.assertEqual(h.frames, [b"x" * 100]) - - # Random stream parity checks - def test_08_parity_random_streams(self): - print("") - rng = random.Random(SEED) - verified = 0 - - def inert(size): return bytes(rng.randrange(1, 0x7D) for _ in range(size)) - - for round in range(20): - stream = bytearray() - intended = [] - for _ in range(rng.randrange(5, 60)): - kind = rng.randrange(5) - if kind == 0: stream += inert(rng.randrange(40, 300)) # Garbage - elif kind == 1: stream += b"\x7e" + inert(rng.randrange(50, 400)) # Unclosed partial - else: - p = rand_payload(rng, rng.randrange(20, 8192)) - intended.append(p) - stream += framed(p) - - legacy = {"buf": b"", "frames": [], "invalids": []} - mine = Harness() - pos = 0 - while pos < len(stream): - step = rng.randrange(1, 4096) - chunk = bytes(stream[pos:pos + step]) - pos += step - legacy_feed(legacy, chunk, MTU, MIN_LEN, None) - mine.rb.feed(chunk) - # Behavioural and internal-state parity after every chunk - self.assertEqual(mine.frames, legacy["frames"]) - self.assertEqual(mine.invalids, legacy["invalids"]) - self.assertEqual(mine.unconsumed(), legacy["buf"]) - - # Full-stream parity and intended-payload presence. - self.assertEqual(mine.frames, legacy["frames"]) - self.assertEqual(mine.unconsumed(), legacy["buf"]) - for p in intended: self.assertGreaterEqual(mine.frames.count(p), intended.count(p)) - verified += 1 - - print(f" Random-stream parity: {verified} streams verified") - - # Small-frame flood parity - def test_09_parity_flood(self): - print("") - rng = random.Random(SEED) - payloads = [rand_payload(rng, 200) for _ in range(20000)] - stream = b"".join(framed(p) for p in payloads) - - legacy = {"buf": b"", "frames": [], "invalids": []} - mine = Harness() - t0 = time.perf_counter() - for i in range(0, len(stream), MTU): - chunk = stream[i:i + MTU] - legacy_feed(legacy, chunk, MTU, MIN_LEN, None) - mine.rb.feed(chunk) - t_elapsed = time.perf_counter() - t0 - - self.assertEqual(mine.frames, legacy["frames"]) - self.assertEqual(mine.frames, payloads) - self.assertEqual(mine.unconsumed(), legacy["buf"]) - self.assertLess(t_elapsed, 10.0) - print(f" 20k x 200B flood ({len(stream)/1e6:.1f} MB) parity-verified in {t_elapsed*1000:.1f} ms") - - # Large frames in small chunks - def test_10_parity_large_frames(self): - print("") - rng = random.Random(SEED ^ 0x1AF) - payloads = [rand_payload(rng, 262144) for _ in range(8)] - stream = b"".join(framed(p) for p in payloads) - - legacy = {"buf": b"", "frames": [], "invalids": []} - mine = Harness(mtu=262144) - pos = 0 - while pos < len(stream): - chunk = stream[pos:pos + 4096] - pos += 4096 - legacy_feed(legacy, chunk, 262144, MIN_LEN, None) - mine.rb.feed(chunk) - self.assertEqual(mine.frames, legacy["frames"]) - self.assertEqual(mine.unconsumed(), legacy["buf"]) - self.assertEqual(mine.frames, payloads) - - # After a burst is fully consumed, the internal buffer must be compacted - def test_11_len_and_compaction(self): - print("") - rng = random.Random(SEED) - h = Harness() - for _ in range(1000): - h.rb.feed(framed(rand_payload(rng, 200))) - self.assertEqual(len(h.rb), 1) # Trailing flag marker - self.assertEqual(len(h.rb._buf), 1) # Fully compacted - self.assertEqual(h.rb._off, 0) - self.assertEqual(len(h.frames), 1000) - -if __name__ == "__main__": unittest.main(verbosity=2) \ No newline at end of file diff --git a/tests/hkdf.py b/tests/hkdf.py deleted file mode 100644 index e77f4a2e..00000000 --- a/tests/hkdf.py +++ /dev/null @@ -1,190 +0,0 @@ -# Partially generated with lc/kimi_k2.6 -# Reviewed and approved by Mark Qvist -import unittest - -import os -import time -import random - -import RNS -from RNS.Cryptography import hkdf -from RNS.Cryptography.HKDF import hkdf_legacy - -HKDF_TEST_SEED = 0x5EE5FAC - -# RFC 5869 test vectors HKDF SHA-256 vectors -RFC5869_CASES = [ - # A.1: basic test case - ( - bytes([0x0b]) * 22, - bytes(range(0x00, 0x0d)), - bytes(range(0xf0, 0xfa)), - 42, - "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865", - ), - # A.2: longer inputs/outputs - ( - bytes(range(0x00, 0x50)), - bytes(range(0x60, 0xb0)), - bytes(range(0xb0, 0x100)), - 82, - "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c" - "59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71" - "cc30c58179ec3e87c14c01d5c1f3434f1d87", - ), - # A.3: zero-length salt/info - ( - bytes([0x0b]) * 22, - b"", - b"", - 42, - "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8", - ), -] - - -def rand_bytes(rng, length): - return bytes(rng.randrange(256) for _ in range(length)) - - -class TestHKDF(unittest.TestCase): - - def test_01_rfc5869_vectors(self): - print("") - - for index, (ikm, salt, info, length, expected_hex) in enumerate(RFC5869_CASES): - expected = bytes.fromhex(expected_hex) - - new_out = hkdf(length=length, derive_from=ikm, salt=salt, context=info) - leg_out = hkdf_legacy(length=length, derive_from=ikm, salt=salt, context=info) - - self.assertEqual(new_out, expected) - self.assertEqual(leg_out, expected) - - print(f"RFC 5869 test case {index+1}: OK (optimized = legacy = reference, {len(expected)} bytes)") - - def test_02_parity_matrix(self): - print("") - - rng = random.Random(HKDF_TEST_SEED) - - # Core lengths: block boundaries, block+1, counter wrap at - # 256 blocks (8192 bytes), +1/-1 around it, and medium sizes. - lengths_core = [ 1, 2, 31, 32, 33, 63, 64, 65, 96, 127, 128, 129, - 255, 256, 257, 511, 512, 513, 1024, 4096, 8191, 8192, 8193 ] - # Large lengths: exercised with a reduced combination set. - lengths_large = [16384, 65536, 262144] - - derive_lens = [1, 7, 16, 32, 64] - salt_lens = [None, 0, 1, 16, 32, 64, 100] - context_lens = [None, 0, 1, 16, 64, 128] - derive_lens_large = [16, 64] - salt_lens_large = [None, 32, 100] - context_lens_large = [None, 16, 128] - - verified = 0 - for length in lengths_core: - for dlen in derive_lens: - for slen in salt_lens: - for clen in context_lens: - derive_from = rand_bytes(rng, dlen) - salt = None if slen is None else (b"" if slen == 0 else rand_bytes(rng, slen)) - context = None if clen is None else (b"" if clen == 0 else rand_bytes(rng, clen)) - - new_out = hkdf(length=length, derive_from=derive_from, salt=salt, context=context) - leg_out = hkdf_legacy(length=length, derive_from=derive_from, salt=salt, context=context) - - self.assertEqual(new_out, leg_out) - verified += 1 - - for length in lengths_large: - for dlen in derive_lens_large: - for slen in salt_lens_large: - for clen in context_lens_large: - derive_from = rand_bytes(rng, dlen) - salt = None if slen is None else (b"" if slen == 0 else rand_bytes(rng, slen)) - context = None if clen is None else (b"" if clen == 0 else rand_bytes(rng, clen)) - - new_out = hkdf(length=length, derive_from=derive_from, salt=salt, context=context) - leg_out = hkdf_legacy(length=length, derive_from=derive_from, salt=salt, context=context) - - self.assertEqual(new_out, leg_out) - verified += 1 - - print(f"HKDF parity matrix: {verified} deterministic cases verified (optimized == legacy)") - - def test_03_error_parity(self): - print("") - - invalid_cases = [ - dict(length=0, derive_from=b"x"), - dict(length=None, derive_from=b"x"), - dict(length=-1, derive_from=b"x"), - dict(length="x", derive_from=b"x"), - dict(length=32, derive_from=""), - dict(length=32, derive_from=None), - ] - - def raised_exception(fn, kwargs): - try: - fn(**kwargs) - return None - except Exception as e: return (type(e).__name__, str(e)) - - for kwargs in invalid_cases: - new_exc = raised_exception(hkdf, kwargs) - leg_exc = raised_exception(hkdf_legacy, kwargs) - - # Both implementations must raise the exact same exception - # type with the exact same message on invalid arguments - self.assertIsNotNone(new_exc) - self.assertEqual(new_exc, leg_exc) - - print(f"HKDF error parity: {len(invalid_cases)} invalid-argument cases raise identical exception types and messages") - - @unittest.skipIf(os.getenv("RNS_SKIP_PERF_TESTS") != None, "Skipping performance tests") - def test_04_benchmark(self): - print("") - - rng = random.Random(HKDF_TEST_SEED ^ 0xA11CE) - salt = RNS.Reticulum.IFAC_SALT - - sizes = [1500, 16384, 262144] - runs = {1500: 40, 16384: 20, 262144: 8} - - print("HKDF implementation benchmark (optimized vs legacy):") - print(f"{'length':>9} {'legacy avg':>12} {'opt avg':>9} {'speedup':>10}") - print("-" * 48) - - leg_total = new_total = 0.0 - for size in sizes: - derive_from = rand_bytes(rng, 16) - context = rand_bytes(rng, 7) - n = runs[size] - - hkdf_legacy(length=size, derive_from=derive_from, salt=salt, context=context) - hkdf(length=size, derive_from=derive_from, salt=salt, context=context) - - t0 = time.perf_counter() - for _ in range(n): - hkdf_legacy(length=size, derive_from=derive_from, salt=salt, context=context) - leg_avg = (time.perf_counter() - t0) / n - - t0 = time.perf_counter() - for _ in range(n): - hkdf(length=size, derive_from=derive_from, salt=salt, context=context) - new_avg = (time.perf_counter() - t0) / n - - leg_total += leg_avg - new_total += new_avg - - print(f"{size:>9} {leg_avg*1000:9.3f}m {new_avg*1000:9.3f}m {leg_avg/new_avg:8.1f}x") - - print(f"\nThroughput at {sizes[-1]} bytes: legacy {sizes[-1]/leg_avg/1e6*8:.2f} Mbps, " - f"new {sizes[-1]/new_avg/1e6*8:.2f} Mbps") - print(f"Overall speedup across benchmarks: {leg_total/new_total:.1f}x") - print() - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/identity.py b/tests/identity.py index c40d2288..623a72fe 100644 --- a/tests/identity.py +++ b/tests/identity.py @@ -145,34 +145,6 @@ class TestIdentity(unittest.TestCase): print("Sign/validate 16KB chunks: "+self.size_str(b/t, "b")+"ps") - def test_3_timings(self): - import random - random.seed(618736) - print("") - blobs = [] - times = [] - t = RNS.Cryptography.Token(key=random.randbytes(64)) - for n in range(0, 1000): blobs.append(random.randbytes(64)) - - for a in blobs: - for b in blobs: - st = time.time() - res = t.verify_hmac(b) - d = time.time()-st - times.append(d) - - import statistics - tmin = min(times)*1000000 - tmax = max(times)*1000000 - tmean = (sum(times)/len(times))*1000000 - tmed = statistics.median(times)*1000000 - tmdev = tmax - tmin - mpct = (tmax/tmed)*100 - print(" HMAC timings:") - print(" µs min/avg/med/max/mdev: "+str(round(tmin, 3))+"/"+str(round(tmean, 3))+"/"+str(round(tmed, 3))+"/"+str(round(tmax, 3))+"/"+str(round(tmdev, 3))) - print(" Max deviation from median: "+str(round(mpct, 1))+"%") - print() - def test_2_encrypt(self): print("") diff --git a/tests/ifac.py b/tests/ifac.py deleted file mode 100644 index 27e41933..00000000 --- a/tests/ifac.py +++ /dev/null @@ -1,298 +0,0 @@ -# Partially generated with lc/kimi_k2.6 -# Reviewed and approved by Mark Qvist -import unittest - -import os -import time -import random - -import RNS -from RNS import Transport - -IFAC_TEST_SEED = 0x1FACAD3 -IFAC_NETNAME = b"rns-ifac-test-net" - - -def rand_bytes(rng, length): - return bytes(rng.randrange(256) for _ in range(length)) - - -class FakeInterface: - def __init__(self, ifac_size, ifac_key): - self.ifac_size = ifac_size - self.ifac_key = ifac_key - self.ifac_identity = RNS.Identity.from_bytes(ifac_key) - self.violations = [] - - def ifac_violation(self, description): - self.violations.append(description) - - -def make_interface(ifac_size): - ifac_key = RNS.Cryptography.hkdf(length=64, derive_from=RNS.Identity.full_hash(IFAC_NETNAME), - salt=RNS.Reticulum.IFAC_SALT, context=None) - - return FakeInterface(ifac_size, ifac_key) - - -def make_plain_packet(rng, size, pattern="random"): - """Generate a deterministic protocol-realistic packet: a random or - structured payload with the IFAC flag bit of the first header byte - clear (as plaintext frames always have).""" - if pattern == "random": raw = rand_bytes(rng, size) - elif pattern == "zeros": raw = bytes(size) - elif pattern == "ones": raw = bytes([0xFF]) * size - elif pattern == "seq": raw = bytes((i % 256) for i in range(size)) - else: raise ValueError(f"Unknown pattern {pattern}") - - raw = bytearray(raw) - raw[0] &= 0x7F - return bytes(raw) - - -class TestIFAC(unittest.TestCase): - - def test_01_outgoing_parity(self): - print("") - - rng = random.Random(IFAC_TEST_SEED) - - sizes = [8, 16, 32, 100, 500, 1064, 4096, 16384] - ifac_sizes = [1, 8, 16] - patterns = ["random", "zeros", "ones", "seq"] - - verified = 0 - for size in sizes: - for ifac_size in ifac_sizes: - iface = make_interface(ifac_size) - for pattern in patterns: - raw = make_plain_packet(rng, size, pattern) - - out_new = Transport.handle_outgoing_ifac(iface, raw) - out_leg = Transport.handle_outgoing_ifac_legacy(iface, raw) - - self.assertEqual(out_new, out_leg) - self.assertEqual(len(out_new), size + ifac_size) - verified += 1 - - for ifac_size in (8, 16): - iface = make_interface(ifac_size) - raw = make_plain_packet(rng, 262144, "random") - out_new = Transport.handle_outgoing_ifac(iface, raw) - out_leg = Transport.handle_outgoing_ifac_legacy(iface, raw) - self.assertEqual(out_new, out_leg) - verified += 1 - - print(f"IFAC outgoing parity: {verified} deterministic cases verified") - - def test_02_incoming_parity(self): - print("") - - rng = random.Random(IFAC_TEST_SEED ^ 0x1FAC) - - sizes = [8, 16, 32, 100, 500, 1064, 4096, 16384] - ifac_sizes = [1, 8, 16] - patterns = ["random", "zeros", "ones", "seq"] - - verified = 0 - for size in sizes: - for ifac_size in ifac_sizes: - iface = make_interface(ifac_size) - for pattern in patterns: - raw = make_plain_packet(rng, size, pattern) - frame = Transport.handle_outgoing_ifac(iface, raw) - - ok_new, out_new = Transport.handle_ifac(frame, iface) - ok_leg, out_leg = Transport.handle_ifac_legacy(frame, iface) - - self.assertTrue(ok_new) - self.assertTrue(ok_leg) - self.assertEqual(out_new, out_leg) - verified += 1 - - for ifac_size in (8, 16): - iface = make_interface(ifac_size) - raw = make_plain_packet(rng, 262144, "random") - frame = Transport.handle_outgoing_ifac(iface, raw) - ok_new, out_new = Transport.handle_ifac(frame, iface) - ok_leg, out_leg = Transport.handle_ifac_legacy(frame, iface) - self.assertTrue(ok_new) - self.assertTrue(ok_leg) - self.assertEqual(out_new, out_leg) - verified += 1 - - print(f"IFAC incoming parity: {verified} deterministic cases verified") - - def test_03_roundtrip(self): - print("") - - rng = random.Random(IFAC_TEST_SEED ^ 0x0FAC) - - # All four TX/RX handler combinations must recover the original packet - combos = [ - (Transport.handle_outgoing_ifac, Transport.handle_ifac), - (Transport.handle_outgoing_ifac, Transport.handle_ifac_legacy), - (Transport.handle_outgoing_ifac_legacy, Transport.handle_ifac), - (Transport.handle_outgoing_ifac_legacy, Transport.handle_ifac_legacy), - ] - - sizes = [16, 1064, 16384] - ifac_sizes = [1, 8, 16] - - verified = 0 - for tx, rx in combos: - for size in sizes: - for ifac_size in ifac_sizes: - iface = make_interface(ifac_size) - raw = make_plain_packet(rng, size, "random") - - frame = tx(iface, raw) - ok, out = rx(frame, iface) - - self.assertTrue(ok) - self.assertEqual(out, raw) - verified += 1 - - print(f"IFAC round-trip: {verified} deterministic TX/RX combinations verified\n All four handler pairings recover the packet exactly.") - - def test_04_invariants(self): - print("") - - rng = random.Random(IFAC_TEST_SEED ^ 0xCAFE) - iface = make_interface(16) - - for size in (16, 1064, 16384): - raw = make_plain_packet(rng, size, "random") - - out_new = Transport.handle_outgoing_ifac(iface, raw) - out_leg = Transport.handle_outgoing_ifac_legacy(iface, raw) - - # IFAC flag must be set on the masked frame - self.assertEqual(out_new[0] & 0x80, 0x80) - - # The IFAC tag region must be transmitted in the clear and - # identical in both implementations - self.assertEqual(out_new[2:2+iface.ifac_size], out_leg[2:2+iface.ifac_size]) - self.assertEqual(out_new[2:2+iface.ifac_size], iface.ifac_identity.sign(raw)[-iface.ifac_size:]) - - # Second header byte must be masked identically - self.assertEqual(out_new[1], out_leg[1]) - - # The incoming handler must clear the IFAC flag in its output - ok, out = Transport.handle_ifac(out_new, iface) - self.assertTrue(ok) - self.assertEqual(out[0] & 0x80, 0) - - print("IFAC invariants: flag set on frame, tag in clear and identical, flag cleared on input recovery") - - def test_05_invalid_tags(self): - print("") - - rng = random.Random(IFAC_TEST_SEED ^ 0xBAD) - - sizes = [100, 1064, 16384] - ifac_sizes = [1, 8, 16] - - for size in sizes: - for ifac_size in ifac_sizes: - iface = make_interface(ifac_size) - raw = make_plain_packet(rng, size, "random") - frame = bytearray(Transport.handle_outgoing_ifac(iface, raw)) - - corruptions = [] - # Corrupt a masked payload byte - corrupted = bytearray(frame) - corrupted[-1] ^= 0x01 - corruptions.append(bytes(corrupted)) - - # Corrupt the second header byte - corrupted = bytearray(frame) - corrupted[1] ^= 0x40 - corruptions.append(bytes(corrupted)) - - # Corrupt a byte in the IFAC tag region - if ifac_size > 0: - corrupted = bytearray(frame) - corrupted[2 + ifac_size//2] ^= 0x01 - corruptions.append(bytes(corrupted)) - - for corrupted in corruptions: - ok_new, out_new = Transport.handle_ifac(corrupted, iface) - ok_leg, out_leg = Transport.handle_ifac_legacy(corrupted, iface) - - # Both implementations must behave identically on any - # corrupted frame, including the violation report - self.assertEqual(ok_new, ok_leg) - self.assertEqual(out_new, out_leg) - self.assertEqual(iface.violations[-1:], ["Invalid IFAC on packet"]) - - # With >= 8 bytes of tag entropy, rejection is - # deterministic. At ifac_size = 1 the truncated tag has - # only 8 bits, so a corrupted frame can statistically - # collide with the original truncation (1/256) - both - # implementations behave identically either way. - if ifac_size >= 8: self.assertFalse(ok_new) - - print("IFAC invalid tags: behaviour and violation reporting identical " - "between implementations; deterministic rejection verified for " - "8+ byte tags") - - @unittest.skipIf(os.getenv("RNS_SKIP_PERF_TESTS") != None, "Skipping performance tests") - def test_06_benchmark(self): - print("") - - rng = random.Random(IFAC_TEST_SEED ^ 0xBEEF) - - sizes = [1500, 16384, 262144] - runs = {1500: 30, 16384: 15, 262144: 5} - - print("IFAC handler benchmark (optimized vs legacy):") - print(f"{'size':>8} {'out lgc':>11} {'out opt':>11} {'out spd':>8} | " - f"{'in lgc':>11} {'in new':>11} {'in spd':>8}") - print("-" * 84) - - total_leg = total_new = 0.0 - for size in sizes: - iface = make_interface(16) - raw = make_plain_packet(rng, size, "random") - frame = Transport.handle_outgoing_ifac(iface, raw) - n = runs[size] - - # Warmup - Transport.handle_outgoing_ifac_legacy(iface, raw) - Transport.handle_outgoing_ifac(iface, raw) - Transport.handle_ifac_legacy(frame, iface) - Transport.handle_ifac(frame, iface) - - t0 = time.perf_counter() - for _ in range(n): Transport.handle_outgoing_ifac_legacy(iface, raw) - out_leg = (time.perf_counter() - t0) / n - - t0 = time.perf_counter() - for _ in range(n): Transport.handle_outgoing_ifac(iface, raw) - out_new = (time.perf_counter() - t0) / n - - t0 = time.perf_counter() - for _ in range(n): Transport.handle_ifac_legacy(frame, iface) - in_leg = (time.perf_counter() - t0) / n - - t0 = time.perf_counter() - for _ in range(n): Transport.handle_ifac(frame, iface) - in_new = (time.perf_counter() - t0) / n - - total_leg += out_leg + in_leg - total_new += out_new + in_new - - def ms(t): return f"{t*1000:.3f}m" - - print(f"{size:>8} {ms(out_leg):>11} {ms(out_new):>11} {out_leg/out_new:7.1f}x | " - f"{ms(in_leg):>11} {ms(in_new):>11} {in_leg/in_new:7.1f}x") - - print(f"\nCombined TX+RX at {sizes[-1]} bytes: legacy " - f"{(out_leg+in_leg)*1000:.2f} ms/pkt, optimized {(out_new+in_new)*1000:.2f} ms/pkt, " - f"{(out_leg+in_leg)/(out_new+in_new):.1f}x speedup") - print(f"Overall speedup across benchmarks: {total_leg/total_new:.1f}x") - print() - -if __name__ == "__main__": - unittest.main(verbosity=2) \ No newline at end of file diff --git a/tests/link.py b/tests/link.py index 6370aadb..9f85645f 100644 --- a/tests/link.py +++ b/tests/link.py @@ -13,6 +13,7 @@ import RNS import os from tests.channel import MessageTest from RNS.Channel import MessageBase +from RNS.Buffer import StreamDataMessage from RNS.Interfaces.LocalInterface import LocalClientInterface from math import ceil @@ -26,9 +27,7 @@ fixed_keys = [ ("08bb35f92b06a0832991165a0d9b4fd91af7b7765ce4572aa6222070b11b767092b61b0fd18b3a59cae6deb9db6d4bfb1c7fcfe076cfd66eea7ddd5f877543b9", "d13712efc45ef87674fb5ac26c37c912"), ] -BUFFER_TEST_TARGET = 256000 -BUFFER_TEST_TARGET_SLOW = 1800 -SLOW_TEST_MARKER = b"@SLOW_TEST_MARKER" +BUFFER_TEST_TARGET = 32000 LINK_UP_WAIT = 0.050 def targets_job(caller): @@ -735,16 +734,23 @@ class TestLink(unittest.TestCase): # 1000)) if local_interface.bitrate < 1000: - target_bytes = BUFFER_TEST_TARGET_SLOW-len(SLOW_TEST_MARKER) - message = SLOW_TEST_MARKER + target_bytes = 3000 else: target_bytes = BUFFER_TEST_TARGET - message = b"" random.seed(154889) - message += random.randbytes(target_bytes) + message = random.randbytes(target_bytes) buffer_read_target = len(message) - expected_rx_message = message + " back at you".encode("utf-8") + + # the return message will have an appendage string " back at you" + # for every StreamDataMessage that arrives. To verify, we need + # to insert that string every MAX_DATA_LEN and also at the end. + expected_rx_message = b"" + for i in range(0, len(message)): + if i > 0 and (i % StreamDataMessage.MAX_DATA_LEN) == 0: + expected_rx_message += " back at you".encode("utf-8") + expected_rx_message += bytes([message[i]]) + expected_rx_message += " back at you".encode("utf-8") # since the segments will be received at max length for a # StreamDataMessage, the appended text will end up in a @@ -754,7 +760,7 @@ class TestLink(unittest.TestCase): buffer.write(message) buffer.flush() - timeout = time.time() + 2.5 + timeout = time.time() + 4 while not time.time() > timeout: time.sleep(1) print(f"Received {len(received)} chunks so far") @@ -858,31 +864,28 @@ def targets(yp=False): response_data = [] def handle_buffer(ready_bytes: int): - global buffer_read_len, BUFFER_TEST_TARGET, BUFFER_TEST_TARGET_SLOW + global buffer_read_len, BUFFER_TEST_TARGET data = buffer.read(ready_bytes) buffer_read_len += len(data) response_data.append(data) - if SLOW_TEST_MARKER in data: - BUFFER_TEST_TARGET = BUFFER_TEST_TARGET_SLOW - if data == "Hi there".encode("utf-8"): RNS.log("Sending response") - buffer.write(b"".join(response_data) + " back at you".encode("utf-8")) - buffer.flush() - response_data.clear() - buffer_read_len = 0 + for data in response_data: + buffer.write(data + " back at you".encode("utf-8")) + buffer.flush() + buffer_read_len = 0 if buffer_read_len == BUFFER_TEST_TARGET: RNS.log("Sending response") - buffer.write(b"".join(response_data) + " back at you".encode("utf-8")) - buffer.flush() - response_data.clear() - buffer_read_len = 0 + for data in response_data: + buffer.write(data + " back at you".encode("utf-8")) + buffer.flush() + buffer_read_len = 0 buffer = RNS.Buffer.create_bidirectional_buffer(0, 0, channel, handle_buffer) - m_rns = RNS.Reticulum("./tests/rnsconfig", logdest=RNS.LOG_FILE) + m_rns = RNS.Reticulum("./tests/rnsconfig", logdest=RNS.LOG_FILE, loglevel=RNS.LOG_EXTREME) id1 = RNS.Identity.from_bytes(bytes.fromhex(fixed_keys[0][0])) d1 = RNS.Destination(id1, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "link", "establish") d1.set_proof_strategy(RNS.Destination.PROVE_ALL) diff --git a/tests/throughput.py b/tests/throughput.py deleted file mode 100644 index 2ec6757a..00000000 --- a/tests/throughput.py +++ /dev/null @@ -1,954 +0,0 @@ -#!/usr/bin/env python3 -# -# Generated with lc/deepseek_v4_flash_q8_k_xl -# Reviewed and corrected by Mark Qvist -# -# Usage examples: -# -# python3 tests/transport_throughput.py -# python3 tests/transport_throughput.py --scenario transit_single_135 -# python3 tests/transport_throughput.py --mode inline --runs 5 -# python3 tests/transport_throughput.py --list-scenarios -# - -import unittest - -import os -import sys -import time -import gc -import struct -import platform -import tempfile -import statistics - -# Ensure that the Reticulum tree this suite lives in is the one being -# benchmarked, even if a different version of Reticulum is installed in -# site-packages. -_SUITE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -if os.path.isdir(os.path.join(_SUITE_ROOT, "RNS")): - sys.path.insert(0, _SUITE_ROOT) - -import RNS - -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- - -BENCHMARK_CONFIG = { - "scenarios": None, # None = all, or a list of scenario names - "mode": "both", # "inline", "drainer" or "both" - "runs": 3, # measurement runs per scenario/mode, median is reported -} - -SCENARIO_DESCRIPTIONS = { - "transit_single_135": "SINGLE transit relay, 135 B payload, 3-hop path", - "transit_single_475": "SINGLE transit relay, 475 B payload, 3-hop path", - "transit_single_1024": "SINGLE transit relay, 1 KiB payload, 3-hop path", - "transit_single_16384": "SINGLE transit relay, 16 KiB payload, 3-hop path", - "transit_single_final": "SINGLE transit relay, final hop, header strip, 135 B", - "transit_link_135": "LINK transit relay via link table, cross-interface, 135 B", - "transit_link_475": "LINK transit relay via link table, cross-interface, 475 B", - "transit_link_1024": "LINK transit relay via link table, cross-interface, 1024 B", - "transit_link_16384": "LINK transit relay via link table, cross-interface, 16384 B", - "transit_link_32768": "LINK transit relay via link table, cross-interface, 32768 B", - "terminus_link": "LINK terminus delivery, token decrypt, 135 B", - "terminus_single": "SINGLE local delivery, ephemeral-key decrypt, 135 B", - "announce_ingress": "Announce ingress, fresh destinations, validation + path insert", - "outbound_path": "Outbound insertion into transport, known 3-hop path, 135 B", -} - -# Default packet counts per scenario and mode. -DEFAULT_PACKETS = { - "transit_single_135": {"inline": 20000, "drainer": 20000}, - "transit_single_475": {"inline": 20000, "drainer": 20000}, - "transit_single_1024": {"inline": 20000, "drainer": 20000}, - "transit_single_16384": {"inline": 10000, "drainer": 10000}, - "transit_single_final": {"inline": 20000, "drainer": 20000}, - "transit_link_135": {"inline": 20000, "drainer": 20000}, - "transit_link_475": {"inline": 20000, "drainer": 20000}, - "transit_link_1024": {"inline": 20000, "drainer": 20000}, - "transit_link_16384": {"inline": 10000, "drainer": 10000}, - "transit_link_32768": {"inline": 10000, "drainer": 10000}, - "terminus_link": {"inline": 6000, "drainer": 6000}, - "terminus_single": {"inline": 3000, "drainer": 3000}, - "announce_ingress": {"inline": 1000, "drainer": 1000}, - "outbound_path": {"inline": 8000, "drainer": 0}, -} - -INLINE_SAMPLE_STEP = 64 # sample a timing point every N packets for p50/p95 -DRAINER_CHUNK = 256 # packets fed per drainer backpressure step - - -class BenchmarkInterface(RNS.Interfaces.Interface.Interface): - HW_MTU = 1048576 - BITRATE = 1_000_000_000 - - def __init__(self, name="bench"): - super().__init__() - self.name = name - self.IN = True - self.OUT = True - self.online = True - self.bitrate = self.BITRATE - self.HW_MTU = BenchmarkInterface.HW_MTU - self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL - self.gravity = 0 - self.ifac_size = 0 - self.ifac_key = None - self.ifac_identity = None - self.parent_interface = None - self.ingress_control = False - self.announce_rate_target = RNS.Interfaces.Interface.Interface.DEFAULT_AR_TARGET - self.announce_rate_grace = RNS.Interfaces.Interface.Interface.DEFAULT_AR_GRACE - self.announce_rate_penalty = RNS.Interfaces.Interface.Interface.DEFAULT_AR_PENALTY - - def process_outgoing(self, data): - self.txb += len(data) - - def __str__(self): - return f"BenchmarkInterface[{self.name}]" - - -class Scenario: - kind = "ingress" # "ingress" or "outbound" - - def __init__(self, name, description, interface, frames, packets, runs, - inline_offset, drainer_offset, size, completion_counter=None, - fresh_per_run=False): - self.name = name - self.description = description - self.interface = interface - self.frame_size = size - self.frames = frames # pool of unique raw frames - self.packets_inline = packets.get("inline", 0) - self.packets_drainer = packets.get("drainer", 0) - self.runs = runs - self.inline_offset = inline_offset - self.drainer_offset = drainer_offset - self.completion_counter = completion_counter - # When true, each run uses a dedicated fresh slice of frames - # (required where processing is not idempotent, e.g. announces). - # When false, runs reuse the same pool with the duplicate filter - # reset in between. - self.fresh_per_run = fresh_per_run - - def can_drainer(self): - return self.packets_drainer > 0 and self.completion_counter is not None - - def n_for_mode(self, mode): - if mode == "inline": return self.packets_inline - if mode == "drainer": return self.packets_drainer - - -class CountingDestination(RNS.Destination): - """A destination that counts successful deliveries, for use as a - completion counter in drainer mode.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.delivered = 0 - - def receive(self, packet): - result = super().receive(packet) - if result: - self.delivered += 1 - return result - - -def _register_link(link): - with RNS.Transport.active_links_lock: - RNS.Transport.active_links.append(link) - if hasattr(RNS.Transport, "active_links_map"): - RNS.Transport.active_links_map[link.link_id] = link - - -def _make_minimal_link(instance, interface): - """Constructs a minimal, active Link object suitable for benchmark delivery.""" - import importlib - _link_mod = importlib.import_module("RNS.Link") - - link = RNS.Link.__new__(RNS.Link) - link.mode = RNS.Link.MODE_DEFAULT - link.rtt = 0.1 - link.mtu = RNS.Reticulum.MTU - link.establishment_cost = 0 - link.establishment_rate = None - link.expected_rate = None - link.callbacks = _link_mod.LinkCallbacks() - link.resource_strategy = RNS.Link.ACCEPT_NONE - link.last_resource_window = None - link.last_resource_eifr = None - link.outgoing_resources = [] - link.incoming_resources = [] - link.pending_requests = [] - link.last_inbound = 0 - link.last_outbound = 0 - link.last_keepalive = 0 - link.last_proof = 0 - link.last_data = 0 - link.tx = 0 - link.rx = 0 - link.txbytes = 0 - link.rxbytes = 0 - link.rssi = None - link.snr = None - link.q = None - link.traffic_timeout_factor = RNS.Link.TRAFFIC_TIMEOUT_FACTOR - link.keepalive_timeout_factor = RNS.Link.KEEPALIVE_TIMEOUT_FACTOR - link.keepalive = RNS.Link.KEEPALIVE - link.stale_time = RNS.Link.STALE_TIME - link.watchdog_lock = False - link.status = RNS.Link.ACTIVE - link.activated_at = time.time() - link.type = RNS.Destination.LINK - link.owner = instance - link.initiator = False - link.expected_hops = 1 - link.rebalanced = None - link.attached_interface = interface - link._channel = None - link._Link__remote_identity = None - link._Link__track_phy_stats = False - link.derived_key = os.urandom(64) - link.token = RNS.Cryptography.Token(link.derived_key) - link.link_id = os.urandom(RNS.Reticulum.TRUNCATED_HASHLENGTH//8) - link.hash = link.link_id - link.hexhash = link.link_id.hex() - from RNS.Cryptography.Proxies import Ed25519PrivateKeyProxy - link.sig_prv = Ed25519PrivateKeyProxy.from_private_bytes(link.derived_key[:32]) - - class _DestinationShim: - pass - - link.destination = _DestinationShim() - link.destination.type = RNS.Destination.LINK - link.destination.proof_strategy = RNS.Destination.PROVE_NONE - _dest_mod = importlib.import_module("RNS.Destination") - link.destination.callbacks = _dest_mod.Callbacks() - link.destination.status = RNS.Link.ACTIVE - link.destination.last_outbound = 0 - link.destination.tx = 0 - link.destination.txbytes = 0 - link.destination.attached_interface = interface - link.destination.mtu = RNS.Reticulum.MTU - link.destination.rssi = None - link.destination.snr = None - link.destination.q = None - link.destination.rtt = RNS.Link.TRAFFIC_TIMEOUT_MIN_MS/1000 - link.destination.traffic_timeout_factor = 1.0 - - return link - - -def _transit_single_frame(interface, dst_h, payload): - """One HEADER_2|TRANSPORT|SINGLE|DATA frame addressed to this instance.""" - flags = (RNS.Packet.HEADER_2 << 6) | (RNS.Transport.TRANSPORT << 4) \ - | (RNS.Destination.SINGLE << 2) | RNS.Packet.DATA - return (struct.pack("!B", flags) + struct.pack("!B", 1) - + RNS.Transport.identity.hash + dst_h - + bytes([RNS.Packet.NONE]) + payload) - - -def _transit_link_frame(interface, link_id, payload): - """One HEADER_2|TRANSPORT|LINK|DATA frame addressed to this instance.""" - flags = (RNS.Packet.HEADER_2 << 6) | (RNS.Transport.TRANSPORT << 4) \ - | (RNS.Destination.LINK << 2) | RNS.Packet.DATA - return (struct.pack("!B", flags) + struct.pack("!B", 1) - + RNS.Transport.identity.hash + link_id - + bytes([RNS.Packet.NONE]) + payload) - - -def _terminus_link_frame(link, payload): - """One HEADER_1|BROADCAST|LINK|DATA frame for a local active link, - encrypted with the link token.""" - flags = (RNS.Packet.HEADER_1 << 6) | (RNS.Transport.BROADCAST << 4) \ - | (RNS.Destination.LINK << 2) | RNS.Packet.DATA - ciphertext = link.encrypt(payload) - return (struct.pack("!B", flags) + struct.pack("!B", 0) - + link.link_id + bytes([RNS.Packet.NONE]) + ciphertext) - - -def _payload(i, size, marker): - """Deterministic, unique payload of `size` bytes.""" - return i.to_bytes(8, "big") + bytes([marker]) * (size - 8) - - -def build_scenarios(instance, interface, interface_b, requested=None): - """Builds all benchmark scenarios and returns a dict name -> Scenario.""" - scenarios = {} - runs = BENCHMARK_CONFIG["runs"] - - def _slices(packets, fresh=False): - inline_n = packets.get("inline", 0) - drainer_n = packets.get("drainer", 0) - if fresh: - # Fresh frames per run for both modes - total = (inline_n + drainer_n) * runs - return total, inline_n, drainer_n, 0, inline_n * runs - else: - # One re-usable pool of the largest mode's frame count - total = max(inline_n, drainer_n) - return total, inline_n, drainer_n, 0, 0 - - # --- SINGLE transit relay, several payload sizes ----------------------- - sizes = [("135", 135), ("475", 475), ("1024", 1024), ("16384", 16384)] - for suffix, size in sizes: - name = f"transit_single_{suffix}" - total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) - dst_h = RNS.Cryptography.hkdf(length=16, - derive_from=f"T{suffix}dst".encode(), - salt=b"bench", context=None) - next_hop = RNS.Cryptography.hkdf(length=16, - derive_from=f"T{suffix}nh".encode(), - salt=b"bench", context=None) - now = time.time() - with RNS.Transport.path_table_lock: - RNS.Transport.path_table[dst_h] = [ - now, next_hop, 3, now + 3600, [], interface, bytes(32) - ] - frames = [_transit_single_frame(interface, dst_h, - _payload(i, size, 0x57)) - for i in range(total)] - scenarios[name] = Scenario( - name, - f"SINGLE transit relay, {size} B payload, 3-hop path", - interface, frames, DEFAULT_PACKETS[name], runs, - off_i, off_d, size, - completion_counter=lambda: RNS.Transport.tx_packets, - ) - - # --- SINGLE transit relay, final hop (header strip) -------------------- - name = "transit_single_final" - total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) - dst_h = RNS.Cryptography.hkdf(length=16, derive_from=b"TFdst", - salt=b"bench", context=None) - next_hop = RNS.Cryptography.hkdf(length=16, derive_from=b"TFnh", - salt=b"bench", context=None) - now = time.time() - with RNS.Transport.path_table_lock: - RNS.Transport.path_table[dst_h] = [ - now, next_hop, 1, now + 3600, [], interface, bytes(32) - ] - frames = [_transit_single_frame(interface, dst_h, - _payload(i, 135, 0x46)) - for i in range(total)] - scenarios[name] = Scenario( - name, - "SINGLE transit relay, final hop, transport header strip, 135 B", - interface, frames, DEFAULT_PACKETS[name], runs, - off_i, off_d, 135, - completion_counter=lambda: RNS.Transport.tx_packets, - ) - - # --- LINK transit relay (cross-interface) ------------------------------ - sizes = [("135", 135), ("475", 475), ("1024", 1024), ("16384", 16384), ("32768", 32768)] - for suffix, size in sizes: - name = f"transit_link_{suffix}" - - total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) - link_id = RNS.Cryptography.hkdf(length=16, derive_from=b"Llink", - salt=b"bench", context=None) - now = time.time() - with RNS.Transport.link_table_lock: - # timestamp, next-hop transport id, outbound iface, remaining hops, - # received-on iface, taken hops, original destination hash, - # validated, proof timeout - RNS.Transport.link_table[link_id] = [ - now, RNS.Transport.identity.hash, interface_b, 2, - interface, 2, os.urandom(16), True, now + 60 - ] - frames = [_transit_link_frame(interface, link_id, - _payload(i, size, 0x4C)) - for i in range(total)] - scenarios[name] = Scenario( - name, - f"LINK transit relay via link table, cross-interface, {size} B", - interface, frames, DEFAULT_PACKETS[name], runs, - off_i, off_d, size, - completion_counter=lambda: RNS.Transport.tx_packets, - ) - - # --- LINK terminus delivery -------------------------------------------- - name = "terminus_link" - total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) - link = _make_minimal_link(instance, interface) - _register_link(link) - frames = [_terminus_link_frame(link, _payload(i, 135, 0x6C)) - for i in range(total)] - scenarios[name] = Scenario( - name, - "LINK terminus delivery, token decrypt, no app callback, 135 B", - interface, frames, DEFAULT_PACKETS[name], runs, - off_i, off_d, 135, - completion_counter=lambda: link.rx, - ) - - # --- SINGLE local delivery --------------------------------------------- - name = "terminus_single" - total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) - identity = RNS.Identity() - # The Destination initialiser automatically registers IN destinations - # with the transport core. - destination = CountingDestination(identity, RNS.Destination.IN, - RNS.Destination.SINGLE, - "bench", "terminus") - frames = [] - for i in range(total): - packet = RNS.Packet(destination, _payload(i, 135, 0x53), - RNS.Packet.DATA, create_receipt=False) - packet.pack() - frames.append(packet.raw) - scenarios[name] = Scenario( - name, - "SINGLE local delivery, ephemeral-key decrypt, 135 B", - interface, frames, DEFAULT_PACKETS[name], runs, - off_i, off_d, 135, - completion_counter=lambda: destination.delivered, - ) - - # --- Announce ingress ---------------------------------------------------- - name = "announce_ingress" - total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name], - fresh=True) - name_hash = RNS.Identity.full_hash(b"rns.throughput.bench")[ - :RNS.Identity.NAME_HASH_LENGTH//8] - frames = []; tsize = 0 - for i in range(total): - ann_identity = RNS.Identity() - dst_h = RNS.Identity.full_hash(name_hash + ann_identity.hash)[ - :RNS.Reticulum.TRUNCATED_HASHLENGTH//8] - random_hash = os.urandom(5) + int(time.time() + i).to_bytes(5, "big") - signed_data = dst_h + ann_identity.get_public_key() + name_hash \ - + random_hash + b"" - signature = ann_identity.sign(signed_data) - announce_data = ann_identity.get_public_key() + name_hash \ - + random_hash + b"" + signature - flags = (RNS.Packet.HEADER_1 << 6) \ - | (RNS.Destination.SINGLE << 2) | RNS.Packet.ANNOUNCE - fbs = struct.pack("!B", flags) + struct.pack("!B", 0) + dst_h + bytes([RNS.Packet.NONE]) + announce_data - frames.append(fbs) - tsize += len(fbs) - - scenarios[name] = Scenario( - name, - "Announce ingress, fresh destinations, validation + path insert", - interface, frames, DEFAULT_PACKETS[name], runs, - off_i, off_d, int(tsize/total), - completion_counter=lambda: len(RNS.Transport.path_table), - fresh_per_run=True, - ) - - # --- Outbound insertion into transport ---------------------------------- - name = "outbound_path" - packets = DEFAULT_PACKETS[name] - inline_n = packets.get("inline", 0) - total = inline_n # pool is re-used across runs - dst_h = RNS.Cryptography.hkdf(length=16, derive_from=b"Odst", - salt=b"bench", context=None) - next_hop = RNS.Cryptography.hkdf(length=16, derive_from=b"Onh", - salt=b"bench", context=None) - now = time.time() - with RNS.Transport.path_table_lock: - RNS.Transport.path_table[dst_h] = [ - now, next_hop, 3, now + 3600, [], interface, bytes(32) - ] - remote_id = RNS.Identity(create_keys=False) - remote_id.load_public_key(os.urandom(RNS.Identity.KEYSIZE//8)) - outbound_destination = RNS.Destination(remote_id, RNS.Destination.OUT, - RNS.Destination.SINGLE, - "bench", "outbound") - outbound_destination.hash = dst_h - outbound_destination.hexhash = dst_h.hex() - frames = [] - for i in range(total): - packet = RNS.Packet(outbound_destination, _payload(i, 135, 0x4F), - RNS.Packet.DATA, create_receipt=False) - packet.pack() - frames.append(packet) - outbound = Scenario( - name, - "Outbound insertion into transport, known 3-hop path, 135 B", - interface, frames, packets, runs, - 0, 0, 135, - completion_counter=None, - ) - outbound.kind = "outbound" - scenarios[name] = outbound - - if requested is not None: - missing = [s for s in requested if s not in scenarios] - if missing: - raise KeyError(f"Unknown scenario(s): {', '.join(missing)}. " - f"Available: {', '.join(sorted(scenarios))}") - scenarios = {name: scenarios[name] for name in requested} - - return scenarios - - -def _reset_transport_state(): - RNS.Transport.packet_hashlist = set() - RNS.Transport.packet_hashlist_prev = set() - RNS.Transport.reverse_table = {} - - -def _percentile(sorted_values, p): - if not sorted_values: - return 0.0 - index = min(len(sorted_values) - 1, int(len(sorted_values) * p)) - return sorted_values[index] - - -def _feed(frame, interface): - RNS.Transport.preprocess_inbound(frame, interface) - -def _bench_inline(interface, frames, n, runs, offset, fresh_per_run=False): - """Synchronous benchmark: process each frame in the calling thread, - bypassing the inbound queue. Returns (run_means_us, samples_us).""" - run_means = [] - all_samples = [] - - # Bypass the inbound queue so processing happens entirely in the - # calling thread; restore the previous setting afterwards. - previous_queue_state = RNS.Transport.USE_INBOUND_QUEUE - RNS.Transport.USE_INBOUND_QUEUE = False - try: - for r in range(runs): - _reset_transport_state() - gc.collect() - gc.disable() - - base = offset + (r * n if fresh_per_run else 0) - t0 = time.perf_counter() - prev = None - samples = [] - step = INLINE_SAMPLE_STEP - next_sample = step - for i in range(n): - _feed(frames[base + i], interface) - if i == next_sample: - now = time.perf_counter() - if prev is not None: - samples.append((now - prev) / step) - prev = now - next_sample += step - dt = time.perf_counter() - t0 - gc.enable() - - run_means.append(dt / n * 1e6) - all_samples.extend(samples) - finally: - RNS.Transport.USE_INBOUND_QUEUE = previous_queue_state - - return run_means, [s * 1e6 for s in all_samples] - - -def _bench_drainer(scenario, n, runs): - """Feed frames through the inbound queue from the calling thread - while the drainer thread processes them. Backpressure is applied - via the scenario completion counter, so no frames are dropped and - the pipeline stays saturated.""" - run_means = [] - - # Ensure the inbound queue and drainer are used; restore afterwards. - previous_queue_state = RNS.Transport.USE_INBOUND_QUEUE - RNS.Transport.USE_INBOUND_QUEUE = True - try: - for r in range(runs): - _reset_transport_state() - gc.collect() - gc.disable() - - base = scenario.drainer_offset \ - + (r * n if scenario.fresh_per_run else 0) - baseline = scenario.completion_counter() - fed = 0 - t0 = time.perf_counter() - chunk = DRAINER_CHUNK - while fed < n: - batch = min(chunk, n - fed) - for i in range(fed, fed + batch): - _feed(scenario.frames[base + i], scenario.interface) - fed += batch - target = baseline + fed - while scenario.completion_counter() < target-800: # Assumes default inbound queue of 1024 - time.sleep(0.0002) - dt = time.perf_counter() - t0 - gc.enable() - - run_means.append(dt / n * 1e6) - finally: - RNS.Transport.USE_INBOUND_QUEUE = previous_queue_state - - return run_means - - -def _bench_outbound(packets, n, runs, offset): - """Synchronous benchmark of the outbound path: insert each packet - into transport for a known path via Transport.outbound(). The packet - pool is re-used across runs.""" - run_means = [] - for r in range(runs): - gc.collect() - gc.disable() - t0 = time.perf_counter() - for i in range(n): - RNS.Transport.outbound(packets[offset + i]) - dt = time.perf_counter() - t0 - gc.enable() - run_means.append(dt / n * 1e6) - return run_means - - -# --------------------------------------------------------------------------- -# Output helpers -# --------------------------------------------------------------------------- - -def _fmt_us(value): - return f"{value:>10.2f}" - - -def _fmt_pps(value): - return f"{value:>12,.0f}" - - -def print_environment_header(): - tree_fastpath = "present" if hasattr(RNS.Transport, "USE_FP_CACHE") else "absent" - print("=" * 40) - print("Reticulum Transport Throughput Benchmark") - print("=" * 40) - print(f" RNS version : {RNS.__version__}") - print(f" Mode : {'compiled' if RNS.compiled else 'interpreted'}") - print(f" crypto : {RNS.Cryptography.backend()}") - print(f" transport : enabled") - print(f" fast path : {tree_fastpath}") - print(f" python : {platform.python_version()}") - print(f" platform : {platform.platform()}") - print(f" machine : {platform.machine()}, cpus: {os.cpu_count()}") - print() - - -def print_scenario_table(scenario, results): - """results: dict mode -> dict(runs, mean_us, pps, stdev, p50, p95).""" - print(f"Scenario: {scenario.name} - {scenario.description}") - print(f" {'mode':<9}{'n':>8}{'runs':>6}{'mean µs':>13}{'pps':>16}" - f"{'p50':>11}{'p95':>11}") - print(" " + "-" * 75) - for mode in ("inline", "drainer"): - if mode not in results: - continue - r = results[mode] - n = scenario.n_for_mode(mode) - p50 = f"{r['p50']:>9.2f} µs" if r["p50"] is not None else "-" - p95 = f"{r['p95']:>9.2f} µs" if r["p95"] is not None else "-" - spread = "" - if r.get("stdev") is not None: - spread = f" ±{r['stdev']:.2f}" - if mode == "inline": mode = "direct" - print(f" {mode:<9}{n:>8}{r['runs']:>6}{_fmt_us(r['mean_us']):>13}" - f"{_fmt_pps(r['pps']):>16}{p50:>11}{p95:>11} " - f"({r['runs']} runs, median{spread})") - - print() - for mode in ("inline", "drainer"): - if mode not in results: - continue - - r = results[mode] - tp = r['pps']*scenario.frame_size*8 - if mode == "inline": mode = "direct" - print(f"{mode:<8} : {RNS.prettyspeed(tp)}") - - print() - - -def print_pps_matrix(rows): - """rows: list of (scenario_name, inline_pps, drainer_pps, fsize)""" - if not rows: - return - print("-" * 72) - print("Transport Throughput, median of runs") - print(f" {'scenario':<22}{'direct':>14}{'drainer':>14}") - print(" " + "-" * 70) - for name, inline_pps, drainer_pps, fsize in rows: - i = f"{inline_pps:>12,.0f}" if inline_pps else "-" - d = f"{drainer_pps:>12,.0f}" if drainer_pps else "-" - itp = RNS.prettyspeed(inline_pps*fsize*8) if inline_pps else "-" - dtp = RNS.prettyspeed(drainer_pps*fsize*8) if drainer_pps else "-" - print(f" {name:<22}{i:>14}{d:>14}{itp:>16} / {dtp}") - print() - - -# --------------------------------------------------------------------------- -# Test suite -# --------------------------------------------------------------------------- - -_instance = None -_interfaces = None -_scenarios = None -_config_dir = None - - -def _start_transport(): - global _instance, _interfaces, _scenarios, _config_dir - - _config_dir = tempfile.mkdtemp(prefix="rns-transport-bench-") - config_dir = _config_dir - with open(os.path.join(config_dir, "config"), "w") as fh: - fh.write( - "[reticulum]\n" - " enable_transport = yes\n" - " share_instance = No\n" - " panic_on_interface_error = No\n" - "\n" - "[logging]\n" - " loglevel = 0\n" - "\n" - "[interfaces]\n" - ) - - # Raise queue lengths for to avoid drops - # at high feed rates - RNS.Transport.INBOUND_DA_QUEUE_LENGTH = 4096 - RNS.Transport.INBOUND_AN_QUEUE_LENGTH = 1024 - RNS.Transport.INBOUND_PR_QUEUE_LENGTH = 1024 - RNS.Transport.INBOUND_IL_QUEUE_LENGTH = 1024 - _instance = RNS.Reticulum(configdir=config_dir, - loglevel=RNS.LOG_CRITICAL, - logdest=lambda *a, **k: None) - - while not RNS.Transport.ready: - time.sleep(0.05) - - interface = BenchmarkInterface("line-a") - interface_b = BenchmarkInterface("line-b") - RNS.Transport.add_interface(interface) - RNS.Transport.add_interface(interface_b) - RNS.Transport.prioritize_interfaces() - _interfaces = (interface, interface_b) - - requested = BENCHMARK_CONFIG["scenarios"] - _scenarios = build_scenarios(_instance, interface, interface_b, - requested=requested) - - -class TestTransportThroughput(unittest.TestCase): - - @classmethod - def setUpClass(cls): - if _scenarios is None: - _start_transport() - - @classmethod - def tearDownClass(cls): - if RNS.Transport._should_run: - RNS.Transport.exit_handler() - if _config_dir is not None: - import shutil - shutil.rmtree(_config_dir, ignore_errors=True) - - def _run_scenario_matrix(self, scenario_names): - rows = [] - # Only include scenarios selected via the command line - scenario_names = [name for name in scenario_names - if name in _scenarios] - for name in scenario_names: - scenario = _scenarios[name] - results = {} - - mode_filter = BENCHMARK_CONFIG["mode"] - runs = BENCHMARK_CONFIG["runs"] - - if scenario.kind == "outbound": - # The outbound path is synchronous; it only has an - # inline-equivalent measurement, and follows the mode - # filter's "inline" selector. - if mode_filter in ("inline", "both"): - run_means = _bench_outbound(scenario.frames, - scenario.packets_inline, - runs, 0) - mean_us = statistics.median(run_means) - results["inline"] = { - "runs": runs, - "mean_us": mean_us, - "pps": 1e6 / mean_us, - "stdev": statistics.stdev(run_means) if len(run_means) > 2 else None, - "p50": None, - "p95": None, - } - else: - if mode_filter in ("inline", "both") and scenario.packets_inline: - n = scenario.packets_inline - run_means, samples = _bench_inline(scenario.interface, - scenario.frames, n, runs, - scenario.inline_offset, - scenario.fresh_per_run) - mean_us = statistics.median(run_means) - stdev = statistics.stdev(run_means) if len(run_means) > 2 else None - sorted_samples = sorted(samples) - results["inline"] = { - "runs": runs, - "mean_us": mean_us, - "pps": 1e6 / mean_us, - "stdev": stdev, - "p50": _percentile(sorted_samples, 0.50), - "p95": _percentile(sorted_samples, 0.95), - } - - if mode_filter in ("drainer", "both") and scenario.can_drainer(): - n = scenario.packets_drainer - run_means = _bench_drainer(scenario, n, runs) - mean_us = statistics.median(run_means) - stdev = statistics.stdev(run_means) if len(run_means) > 2 else None - results["drainer"] = { - "runs": runs, - "mean_us": mean_us, - "pps": 1e6 / mean_us, - "stdev": stdev, - "p50": None, - "p95": None, - } - - if results: - print_scenario_table(scenario, results) - rows.append((name, - results.get("inline", {}).get("pps"), - results.get("drainer", {}).get("pps"), - scenario.frame_size)) - - print_pps_matrix(rows) - - def test_01_transit_throughput(self): - print("") - print_environment_header() - self._run_scenario_matrix([ - "transit_single_135", - "transit_single_475", - "transit_single_1024", - "transit_single_16384", - "transit_single_final", - "transit_link_135", - "transit_link_475", - "transit_link_1024", - "transit_link_16384", - "transit_link_32768", - ]) - - def test_02_delivery_throughput(self): - print("") - self._run_scenario_matrix([ - "terminus_link_135", - "terminus_link_475", - "terminus_link_1024", - "terminus_link_16384", - "terminus_single", - "announce_ingress", - "outbound_path", - ]) - - -def _usage(): - return ( - "\nUsage: python3 tests/transport_throughput.py [options]\n" - "\n" - "Options:\n" - " -s, --scenario NAME Run only the named scenario\n" - " --mode MODE Measurement mode: inline, drainer or both\n" - " (default: both)\n" - " --runs N Measurement runs per scenario/mode, median\n" - " is reported (default: 3)\n" - " --list-scenarios List available scenarios and exit\n" - " -h, --help Show this help and exit\n" - ) - - -if __name__ == "__main__": - argv = sys.argv[1:] - rest = [] - i = 0 - while i < len(argv): - arg = argv[i] - if arg in ("-s", "--scenario"): - BENCHMARK_CONFIG["scenarios"] = BENCHMARK_CONFIG["scenarios"] or [] - BENCHMARK_CONFIG["scenarios"].append(argv[i + 1]) - i += 2 - elif arg == "--mode": - mode = argv[i + 1] - if mode not in ("inline", "drainer", "both"): - raise SystemExit(f"Invalid mode '{mode}'" + _usage()) - BENCHMARK_CONFIG["mode"] = mode - i += 2 - elif arg == "--runs": - BENCHMARK_CONFIG["runs"] = max(1, int(argv[i + 1])) - i += 2 - elif arg == "--list-scenarios": - print("\nAvailable scenarios:") - for name in sorted(SCENARIO_DESCRIPTIONS): - print(f" {name:<22} {SCENARIO_DESCRIPTIONS[name]}") - raise SystemExit(0) - elif arg in ("-h", "--help"): - raise SystemExit(_usage()) - else: - rest.append(arg) - i += 1 - - unittest.main(argv=[sys.argv[0]] + rest, verbosity=2) - -# Pre: -# -# Transport Throughput - PPS matrix (median of runs) -# scenario direct drainer -# ---------------------------------------------------------------------- -# transit_single_135 208,550 184,075 225.23 Mbps / 198.80 Mbps -# transit_single_475 195,652 130,808 743.48 Mbps / 497.07 Mbps -# transit_single_1024 169,828 111,446 1.39 Gbps / 912.97 Mbps -# transit_single_16384 49,618 74,101 6.50 Gbps / 9.71 Gbps -# transit_single_final 204,604 112,925 220.97 Mbps / 121.96 Mbps -# transit_link_135 272,813 234,430 294.64 Mbps / 253.18 Mbps -# transit_link_475 260,003 227,895 988.01 Mbps / 866.00 Mbps -# transit_link_1024 232,763 145,872 1.91 Gbps / 1.19 Gbps -# transit_link_16384 84,028 73,313 11.01 Gbps / 9.61 Gbps -# -# terminus_single 29,813 25,663 32.20 Mbps / 27.72 Mbps -# announce_ingress 7,561 7,242 10.10 Mbps / 9.68 Mbps -# outbound_path 696,106 - 751.79 Mbps / - -# -# No Fastpath: -# -# Transport Throughput - PPS matrix (median of runs) -# scenario direct drainer -# ---------------------------------------------------------------------- -# transit_single_135 247,667 162,090 267.48 Mbps / 175.06 Mbps -# transit_single_475 233,514 159,717 887.35 Mbps / 606.92 Mbps -# transit_single_1024 211,443 175,994 1.73 Gbps / 1.44 Gbps -# transit_single_16384 78,963 72,532 10.35 Gbps / 9.51 Gbps -# transit_single_final 240,855 112,702 260.12 Mbps / 121.72 Mbps -# transit_link_135 266,614 220,571 287.94 Mbps / 238.22 Mbps -# transit_link_475 256,347 152,410 974.12 Mbps / 579.16 Mbps -# transit_link_1024 228,080 194,444 1.87 Gbps / 1.59 Gbps -# transit_link_16384 82,149 73,720 10.77 Gbps / 9.66 Gbps -# -# terminus_single 29,764 28,308 32.15 Mbps / 30.57 Mbps -# announce_ingress 7,394 7,150 9.88 Mbps / 9.55 Mbps -# outbound_path 872,747 - 942.57 Mbps / - -# ---------------------------------------------------------------------- -# -# -# Fastpath: -# -# Transport Throughput - PPS matrix (median of runs) -# scenario direct drainer -# ---------------------------------------------------------------------- -# transit_single_135 407,561 411,426 440.17 Mbps / 444.34 Mbps -# transit_single_475 378,867 377,964 1.44 Gbps / 1.44 Gbps -# transit_single_1024 320,878 325,910 2.63 Gbps / 2.67 Gbps -# transit_single_16384 94,265 94,357 12.36 Gbps / 12.37 Gbps -# transit_single_final 395,245 399,247 426.86 Mbps / 431.19 Mbps -# transit_link_135 453,216 451,863 489.47 Mbps / 488.01 Mbps -# transit_link_475 417,013 417,294 1.58 Gbps / 1.59 Gbps -# transit_link_1024 347,548 353,783 2.85 Gbps / 2.90 Gbps -# transit_link_16384 96,682 96,871 12.67 Gbps / 12.70 Gbps -# -# terminus_single 29,451 25,862 31.81 Mbps / 27.93 Mbps -# announce_ingress 7,495 6,996 10.01 Mbps / 9.35 Mbps -# outbound_path 840,738 - 908.00 Mbps / -