diff --git a/Cargo.lock b/Cargo.lock index c85e92d..4ae491f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,13 +444,6 @@ dependencies = [ "libc", ] -[[package]] -name = "no-alloc-network-test" -version = "0.1.0" -dependencies = [ - "libc", -] - [[package]] name = "num-traits" version = "0.2.19" diff --git a/LICENSE b/LICENSE index 30808e0..496acdb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,675 @@ -MIT License +# GNU GENERAL PUBLIC LICENSE -Copyright (c) 2025 Michael Mikovsky +Version 3, 29 June 2007 -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: +Copyright (C) 2007 Free Software Foundation, Inc. + -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. -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. +## Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom +to share and change all versions of a program--to make sure it remains +free software for all its users. We, the Free Software Foundation, use +the GNU General Public License for most of our software; it applies +also to any other work released this way by its authors. You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you +have certain responsibilities if you distribute copies of the +software, or if you modify it: responsibilities to respect the freedom +of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the +manufacturer can do so. This is fundamentally incompatible with the +aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for +individuals to use, which is precisely where it is most unacceptable. +Therefore, we have designed this version of the GPL to prohibit the +practice for those products. If such problems arise substantially in +other domains, we stand ready to extend this provision to those +domains in future versions of the GPL, as needed to protect the +freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish +to avoid the special danger that patents applied to a free program +could make it effectively proprietary. To prevent this, the GPL +assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in +detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU General Public +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that numbered version or +of any later version published by the Free Software Foundation. If the +Program does not specify a version number of the GNU General Public +License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU General Public License can be used, that proxy's public +statement of acceptance of a version permanently authorizes you to +choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper +mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands \`show w' and \`show c' should show the +appropriate parts of the General Public License. Of course, your +program's commands might be different; for a GUI interface, you would +use an "about box". + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU GPL, see . + +The GNU General Public License does not permit incorporating your +program into proprietary programs. If your program is a subroutine +library, you may consider it more useful to permit linking proprietary +applications with the library. If this is what you want to do, use the +GNU Lesser General Public License instead of this License. But first, +please read . diff --git a/PROTOCOL.md b/PROTOCOL.md index 0105c6e..1c7da04 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -1,679 +1,634 @@ -# UnShell Network Protocol Specification +# UnShell Protocol Specification -**Version:** 0.4.0 -**Status:** Draft — implementation in progress -**Last updated:** 2026-04-22 +**Version:** 0.6.0 +**Status:** Draft +**Last updated:** 2026-04-23 ---- +## 1. Introduction -## Core Design Tenants +**Non-Normative** -Two constraints govern every structural decision in this protocol. +The UnShell protocol is a tree-addressed packet protocol for remote procedure calls, response hooks, and bidirectional streams across a hierarchy of connected endpoints. -**Minimal complexity.** The protocol's minimal form must fit inside shellcode or a small embedded implant. Features that can be implemented as a leaf or an application-layer convention must not be part of the protocol. The protocol exists only to move packets between tree endpoints and to enforce authority relationships at the connection level. +The protocol is intended to be small, extensible, and canonical. -**Extensibility.** The protocol defines a substrate for arbitrary application-layer capabilities. Content types, leaf procedures, and packet payloads are opaque to the router. New capabilities are added by defining new leaves and content types, not by modifying the protocol itself. +Small means the core protocol stays narrow enough for constrained implementations. Extensible means new behavior is introduced through leaves, procedures, and payload schemas instead of frequent protocol redesign. Canonical means there should be one clearly defined way to express each core protocol behavior. -When these two principles appear to conflict, prefer the minimal option and delegate complexity to the leaf or application layer. +This document combines exact protocol definition with rationale. Rationale blocks explain why a rule exists, but do not define interoperability requirements. ---- +> **Rationale:** This document uses a formal specification layout: descriptive sections first, exact protocol definition later, and rationale kept adjacent to the rules it explains. -## Glossary +## 2. Document Conventions + +**Normative** + +The key words `MUST`, `MUST NOT`, `REQUIRED`, `SHALL`, `SHALL NOT`, `SHOULD`, `SHOULD NOT`, `RECOMMENDED`, `MAY`, and `OPTIONAL` in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119.txt) when, and only when, they appear in all capitals. + +Unless a section is explicitly marked otherwise, sections labeled `Normative` define protocol requirements and sections labeled `Non-Normative` provide description, rationale, deployment guidance, or open design commentary. + +All `Rationale` blocks in this document are non-normative. + +## 3. Purpose and Scope + +**Non-Normative** + +The purpose of this specification is to define the set of protocol components required to assemble complete UnShell protocol packets and to provide a framework through which the protocol can be extended through leaves and procedure contracts. + +To achieve this purpose, the scope of this specification includes: + +- endpoint addressing by path +- packet framing +- packet structure +- local authority rules for downwards procedure calls +- path-based routing behavior +- upwards and downwards packet semantics +- hook behavior +- stream behavior +- the required introspection procedure +- extension through leaves, procedures, and payload schemas + +The UnShell protocol assumes that a connection already exists, that the local implementation has decided whether a peer should be admitted into routing, and that any required authentication or authorization has already been handled by the surrounding system. + +The following items are beyond the scope of this specification: + +- authentication +- authorization +- connection establishment +- admission protocol +- transport selection +- transport-specific serialization formats +- encryption +- obfuscation +- router management interfaces +- deployment-specific orchestration behavior +- sensing, analytics, and decision-making systems above the protocol layer + +Every implementation is expected to maintain its own live connection set and its own ground truth about which peers are connected, admitted, and routable. + +> **Rationale:** Authentication and handshakes were intentionally removed from the core scope. They are too deployment-specific to define canonically without bloating the protocol. + +## 4. Protocol Overview + +**Non-Normative** + +Endpoints are addressed by path. + +Leaves are hosted by endpoints. + +A superior endpoint issues a downwards `Call` toward a subordinate endpoint or one of its leaves. + +If the caller wants output, it declares a hook inside the call. The recipient returns one or more `Data` packets upwards toward the hook host. If that hook is stream-oriented, the same `Data` packet type is also used for subsequent bidirectional stream traffic. + +The protocol therefore has two core packet roles: + +- `Call` for downwards invocation +- `Data` for returned data and stream traffic + +This document uses the following notation for readability: + +- `/a/b/c` for endpoint paths +- `/a/b/c { leaf: tty0 }` for a leaf on an endpoint +- `/a/b/c { hook: 7 }` for a hook hosted by an endpoint + +These notations are descriptive only. Leaves and hooks are not encoded as path segments. + +## 5. Terms and Definitions + +**Normative** | Term | Definition | -|------|------------| -| **Tree** | The network of all connected endpoints, addressed by path. | -| **Endpoint** | Any node connected to the tree, identified by its registered path. | -| **Leaf** | A hosted service or data object on an endpoint (e.g. a shell session, a file system). Addressed by endpoint path plus leaf name. | -| **Path** | An ordered sequence of segments uniquely identifying an endpoint. Written as `/seg1/seg2` for readability; transmitted as `Vec`. | -| **Actual Authority** | The endpoint that directly admitted another into the tree via the handshake. Has protocol-enforced control over that specific connection only. | -| **Router** | An endpoint that forwards packets rather than handling them. Not a special node type — any endpoint may act as a router. | -| **Hook** | A response channel declared by the authority inside a `CallProcedure` request. The target leaf fires data back through it. | -| **Stream** | A persistent bidirectional data channel established as part of a `Stream`-type hook. | -| **Packet** | A single framed transmission: one header plus one payload. | +|---|---| +| Tree | The set of connected endpoints arranged by path. | +| Endpoint | A participant in the protocol that can send, receive, host leaves, and route packets. | +| Path | An ordered sequence of segments identifying an endpoint, serialized as `Vec`. | +| Upwards | In the direction of rising authority, closer to the root node. | +| Downwards | In the direction of falling authority, farther from the root node. | +| Leaf | A named service or object hosted by an endpoint. | +| Call | A downwards packet that invokes a procedure on an endpoint or leaf. | +| Procedure | An application-defined operation identified by `procedure_id`. | +| Hook | A response channel declared inside a `Call`. | +| Stream | A bidirectional exchange of `Data` packets associated with a hook and a local `stream_id`. | +| Authority | The endpoint that directly maintains a child connection at a local routing boundary. | +| Subordinate | The lower of two endpoints in a described authority relationship. | +| Registered | Local connection state in which a peer participates in routing. | +| Unregistered | Local connection state in which a peer is connected but not routable. | ---- +## 6. Naming and Structural Conventions -## Overview +**Normative** -UnShell is a **tree-addressed, authority-hierarchical, message-passing protocol** for command and control (C2) operations. +Paths are serialized as `Vec`. -Endpoints are arranged in a tree. Each endpoint owns a path. A parent endpoint is the actual authority over the children it has directly admitted. Communication is directional: authorities send `Request` packets downward to their clients; clients send data upward exclusively through hooks. +Leaf identity is carried in `dst_leaf`. -``` -/ ← root (operator or root router) -/abc123 ← endpoint registered under root -/abc123/pivot ← sub-endpoint registered under /abc123 -``` +Hook identity is carried in `hook_id`. -Leaves are addressed by endpoint path plus a leaf name. The notation used throughout this document is: +Stream identity is carried in `stream_id`. -``` -/abc123 { leaf: tty0 } ← TTY leaf on /abc123 -/abc123 { leaf: files } ← filesystem leaf on /abc123 -/abc123/pivot { leaf: tty0 } ← TTY leaf on /abc123/pivot -``` +No path prefixes are reserved by this protocol. -The `{ leaf: name }` notation is a documentation convention. On the wire, the endpoint path is carried in `dst_path` and the leaf name is carried in the separate `dst_leaf` field of the packet header. Leaf names are not path segments and are invisible to the router. +`procedure_id` is the canonical identifier for a procedure contract. A procedure contract includes the source library or namespace, the specific procedure identity, and the expected input and output schema pair. ---- +The same `procedure_id` is used on both `Call` and `Data` packets. -## Authority Model +> **Rationale:** `procedure_id` is intentionally stricter than a method name or content type. It identifies a full callable contract, not just a label. -### Actual Authority +## 7. Endpoint Model -Each connection has exactly one authority and one client. The authority is the endpoint that accepted the connection and ran the handshake. Actual authority grants: +**Normative** -1. The right to admit or reject the client's registration. -2. The right to send unsolicited `Request` packets to the client. -3. The right to declare hooks on the client via a `CallProcedure`. +### 7.1 Local Authority -Actual authority is **per-connection and one hop only**. The root has actual authority over `/abc123` because it directly admitted it. The root does not have actual authority over `/abc123/pivot` — that connection is managed by `/abc123` independently. Routers must reject `Request` packets whose sender is not the direct parent of the destination. +Each endpoint enforces authority only at the connections it directly maintains. -### Hierarchy +At a local routing boundary: -Endpoints closer to the root have implied precedence over deeper endpoints they did not directly admit. This is an operational expectation and is not enforced by the protocol. The operator at `/` trusts that `/abc123` will not admit hostile sub-endpoints. Only network architecture and pre-shared secrets can enforce this on the protocol's behalf. +- a `Call` packet MUST be accepted only if it arrives from the direct parent connection permitted to issue downwards calls into the destination subtree represented by that boundary +- a `Call` packet that violates that rule MUST be dropped silently +- a `Data` packet MAY arrive from either direction if it belongs to a valid hook or stream flow and routes correctly by path -### Cycles +This protocol does not define a protocol-level authority error packet. -Two endpoints may each be registered in the other's subtree, creating mutual actual authority. This is useful in multi-datacenter topologies where either site should be able to issue commands to the other's endpoints. A compromised node in a cycle has upward reach into the other side; cycles should be created deliberately and documented explicitly in deployment architecture. +### 7.2 Local Connection States ---- +Each implementation MUST maintain at least the following local states: -## Path Conventions +| State | Meaning | +|---|---| +| `Unregistered` | The connection exists locally but is not part of routing state. | +| `Registered` | The connection is admitted into local routing state and may send, receive, or forward protocol traffic. | -Paths are transmitted as `Vec`. Each element is one segment. Written in this document as `/seg1/seg2` for readability. The router operates on the segment array directly — no string joining or splitting occurs. +While a connection is `Unregistered`, an implementation: -Segments beginning with `_` are **protocol-reserved**. External endpoints may not register paths containing `_`-prefixed segments. +- MUST NOT forward protocol packets through it +- MUST NOT trust its path claims for routing +- MUST NOT allocate hook or stream state on its behalf -| Reserved prefix | Owner | Purpose | -|-----------------|-------|---------| -| `_router` | Router | Built-in router endpoints (e.g. `/_router/nodes`) | +Transition into `Registered` is implementation-defined and out of scope for this document. -All other path segments are application-defined. Leaf names, hook IDs, and stream IDs are carried in dedicated header fields — not encoded into path segments. +Transition out of `Registered` MUST invalidate all local routing entries, hook state, and stream state associated with that connection. ---- +> **Rationale:** The protocol no longer defines a handshake, but it still needs a hard boundary between connected peers and admitted peers. -## Packet Format +## 8. Packet Framing -Every transmission is a two-part framed packet: +**Normative** -``` -+----------------------------------+------------------------------+ -| Part 1: Header | Part 2: Payload | -| | | -| [u32 big-endian length] | [u32 big-endian length] | -| [rkyv-serialised PacketHeader] | [rkyv payload bytes] | -| | | -| Router reads this to route | Router forwards opaque | -+----------------------------------+------------------------------+ -``` +Each protocol packet consists of two length-prefixed byte sections: -Both length prefixes are big-endian `u32`. The `packet_type` field in the header fully determines the structure of the payload. The router never inspects the payload — it reads only the header to make all routing decisions. +1. header bytes +2. payload bytes -Packet types are `u16` discriminants produced by rkyv serialisation of the `PacketType` enum. Parsers in any language should treat them as `u16` values matching the discriminants defined below. +Both lengths MUST be encoded as big-endian `u32`. -### PacketHeader +The header MUST be serialized before the payload. -```rust -#[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct PacketHeader { - /// Determines the payload type and routing behaviour. - pub packet_type: PacketType, +Routing decisions MUST be made from header fields only. - /// Destination endpoint path. - /// Required for: Request, HookData. - /// None for: Response (routed by request_id), - /// StreamData, StreamClose (routed by stream_id). - pub dst_path: Option>, +Routers MUST NOT inspect payload structure in order to route a packet. - /// Destination leaf name. Set when the packet targets a specific leaf - /// on the destination endpoint. None for packets targeting the endpoint - /// itself (e.g. GetProcedures, Handshake). - pub dst_leaf: Option, +## 9. Packet Types - /// Source path of the sender. Used by the router to route responses - /// and hook data back to the originating endpoint. - pub src_path: Vec, +**Normative** - /// Correlation ID for Request / Response pairs. - /// Set on Request; echoed on Response. None otherwise. - pub request_id: Option, +This protocol defines exactly two packet types. - /// Stream ID for StreamData / StreamClose fastpath routing. - /// Also set on a CallProcedure that establishes a Stream-type hook, - /// pre-assigned by the authority. None otherwise. - pub stream_id: Option, +| Packet Type | Value | Meaning | +|---|---|---| +| `Call` | `0x01` | Downwards procedure invocation. | +| `Data` | `0x02` | Hook output, event output, or stream traffic. | - /// Hook ID for HookData packets. Set by the authority when declaring - /// the hook in a CallProcedure. Used by the receiving authority to - /// demultiplex incoming hook data. None for non-hook packets. - pub hook_id: Option, -} -``` - -### PacketType → Payload Mapping - -Each `PacketType` variant maps to exactly one payload type. The router discards packets with unknown variants without closing the connection. +Example in the current Rust implementation: ```rust #[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum PacketType { - // -- Handshake ----------------------------------------------------------- - /// Authority → Client. Payload: AuthChallenge - AuthChallenge = 0x10, - /// Client → Authority. Payload: AuthResponse - AuthResponse = 0x11, - /// Client → Authority. Payload: HandshakeMessage - Handshake = 0x12, - /// Authority → Client. Payload: HandshakeAck - HandshakeAck = 0x13, - - // -- Request / Response -------------------------------------------------- - /// Authority → Client. Payload: TreeRequest - Request = 0x01, - /// Client → Authority. Payload: TreeResponse - Response = 0x02, - - // -- Streams ------------------------------------------------------------- - /// Data on an established stream. Fastpath routed by stream_id. - /// Payload: raw bytes. - StreamData = 0x04, - /// Closes an established stream. Fastpath routed by stream_id. - /// Payload: empty. - StreamClose = 0x05, - - // -- Hooks --------------------------------------------------------------- - /// Leaf fires a hook declared by the authority in a prior CallProcedure. - /// Routed to the authority's base path via dst_path. - /// Payload: HookDataMessage - HookData = 0x06, + Call = 0x01, + Data = 0x02, } ``` ---- +`Call` is used for downwards invocation. -## Handshake and Authentication +`Data` is used for hook output, event output, and stream traffic. -The handshake is **authority-initiated**. The connecting node does not speak until challenged. If a connecting node sends any packet before receiving `AuthChallenge`, the authority closes the connection immediately without sending a response. +> **Rationale:** This is the canonical simplification of the earlier model. Separate response and stream-close packet types were removed. -``` -Client Authority - | | - |──── TCP connect ────────────────────────→ | - | | - |←─── AuthChallenge (nonce: [u8;32]) ───────| - | | - |──── AuthResponse (hmac: [u8;32]) ───────→ | - | | - |──── Handshake (registered_paths) ───────→ | - | | - |←─── HandshakeAck (accepted/rejected) ─────| - | | - | [registered; may now send/receive] | -``` +## 10. Packet Header -The authority issues a 32-byte random nonce. The client responds with `HMAC-SHA256(pre_shared_secret, nonce)`. The pre-shared secret is provisioned out-of-band. A failed HMAC closes the connection immediately, before any path data is exchanged. +**Normative** -**Timeouts:** -- Client must respond to `AuthChallenge` within 5 seconds. -- Client must send `Handshake` within 10 seconds of sending a valid `AuthResponse`. -- Client must receive `HandshakeAck` within 5 seconds of sending `Handshake`. +| Field | Meaning | +|---|---| +| `packet_type` | Selects packet semantics. | +| `src_path` | Path of the sending endpoint. | +| `dst_path` | Path of the destination endpoint. | +| `dst_leaf` | Target leaf for a `Call`, if any. | +| `hook_id` | Hook identifier local to the endpoint hosting the hook. | +| `stream_id` | Stream identifier local to the endpoint receiving the stream traffic. | -### Payload Types +Header rules: + +- `src_path` and `dst_path` MUST be present on all packets +- `dst_leaf` MUST be `None` on `Data` +- `stream_id` MUST NOT appear on `Call` unless the call declares a stream-oriented hook +- `hook_id` MUST appear on `Data` when the packet belongs to a hook or hook-backed stream +- `stream_id` MUST appear on `Data` when the packet belongs to a stream + +A packet whose header violates these rules MUST be discarded. + +Example in the current Rust implementation: ```rust -/// Payload for PacketType::AuthChallenge #[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct AuthChallenge { - pub nonce: [u8; 32], -} - -/// Payload for PacketType::AuthResponse -#[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct AuthResponse { - /// HMAC-SHA256(pre_shared_secret, nonce) - pub hmac: [u8; 32], -} - -/// Payload for PacketType::Handshake -#[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct HandshakeMessage { - /// Paths this node wants to own. Each entry must be a single segment, - /// exactly one level below the authority's base path. - /// Segments beginning with `_` are rejected. - pub registered_paths: Vec>, - - /// Human-readable label for diagnostics. Stored by the router and - /// returned via /_router/nodes. Not used for routing. - /// Cannot be updated after registration. - pub display_name: Option, -} - -/// Payload for PacketType::HandshakeAck -#[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct HandshakeAck { - pub accepted: bool, - - /// The canonical base path the authority assigned. May differ from the - /// requested path if the authority adjusts it (e.g. to avoid collisions). - pub assigned_base_path: Vec, - - /// Human-readable rejection reason when accepted == false. - pub rejection_reason: Option, +pub struct PacketHeader { + pub packet_type: PacketType, + pub src_path: Vec, + pub dst_path: Vec, + pub dst_leaf: Option, + pub hook_id: Option, + pub stream_id: Option, } ``` -**Registration is all-or-nothing.** If any path in `registered_paths` fails validation, the entire handshake is rejected with the reason for the first failed path. Partial registration is not supported. +## 11. Routing Rules -**Rejection reasons:** +**Normative** -| Reason | Meaning | -|--------|---------| -| `"auth_failed"` | HMAC did not match | -| `"invalid_path"` | A path segment is malformed | -| `"duplicate_path"` | Path already registered by another endpoint | -| `"reserved_segment"` | A segment begins with `_` | -| `"out_of_subtree"` | Requested path is not within the authority's own subtree | +### 11.1 Path Routing ---- +All protocol routing is path-based. -## Request / Response +When forwarding a packet, an implementation MUST: -The authority sends a `Request` to an endpoint it has actual authority over. The endpoint replies with a `Response` carrying the same `request_id` in the header. +1. compare `dst_path` against its locally registered child paths +2. choose the longest matching prefix +3. forward the packet toward that child if such a child exists +4. otherwise, deliver the packet locally if `dst_path` identifies the local endpoint +5. otherwise, drop the packet silently -**Direction enforcement.** A lower-authority endpoint may never send a `Request` to a higher-authority endpoint. All upward data flow goes through hooks. The router rejects `Request` packets whose sender is not the direct parent of the destination, returning `AuthorityViolation` to the sender. +The protocol defines no mandatory error packet for unresolved destinations. -**Response routing.** When the router forwards a `Request`, it records `request_id → src_connection` in an internal request table. When the corresponding `Response` arrives, the router forwards it to the recorded source and removes the entry. A `Response` with an unrecognised `request_id` is discarded with a warning. +### 11.2 Call Enforcement -**Timeouts.** There is no protocol-level timeout on a `Request` / `Response` pair. The calling endpoint is responsible for implementing application-layer timeouts. +When forwarding or receiving a `Call`, an endpoint MUST verify the local parent-child relationship at the boundary where the packet arrives. -### Payload Types +If the sender on that connection is not the direct parent permitted to issue downwards calls into the relevant subtree, the endpoint MUST drop the packet silently. + +### 11.3 Data Routing + +`Data` packets are routed by `dst_path` using the same path-routing rules as `Call` packets. + +The sender of a `Data` packet MUST set `dst_path` to the path of the stream peer or the hook host. + +### 11.4 Stream Fastpath + +An implementation MAY maintain an internal fastpath keyed by `(local_connection, stream_id)` for performance. + +Such an optimization MUST remain behaviorally equivalent to path-based routing. + +The protocol itself does not route by `stream_id` alone. + +> **Rationale:** `stream_id` is intentionally not treated as a globally routable identifier. + +## 12. Call Definition + +**Normative** + +| Field | Meaning | +|---|---| +| `procedure_id` | Identifier of the invoked procedure contract. | +| `data` | Application-defined procedure input payload. | +| `response_hook` | Optional hook declaration for returned data. | + +Rules: + +- the receiver MUST interpret `procedure_id` as the identifier of the procedure being invoked +- the protocol does not define argument encoding beyond raw bytes in `data` +- a `Call` that expects a result MUST include `response_hook` +- if `response_hook` is absent, the receiver MAY execute the procedure but MUST NOT fabricate an implicit response path + +Example in the current Rust implementation: ```rust -/// Payload for PacketType::Request #[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct TreeRequest { - pub request_type: RequestType, - pub content_type: String, +pub struct CallMessage { + pub procedure_id: String, pub data: Vec, - - /// Required for CallProcedure; must be None for all other request types. - /// If set on a non-CallProcedure request, it is silently ignored. pub response_hook: Option, } - -#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq)] -pub enum RequestType { - /// Read the current value or state at this path. - /// Behavior when targeting a leaf is undefined — see Known Open Problems. - Read = 0, - /// List available leaves and their procedures at this endpoint. - GetProcedures = 1, - /// Write a value to this path. - /// Behavior when targeting a leaf is undefined — see Known Open Problems. - Write = 2, - /// Invoke a named procedure on a leaf. response_hook must be set. - CallProcedure = 3, -} - -/// Payload for PacketType::Response -/// Used for GetProcedures replies and router-generated error responses. -/// CallProcedure always responds via hook and never produces a TreeResponse, -/// except when the router itself cannot resolve the destination path, -/// in which case it returns NoBranchError. -#[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct TreeResponse { - pub status: ResponseStatus, - pub content_type: String, - pub data: Vec, -} - -#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq)] -pub enum ResponseStatus { - Ok = 0, - /// Destination path not found at this endpoint or router. - NoBranchError = 1, - /// Operation not supported at this path. - UnsupportedOp = 2, - /// Endpoint-side execution error. - ExecutionError = 3, - /// Request payload was malformed. - ProtocolError = 4, - /// Attempt by a non-parent endpoint to send a Request downward. - /// See Known Open Problems for enforcement details. - AuthorityViolation = 5, -} ``` ---- +### 12.1 Required Introspection Procedure -## Hooks +The empty string `""` is reserved as the required introspection procedure. -A hook is a response channel declared by the authority inside a `CallProcedure` request. There is no separate hook declaration packet — the hook is born inside the call that establishes it. +Every endpoint MUST implement `procedure_id == ""`. -### Declaration +Behavior: -The authority embeds a `HookTarget` in the `response_hook` field of `TreeRequest` when invoking `CallProcedure`: +- when `dst_leaf` is `None`, the call requests endpoint introspection +- when `dst_leaf` is set, the call requests introspection for that specific leaf + +The result MUST be returned through the declared response hook. + +### 12.2 Failure Behavior + +If the destination endpoint does not exist, the packet is dropped during routing. + +If the destination endpoint exists but `dst_leaf` names no local leaf, the endpoint MUST discard the `Call` silently. + +If `procedure_id` is unknown or unsupported, the endpoint SHOULD report failure through the declared hook using an application-defined error payload. If no hook exists, the endpoint MUST discard the call silently. + +## 13. Hook Definition + +**Normative** + +Hooks are declared only inside `CallMessage.response_hook`. + +There is no standalone hook-open packet. + +| Field | Meaning | +|---|---| +| `hook_id` | Identifier local to the endpoint that hosts the hook and expects responses. | +| `return_path` | Endpoint path to which returned `Data` packets are sent. | +| `response_type` | Advisory indication of whether the expected response is event-like or stream-like. | + +Rules: + +- `hook_id` MUST be unique within the receiving endpoint's active hook set +- `return_path` MUST name the endpoint hosting the hook +- `response_type` is advisory and does not itself terminate or prolong hook lifetime + +Example in the current Rust implementation: ```rust -/// Embedded in TreeRequest.response_hook for CallProcedure requests. #[derive(Archive, Serialize, Deserialize, Debug, Clone)] pub struct HookTarget { - /// Authority-assigned identifier, unique within this authority's active hooks. pub hook_id: u64, - - /// The authority's own base path. HookData packets are sent here - /// and routed by the router using normal path-based routing. - pub fire_path: Vec, - - /// Advisory declaration of expected response cardinality. - /// Event: the leaf is expected to fire once and set end_hook = true. - /// Stream: the leaf is expected to fire repeatedly. - /// The protocol does not enforce this — end_hook governs actual termination. + pub return_path: Vec, pub response_type: HookResponseType, } #[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum HookResponseType { - /// Leaf fires exactly once. - Event = 0, - /// Leaf fires repeatedly until end_hook = true. + Event = 0, Stream = 1, } ``` -`hook_id` is assigned by the authority and must be unique within that authority's set of currently active hooks. The router routes `HookData` to `fire_path` using normal path-based routing (longest-prefix match on `dst_path`). The authority demultiplexes incoming `HookData` packets by `hook_id` from the packet header. +## 14. Data Definition -### HookData Payload +**Normative** + +| Field | Meaning | +|---|---| +| `procedure_id` | Identifier of the procedure contract to which this returned payload belongs. | +| `data` | Application-defined output payload. | +| `end` | Sender indicates completion of its participation in the hook or stream. | +| `cancel` | Sender requests termination of the associated stream processing. | + +Rules: + +- the receiver MUST interpret `procedure_id` as the contract identifier for the returned payload +- the router MUST NOT inspect or validate `procedure_id` +- the receiver MAY validate that the returned `procedure_id` matches the hook or stream context it established + +Example in the current Rust implementation: ```rust -/// Payload for PacketType::HookData #[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct HookDataMessage { - pub content_type: String, +pub struct DataMessage { + pub procedure_id: String, pub data: Vec, - /// When true, the leaf considers this hook complete and will fire - /// no further HookData on this hook_id. The authority removes the - /// hook from its table on receipt. No protocol response is required. - pub end_hook: bool, + pub end: bool, + pub cancel: bool, } ``` -When `end_hook: true` is received, the hook is complete. The authority removes it from its internal hook table. The router may discard any associated routing state for that `hook_id` after forwarding the final packet. +### 14.1 Event Data -### Hook Cancellation +For non-stream responses: -There is no protocol-level mechanism to cancel a running hook. To abort a streaming hook early, the authority invokes a leaf-defined cancel procedure (e.g. `CallProcedure("halt", {})`). Leaf implementers must provide such a procedure if early cancellation is required. +- `hook_id` MUST be present +- `stream_id` MUST be absent +- `end` SHOULD be `true` on the final packet for that hook -### Stream Establishment via Hook +An event-style hook MAY still emit multiple `Data` packets if the application requires chunking or phased output. -When a `CallProcedure` declares a `Stream`-type hook, a bidirectional `StreamData` channel is established as part of the same call. The authority pre-assigns a `stream_id` (a `u32`) and places it in the `PacketHeader` of the `CallProcedure` packet alongside `stream_id`. The router, upon forwarding the `CallProcedure`, registers this `stream_id` in its stream table, mapping it to the pair `(authority_connection, leaf_connection)`. +### 14.2 Stream Establishment -The stream is considered live when the leaf sends its first `HookData` packet. Both sides may then exchange `StreamData` packets freely using the pre-assigned `stream_id`. The stream is closed when either side sends `StreamClose`, or when `end_hook: true` is received on the associated hook. +A stream exists only as part of a hook whose `response_type` is `Stream`. -The authority is responsible for assigning unique `stream_id` values across all streams it currently manages. Because `stream_id` is a `u32` and the router's stream table is a flat `HashMap`, values from two different authorities that happen to collide will corrupt each other's streams. Authorities should use random or cryptographically generated `stream_id` values to make collision probability negligible in practice. +There is no standalone stream-open packet. ---- +The first `Data` packet for a stream MUST: -## Streams +- carry the hook's `hook_id` +- carry a `stream_id` +- set `dst_path` to the hook host's `return_path` -Streams provide a bidirectional, low-overhead data channel between an authority and a leaf. They are established exclusively via the hook mechanism described above. There is no standalone stream-open packet. +Once established, either side MAY continue exchanging `Data` packets carrying that `stream_id` and the appropriate peer `dst_path`. -`StreamData` payload is raw bytes. `StreamClose` payload is empty. +`stream_id` is local to the endpoint that receives and demultiplexes that stream. -The router maintains a `HashMap` for active streams. Populated when a `CallProcedure` establishing a `Stream`-type hook is forwarded. Cleared by `StreamClose` or on endpoint disconnect. +An endpoint MUST NOT reuse an active `stream_id` within its local stream table. -If a `StreamData` or `StreamClose` packet arrives with an unrecognised `stream_id` — which may occur in the race window following a payload reconnect, before the stale stream entry has been cleared — the router returns an error to the sender and closes the stream entry if it exists. The sender should treat this as a hard stream termination. +### 14.3 Stream Data -**Flow control.** The protocol has no acknowledgement or backpressure mechanism. Flow control is delegated entirely to the transport. `TcpTransport` inherits TCP's sliding window natively. Any custom transport must implement equivalent backpressure internally. Application-level rate limiting is the responsibility of the leaf implementation. +For stream-associated traffic: ---- +- `stream_id` MUST be present +- `hook_id` SHOULD be present on every packet and MUST be present on the first packet +- `dst_path` MUST identify the peer endpoint for that stream packet -## Leaf System +### 14.4 End and Cancel -### Purpose +Rules: -Leaves are the application layer of the protocol. A leaf represents a remote service or data object hosted on an endpoint: a shell session, a TCP tunnel, a file system, a running process. The protocol places no constraints on what a leaf does or what procedures it exposes. +- a sender MAY set `end = true` without `cancel = true` +- a sender MAY set `cancel = true` without `end = true` +- a sender MAY set both when it intends immediate termination +- a receiver of `cancel = true` SHOULD stop local processing for that stream as soon as practical -### Addressing +There is no separate stream-close or hook-close packet. -A leaf is identified by its host endpoint path and its leaf name: +### 14.5 Unknown Stream IDs -``` -/abc123 { leaf: tty0 } ← leaf tty0 on endpoint /abc123 -/abc123 { leaf: files } ← leaf files on endpoint /abc123 -/abc123/pivot { leaf: tty0 } ← leaf tty0 on endpoint /abc123/pivot -``` +If an endpoint receives `Data` with an unknown or expired `stream_id`, it MUST discard the packet. -On the wire, `dst_path` carries the endpoint path and `dst_leaf` carries the leaf name. The leaf name is not part of the path and is not visible to the router. +The protocol does not define a mandatory error response for this case. -### Procedures and Parameters +## 15. Introspection Payloads -Leaves expose named procedures invoked via `CallProcedure`. Call parameters directly correspond to the fields of a configurable struct on the leaf. +**Normative** -- Calling a procedure **with parameters** updates those fields and fires the response hook with the resulting state. -- Calling a procedure **without parameters** reads the current state via the hook without modifying anything. +The required introspection procedure `""` MUST return one of the following payloads through the declared hook. -### Leaf Discovery +### 15.1 Endpoint Introspection -The authority sends `GetProcedures` to an endpoint's base path (with `dst_leaf: None`) to enumerate all leaves and their procedures: +Returned when `procedure_id == ""` and `dst_leaf == None`. -``` -Request - dst_path: ["abc123"] - dst_leaf: None - request_type: GetProcedures - content_type: "core/None" - data: [] -``` +| Field | Meaning | +|---|---| +| `leaves` | List of introspection summaries for the endpoint's hosted leaves. | -The endpoint replies with a `TreeResponse` whose `data` is an rkyv-serialised `Vec`: +Each `LeafIntrospectionSummary` contains: + +| Field | Meaning | +|---|---| +| `leaf_name` | The leaf's local name. | +| `description` | Optional human-readable description. | +| `procedures` | Introspection records for the leaf's supported procedures. | +| `state_procedure_id` | Procedure contract identifier associated with the serialized `state` payload. | +| `state` | Serialized current-state payload. | + +Example in the current Rust implementation: ```rust #[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct LeafInfo { - /// The name of this leaf (e.g. "tty0", "files"). +pub struct EndpointIntrospection { + pub leaves: Vec, +} + +#[derive(Archive, Serialize, Deserialize, Debug, Clone)] +pub struct LeafIntrospectionSummary { pub leaf_name: String, - pub state: LeafState, - pub procedures: Vec, + pub description: Option, + pub procedures: Vec, + pub state_procedure_id: String, + pub state: Vec, +} +``` + +### 15.2 Leaf Introspection + +Returned when `procedure_id == ""` and `dst_leaf` names a specific leaf. + +| Field | Meaning | +|---|---| +| `leaf_name` | The leaf's local name. | +| `description` | Optional human-readable description. | +| `procedures` | Introspection records for the leaf's supported procedures. | +| `state_procedure_id` | Procedure contract identifier associated with the serialized `state` payload. | +| `state` | Serialized current-state payload. | + +Each `ProcedureIntrospection` contains: + +| Field | Meaning | +|---|---| +| `name` | Procedure name within the leaf. | +| `description` | Optional human-readable description. | +| `params` | Parameter definitions accepted by the procedure. | +| `response_type` | Advisory indication of whether the procedure normally responds as an event or stream. | + +Each `ProcedureParameter` contains: + +| Field | Meaning | +|---|---| +| `name` | Parameter name. | +| `value_type` | Application-defined parameter type name. | + +Example in the current Rust implementation: + +```rust +#[derive(Archive, Serialize, Deserialize, Debug, Clone)] +pub struct LeafIntrospection { + pub leaf_name: String, + pub description: Option, + pub procedures: Vec, + pub state_procedure_id: String, + pub state: Vec, } #[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct LeafState { - pub running: bool, - /// Host process ID. None for leaves that do not correspond to an OS process. - pub pid: Option, - pub error: Option, -} - -#[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub enum LeafValue { - Int(i64), - Bool(bool), - String(String), - Bytes(Vec), -} - -#[derive(Archive, Serialize, Deserialize, Debug, Clone)] -pub struct ProcedureDescriptor { +pub struct ProcedureIntrospection { pub name: String, pub description: Option, - /// Parameter names and their current values, in call order. - pub params: Vec<(String, LeafValue)>, - /// Advisory: whether the hook fires once or streams repeatedly. - pub hook_response_type: HookResponseType, + pub params: Vec, + pub response_type: HookResponseType, +} + +#[derive(Archive, Serialize, Deserialize, Debug, Clone)] +pub struct ProcedureParameter { + pub name: String, + pub value_type: String, } ``` -### Unresolvable CallProcedure +Rules: -If the router cannot resolve the destination endpoint path, it returns `NoBranchError` to the sender. If the path resolves but the leaf name specified in `dst_leaf` is unknown to the endpoint, the endpoint silently discards the request. In either case, no hook fires. The calling endpoint is responsible for implementing an application-layer timeout. +- `state_procedure_id` MUST identify the procedure contract associated with the serialized `state` payload +- `params` MUST describe the accepted parameter names and parameter types for that procedure +- introspection SHOULD describe current state, but does not establish a cache coherence guarantee -### Reference Implementation: TTY Leaf +## 16. Protocol Description -| Procedure | Parameters | Hook Type | Description | -|-----------|-----------|-----------|-------------| -| `start` | `shell: String, rows: u16, cols: u16` | Event | Spawn a PTY with the given config | -| `halt` | — | Event | Kill the PTY process | -| `resize` | `rows: u16, cols: u16` | Event | Update terminal dimensions | -| `state` | — | Event | Read current leaf state without modifying it | -| `stream` | `name: String ("input"\|"output"\|"both")` | Stream | Attach to PTY I/O bidirectionally | +**Non-Normative** -Calling `start` with no arguments uses the leaf's stored defaults. Calling with arguments updates the stored defaults and spawns the process. The `stream` procedure establishes a `Stream`-type hook; the authority must pre-assign a `stream_id` in the `CallProcedure` header as described in the Hooks section. +The UnShell protocol has a deliberately narrow center: ---- +- addressing by path +- one downwards packet type +- one returned-data packet type +- hooks for correlation +- streams as an extension of hook-backed data flow -## Path Routing +This is meant to make the protocol easier to reason about and easier to implement in small agents. -The router uses three routing mechanisms, selected by `PacketType`. +`procedure_id` is the main semantic anchor. In this design, the caller and callee are expected to share knowledge of what a procedure contract means. The protocol does not carry a global registry. -### 1. Path-Based Routing (Request, HookData) +## 17. Security Considerations -Longest-prefix match on `dst_path`: +**Non-Normative** -``` -Registered paths Incoming dst_path Routes to -["abc123"] ["abc123"] → node abc123 -["abc123","pivot"] ["abc123","pivot"] → node pivot -["_router"] ["_router","nodes"] → router built-in -``` +Although security is not defined by the protocol itself, implementations should treat the `Unregistered` state as a strict quarantine boundary. -**Rules:** -1. Find all registered paths that are a prefix of `dst_path`. -2. Choose the longest matching prefix. -3. No match → return `TreeResponse { status: NoBranchError }` to sender. -4. Tie → route to the most recently registered node. +Recommended behavior: -### 2. Stream ID Fastpath (StreamData, StreamClose) +- authenticate or otherwise validate a peer before moving it to `Registered` +- rate-limit or expire idle unregistered peers +- avoid disclosing topology before admission +- avoid detailed admission failure reasons +- invalidate hooks and streams on disconnect unless a higher-layer session mechanism exists -O(1) lookup in `HashMap`. The entry is created when a `CallProcedure` carrying a `stream_id` is forwarded. The entry is removed on `StreamClose` or on endpoint disconnect. If `stream_id` is not found, the packet is discarded and an error is returned to the sender. +## 18. Serialization and Implementation Notes -### 3. Response Routing (Response) +**Non-Normative** -When a `Request` is forwarded, the router records `request_id → src_connection`. When the corresponding `Response` arrives, the router forwards it to the recorded source connection and removes the entry. A `Response` with an unrecognised `request_id` is discarded with a warning. +This document uses Rust-like `rkyv` struct notation to describe fields because it matches the current implementation language. The notation is explanatory. The protocol semantics are language-agnostic. ---- +Recommended implementation limits: -## Router Built-in Endpoints - -Router built-ins are addressed at `/_router`. Sending any `RequestType` other than the one listed returns `UnsupportedOp`. - -| Path | RequestType | Returns | -|------|-------------|---------| -| `/_router/nodes` | `GetProcedures` | All connected endpoints, their registered paths, and their `display_name` values | -| `/_router/ping` | `Read` | `"pong"` as `core/Utf8String` | - ---- - -## Content Types - -`content_type` is a free-form string namespaced by module. The protocol does not validate or interpret it — it is an application-layer concern. The router forwards it opaquely as part of the payload. - -| Content type | Meaning | +| Item | Recommended limit | |---|---| -| `"core/None"` | No data | -| `"core/Utf8String"` | Raw UTF-8 string | -| `"core/Bytes"` | Raw bytes | -| `"core/LeafList"` | rkyv `Vec` | -| `"tty/Data"` | PTY byte stream | -| `"tty/Output"` | Shell stdout/stderr (UTF-8) | -| `"files/Bytes"` | Raw file contents | +| header length | 64 KiB | +| payload length | 64 MiB | -Custom modules use their module name as the namespace: `"mymodule/MyType"`. +## 19. Known Hard Problems ---- +**Non-Normative** -## Transport Trait +### 19.1 Loop Prevention Outside Strict Trees -The `Transport` trait abstracts the underlying network mechanism. The protocol layer is fully independent of transport. +The protocol does not carry a hop count, route vector, or loop-detection token. -```rust -pub trait Transport: Send { - fn send(&mut self, header: &PacketHeader, payload: &[u8]) -> Result<(), TransportError>; - fn recv(&mut self) -> Result<(PacketHeader, Vec), TransportError>; -} +That keeps packets small, but it means loop prevention must be handled by topology discipline or implementation policy. -#[derive(Debug, thiserror::Error)] -pub enum TransportError { - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - #[error("header too large: {0} bytes (max {1})")] - HeaderTooLarge(usize, usize), - #[error("payload too large: {0} bytes (max {1})")] - PayloadTooLarge(usize, usize), - #[error("connection closed cleanly")] - Disconnected, - #[error("rkyv deserialisation failed")] - DeserialiseError, - #[error("authentication failed")] - AuthFailed, -} -``` +### 19.2 Canonical Connection Management -| Transport | Use case | -|-----------|----------| -| `TcpTransport` | Default | -| `TlsTransport` | Encrypted channel | -| `HttpTransport` | Tunnel over HTTP | -| `DnsTransport` | Tunnel over DNS | -| `IcmpTransport` | Tunnel over ICMP | +The document defines `Registered` and `Unregistered` states but intentionally does not define how a peer moves between them. -**Reconnect policy (payloads):** On `Disconnected` or `Io(_)`: close transport, wait 5 seconds, reconnect, run full auth and handshake. No maximum retry limit. All hook and stream state from the previous session is lost on disconnect. The authority must reissue any `CallProcedure` requests whose hooks it still needs after the payload reconnects. +That preserves flexibility, but it means interoperable admission behavior requires a higher-layer convention. -**Reconnect policy (operator CLI):** Print a message and exit. The operator restarts manually. +### 19.3 Shared Meaning of `procedure_id` ---- +`procedure_id` is only useful if both sides share its meaning. -## Packet Size Limits +The protocol intentionally does not define a global registry or schema negotiation mechanism. That keeps the core minimal, but it pushes interoperability for procedure contracts into shared libraries, operator knowledge, or higher-layer conventions. -| Limit | Value | Reason | -|---|---|---| -| Max header length | 64 KB | Anything larger is a bug or attack | -| Max payload length | 64 MB | Sufficient for most single-packet transfers | -| Auth challenge timeout | 5 s (client) | Fail fast on unresponsive authority | -| Auth response timeout | 5 s (authority) | Prevent connection table exhaustion | -| Handshake timeout | 10 s (authority) | After successful auth challenge | -| Handshake ack timeout | 5 s (client) | Keep reconnect loops responsive | +### 19.4 Stream Resumption Across Disconnects ---- +Hook and stream state are tied to local connection state. -## Known Open Problems - -The following issues have no clean resolution within the current design. They are documented here so that implementers understand the tradeoffs and can make informed decisions. - ---- - -### 1. `AuthorityViolation` enforcement is unspecified - -`ResponseStatus::AuthorityViolation` is defined for the case where a non-parent endpoint attempts to send a `Request` downward past a node it did not directly admit. The spec states that routers must reject such packets, but no mechanism for performing this check is defined. - -To enforce this, the router would need to verify the authority relationship between a packet's sender and its destination before forwarding. The information is available in the routing table populated during admission. However, producing `AuthorityViolation` requires the router to generate a `TreeResponse` — an application-layer packet type — which conflicts with the design principle that the router is opaque to payloads and does not generate protocol-level responses of its own. - -The options are: - -- **Enforce it, accept the exception.** The router generates `AuthorityViolation` as a special case, accepting that this is the one place where the router produces application-layer content. This provides clear operational feedback but breaks the design principle. -- **Drop silently.** Consistent with the behavior of unresolvable `CallProcedure` destinations (no response, timeout at the application layer), but provides no diagnostic signal to a misbehaving or misconfigured endpoint. -- **Remove `AuthorityViolation`.** Drop the status code entirely on the grounds that a correctly implemented client will never send an upward `Request`. The check becomes a deploy-time correctness property rather than a runtime one. - -Each option is a legitimate design choice. A decision should be made explicitly before v1.0. - ---- - -### 2. `Read` and `Write` request types have no defined behavior - -`RequestType::Read` and `RequestType::Write` are defined in the enum but no section of the spec describes what they do, what `data` should contain, or how an endpoint should respond to them. Every actual leaf interaction in the spec uses `CallProcedure`. - -The coherent role for `Read` and `Write` would be for plain data endpoints — nodes that store a value without hosting a full leaf. However, the spec has no concept of a non-leaf data endpoint anywhere else. Introducing one would require defining how such nodes are registered, what types they hold, and how reads and writes are serialised. - -The alternative is to remove `Read` and `Write` from `RequestType`, leaving only `GetProcedures` and `CallProcedure`. This is a deliberate narrowing of the protocol's scope: all structured interaction goes through leaves and procedures, and there is no raw read/write layer below that. This is consistent with the complexity budget and the extensibility tenant, but should be an explicit decision rather than an implicit one made by leaving the types undefined. - ---- - -### 3. `LeafInfo` contains overlapping state representations - -`GetProcedures` returns a `Vec`, each containing a `LeafState` (running, pid, error) and a `Vec` whose `params` fields carry current parameter values. `CallProcedure("state", {})` returns the same information through a hook. There are three overlapping paths to the current state of a leaf, with no stated difference in authority, freshness, or purpose. - -The problem is that these serve roles that are conceptually distinct but practically redundant. `GetProcedures` is a discovery call — the operator asks what the leaf can do and what its current configuration is. `LeafState` answers whether the process is alive. `ProcedureDescriptor.params` answers what the current settings are. `CallProcedure("state")` is a live query that returns the same data. - -The question is whether discovery should return live state at all. One approach is to separate static schema from dynamic state: `GetProcedures` returns only the shape of the leaf (available procedures, parameter names, and their types) with no live values, and all live state queries go through `CallProcedure`. This removes the redundancy but requires a separate round-trip to learn current state after discovery. The other approach is to keep the current design and simply document that `GetProcedures` returns a snapshot that may be stale by the time it is acted on, and that `CallProcedure("state")` is the authoritative live query. Either is defensible; the spec needs to commit to one. +When a connection disappears, the associated hook and stream context disappears with it. Any resumable behavior therefore requires a higher-layer session mechanism. diff --git a/no-alloc-network-test/.cargo/config.toml b/no-alloc-network-test/.cargo/config.toml new file mode 100644 index 0000000..73066e8 --- /dev/null +++ b/no-alloc-network-test/.cargo/config.toml @@ -0,0 +1,2 @@ +[unstable] +build-std = ["core"] \ No newline at end of file diff --git a/no-alloc-network-test/Cargo.lock b/no-alloc-network-test/Cargo.lock new file mode 100644 index 0000000..0d41ed1 --- /dev/null +++ b/no-alloc-network-test/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "no-alloc-network-test" +version = "0.1.0" diff --git a/no-alloc-network-test/Cargo.toml b/no-alloc-network-test/Cargo.toml index cfd5eb2..0b88ec2 100644 --- a/no-alloc-network-test/Cargo.toml +++ b/no-alloc-network-test/Cargo.toml @@ -1,14 +1,12 @@ [package] name = "no-alloc-network-test" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -include.workspace = true +version = "0.1.0" +edition = "2024" +authors = ["ASTATIN3"] +license = "MIT" +repository = "https://github.com/Astatin3/unshell" +include = ["LICENSE", "**/*.rs", "Cargo.toml"] -[dependencies] -libc = "0.2" +[workspace] -[lints] -workspace = true \ No newline at end of file +[dependencies] \ No newline at end of file diff --git a/no-alloc-network-test/src/main.rs b/no-alloc-network-test/src/main.rs index e1d7617..a915f3c 100644 --- a/no-alloc-network-test/src/main.rs +++ b/no-alloc-network-test/src/main.rs @@ -1,6 +1,6 @@ //! # TCP Network Stack using Raw Syscalls //! -//! A TCP server using raw syscalls via libc - no std::net, no smoltcp. +//! A TCP server using raw x86/64 Linux syscalls via inline assembly - no libc, no std. //! //! ## Usage //! ```bash @@ -9,195 +9,393 @@ //! ``` #![no_std] +#![no_main] -use core::mem::zeroed; -use libc::{ - AF_INET, SOCK_STREAM, accept, bind, close, listen, read, sockaddr_in, socket, socklen_t, write, -}; +use core::arch::asm; const PORT: u16 = 1337; const BACKLOG: i32 = 128; -fn main() -> Result<(), NetError> { - let server_fd = create_socket()?; - bind_socket(server_fd, PORT)?; - listen_socket(server_fd, BACKLOG)?; +const AF_INET: i32 = 2; +const SOCK_STREAM: i32 = 1; +const IPPROTO_IP: i32 = 0; - println!("TCP Server listening on port {}", PORT); +const SYS_SOCKET: i32 = 41; +const SYS_BIND: i32 = 49; +const SYS_LISTEN: i32 = 50; +const SYS_ACCEPT: i32 = 43; +const SYS_WRITE: i32 = 1; +const SYS_CLOSE: i32 = 3; +const SYS_EXIT: i32 = 60; + +#[repr(C)] +struct SockAddrIn { + sin_family: u16, + sin_port: u16, + sin_addr: u32, + sin_zero: [u8; 8], +} + +#[repr(C)] +struct SockLen { + len: u32, +} + +impl SockLen { + fn new() -> Self { + Self { len: core::mem::size_of::() as u32 } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn _start() { + log_info("starting tcp server"); + + let server_fd = match create_socket() { + Ok(fd) => { + log_num("socket fd=", fd as i64); + fd + } + Err(err) => { + log_num("socket() failed errno=", err.errno as i64); + exit_with(1) + } + }; + + if let Err(err) = bind_socket(server_fd, PORT) { + log_num("bind() failed errno=", err.errno as i64); + exit_with(1); + } + log_info("bound to 127.0.0.1"); + + if let Err(err) = listen_socket(server_fd, BACKLOG) { + log_num("listen() failed errno=", err.errno as i64); + exit_with(1); + } + + log_info("socket is now listening"); + + print_string("TCP Server listening on port "); + print_u16(PORT); + print_string("\n"); let mut counter: u32 = 0; loop { match accept_client(server_fd) { Ok(client_fd) => { - println!("Connect with: nc 127.0.0.1 {}", PORT); + log_num("accepted client fd=", client_fd as i64); + print_string("Connect with: nc 127.0.0.1 "); + print_u16(PORT); + print_string("\n"); + counter += 1; let message = make_packet(counter); + let _ = syscall3(SYS_WRITE, client_fd as u64, message.as_ptr() as u64, message.len as u64); - unsafe { - let _ = write( - client_fd, - message.as_bytes().as_ptr() as *const libc::c_void, - message.len(), - ); - } - - // let mut buf = [0u8; 1024]; - // loop { - // let n = unsafe { - // read(client_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) - // }; - // if n <= 0 { - // break; - // } - // } - - unsafe { - println!("Closed"); - close(client_fd); - } + syscall1(SYS_CLOSE, client_fd as u64); + print_string("Closed\n"); + } + Err(err) => { + log_num("accept() failed errno=", err.errno as i64); + continue; } - Err(_) => continue, } } } -fn create_socket() -> Result { - let fd = unsafe { socket(AF_INET, SOCK_STREAM, 0) }; - if fd < 0 { - return Err(NetError::DeviceError); +fn syscall1(num: i32, arg1: u64) -> i64 { + let result: i64; + unsafe { + asm!( + "syscall", + in("rax") num as u64, + in("rdi") arg1, + lateout("rax") result, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); } - Ok(fd) -} - -fn bind_socket(fd: i32, port: u16) -> Result<(), NetError> { - let addr = build_sockaddr(port); - let result = unsafe { - bind( - fd, - &addr as *const sockaddr_in as *const libc::sockaddr, - std::mem::size_of::() as socklen_t, - ) - }; - if result < 0 { - return Err(NetError::BindFailed); - } - Ok(()) -} - -fn listen_socket(fd: i32, backlog: i32) -> Result<(), NetError> { - let result = unsafe { listen(fd, backlog) }; - if result < 0 { - return Err(NetError::BindFailed); - } - Ok(()) -} - -fn accept_client(server_fd: i32) -> Result { - let mut client_addr: sockaddr_in = unsafe { zeroed() }; - let mut client_len: socklen_t = std::mem::size_of::() as socklen_t; - - let client_fd = unsafe { - accept( - server_fd, - &mut client_addr as *mut sockaddr_in as *mut libc::sockaddr, - &mut client_len, - ) - }; - - if client_fd < 0 { - return Err(NetError::NoConnection); - } - Ok(client_fd) -} - -fn build_sockaddr(port: u16) -> sockaddr_in { - sockaddr_in { - sin_family: libc::AF_INET as u16, - sin_port: port.to_be(), - sin_addr: libc::in_addr { - s_addr: u32::from_le_bytes([127, 0, 0, 1]), - }, - sin_zero: [0; 8], - } -} - -#[derive(Debug, Clone, Copy)] -pub enum NetError { - NoConnection, - BindFailed, - DeviceError, -} - -impl core::fmt::Display for NetError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::NoConnection => write!(f, "No connection"), - Self::BindFailed => write!(f, "Bind failed"), - Self::DeviceError => write!(f, "Device error"), - } - } -} - -fn make_packet(n: u32) -> SmallStr<16> { - let mut result = SmallStr::<16>::new(); - result.push_slice(b"Packet #"); - push_number(&mut result, n); - result.push(b'\n'); result } -fn push_number(s: &mut SmallStr<16>, n: u32) { - if n == 0 { - s.push(b'0'); - return; +fn syscall3(num: i32, arg1: u64, arg2: u64, arg3: u64) -> i64 { + let result: i64; + unsafe { + asm!( + "syscall", + in("rax") num as u64, + in("rdi") arg1, + in("rsi") arg2, + in("rdx") arg3, + lateout("rax") result, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); } - let mut digits = [0u8; 10]; - let mut len = 0; - let mut num = n; - while num > 0 { - digits[len] = b'0' + (num % 10) as u8; - len += 1; - num /= 10; + result +} + +fn syscall6(num: i32, arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64, arg6: u64) -> i64 { + let result: i64; + unsafe { + asm!( + "syscall", + in("rax") num as u64, + in("rdi") arg1, + in("rsi") arg2, + in("rdx") arg3, + in("r10") arg4, + in("r8") arg5, + in("r9") arg6, + lateout("rax") result, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); } - for i in (0..len).rev() { - s.push(digits[i]); + result +} + +fn create_socket() -> Result { + let fd = syscall3(SYS_SOCKET, AF_INET as u64, SOCK_STREAM as u64, IPPROTO_IP as u64); + if fd < 0 { + return Err(SysErr::from_ret(fd)); + } + Ok(fd as i32) +} + +fn bind_socket(fd: i32, port: u16) -> Result<(), SysErr> { + let addr = SockAddrIn { + sin_family: AF_INET as u16, + sin_port: port.to_be(), + sin_addr: 0x0100007F, + sin_zero: [0; 8], + }; + + let result = syscall6( + SYS_BIND, + fd as u64, + (&addr as *const SockAddrIn) as u64, + core::mem::size_of::() as u64, + 0, + 0, + 0, + ); + if result < 0 { + return Err(SysErr::from_ret(result)); + } + Ok(()) +} + +fn listen_socket(fd: i32, backlog: i32) -> Result<(), SysErr> { + let result = syscall2(SYS_LISTEN, fd as u64, backlog as u64); + if result < 0 { + return Err(SysErr::from_ret(result)); + } + Ok(()) +} + +fn syscall2(num: i32, arg1: u64, arg2: u64) -> i64 { + let result: i64; + unsafe { + asm!( + "syscall", + in("rax") num as u64, + in("rdi") arg1, + in("rsi") arg2, + lateout("rax") result, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); + } + result +} + +fn accept_client(server_fd: i32) -> Result { + let mut addr: SockAddrIn = SockAddrIn { + sin_family: 0, + sin_port: 0, + sin_addr: 0, + sin_zero: [0; 8], + }; + let mut addr_len: SockLen = SockLen::new(); + + let client_fd = syscall6( + SYS_ACCEPT, + server_fd as u64, + (&mut addr as *mut SockAddrIn) as u64, + (&mut addr_len as *mut SockLen) as u64, + 0, + 0, + 0, + ); + + if client_fd < 0 { + return Err(SysErr::from_ret(client_fd)); + } + Ok(client_fd as i32) +} + +#[derive(Clone, Copy)] +struct SysErr { + errno: i32, +} + +impl SysErr { + fn from_ret(ret: i64) -> Self { + Self { errno: (-ret) as i32 } } } -pub struct SmallStr { - data: [u8; N], +fn exit_with(code: i32) -> ! { + let _ = syscall1(SYS_EXIT, code as u64); + loop {} +} + +fn log_info(msg: &str) { + write_stderr("[net] "); + write_stderr(msg); + write_stderr("\n"); +} + +fn log_num(prefix: &str, value: i64) { + write_stderr("[net] "); + write_stderr(prefix); + print_i64_stderr(value); + write_stderr("\n"); +} + +fn write_stderr(s: &str) { + let _ = syscall3(SYS_WRITE, 2, s.as_ptr() as u64, s.len() as u64); +} + +fn print_i64_stderr(n: i64) { + if n < 0 { + write_stderr("-"); + } + print_u64_stderr(n.unsigned_abs()); +} + +fn print_u64_stderr(mut n: u64) { + let mut buf = [0u8; 20]; + if n == 0 { + buf[0] = b'0'; + let _ = syscall3(SYS_WRITE, 2, buf.as_ptr() as u64, 1); + return; + } + + let mut len = 0usize; + while n > 0 { + buf[len] = b'0' + (n % 10) as u8; + len += 1; + n /= 10; + } + let mut out = [0u8; 20]; + let mut i = 0usize; + while i < len { + out[i] = buf[len - 1 - i]; + i += 1; + } + let _ = syscall3(SYS_WRITE, 2, out.as_ptr() as u64, len as u64); +} + +fn print_string(s: &str) { + let stdout = 1u64; + let buf_ptr = s.as_bytes().as_ptr(); + let len = s.len() as u64; + let _ = syscall3(SYS_WRITE, stdout, buf_ptr as u64, len); +} + +fn print_u16(n: u16) { + let mut buf = [0u8; 6]; + let mut pos = 0; + + if n == 0 { + buf[0] = b'0'; + pos = 1; + } else { + let mut digits = [0u8; 5]; + let mut count = 0; + let mut num = n; + while num > 0 { + digits[count] = b'0' + (num % 10) as u8; + count += 1; + num /= 10; + } + let mut i = count; + while i > 0 { + i -= 1; + buf[pos] = digits[i]; + pos += 1; + } + } + + let _ = syscall3(SYS_WRITE, 1u64, buf.as_ptr() as u64, pos as u64); +} + +struct PacketBuf { + data: [u8; 32], len: usize, } -impl SmallStr { - pub const fn new() -> Self { +impl PacketBuf { + fn new() -> Self { Self { - data: [0u8; N], + data: [0u8; 32], len: 0, } } - pub fn push(&mut self, byte: u8) { - if self.len < N { + + fn push(&mut self, byte: u8) { + if self.len < 32 { self.data[self.len] = byte; self.len += 1; } } - pub fn push_slice(&mut self, bytes: &[u8]) { - for &byte in bytes { - self.push(byte); + + fn push_str(&mut self, s: &str) { + for &b in s.as_bytes() { + self.push(b); } } - pub fn as_bytes(&self) -> &[u8] { - &self.data[..self.len] + + fn push_u32(&mut self, n: u32) { + if n == 0 { + self.push(b'0'); + return; + } + let mut digits = [0u8; 10]; + let mut count = 0; + let mut num = n; + while num > 0 { + digits[count] = b'0' + (num % 10) as u8; + count += 1; + num /= 10; + } + for i in (0..count).rev() { + self.push(digits[i]); + } } - pub fn len(&self) -> usize { - self.len + + fn as_ptr(&self) -> *const u8 { + self.data.as_ptr() } } -impl Default for SmallStr { - fn default() -> Self { - Self::new() - } +fn make_packet(n: u32) -> PacketBuf { + let mut buf = PacketBuf::new(); + buf.push_str("Packet #"); + buf.push_u32(n); + buf.push(b'\n'); + buf +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo<'_>) -> ! { + log_info("panic"); + loop {} }