From 25b5ee24585af2dd42e250c81dcaf261dfe58b49 Mon Sep 17 00:00:00 2001 From: Soumya Kushwaha Date: Mon, 17 Apr 2023 08:52:12 +0530 Subject: [PATCH 001/128] Create LICENSE.md This commit adds the GPL-3 license to the repository. --- LICENSE.md | 675 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 675 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2fb2e74 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,675 @@ +### GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +### 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 . From 45499da9c2027f7ca846c178f9b472271da8bce6 Mon Sep 17 00:00:00 2001 From: Malted Date: Sat, 22 Apr 2023 16:41:22 +0100 Subject: [PATCH 002/128] =?UTF-8?q?=F0=9F=94=A5=20Show=20.gitignored=20fil?= =?UTF-8?q?es=20in=20VS=20Code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This disables a workspace setting that hid .gitignored files --- .vscode/settings.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 287ffdd..55c07df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,6 @@ { "files.autoSave": "onFocusChange", "files.defaultLanguage": "rust", - "explorer.excludeGitIgnore": true, "editor.formatOnPaste": true, "editor.formatOnSave": true, "files.trimTrailingWhitespace": true, From 1378eb7eb389ef8c7c3ebc14ed274f20f49ebf99 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 22 Apr 2023 14:12:57 -0400 Subject: [PATCH 003/128] Implement IPv4 address configuration on Linux This involved refactoring the crate structure to share code between macOS and Linux. The new methods have not yet been implemented on macOS, but they have todo!() placeholders. --- Cargo.lock | 21 ++++++ tun/Cargo.toml | 1 + tun/src/apple/mod.rs | 90 ------------------------ tun/src/apple/queue.rs | 17 ----- tun/src/lib.rs | 11 +-- tun/src/linux.rs | 58 --------------- tun/src/unix.rs | 11 --- tun/src/{ => unix}/apple/kern_control.rs | 36 ++++------ tun/src/unix/apple/mod.rs | 76 ++++++++++++++++++++ tun/src/unix/apple/sys.rs | 35 +++++++++ tun/src/unix/linux/mod.rs | 84 ++++++++++++++++++++++ tun/src/unix/linux/sys.rs | 19 +++++ tun/src/unix/mod.rs | 29 ++++++++ tun/src/unix/queue.rs | 59 ++++++++++++++++ tun/tests/configure.rs | 22 ++++++ 15 files changed, 361 insertions(+), 208 deletions(-) delete mode 100644 tun/src/apple/mod.rs delete mode 100644 tun/src/apple/queue.rs delete mode 100644 tun/src/linux.rs delete mode 100644 tun/src/unix.rs rename tun/src/{ => unix}/apple/kern_control.rs (53%) create mode 100644 tun/src/unix/apple/mod.rs create mode 100644 tun/src/unix/apple/sys.rs create mode 100644 tun/src/unix/linux/mod.rs create mode 100644 tun/src/unix/linux/sys.rs create mode 100644 tun/src/unix/mod.rs create mode 100644 tun/src/unix/queue.rs create mode 100644 tun/tests/configure.rs diff --git a/Cargo.lock b/Cargo.lock index 7fa5b6c..0817761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,26 @@ dependencies = [ "instant", ] +[[package]] +name = "fehler" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5729fe49ba028cd550747b6e62cd3d841beccab5390aa398538c31a2d983635" +dependencies = [ + "fehler-macros", +] + +[[package]] +name = "fehler-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccb5acb1045ebbfa222e2c50679e392a71dd77030b78fb0189f2d9c5974400f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "flate2" version = "1.0.24" @@ -1163,6 +1183,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bindgen", + "fehler", "hex-literal", "libc", "libloading", diff --git a/tun/Cargo.toml b/tun/Cargo.toml index 9a6f8fd..f203870 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] libc = "0.2" +fehler = "1.0" nix = { version = "0.25", features = ["ioctl"] } socket2 = "0.4" tokio = { version = "1.21", features = [] } diff --git a/tun/src/apple/mod.rs b/tun/src/apple/mod.rs deleted file mode 100644 index cce4ea1..0000000 --- a/tun/src/apple/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -use socket2::SockAddr; -use std::io::Result; -use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; - -mod kern_control; -mod queue; - -pub use queue::TunQueue; - -use crate::syscall; -use crate::unix::copy_if_name; -use kern_control::SysControlSocket; - -pub struct TunInterface { - socket: socket2::Socket, -} - -impl TunInterface { - pub fn new() -> Result { - TunInterface::connect(None) - } - - fn connect(addr: Option) -> Result { - use socket2::{Domain, Protocol, Socket, Type}; - - let socket = Socket::new( - Domain::from(libc::AF_SYSTEM), - Type::DGRAM, - Some(Protocol::from(libc::SYSPROTO_CONTROL)), - )?; - let addr = match addr { - Some(addr) => addr, - None => socket.resolve(sys::UTUN_CONTROL_NAME, 0)?, - }; - socket.connect(&addr)?; - - Ok(TunInterface { socket }) - } - - pub fn name(&self) -> Result { - let mut buf = [0i8; libc::IFNAMSIZ]; - let mut len = buf.len() as libc::socklen_t; - syscall!(getsockopt( - self.as_raw_fd(), - libc::SYSPROTO_CONTROL, - sys::UTUN_OPT_IFNAME, - buf.as_mut_ptr() as *mut libc::c_void, - &mut len, - ))?; - let name = copy_if_name(buf); - Ok(name) - } - - pub fn queue(&self) -> Result { - todo!() - } -} - -impl AsRawFd for TunInterface { - fn as_raw_fd(&self) -> RawFd { - self.socket.as_raw_fd() - } -} - -impl IntoRawFd for TunInterface { - fn into_raw_fd(self) -> RawFd { - self.socket.into_raw_fd() - } -} - -mod sys { - pub const UTUN_CONTROL_NAME: &str = "com.apple.net.utun_control"; - - pub const UTUN_OPT_IFNAME: libc::c_int = 2; - - /// Copied from https://github.com/rust-lang/socket2/blob/61314a231f73964b3db969ef72c0e9479df320f3/src/sys/unix.rs#L168-L178 - /// getsockopt is not exposed by socket2 - #[macro_export] - macro_rules! syscall { - ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{ - #[allow(unused_unsafe)] - let res = unsafe { libc::$fn($($arg, )*) }; - if res == -1 { - Err(std::io::Error::last_os_error()) - } else { - Ok(res) - } - }}; - } -} diff --git a/tun/src/apple/queue.rs b/tun/src/apple/queue.rs deleted file mode 100644 index fdc23b6..0000000 --- a/tun/src/apple/queue.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; - -pub struct TunQueue { - socket: socket2::Socket, -} - -impl AsRawFd for TunQueue { - fn as_raw_fd(&self) -> RawFd { - self.socket.as_raw_fd() - } -} - -impl IntoRawFd for TunQueue { - fn into_raw_fd(self) -> RawFd { - self.socket.into_raw_fd() - } -} diff --git a/tun/src/lib.rs b/tun/src/lib.rs index 7082e2d..7e86059 100644 --- a/tun/src/lib.rs +++ b/tun/src/lib.rs @@ -1,16 +1,9 @@ -#[cfg(target_vendor = "apple")] -#[path = "apple/mod.rs"] -mod imp; - -#[cfg(target_os = "linux")] -#[path = "linux.rs"] -mod imp; - #[cfg(target_os = "windows")] #[path = "windows/mod.rs"] mod imp; #[cfg(any(target_os = "linux", target_vendor = "apple"))] -pub(crate) mod unix; +#[path = "unix/mod.rs"] +pub(crate) mod imp; pub use imp::{TunInterface, TunQueue}; diff --git a/tun/src/linux.rs b/tun/src/linux.rs deleted file mode 100644 index b1ef0ae..0000000 --- a/tun/src/linux.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::fs::OpenOptions; -use std::io::Result; -use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; - -use crate::unix::copy_if_name; - -pub struct TunInterface { - inner: socket2::Socket, -} - -impl TunInterface { - pub fn new() -> Result { - let file = OpenOptions::new() - .read(true) - .write(true) - .open("/dev/net/tun")?; - - let iff = libc::ifreq { - ifr_name: [0; libc::IFNAMSIZ], - ifr_ifru: libc::__c_anonymous_ifr_ifru { - ifru_flags: (libc::IFF_TUN | libc::IFF_TUN_EXCL | libc::IFF_NO_PI) as i16, - }, - }; - unsafe { sys::tun_set_iff(file.as_raw_fd(), &iff)? }; - - let inner = unsafe { socket2::Socket::from_raw_fd(file.into_raw_fd()) }; - Ok(TunInterface { inner }) - } - - pub fn name(&self) -> Result { - let mut iff = libc::ifreq { - ifr_name: [0; libc::IFNAMSIZ], - ifr_ifru: libc::__c_anonymous_ifr_ifru { ifru_flags: 0 }, - }; - unsafe { sys::tun_get_iff(self.inner.as_raw_fd(), &mut iff)? }; - - let name = copy_if_name(iff.ifr_name); - Ok(name) - } -} - -mod sys { - use nix::{ioctl_read_bad, ioctl_write_ptr_bad, request_code_read, request_code_write}; - use std::mem::size_of; - - ioctl_write_ptr_bad!( - tun_set_iff, - request_code_write!(b'T', 202, size_of::()), - libc::ifreq - ); - ioctl_read_bad!( - tun_get_iff, - request_code_read!(b'T', 210, size_of::()), - libc::ifreq - ); -} - -pub struct TunQueue; diff --git a/tun/src/unix.rs b/tun/src/unix.rs deleted file mode 100644 index 17c425c..0000000 --- a/tun/src/unix.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::ffi::{c_char, CStr}; - -pub fn copy_if_name(buf: [c_char; libc::IFNAMSIZ]) -> String { - // TODO: Switch to `CStr::from_bytes_until_nul` when stabilized - unsafe { - CStr::from_ptr(buf.as_ptr() as *const _) - .to_str() - .unwrap() - .to_string() - } -} diff --git a/tun/src/apple/kern_control.rs b/tun/src/unix/apple/kern_control.rs similarity index 53% rename from tun/src/apple/kern_control.rs rename to tun/src/unix/apple/kern_control.rs index f913fb6..abc1e04 100644 --- a/tun/src/apple/kern_control.rs +++ b/tun/src/unix/apple/kern_control.rs @@ -1,17 +1,21 @@ -use libc::{sockaddr_ctl, AF_SYSTEM, AF_SYS_CONTROL}; -use std::io::Result; +use fehler::throws; +use std::io::Error; use std::mem::size_of; use std::os::unix::io::AsRawFd; +use super::sys; + /// Trait to connect to kernel extensions on Apple platforms /// /// Pulled from XNU source: https://github.com/apple/darwin-xnu/blob/main/bsd/sys/kern_control.h pub trait SysControlSocket { - fn resolve(&self, name: &str, index: u32) -> Result; + #[throws] + fn resolve(&self, name: &str, index: u32) -> socket2::SockAddr; } impl SysControlSocket for socket2::Socket { - fn resolve(&self, name: &str, index: u32) -> Result { + #[throws] + fn resolve(&self, name: &str, index: u32) -> socket2::SockAddr { let mut info = sys::ctl_info { ctl_id: 0, ctl_name: [0; 96], @@ -22,32 +26,18 @@ impl SysControlSocket for socket2::Socket { let (_, addr) = unsafe { socket2::SockAddr::init(|addr_storage, len| { - *len = size_of::() as u32; + *len = size_of::() as u32; - let mut addr: &mut sockaddr_ctl = &mut *addr_storage.cast(); + let mut addr: &mut sys::sockaddr_ctl = &mut *addr_storage.cast(); addr.sc_len = *len as u8; - addr.sc_family = AF_SYSTEM as u8; - addr.ss_sysaddr = AF_SYS_CONTROL as u16; + addr.sc_family = sys::AF_SYSTEM as u8; + addr.ss_sysaddr = sys::AF_SYS_CONTROL as u16; addr.sc_id = info.ctl_id; addr.sc_unit = index; Ok(()) }) }?; - Ok(addr) + addr } } - -mod sys { - use nix::ioctl_readwrite; - - const MAX_KCTL_NAME: usize = 96; - - #[repr(C)] - pub struct ctl_info { - pub ctl_id: u32, - pub ctl_name: [u8; MAX_KCTL_NAME], - } - - ioctl_readwrite!(resolve_ctl_info, b'N', 3, ctl_info); -} diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs new file mode 100644 index 0000000..178ec6a --- /dev/null +++ b/tun/src/unix/apple/mod.rs @@ -0,0 +1,76 @@ +use fehler::throws; +use libc::c_char; +use std::net::Ipv4Addr; +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +use std::io::Error; + +mod sys; +mod kern_control; + +pub use super::queue::TunQueue; + +use super::ifname_to_string; +use kern_control::SysControlSocket; + +#[derive(Debug)] +pub struct TunInterface { + pub(crate) socket: socket2::Socket, +} + +impl TunInterface { + #[throws] + pub fn new() -> TunInterface { + TunInterface::connect(0)? + } + + #[throws] + fn connect(index: u32) -> TunInterface { + use socket2::{Domain, Protocol, Socket, Type}; + + let socket = Socket::new( + Domain::from(sys::AF_SYSTEM), + Type::DGRAM, + Some(Protocol::from(sys::SYSPROTO_CONTROL)), + )?; + let addr = socket.resolve(sys::UTUN_CONTROL_NAME, index)?; + socket.connect(&addr)?; + + TunInterface { socket } + } + + #[throws] + pub fn name(&self) -> String { + let mut buf = [0 as c_char; sys::IFNAMSIZ]; + let mut len = buf.len() as sys::socklen_t; + sys::syscall!(getsockopt( + self.as_raw_fd(), + sys::SYSPROTO_CONTROL, + sys::UTUN_OPT_IFNAME, + buf.as_mut_ptr() as *mut sys::c_void, + &mut len, + ))?; + ifname_to_string(buf) + } + + #[throws] + pub fn set_ipv4_addr(&self, _addr: Ipv4Addr) { + todo!() + } + + #[throws] + pub fn ipv4_addr(&self) -> Ipv4Addr { + todo!() + } +} + +impl AsRawFd for TunInterface { + fn as_raw_fd(&self) -> RawFd { + self.socket.as_raw_fd() + } +} + +impl IntoRawFd for TunInterface { + fn into_raw_fd(self) -> RawFd { + self.socket.into_raw_fd() + } +} diff --git a/tun/src/unix/apple/sys.rs b/tun/src/unix/apple/sys.rs new file mode 100644 index 0000000..ff909ed --- /dev/null +++ b/tun/src/unix/apple/sys.rs @@ -0,0 +1,35 @@ +pub use libc::{c_void, socklen_t, SYSPROTO_CONTROL, IFNAMSIZ, sockaddr_ctl, AF_SYSTEM, AF_SYS_CONTROL}; +use nix::ioctl_readwrite; + +pub const UTUN_CONTROL_NAME: &str = "com.apple.net.utun_control"; +pub const UTUN_OPT_IFNAME: libc::c_int = 2; + +pub const MAX_KCTL_NAME: usize = 96; + + #[repr(C)] +pub struct ctl_info { + pub ctl_id: u32, + pub ctl_name: [u8; MAX_KCTL_NAME], +} + +#[repr(C)] +pub struct ifreq {} + +ioctl_readwrite!(resolve_ctl_info, b'N', 3, ctl_info); + +/// Copied from https://github.com/rust-lang/socket2/blob/61314a231f73964b3db969ef72c0e9479df320f3/src/sys/unix.rs#L168-L178 +/// getsockopt is not exposed by socket2 +#[macro_export] +macro_rules! syscall { + ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{ + #[allow(unused_unsafe)] + let res = unsafe { libc::$fn($($arg, )*) }; + if res == -1 { + Err(std::io::Error::last_os_error()) + } else { + Ok(res) + } + }}; +} + +pub use syscall; diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs new file mode 100644 index 0000000..cc80f97 --- /dev/null +++ b/tun/src/unix/linux/mod.rs @@ -0,0 +1,84 @@ +use fehler::throws; + +use socket2::{Domain, SockAddr, Socket, Type}; +use std::fs::OpenOptions; +use std::io::Error; +use std::mem; +use std::net::{Ipv4Addr, SocketAddrV4}; +use std::os::fd::RawFd; +use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; + +use super::{ifname_to_string, string_to_ifname}; + +mod sys; + +#[derive(Debug)] +pub struct TunInterface { + pub(crate) socket: socket2::Socket, +} + +impl TunInterface { + #[throws] + pub fn new() -> TunInterface { + let file = OpenOptions::new() + .read(true) + .write(true) + .open("/dev/net/tun")?; + + let iff = libc::ifreq { + ifr_name: [0; libc::IFNAMSIZ], + ifr_ifru: libc::__c_anonymous_ifr_ifru { + ifru_flags: (libc::IFF_TUN | libc::IFF_TUN_EXCL | libc::IFF_NO_PI) as i16, + }, + }; + unsafe { sys::tun_set_iff(file.as_raw_fd(), &iff)? }; + + let socket = unsafe { socket2::Socket::from_raw_fd(file.into_raw_fd()) }; + TunInterface { socket } + } + + #[throws] + pub fn name(&self) -> String { + let mut iff = unsafe { mem::zeroed() }; + unsafe { sys::tun_get_iff(self.socket.as_raw_fd(), &mut iff)? }; + ifname_to_string(iff.ifr_name) + } + + #[throws] + fn ifreq(&self) -> sys::ifreq { + let mut iff: sys::ifreq = unsafe { mem::zeroed() }; + iff.ifr_name = string_to_ifname(&self.name()?); + iff + } + + #[throws] + pub fn index(&self) -> i32 { + let mut iff = self.ifreq()?; + self.perform(|fd| unsafe { sys::if_get_index(fd, &mut iff) })?; + unsafe { iff.ifr_ifru.ifru_ifindex } + } + + #[throws] + pub fn set_ipv4_addr(&self, addr: Ipv4Addr) { + let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); + + let mut iff = self.ifreq()?; + iff.ifr_ifru.ifru_addr = unsafe { *addr.as_ptr() }; + + self.perform(|fd| unsafe { sys::if_set_addr(fd, &iff) })?; + } + + #[throws] + pub fn ipv4_addr(&self) -> Ipv4Addr { + let mut iff = self.ifreq()?; + self.perform(|fd| unsafe { sys::if_get_addr(fd, &mut iff) })?; + let addr = unsafe { *(&iff.ifr_ifru.ifru_addr as *const _ as *const sys::sockaddr_in) }; + Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)) + } + + #[throws] + fn perform(&self, perform: impl FnOnce(RawFd) -> Result) -> R { + let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?; + perform(socket.as_raw_fd())? + } +} diff --git a/tun/src/unix/linux/sys.rs b/tun/src/unix/linux/sys.rs new file mode 100644 index 0000000..5df22d9 --- /dev/null +++ b/tun/src/unix/linux/sys.rs @@ -0,0 +1,19 @@ +use nix::{ioctl_read_bad, ioctl_write_ptr_bad, request_code_read, request_code_write}; +use std::mem::size_of; + +pub use libc::ifreq; +pub use libc::sockaddr_in; + +ioctl_write_ptr_bad!( + tun_set_iff, + request_code_write!(b'T', 202, size_of::()), + libc::ifreq +); +ioctl_read_bad!( + tun_get_iff, + request_code_read!(b'T', 210, size_of::()), + libc::ifreq +); +ioctl_read_bad!(if_get_index, libc::SIOCGIFINDEX, libc::ifreq); +ioctl_read_bad!(if_get_addr, libc::SIOCGIFADDR, libc::ifreq); +ioctl_write_ptr_bad!(if_set_addr, libc::SIOCSIFADDR, libc::ifreq); diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs new file mode 100644 index 0000000..615e647 --- /dev/null +++ b/tun/src/unix/mod.rs @@ -0,0 +1,29 @@ +mod queue; + +#[cfg(target_vendor = "apple")] +#[path = "apple/mod.rs"] +mod imp; + +#[cfg(target_os = "linux")] +#[path = "linux/mod.rs"] +mod imp; + +pub use imp::TunInterface; +pub use queue::TunQueue; + +pub fn ifname_to_string(buf: [libc::c_char; libc::IFNAMSIZ]) -> String { + // TODO: Switch to `CStr::from_bytes_until_nul` when stabilized + unsafe { + std::ffi::CStr::from_ptr(buf.as_ptr() as *const _) + .to_str() + .unwrap() + .to_string() + } +} + +pub fn string_to_ifname(name: &str) -> [libc::c_char; libc::IFNAMSIZ] { + let mut buf = [0 as libc::c_char; libc::IFNAMSIZ]; + let len = name.len().min(buf.len()); + buf[..len].copy_from_slice(unsafe { &*(name.as_bytes() as *const _ as *const [libc::c_char]) }); + buf +} diff --git a/tun/src/unix/queue.rs b/tun/src/unix/queue.rs new file mode 100644 index 0000000..1288a3b --- /dev/null +++ b/tun/src/unix/queue.rs @@ -0,0 +1,59 @@ +use fehler::throws; + +use std::{ + io::{Error, Read, Write}, + mem::MaybeUninit, + os::unix::io::{AsRawFd, IntoRawFd, RawFd}, +}; + +use crate::TunInterface; + +pub struct TunQueue { + socket: socket2::Socket, +} + +impl TunQueue { + #[throws] + pub fn recv(&self, buf: &mut [MaybeUninit]) -> usize { + self.socket.recv(buf)? + } +} + +impl Read for TunQueue { + #[throws] + fn read(&mut self, buf: &mut [u8]) -> usize { + self.socket.read(buf)? + } +} + +impl Write for TunQueue { + #[throws] + fn write(&mut self, buf: &[u8]) -> usize { + self.socket.write(buf)? + } + + #[throws] + fn flush(&mut self) { + self.socket.flush()? + } +} + +impl From for TunQueue { + fn from(interface: TunInterface) -> TunQueue { + TunQueue { + socket: interface.socket, + } + } +} + +impl AsRawFd for TunQueue { + fn as_raw_fd(&self) -> RawFd { + self.socket.as_raw_fd() + } +} + +impl IntoRawFd for TunQueue { + fn into_raw_fd(self) -> RawFd { + self.socket.into_raw_fd() + } +} diff --git a/tun/tests/configure.rs b/tun/tests/configure.rs new file mode 100644 index 0000000..29674b6 --- /dev/null +++ b/tun/tests/configure.rs @@ -0,0 +1,22 @@ +use fehler::throws; +use tun::TunInterface; +use std::io::Error; +use std::net::Ipv4Addr; + +#[test] +#[throws] +fn test_create() { + TunInterface::new()?; +} + +#[test] +#[throws] +fn test_set_get_ipv4() { + let tun = TunInterface::new()?; + + let addr = Ipv4Addr::new(10, 0, 0, 1); + tun.set_ipv4_addr(addr)?; + let result = tun.ipv4_addr()?; + + assert_eq!(addr, result); +} \ No newline at end of file From 1ffa01df5b51faa6423e48da556c23fcc2fef27d Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 29 Apr 2023 15:21:33 -0400 Subject: [PATCH 004/128] Commit Xcode schemes to version control This commit adds the Burrow and NetworkExtension schemes to version control so that they can be shared across developers. --- .../xcshareddata/WorkspaceSettings.xcsettings | 8 ++ .../xcshareddata/xcschemes/Burrow.xcscheme | 77 +++++++++++++++ .../xcschemes/NetworkExtension.xcscheme | 96 +++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 Apple/Burrow.xcodeproj/xcshareddata/xcschemes/Burrow.xcscheme create mode 100644 Apple/Burrow.xcodeproj/xcshareddata/xcschemes/NetworkExtension.xcscheme diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..08de0be --- /dev/null +++ b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/Burrow.xcscheme b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/Burrow.xcscheme new file mode 100644 index 0000000..005a84e --- /dev/null +++ b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/Burrow.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/NetworkExtension.xcscheme b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/NetworkExtension.xcscheme new file mode 100644 index 0000000..24ca402 --- /dev/null +++ b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/NetworkExtension.xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 4bf0fb639cc95e7a6e441fbc01352bd53c41aeef Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 29 Apr 2023 12:27:41 -0400 Subject: [PATCH 005/128] Switch to the Hack Club development team This change switches the project over to the Hack Club developer team in the Hack Club developer account. This is needed to get the app in TestFlight. --- Apple/Configuration/Identity.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apple/Configuration/Identity.xcconfig b/Apple/Configuration/Identity.xcconfig index e4ef90e..e9ca78d 100644 --- a/Apple/Configuration/Identity.xcconfig +++ b/Apple/Configuration/Identity.xcconfig @@ -1,2 +1,2 @@ -DEVELOPMENT_TEAM = 87PW93R2ZR +DEVELOPMENT_TEAM = P6PV2R9443 APP_BUNDLE_IDENTIFIER = com.hackclub.burrow From d966c0ff779acb7fd39698693bc07d99a7715e28 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 29 Apr 2023 15:31:12 -0400 Subject: [PATCH 006/128] Add packet tunnel provider entitlement This entitlement is required to communicate with the VPN API on iOS and macOS. --- Apple/App/App-iOS.entitlements | 4 ++++ Apple/App/App-macOS.entitlements | 4 ++++ Apple/NetworkExtension/NetworkExtension-iOS.entitlements | 4 ++++ Apple/NetworkExtension/NetworkExtension-macOS.entitlements | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/Apple/App/App-iOS.entitlements b/Apple/App/App-iOS.entitlements index d9849a8..02ee960 100644 --- a/Apple/App/App-iOS.entitlements +++ b/Apple/App/App-iOS.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.networking.networkextension + + packet-tunnel-provider + com.apple.security.application-groups $(APP_GROUP_IDENTIFIER) diff --git a/Apple/App/App-macOS.entitlements b/Apple/App/App-macOS.entitlements index d9849a8..02ee960 100644 --- a/Apple/App/App-macOS.entitlements +++ b/Apple/App/App-macOS.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.networking.networkextension + + packet-tunnel-provider + com.apple.security.application-groups $(APP_GROUP_IDENTIFIER) diff --git a/Apple/NetworkExtension/NetworkExtension-iOS.entitlements b/Apple/NetworkExtension/NetworkExtension-iOS.entitlements index d9849a8..02ee960 100644 --- a/Apple/NetworkExtension/NetworkExtension-iOS.entitlements +++ b/Apple/NetworkExtension/NetworkExtension-iOS.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.networking.networkextension + + packet-tunnel-provider + com.apple.security.application-groups $(APP_GROUP_IDENTIFIER) diff --git a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements index d9849a8..02ee960 100644 --- a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements +++ b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.networking.networkextension + + packet-tunnel-provider + com.apple.security.application-groups $(APP_GROUP_IDENTIFIER) From 4b0965b846d54ea9f1b85560fc6a5be1ddb9b769 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 29 Apr 2023 15:19:46 -0400 Subject: [PATCH 007/128] Build burrow library into iOS and macOS app This commit adds a build script, build-rust.sh, which compiles the burrow crate from inside of Xcode. The network extension then links against this crate. --- .github/workflows/build-apple.yml | 13 +++++ .github/workflows/build-rust.yml | 2 +- Apple/Burrow.xcodeproj/project.pbxproj | 39 +++++++++++++++ Apple/Configuration/build-rust.sh | 69 ++++++++++++++++++++++++++ burrow/Cargo.toml | 3 +- burrow/src/lib.rs | 3 ++ burrow/src/main.rs | 4 +- 7 files changed, 129 insertions(+), 4 deletions(-) create mode 100755 Apple/Configuration/build-rust.sh create mode 100644 burrow/src/lib.rs diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index d1dba49..3b9516a 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -18,14 +18,22 @@ jobs: destination: generic/platform=iOS platform: iOS sdk-name: iphoneos + rust-targets: + - aarch64-apple-ios - scheme: Burrow destination: platform=iOS Simulator,OS=16.2,name=iPhone 14 Pro platform: iOS Simulator sdk-name: iphonesimulator + rust-targets: + - aarch64-apple-ios-sim + - x86_64-apple-ios - scheme: Burrow destination: platform=macOS platform: macOS sdk-name: macos + rust-targets: + - x86_64-apple-darwin + - aarch64-apple-darwin env: DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer steps: @@ -39,6 +47,11 @@ jobs: with: certificate: ${{ secrets.DEVELOPER_CERT }} password: ${{ secrets.DEVELOPER_CERT_PASSWORD }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: ${{ join(matrix.rust-targets, ', ') }} - name: Build id: build uses: ./.github/actions/build-for-testing diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 8048a3a..c767eca 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -50,7 +50,7 @@ jobs: shell: bash run: sudo apt-get install -y ${{ join(matrix.packages, ' ') }} - name: Install Rust - uses: dtolnay/rust-toolchain@master + uses: dtolnay/rust-toolchain@stable with: toolchain: stable components: rustfmt diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index fa4009a..adee0ed 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; D05B9F7829E39EEC008CB1F9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* ContentView.swift */; }; D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D05B9F7929E39EED008CB1F9 /* Assets.xcassets */; }; + D0B98FC529FDA49E004E7149 /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B98FC429FDA476004E7149 /* libburrow.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,6 +58,8 @@ D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowApp.swift; sourceTree = ""; }; D05B9F7729E39EEC008CB1F9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; D05B9F7929E39EED008CB1F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D0B98FBF29FD8072004E7149 /* build-rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-rust.sh"; sourceTree = ""; }; + D0B98FC429FDA476004E7149 /* libburrow.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libburrow.a; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -64,6 +67,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D0B98FC529FDA49E004E7149 /* libburrow.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -84,6 +88,7 @@ D020F64A29E4A452002790F6 /* App.xcconfig */, D020F66329E4A703002790F6 /* Extension.xcconfig */, D020F64029E4A1FF002790F6 /* Compiler.xcconfig */, + D0B98FBF29FD8072004E7149 /* build-rust.sh */, D020F64229E4A1FF002790F6 /* Info.plist */, ); path = Configuration; @@ -108,6 +113,7 @@ D020F65629E4A697002790F6 /* NetworkExtension */, D020F63C29E4A1FF002790F6 /* Configuration */, D05B9F7329E39EEC008CB1F9 /* Products */, + D0B98FC129FDA45D004E7149 /* Frameworks */, ); sourceTree = ""; }; @@ -133,6 +139,14 @@ path = App; sourceTree = ""; }; + D0B98FC129FDA45D004E7149 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0B98FC429FDA476004E7149 /* libburrow.a */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -140,6 +154,7 @@ isa = PBXNativeTarget; buildConfigurationList = D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */; buildPhases = ( + D0B98FC029FD809A004E7149 /* Compile Rust */, D020F64F29E4A697002790F6 /* Sources */, D020F65029E4A697002790F6 /* Frameworks */, D020F65129E4A697002790F6 /* Resources */, @@ -227,6 +242,30 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + D0B98FC029FD809A004E7149 /* Compile Rust */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Rust"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(BUILT_PRODUCTS_DIR)/libburrow.a", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PROJECT_DIR}/Configuration/build-rust.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ D020F64F29E4A697002790F6 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/Apple/Configuration/build-rust.sh b/Apple/Configuration/build-rust.sh new file mode 100755 index 0000000..1f62701 --- /dev/null +++ b/Apple/Configuration/build-rust.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +export PATH="${PATH}:${HOME}/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:/etc/profiles/per-user/${USER}/bin" + +if ! [[ -x "$(command -v cargo)" ]]; then + echo 'error: Unable to find cargo' + exit 127 +fi + +set -e + +cd -- "$(dirname -- "${BASH_SOURCE[0]}")"/../../burrow + +RUST_TARGETS=() + +IFS=' ' read -a BURROW_ARCHS <<< "${ARCHS[@]}" +for ARCH in "${BURROW_ARCHS[@]}"; do + case $PLATFORM_NAME in + iphonesimulator) + case $ARCH in + arm64) RUST_TARGETS+=("aarch64-apple-ios-sim") ;; + x86_64) RUST_TARGETS+=("x86_64-apple-ios") ;; + *) echo "error: Unknown $PLATFORM_NAME arch, $ARCH"; exit 1 ;; + esac + ;; + iphoneos) + case $ARCH in + arm64) RUST_TARGETS+=("aarch64-apple-ios") ;; + *) echo "error: Unknown $PLATFORM_NAME arch, $ARCH"; exit 1 ;; + esac + ;; + macos*) + case $ARCH in + arm64) RUST_TARGETS+=("aarch64-apple-darwin") ;; + x86_64) RUST_TARGETS+=("x86_64-apple-darwin") ;; + *) echo "error: Unknown $PLATFORM_NAME arch, $ARCH"; exit 1 ;; + esac + ;; + *) echo "error: Unsupported platform $PLATFORM_NAME"; exit 1 ;; + esac +done + +CARGO_ARGS=() +for TARGET in "${RUST_TARGETS[@]}"; do + CARGO_ARGS+=("--target") + CARGO_ARGS+=("$TARGET") +done + +CARGO_ARGS+=("--lib") + +if [[ $SWIFT_ACTIVE_COMPILATION_CONDITIONS == *DEBUG* ]]; then + CARGO_DIR="debug" +else + CARGO_ARGS+=("--release") + CARGO_DIR="release" +fi + +if [[ -x "$(command -v rustup)" ]]; then + CARGO_PATH="$(dirname $(rustup which cargo)):/usr/bin" +else + CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin" +fi + +env -i PATH="$CARGO_PATH" cargo build "${CARGO_ARGS[@]}" + +mkdir -p "${BUILT_PRODUCTS_DIR}" +/usr/bin/xcrun --sdk $PLATFORM_NAME lipo \ + -create $(printf "${PROJECT_DIR}/../target/%q/${CARGO_DIR}/libburrow.a " "${RUST_TARGETS[@]}") \ + -output "${BUILT_PRODUCTS_DIR}/libburrow.a" diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index ec16981..a984553 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -3,7 +3,8 @@ name = "burrow" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["lib", "staticlib"] [dependencies] tokio = { version = "1.21", features = ["rt", "macros"] } diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs new file mode 100644 index 0000000..7d83484 --- /dev/null +++ b/burrow/src/lib.rs @@ -0,0 +1,3 @@ +pub fn hello_world() { + println!("Hello, world!"); +} diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 3d59fd3..8dae842 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -1,7 +1,7 @@ use tokio::io::Result; use tun::TunInterface; -async fn lol() -> Result<()> { +async fn try_main() -> Result<()> { let iface = TunInterface::new()?; println!("{:?}", iface.name()); @@ -10,5 +10,5 @@ async fn lol() -> Result<()> { #[tokio::main(flavor = "current_thread")] async fn main() { - lol().await.unwrap(); + try_main().await.unwrap(); } From a13b2243e6775db77ea1361bd771f1a94ea5b435 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Sat, 29 Apr 2023 10:39:34 -0400 Subject: [PATCH 008/128] tun: Enable setting/getting of MTU Works similarly to getting and setting IP addresses, can pretty much be copy-pasted for the rest of the settings. --- tun/src/unix/linux/mod.rs | 16 ++++++++++++++++ tun/src/unix/linux/sys.rs | 3 +++ 2 files changed, 19 insertions(+) diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index cc80f97..db22beb 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -68,6 +68,13 @@ impl TunInterface { self.perform(|fd| unsafe { sys::if_set_addr(fd, &iff) })?; } + #[throws] + pub fn set_mtu(&self, mtu: i32) { + let mut iff = self.ifreq()?; + iff.ifr_ifru.ifru_mtu = mtu; + self.perform(|fd| unsafe { sys::if_set_mtu(fd, &iff) })?; + } + #[throws] pub fn ipv4_addr(&self) -> Ipv4Addr { let mut iff = self.ifreq()?; @@ -76,6 +83,15 @@ impl TunInterface { Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)) } + #[throws] + pub fn mtu(&self) -> i32 { + let mut iff = self.ifreq()?; + self.perform(|fd| unsafe { sys::if_get_mtu(fd, &mut iff) })?; + let mtu = unsafe { iff.ifr_ifru.ifru_mtu }; + + mtu + } + #[throws] fn perform(&self, perform: impl FnOnce(RawFd) -> Result) -> R { let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?; diff --git a/tun/src/unix/linux/sys.rs b/tun/src/unix/linux/sys.rs index 5df22d9..e8e373f 100644 --- a/tun/src/unix/linux/sys.rs +++ b/tun/src/unix/linux/sys.rs @@ -16,4 +16,7 @@ ioctl_read_bad!( ); ioctl_read_bad!(if_get_index, libc::SIOCGIFINDEX, libc::ifreq); ioctl_read_bad!(if_get_addr, libc::SIOCGIFADDR, libc::ifreq); +ioctl_read_bad!(if_get_mtu, libc::SIOCGIFMTU, libc::ifreq); + ioctl_write_ptr_bad!(if_set_addr, libc::SIOCSIFADDR, libc::ifreq); +ioctl_write_ptr_bad!(if_set_mtu, libc::SIOCSIFMTU, libc::ifreq); From c444bf293e238c02fa87e68d63c2afab6ee3c61d Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Sat, 29 Apr 2023 15:25:49 -0400 Subject: [PATCH 009/128] tun: Create integration tests for MTU Tests are good, y'all :) --- tun/src/unix/linux/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index db22beb..82a2125 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -98,3 +98,16 @@ impl TunInterface { perform(socket.as_raw_fd())? } } + +mod test { + use super::TunInterface; + + #[test] + fn mtu() { + let interf = TunInterface::new().unwrap(); + + interf.set_mtu(500).unwrap(); + + assert_eq!(interf.mtu().unwrap(), 500); + } +} From 6ea4b596c2836981980189b1f17f08c5268c9f0e Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Sat, 29 Apr 2023 19:50:10 -0400 Subject: [PATCH 010/128] tun: Initial work on getting/setting netmask Seems to run into an issue with setting netmasks like 255.0.0.0 with an "AddressNotAvailable" error --- tun/src/unix/linux/mod.rs | 33 +++++++++++++++++++++++++++++++++ tun/src/unix/linux/sys.rs | 3 +++ 2 files changed, 36 insertions(+) diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 82a2125..7167a32 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -75,6 +75,16 @@ impl TunInterface { self.perform(|fd| unsafe { sys::if_set_mtu(fd, &iff) })?; } + #[throws] + pub fn set_netmask(&self, addr: Ipv4Addr) { + let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); + + let mut iff = self.ifreq()?; + iff.ifr_ifru.ifru_netmask = unsafe { *addr.as_ptr() }; + + self.perform(|fd| unsafe { sys::if_set_netmask(fd, &iff) })?; + } + #[throws] pub fn ipv4_addr(&self) -> Ipv4Addr { let mut iff = self.ifreq()?; @@ -92,6 +102,17 @@ impl TunInterface { mtu } + #[throws] + pub fn netmask(&self) -> Ipv4Addr { + let mut iff = self.ifreq()?; + self.perform(|fd| unsafe { sys::if_get_netmask(fd, &mut iff) })?; + + let netmask = + unsafe { *(&iff.ifr_ifru.ifru_netmask as *const _ as *const sys::sockaddr_in) }; + + Ipv4Addr::from(u32::from_be(netmask.sin_addr.s_addr)) + } + #[throws] fn perform(&self, perform: impl FnOnce(RawFd) -> Result) -> R { let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?; @@ -101,6 +122,7 @@ impl TunInterface { mod test { use super::TunInterface; + use std::net::Ipv4Addr; #[test] fn mtu() { @@ -110,4 +132,15 @@ mod test { assert_eq!(interf.mtu().unwrap(), 500); } + + #[test] + fn netmask() { + let interf = TunInterface::new().unwrap(); + + let addr = Ipv4Addr::new(255, 0, 0, 0); + + interf.set_netmask(addr); + + assert_eq!(interf.netmask().unwrap(), addr); + } } diff --git a/tun/src/unix/linux/sys.rs b/tun/src/unix/linux/sys.rs index e8e373f..8bac12d 100644 --- a/tun/src/unix/linux/sys.rs +++ b/tun/src/unix/linux/sys.rs @@ -2,6 +2,7 @@ use nix::{ioctl_read_bad, ioctl_write_ptr_bad, request_code_read, request_code_w use std::mem::size_of; pub use libc::ifreq; +pub use libc::sockaddr; pub use libc::sockaddr_in; ioctl_write_ptr_bad!( @@ -17,6 +18,8 @@ ioctl_read_bad!( ioctl_read_bad!(if_get_index, libc::SIOCGIFINDEX, libc::ifreq); ioctl_read_bad!(if_get_addr, libc::SIOCGIFADDR, libc::ifreq); ioctl_read_bad!(if_get_mtu, libc::SIOCGIFMTU, libc::ifreq); +ioctl_read_bad!(if_get_netmask, libc::SIOCGIFNETMASK, libc::ifreq); ioctl_write_ptr_bad!(if_set_addr, libc::SIOCSIFADDR, libc::ifreq); ioctl_write_ptr_bad!(if_set_mtu, libc::SIOCSIFMTU, libc::ifreq); +ioctl_write_ptr_bad!(if_set_netmask, libc::SIOCSIFNETMASK, libc::ifreq); From 4a0d53bdd3fc7d0334ce4f8b86c54046ad8e6546 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Sat, 29 Apr 2023 19:57:08 -0400 Subject: [PATCH 011/128] tun: Fix implementation tests for netmask Note that there needs to be an address set on the interface prior to assigning a netmask. --- tun/src/unix/linux/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 7167a32..3e6d352 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -137,10 +137,12 @@ mod test { fn netmask() { let interf = TunInterface::new().unwrap(); - let addr = Ipv4Addr::new(255, 0, 0, 0); + let netmask = Ipv4Addr::new(255, 0, 0, 0); + let addr = Ipv4Addr::new(192, 168, 1, 1); - interf.set_netmask(addr); + interf.set_ipv4_addr(addr); + interf.set_netmask(netmask).unwrap(); - assert_eq!(interf.netmask().unwrap(), addr); + assert_eq!(interf.netmask().unwrap(), netmask); } } From 5e265632a31595088c5bc766e640754c9be68759 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Sat, 29 Apr 2023 20:05:19 -0400 Subject: [PATCH 012/128] tun/test: Add #[throws] to netmask test Enables tests to avoid using `.unwrap()`. Co-authored-by: Conrad Kramer --- tun/src/unix/linux/mod.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 3e6d352..42a1754 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -134,15 +134,16 @@ mod test { } #[test] + #[throws] fn netmask() { - let interf = TunInterface::new().unwrap(); + let interf = TunInterface::new()?; let netmask = Ipv4Addr::new(255, 0, 0, 0); let addr = Ipv4Addr::new(192, 168, 1, 1); - interf.set_ipv4_addr(addr); - interf.set_netmask(netmask).unwrap(); + interf.set_ipv4_addr(addr)?; + interf.set_netmask(netmask)?; - assert_eq!(interf.netmask().unwrap(), netmask); + assert_eq!(interf.netmask()?, netmask); } } From eeb0130156bc33ef4d99cf5528bc9e28382aec6a Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Mon, 8 May 2023 18:14:21 -0400 Subject: [PATCH 013/128] Add Swift module for libburrow This commit also renames the app target to App. --- .github/actions/build-for-testing/action.yml | 1 + .github/workflows/build-apple.yml | 6 +-- Apple/App/{Burrow.xcconfig => App.xcconfig} | 5 +- Apple/Burrow.xcodeproj/project.pbxproj | 53 +++++++++++-------- .../{Burrow.xcscheme => App.xcscheme} | 6 +-- .../xcschemes/NetworkExtension.xcscheme | 7 ++- Apple/Configuration/Compiler.xcconfig | 1 + Apple/NetworkExtension/Info.plist | 2 + .../NetworkExtension.xcconfig | 5 +- .../libburrow}/build-rust.sh | 2 +- Apple/NetworkExtension/libburrow/libburrow.h | 1 + .../libburrow/module.modulemap | 4 ++ 12 files changed, 57 insertions(+), 36 deletions(-) rename Apple/App/{Burrow.xcconfig => App.xcconfig} (82%) rename Apple/Burrow.xcodeproj/xcshareddata/xcschemes/{Burrow.xcscheme => App.xcscheme} (95%) rename Apple/{Configuration => NetworkExtension/libburrow}/build-rust.sh (97%) create mode 100644 Apple/NetworkExtension/libburrow/libburrow.h create mode 100644 Apple/NetworkExtension/libburrow/module.modulemap diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index 9691122..964cd08 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -30,6 +30,7 @@ runs: -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ -onlyUsePackageVersionsFromResolvedFile \ + -skipPackagePluginValidation \ -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ -resultBundlePath BuildResults.xcresult diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 3b9516a..1aadcc2 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -14,20 +14,20 @@ jobs: fail-fast: false matrix: include: - - scheme: Burrow + - scheme: App destination: generic/platform=iOS platform: iOS sdk-name: iphoneos rust-targets: - aarch64-apple-ios - - scheme: Burrow + - scheme: App destination: platform=iOS Simulator,OS=16.2,name=iPhone 14 Pro platform: iOS Simulator sdk-name: iphonesimulator rust-targets: - aarch64-apple-ios-sim - x86_64-apple-ios - - scheme: Burrow + - scheme: App destination: platform=macOS platform: macOS sdk-name: macos diff --git a/Apple/App/Burrow.xcconfig b/Apple/App/App.xcconfig similarity index 82% rename from Apple/App/Burrow.xcconfig rename to Apple/App/App.xcconfig index a4922de..1d63205 100644 --- a/Apple/App/Burrow.xcconfig +++ b/Apple/App/App.xcconfig @@ -2,6 +2,7 @@ PRODUCT_NAME = Burrow PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER) +PRODUCT_MODULE_NAME = BurrowApp INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphone*] = YES INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphone*] = YES @@ -11,7 +12,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad[sdk=iphone*] = UIInterfaceOr INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone[sdk=iphone*] = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2 -INFOPLIST_KEY_LSApplicationCategoryType[sdk=macos*] = public.app-category.utilities +INFOPLIST_KEY_LSApplicationCategoryType[sdk=macosx*] = public.app-category.utilities CODE_SIGN_ENTITLEMENTS = App/App-iOS.entitlements -CODE_SIGN_ENTITLEMENTS[sdk=macos*] = App/App-macOS.entitlements +CODE_SIGN_ENTITLEMENTS[sdk=macosx*] = App/App-macOS.entitlements diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index adee0ed..e69eb6c 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -12,7 +12,8 @@ D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; D05B9F7829E39EEC008CB1F9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* ContentView.swift */; }; D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D05B9F7929E39EED008CB1F9 /* Assets.xcassets */; }; - D0B98FC529FDA49E004E7149 /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B98FC429FDA476004E7149 /* libburrow.a */; }; + D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BCC6032A09535900AD070D /* libburrow.a */; }; + D0BCC60A2A09A0B800AD070D /* build-rust.sh in Resources */ = {isa = PBXBuildFile; fileRef = D0B98FBF29FD8072004E7149 /* build-rust.sh */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -43,7 +44,7 @@ D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = ""; }; D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D020F64929E4A34B002790F6 /* Burrow.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Burrow.xcconfig; sourceTree = ""; }; + D020F64929E4A34B002790F6 /* App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = App.xcconfig; sourceTree = ""; }; D020F64A29E4A452002790F6 /* App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = App.xcconfig; sourceTree = ""; }; D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BurrowNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; @@ -59,7 +60,10 @@ D05B9F7729E39EEC008CB1F9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; D05B9F7929E39EED008CB1F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D0B98FBF29FD8072004E7149 /* build-rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-rust.sh"; sourceTree = ""; }; - D0B98FC429FDA476004E7149 /* libburrow.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libburrow.a; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B98FD829FDDB6F004E7149 /* libburrow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libburrow.h; sourceTree = ""; }; + D0B98FDC29FDDDCF004E7149 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + D0BCC5FE2A086E1C00AD070D /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; + D0BCC6032A09535900AD070D /* libburrow.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libburrow.a; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,7 +71,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D0B98FC529FDA49E004E7149 /* libburrow.a in Frameworks */, + D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -88,7 +92,6 @@ D020F64A29E4A452002790F6 /* App.xcconfig */, D020F66329E4A703002790F6 /* Extension.xcconfig */, D020F64029E4A1FF002790F6 /* Compiler.xcconfig */, - D0B98FBF29FD8072004E7149 /* build-rust.sh */, D020F64229E4A1FF002790F6 /* Info.plist */, ); path = Configuration; @@ -102,6 +105,7 @@ D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */, D020F66629E4A95D002790F6 /* NetworkExtension-macOS.entitlements */, D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */, + D0B98FD729FDDB57004E7149 /* libburrow */, ); path = NetworkExtension; sourceTree = ""; @@ -113,7 +117,6 @@ D020F65629E4A697002790F6 /* NetworkExtension */, D020F63C29E4A1FF002790F6 /* Configuration */, D05B9F7329E39EEC008CB1F9 /* Products */, - D0B98FC129FDA45D004E7149 /* Frameworks */, ); sourceTree = ""; }; @@ -130,21 +133,27 @@ isa = PBXGroup; children = ( D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */, - D05B9F7729E39EEC008CB1F9 /* ContentView.swift */, + D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */, + D0B98FC629FDC5B5004E7149 /* Tunnel.swift */, + D0BCC5FE2A086E1C00AD070D /* Status.swift */, + D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */, D05B9F7929E39EED008CB1F9 /* Assets.xcassets */, D020F66829E4AA74002790F6 /* App-iOS.entitlements */, D020F66929E4AA74002790F6 /* App-macOS.entitlements */, - D020F64929E4A34B002790F6 /* Burrow.xcconfig */, + D020F64929E4A34B002790F6 /* App.xcconfig */, ); path = App; sourceTree = ""; }; - D0B98FC129FDA45D004E7149 /* Frameworks */ = { + D0B98FD729FDDB57004E7149 /* libburrow */ = { isa = PBXGroup; children = ( - D0B98FC429FDA476004E7149 /* libburrow.a */, + D0B98FBF29FD8072004E7149 /* build-rust.sh */, + D0B98FDC29FDDDCF004E7149 /* module.modulemap */, + D0B98FD829FDDB6F004E7149 /* libburrow.h */, + D0BCC6032A09535900AD070D /* libburrow.a */, ); - name = Frameworks; + path = libburrow; sourceTree = ""; }; /* End PBXGroup section */ @@ -154,7 +163,7 @@ isa = PBXNativeTarget; buildConfigurationList = D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */; buildPhases = ( - D0B98FC029FD809A004E7149 /* Compile Rust */, + D0BCC60B2A09A0C100AD070D /* Compile Rust */, D020F64F29E4A697002790F6 /* Sources */, D020F65029E4A697002790F6 /* Frameworks */, D020F65129E4A697002790F6 /* Resources */, @@ -168,9 +177,9 @@ productReference = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; productType = "com.apple.product-type.app-extension"; }; - D05B9F7129E39EEC008CB1F9 /* Burrow */ = { + D05B9F7129E39EEC008CB1F9 /* App */ = { isa = PBXNativeTarget; - buildConfigurationList = D05B9F8129E39EED008CB1F9 /* Build configuration list for PBXNativeTarget "Burrow" */; + buildConfigurationList = D05B9F8129E39EED008CB1F9 /* Build configuration list for PBXNativeTarget "App" */; buildPhases = ( D05B9F6E29E39EEC008CB1F9 /* Sources */, D05B9F6F29E39EEC008CB1F9 /* Frameworks */, @@ -182,7 +191,7 @@ dependencies = ( D020F65C29E4A697002790F6 /* PBXTargetDependency */, ); - name = Burrow; + name = App; productName = Burrow; productReference = D05B9F7229E39EEC008CB1F9 /* Burrow.app */; productType = "com.apple.product-type.application"; @@ -218,7 +227,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - D05B9F7129E39EEC008CB1F9 /* Burrow */, + D05B9F7129E39EEC008CB1F9 /* App */, D020F65229E4A697002790F6 /* NetworkExtension */, ); }; @@ -229,6 +238,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D0BCC60A2A09A0B800AD070D /* build-rust.sh in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -243,7 +253,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - D0B98FC029FD809A004E7149 /* Compile Rust */ = { + D0BCC60B2A09A0C100AD070D /* Compile Rust */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -257,11 +267,10 @@ outputFileListPaths = ( ); outputPaths = ( - "$(BUILT_PRODUCTS_DIR)/libburrow.a", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PROJECT_DIR}/Configuration/build-rust.sh\"\n"; + shellScript = "\"${PROJECT_DIR}/NetworkExtension/libburrow/build-rust.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -325,14 +334,14 @@ }; D05B9F8229E39EED008CB1F9 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D020F64929E4A34B002790F6 /* Burrow.xcconfig */; + baseConfigurationReference = D020F64929E4A34B002790F6 /* App.xcconfig */; buildSettings = { }; name = Debug; }; D05B9F8329E39EED008CB1F9 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D020F64929E4A34B002790F6 /* Burrow.xcconfig */; + baseConfigurationReference = D020F64929E4A34B002790F6 /* App.xcconfig */; buildSettings = { }; name = Release; @@ -358,7 +367,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - D05B9F8129E39EED008CB1F9 /* Build configuration list for PBXNativeTarget "Burrow" */ = { + D05B9F8129E39EED008CB1F9 /* Build configuration list for PBXNativeTarget "App" */ = { isa = XCConfigurationList; buildConfigurations = ( D05B9F8229E39EED008CB1F9 /* Debug */, diff --git a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/Burrow.xcscheme b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme similarity index 95% rename from Apple/Burrow.xcodeproj/xcshareddata/xcschemes/Burrow.xcscheme rename to Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme index 005a84e..7bb7808 100644 --- a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/Burrow.xcscheme +++ b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -16,7 +16,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D05B9F7129E39EEC008CB1F9" BuildableName = "Burrow.app" - BlueprintName = "Burrow" + BlueprintName = "App" ReferencedContainer = "container:Burrow.xcodeproj"> @@ -45,7 +45,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D05B9F7129E39EEC008CB1F9" BuildableName = "Burrow.app" - BlueprintName = "Burrow" + BlueprintName = "App" ReferencedContainer = "container:Burrow.xcodeproj"> @@ -62,7 +62,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D05B9F7129E39EEC008CB1F9" BuildableName = "Burrow.app" - BlueprintName = "Burrow" + BlueprintName = "App" ReferencedContainer = "container:Burrow.xcodeproj"> diff --git a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/NetworkExtension.xcscheme b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/NetworkExtension.xcscheme index 24ca402..e03c201 100644 --- a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/NetworkExtension.xcscheme +++ b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/NetworkExtension.xcscheme @@ -1,7 +1,6 @@ @@ -62,7 +61,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D05B9F7129E39EEC008CB1F9" BuildableName = "Burrow.app" - BlueprintName = "Burrow" + BlueprintName = "App" ReferencedContainer = "container:Burrow.xcodeproj"> @@ -81,7 +80,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D05B9F7129E39EEC008CB1F9" BuildableName = "Burrow.app" - BlueprintName = "Burrow" + BlueprintName = "App" ReferencedContainer = "container:Burrow.xcodeproj"> diff --git a/Apple/Configuration/Compiler.xcconfig b/Apple/Configuration/Compiler.xcconfig index ebf6136..de6261a 100644 --- a/Apple/Configuration/Compiler.xcconfig +++ b/Apple/Configuration/Compiler.xcconfig @@ -35,6 +35,7 @@ FUSE_BUILD_SCRIPT_PHASES = YES APP_GROUP_IDENTIFIER = group.$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER[sdk=macosx*] = $(DEVELOPMENT_TEAM).$(APP_BUNDLE_IDENTIFIER) +NETWORK_EXTENSION_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).network // Swift SWIFT_VERSION = 5.0 diff --git a/Apple/NetworkExtension/Info.plist b/Apple/NetworkExtension/Info.plist index 3059459..e2cf604 100644 --- a/Apple/NetworkExtension/Info.plist +++ b/Apple/NetworkExtension/Info.plist @@ -2,6 +2,8 @@ + CFBundleName + $(INFOPLIST_KEY_CFBundleDisplayName) NSExtension NSExtensionPointIdentifier diff --git a/Apple/NetworkExtension/NetworkExtension.xcconfig b/Apple/NetworkExtension/NetworkExtension.xcconfig index f606079..3b94990 100644 --- a/Apple/NetworkExtension/NetworkExtension.xcconfig +++ b/Apple/NetworkExtension/NetworkExtension.xcconfig @@ -1,8 +1,11 @@ #include "../Configuration/Extension.xcconfig" PRODUCT_NAME = BurrowNetworkExtension -PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).network +PRODUCT_BUNDLE_IDENTIFIER = $(NETWORK_EXTENSION_BUNDLE_IDENTIFIER) + INFOPLIST_FILE = NetworkExtension/Info.plist CODE_SIGN_ENTITLEMENTS = NetworkExtension/NetworkExtension-iOS.entitlements CODE_SIGN_ENTITLEMENTS[sdk=macos*] = NetworkExtension/NetworkExtension-macOS.entitlements + +SWIFT_INCLUDE_PATHS = $(inherited) $(PROJECT_DIR)/NetworkExtension diff --git a/Apple/Configuration/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh similarity index 97% rename from Apple/Configuration/build-rust.sh rename to Apple/NetworkExtension/libburrow/build-rust.sh index 1f62701..f8c943c 100755 --- a/Apple/Configuration/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -9,7 +9,7 @@ fi set -e -cd -- "$(dirname -- "${BASH_SOURCE[0]}")"/../../burrow +cd -- "$(dirname -- "${BASH_SOURCE[0]}")"/../../../burrow RUST_TARGETS=() diff --git a/Apple/NetworkExtension/libburrow/libburrow.h b/Apple/NetworkExtension/libburrow/libburrow.h new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Apple/NetworkExtension/libburrow/libburrow.h @@ -0,0 +1 @@ + diff --git a/Apple/NetworkExtension/libburrow/module.modulemap b/Apple/NetworkExtension/libburrow/module.modulemap new file mode 100644 index 0000000..c53a032 --- /dev/null +++ b/Apple/NetworkExtension/libburrow/module.modulemap @@ -0,0 +1,4 @@ +module libburrow { + header "libburrow.h" + export * +} From b3a540fc487c844681521ec9b272514d668fd639 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Mon, 8 May 2023 18:19:13 -0400 Subject: [PATCH 014/128] Add support for starting and stopping the tunnel This commit introduces the Tunnel view model object which has support for asking for permission, starting and stopping the tunnel. It automatically updates its state and publishes changes as an ObservableObject. --- Apple/App/BurrowApp.swift | 10 +- Apple/App/ContentView.swift | 19 --- Apple/App/NetworkExtension+Async.swift | 41 ++++++ Apple/App/Status.swift | 43 ++++++ Apple/App/Tunnel.swift | 135 ++++++++++++++++++ Apple/App/TunnelView.swift | 37 +++++ Apple/Burrow.xcodeproj/project.pbxproj | 14 +- .../PacketTunnelProvider.swift | 6 +- 8 files changed, 277 insertions(+), 28 deletions(-) delete mode 100644 Apple/App/ContentView.swift create mode 100644 Apple/App/NetworkExtension+Async.swift create mode 100644 Apple/App/Status.swift create mode 100644 Apple/App/Tunnel.swift create mode 100644 Apple/App/TunnelView.swift diff --git a/Apple/App/BurrowApp.swift b/Apple/App/BurrowApp.swift index b145dea..da0bf52 100644 --- a/Apple/App/BurrowApp.swift +++ b/Apple/App/BurrowApp.swift @@ -1,10 +1,18 @@ +import NetworkExtension import SwiftUI @main +@MainActor struct BurrowApp: App { + + static let tunnel = Tunnel { manager, proto in + proto.serverAddress = "hackclub.com" + manager.localizedDescription = "Burrow" + } + var body: some Scene { WindowGroup { - ContentView() + TunnelView(tunnel: Self.tunnel) } } } diff --git a/Apple/App/ContentView.swift b/Apple/App/ContentView.swift deleted file mode 100644 index f54deab..0000000 --- a/Apple/App/ContentView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") - } - .padding() - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/Apple/App/NetworkExtension+Async.swift b/Apple/App/NetworkExtension+Async.swift new file mode 100644 index 0000000..ba478f3 --- /dev/null +++ b/Apple/App/NetworkExtension+Async.swift @@ -0,0 +1,41 @@ +import NetworkExtension + +extension NEVPNManager { + func remove() async throws { + let _: Void = try await withUnsafeThrowingContinuation { continuation in + removeFromPreferences(completionHandler: completion(continuation)) + } + } + + func save() async throws { + let _: Void = try await withUnsafeThrowingContinuation { continuation in + saveToPreferences(completionHandler: completion(continuation)) + } + } +} + +extension NETunnelProviderManager { + class var managers: [NETunnelProviderManager] { + get async throws { + try await withUnsafeThrowingContinuation { continuation in + loadAllFromPreferences { managers, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: managers ?? []) + } + } + } + } + } +} + +private func completion(_ continuation: UnsafeContinuation) -> (Error?) -> Void { + return { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } + } +} diff --git a/Apple/App/Status.swift b/Apple/App/Status.swift new file mode 100644 index 0000000..d2ed65a --- /dev/null +++ b/Apple/App/Status.swift @@ -0,0 +1,43 @@ +import Foundation +import NetworkExtension + +extension Tunnel { + enum Status: CustomStringConvertible, Equatable, Hashable { + case unknown + case permissionRequired + case disabled + case connecting + case connected(Date) + case disconnecting + case disconnected + case reasserting + case invalid + case configurationReadWriteFailed + + var description: String { + switch self { + case .unknown: + return "Unknown" + case .permissionRequired: + return "Permission Required" + case .disconnected: + return "Disconnected" + case .disabled: + return "Disabled" + case .connecting: + return "Connecting" + case .connected(_): + return "Connected" + case .disconnecting: + return "Disconnecting" + case .reasserting: + return "Reasserting" + case .invalid: + return "Invalid" + case .configurationReadWriteFailed: + return "System Error" + } + } + } +} + diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift new file mode 100644 index 0000000..64808d5 --- /dev/null +++ b/Apple/App/Tunnel.swift @@ -0,0 +1,135 @@ +import NetworkExtension +import SwiftUI +import Combine + +@MainActor +class Tunnel: ObservableObject { + @Published private(set) var status: Status = .unknown + @Published private var error: NEVPNError? + + private let bundleIdentifier: String + private let configure: (NETunnelProviderManager, NETunnelProviderProtocol) -> Void + private var tasks: [Task] = [] + + private var managers: [NEVPNManager]? { + didSet { status = currentStatus } + } + + private var currentStatus: Status { + guard let managers = managers else { + guard let error = error else { + return .unknown + } + + switch error.code { + case .configurationReadWriteFailed: + return .configurationReadWriteFailed + default: + return .unknown + } + } + + guard let manager = managers.first else { + return .permissionRequired + } + + guard manager.isEnabled else { + return .disabled + } + + return manager.connection.tunnelStatus + } + + convenience init(configure: @escaping (NETunnelProviderManager, NETunnelProviderProtocol) -> Void) { + self.init("com.hackclub.burrow.network", configure: configure) + } + + init(_ bundleIdentifier: String, configure: @escaping (NETunnelProviderManager, NETunnelProviderProtocol) -> Void) { + self.bundleIdentifier = bundleIdentifier + self.configure = configure + + let statusTask = Task { + for try await _ in NotificationCenter.default.notifications(named: .NEVPNStatusDidChange) { + status = currentStatus + } + } + let configurationTask = Task { + for try await _ in NotificationCenter.default.notifications(named: .NEVPNConfigurationChange) { + await update() + } + } + tasks = [statusTask, configurationTask] + } + + deinit { + tasks.forEach { $0.cancel() } + } + + func update() async { + do { + managers = try await NETunnelProviderManager.managers + } catch let error as NEVPNError { + self.error = error + } catch { + print(error) + } + } + + func configure() async throws { + if managers == nil { + await update() + } + + guard let managers = managers else { return } + + if managers.count > 1 { + try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in + for manager in managers.suffix(from: 1) { + group.addTask { try await manager.remove() } + } + try await group.waitForAll() + } + } + + if managers.isEmpty { + let manager = NETunnelProviderManager() + let proto = NETunnelProviderProtocol() + proto.providerBundleIdentifier = bundleIdentifier + manager.protocolConfiguration = proto + + configure(manager, proto) + try await manager.save() + } + } + + func start() throws { + guard let manager = managers?.first else { return } + try manager.connection.startVPNTunnel() + } + + func stop() { + guard let manager = managers?.first else { return } + manager.connection.stopVPNTunnel() + } +} + +private extension NEVPNConnection { + var tunnelStatus: Tunnel.Status { + switch status { + case .connected: + return .connected(connectedDate!) + case .connecting: + return .connecting + case .disconnecting: + return .disconnecting + case .disconnected: + return .disconnected + case .reasserting: + return .reasserting + case .invalid: + return .invalid + @unknown default: + return .unknown + } + } +} diff --git a/Apple/App/TunnelView.swift b/Apple/App/TunnelView.swift new file mode 100644 index 0000000..58153d4 --- /dev/null +++ b/Apple/App/TunnelView.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct TunnelView: View { + + @ObservedObject + var tunnel: Tunnel + + var body: some View { + VStack { + Text(verbatim: tunnel.status.description) + switch tunnel.status { + case .connected(_): + Button("Disconnect", action: stop) + case .permissionRequired: + Button("Allow", action: configure) + case .disconnected: + Button("Start", action: start) + default: + EmptyView() + } + } + .task { await tunnel.update() } + .padding() + } + + private func start() { + try? tunnel.start() + } + + private func stop() { + tunnel.stop() + } + + private func configure() { + Task { try await tunnel.configure() } + } +} diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index e69eb6c..f65dc39 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -10,8 +10,11 @@ D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; - D05B9F7829E39EEC008CB1F9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* ContentView.swift */; }; + D05B9F7829E39EEC008CB1F9 /* TunnelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */; }; D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D05B9F7929E39EED008CB1F9 /* Assets.xcassets */; }; + D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */; }; + D0BCC5FF2A086E1C00AD070D /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FE2A086E1C00AD070D /* Status.swift */; }; + D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B98FC629FDC5B5004E7149 /* Tunnel.swift */; }; D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BCC6032A09535900AD070D /* libburrow.a */; }; D0BCC60A2A09A0B800AD070D /* build-rust.sh in Resources */ = {isa = PBXBuildFile; fileRef = D0B98FBF29FD8072004E7149 /* build-rust.sh */; }; /* End PBXBuildFile section */ @@ -57,11 +60,13 @@ D020F66929E4AA74002790F6 /* App-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "App-macOS.entitlements"; sourceTree = ""; }; D05B9F7229E39EEC008CB1F9 /* Burrow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Burrow.app; sourceTree = BUILT_PRODUCTS_DIR; }; D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowApp.swift; sourceTree = ""; }; - D05B9F7729E39EEC008CB1F9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelView.swift; sourceTree = ""; }; D05B9F7929E39EED008CB1F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D0B98FBF29FD8072004E7149 /* build-rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-rust.sh"; sourceTree = ""; }; + D0B98FC629FDC5B5004E7149 /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; D0B98FD829FDDB6F004E7149 /* libburrow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libburrow.h; sourceTree = ""; }; D0B98FDC29FDDDCF004E7149 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkExtension+Async.swift"; sourceTree = ""; }; D0BCC5FE2A086E1C00AD070D /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; D0BCC6032A09535900AD070D /* libburrow.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libburrow.a; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -288,8 +293,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D05B9F7829E39EEC008CB1F9 /* ContentView.swift in Sources */, + D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */, + D05B9F7829E39EEC008CB1F9 /* TunnelView.swift in Sources */, + D0BCC5FF2A086E1C00AD070D /* Status.swift in Sources */, D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */, + D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index c8a87cf..1ac4a3b 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -3,27 +3,23 @@ import NetworkExtension class PacketTunnelProvider: NEPacketTunnelProvider { override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { - // Add code here to start the process of connecting the tunnel. + completionHandler(nil) } override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - // Add code here to start the process of stopping the tunnel. completionHandler() } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - // Add code here to handle the message. if let handler = completionHandler { handler(messageData) } } override func sleep(completionHandler: @escaping () -> Void) { - // Add code here to get ready to sleep. completionHandler() } override func wake() { - // Add code here to wake up. } } From c1507ba37fee092f203825e37143c5c8815839aa Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Mon, 8 May 2023 18:19:04 -0400 Subject: [PATCH 015/128] Fix SwiftUI view model updating The reflection data is needed by SwiftUI in order to operate correctly. --- Apple/Configuration/Compiler.xcconfig | 1 - 1 file changed, 1 deletion(-) diff --git a/Apple/Configuration/Compiler.xcconfig b/Apple/Configuration/Compiler.xcconfig index de6261a..967f024 100644 --- a/Apple/Configuration/Compiler.xcconfig +++ b/Apple/Configuration/Compiler.xcconfig @@ -43,7 +43,6 @@ SWIFT_EMIT_LOC_STRINGS = YES // Release DEBUG_INFORMATION_FORMAT = dwarf-with-dsym -SWIFT_REFLECTION_METADATA_LEVEL = none SWIFT_COMPILATION_MODE = wholemodule SWIFT_OPTIMIZATION_LEVEL = -Osize LLVM_LTO = YES From cf95ac819c6de0a5ab509c2be7d84aa17bb94f55 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Mon, 8 May 2023 18:23:38 -0400 Subject: [PATCH 016/128] Share *RawFd trait implementations on Unix They were previously only present on Apple platforms. --- tun/src/unix/apple/mod.rs | 18 +++--------------- tun/src/unix/mod.rs | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 178ec6a..11a7350 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,11 +1,11 @@ use fehler::throws; use libc::c_char; -use std::net::Ipv4Addr; -use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; use std::io::Error; +use std::net::Ipv4Addr; +use std::os::fd::AsRawFd; -mod sys; mod kern_control; +mod sys; pub use super::queue::TunQueue; @@ -62,15 +62,3 @@ impl TunInterface { todo!() } } - -impl AsRawFd for TunInterface { - fn as_raw_fd(&self) -> RawFd { - self.socket.as_raw_fd() - } -} - -impl IntoRawFd for TunInterface { - fn into_raw_fd(self) -> RawFd { - self.socket.into_raw_fd() - } -} diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs index 615e647..6ee3bf4 100644 --- a/tun/src/unix/mod.rs +++ b/tun/src/unix/mod.rs @@ -1,3 +1,5 @@ +use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}; + mod queue; #[cfg(target_vendor = "apple")] @@ -11,6 +13,25 @@ mod imp; pub use imp::TunInterface; pub use queue::TunQueue; +impl AsRawFd for TunInterface { + fn as_raw_fd(&self) -> RawFd { + self.socket.as_raw_fd() + } +} + +impl FromRawFd for TunInterface { + unsafe fn from_raw_fd(fd: RawFd) -> TunInterface { + TunInterface { + socket: socket2::Socket::from_raw_fd(fd), + } + } +} + +impl IntoRawFd for TunInterface { + fn into_raw_fd(self) -> RawFd { + self.socket.into_raw_fd() + } +} pub fn ifname_to_string(buf: [libc::c_char; libc::IFNAMSIZ]) -> String { // TODO: Switch to `CStr::from_bytes_until_nul` when stabilized unsafe { From 941d465570b7924c0095951669c9ad085a28cd59 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Tue, 9 May 2023 21:52:16 -0400 Subject: [PATCH 017/128] Document the Rust Xcode build script The Bash source is a little hard to read. --- Apple/NetworkExtension/libburrow/build-rust.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index f8c943c..1ac73fb 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -1,5 +1,8 @@ #!/bin/bash +# This is a build script. It is run by Xcode as a build step. +# The type of build is described in various environment variables set by Xcode. + export PATH="${PATH}:${HOME}/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:/etc/profiles/per-user/${USER}/bin" if ! [[ -x "$(command -v cargo)" ]]; then @@ -9,10 +12,12 @@ fi set -e +# Change directories relative to the location of this script cd -- "$(dirname -- "${BASH_SOURCE[0]}")"/../../../burrow RUST_TARGETS=() +# Match the PLATFORM_NAME (iphoneos) and ARCHS (arm64) to a set of RUST_TARGETS (aarch64-apple-ios) IFS=' ' read -a BURROW_ARCHS <<< "${ARCHS[@]}" for ARCH in "${BURROW_ARCHS[@]}"; do case $PLATFORM_NAME in @@ -40,6 +45,7 @@ for ARCH in "${BURROW_ARCHS[@]}"; do esac done +# Pass all RUST_TARGETS in a single invocation CARGO_ARGS=() for TARGET in "${RUST_TARGETS[@]}"; do CARGO_ARGS+=("--target") @@ -48,6 +54,7 @@ done CARGO_ARGS+=("--lib") +# Pass the configuration (Debug or Release) through to cargo if [[ $SWIFT_ACTIVE_COMPILATION_CONDITIONS == *DEBUG* ]]; then CARGO_DIR="debug" else @@ -61,9 +68,13 @@ else CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin" fi +# Run cargo without the various environment variables set by Xcode. +# Those variables can confuse cargo and the build scripts it runs. env -i PATH="$CARGO_PATH" cargo build "${CARGO_ARGS[@]}" mkdir -p "${BUILT_PRODUCTS_DIR}" + +# Use `lipo` to merge the architectures together into BUILT_PRODUCTS_DIR /usr/bin/xcrun --sdk $PLATFORM_NAME lipo \ -create $(printf "${PROJECT_DIR}/../target/%q/${CARGO_DIR}/libburrow.a " "${RUST_TARGETS[@]}") \ -output "${BUILT_PRODUCTS_DIR}/libburrow.a" From 3c30a4b33641adec1711d5e0581b194e82ed8bad Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Tue, 9 May 2023 22:06:56 -0400 Subject: [PATCH 018/128] Enable SwiftLint inside of Xcode This commit also fixes all linter warnings and errors. --- Apple/App/BurrowApp.swift | 1 - Apple/App/Status.swift | 3 +- Apple/App/Tunnel.swift | 16 ++-- Apple/App/TunnelView.swift | 6 +- Apple/Burrow.xcodeproj/project.pbxproj | 37 +++++++++ .../xcshareddata/swiftpm/Package.resolved | 77 +++++++++++++++++++ .../PacketTunnelProvider.swift | 11 ++- 7 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Apple/App/BurrowApp.swift b/Apple/App/BurrowApp.swift index da0bf52..50a7231 100644 --- a/Apple/App/BurrowApp.swift +++ b/Apple/App/BurrowApp.swift @@ -4,7 +4,6 @@ import SwiftUI @main @MainActor struct BurrowApp: App { - static let tunnel = Tunnel { manager, proto in proto.serverAddress = "hackclub.com" manager.localizedDescription = "Burrow" diff --git a/Apple/App/Status.swift b/Apple/App/Status.swift index d2ed65a..c08cdd1 100644 --- a/Apple/App/Status.swift +++ b/Apple/App/Status.swift @@ -26,7 +26,7 @@ extension Tunnel { return "Disabled" case .connecting: return "Connecting" - case .connected(_): + case .connected: return "Connected" case .disconnecting: return "Disconnecting" @@ -40,4 +40,3 @@ extension Tunnel { } } } - diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift index 64808d5..e782eff 100644 --- a/Apple/App/Tunnel.swift +++ b/Apple/App/Tunnel.swift @@ -1,6 +1,6 @@ +import Combine import NetworkExtension import SwiftUI -import Combine @MainActor class Tunnel: ObservableObject { @@ -61,10 +61,6 @@ class Tunnel: ObservableObject { tasks = [statusTask, configurationTask] } - deinit { - tasks.forEach { $0.cancel() } - } - func update() async { do { managers = try await NETunnelProviderManager.managers @@ -96,7 +92,7 @@ class Tunnel: ObservableObject { let proto = NETunnelProviderProtocol() proto.providerBundleIdentifier = bundleIdentifier manager.protocolConfiguration = proto - + configure(manager, proto) try await manager.save() } @@ -111,10 +107,14 @@ class Tunnel: ObservableObject { guard let manager = managers?.first else { return } manager.connection.stopVPNTunnel() } + + deinit { + tasks.forEach { $0.cancel() } + } } -private extension NEVPNConnection { - var tunnelStatus: Tunnel.Status { +extension NEVPNConnection { + var tunnelStatus: Tunnel.Status { switch status { case .connected: return .connected(connectedDate!) diff --git a/Apple/App/TunnelView.swift b/Apple/App/TunnelView.swift index 58153d4..6692a04 100644 --- a/Apple/App/TunnelView.swift +++ b/Apple/App/TunnelView.swift @@ -1,15 +1,13 @@ import SwiftUI struct TunnelView: View { - - @ObservedObject - var tunnel: Tunnel + @ObservedObject var tunnel: Tunnel var body: some View { VStack { Text(verbatim: tunnel.status.description) switch tunnel.status { - case .connected(_): + case .connected: Button("Disconnect", action: stop) case .permissionRequired: Button("Allow", action: configure) diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index f65dc39..56b64e4 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -176,6 +176,7 @@ buildRules = ( ); dependencies = ( + D0BCC6122A0B328800AD070D /* PBXTargetDependency */, ); name = NetworkExtension; productName = BurrowNetworkExtension; @@ -194,6 +195,7 @@ buildRules = ( ); dependencies = ( + D0BCC6142A0B329200AD070D /* PBXTargetDependency */, D020F65C29E4A697002790F6 /* PBXTargetDependency */, ); name = App; @@ -228,6 +230,9 @@ Base, ); mainGroup = D05B9F6929E39EEC008CB1F9; + packageReferences = ( + D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */, + ); productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -309,6 +314,14 @@ target = D020F65229E4A697002790F6 /* NetworkExtension */; targetProxy = D020F65B29E4A697002790F6 /* PBXContainerItemProxy */; }; + D0BCC6122A0B328800AD070D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D0BCC6112A0B328800AD070D /* SwiftLintPlugin */; + }; + D0BCC6142A0B329200AD070D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D0BCC6132A0B329200AD070D /* SwiftLintPlugin */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -385,6 +398,30 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/realm/SwiftLint.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.51.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D0BCC6112A0B328800AD070D /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */; + productName = "plugin:SwiftLintPlugin"; + }; + D0BCC6132A0B329200AD070D /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */; + productName = "plugin:SwiftLintPlugin"; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = D05B9F6A29E39EEC008CB1F9 /* Project object */; } diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..233bbf9 --- /dev/null +++ b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,77 @@ +{ + "pins" : [ + { + "identity" : "collectionconcurrencykit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", + "state" : { + "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", + "version" : "0.2.0" + } + }, + { + "identity" : "sourcekitten", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/SourceKitten.git", + "state" : { + "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", + "version" : "0.34.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", + "version" : "1.2.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "013a48e2312e57b7b355db25bd3ea75282ebf274", + "version" : "0.50900.0-swift-DEVELOPMENT-SNAPSHOT-2023-02-06-a" + } + }, + { + "identity" : "swiftlint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/realm/SwiftLint.git", + "state" : { + "revision" : "eb85125a5f293de3d3248af259980c98bc2b1faa", + "version" : "0.51.0" + } + }, + { + "identity" : "swiftytexttable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", + "state" : { + "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", + "version" : "0.9.0" + } + }, + { + "identity" : "swxmlhash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/drmohundro/SWXMLHash.git", + "state" : { + "revision" : "4d0f62f561458cbe1f732171e625f03195151b60", + "version" : "7.0.1" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "f47ba4838c30dbd59998a4e4c87ab620ff959e8a", + "version" : "5.0.5" + } + } + ], + "version" : 2 +} diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 1ac4a3b..ff08261 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -1,25 +1,24 @@ import NetworkExtension class PacketTunnelProvider: NEPacketTunnelProvider { - - override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { + override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { completionHandler(nil) } - + override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { completionHandler() } - + override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { if let handler = completionHandler { handler(messageData) } } - + override func sleep(completionHandler: @escaping () -> Void) { completionHandler() } - + override func wake() { } } From bea8af0d6618f4685639cf0e1da2fc758fcfcac6 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Tue, 9 May 2023 22:36:54 -0400 Subject: [PATCH 019/128] Cache Swift packages in Github Actions This is helpful now that SwiftLint is a package dependency. --- .github/actions/build-for-testing/action.yml | 24 +++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index 964cd08..8ebc86b 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -18,7 +18,27 @@ inputs: runs: using: composite steps: - - shell: bash + - name: Resolve Swift Packages + shell: bash + working-directory: Apple + run: | + xcodebuild -resolvePackageDependencies \ + -scheme '${{ inputs.scheme }}' \ + -destination '${{ inputs.destination }}' \ + -onlyUsePackageVersionsFromResolvedFile \ + -clonedSourcePackagesDirPath SourcePackages \ + -packageCachePath $PWD/PackageCache + - name: Cache Swift Packages + uses: actions/cache@v3 + with: + path: | + Apple/PackageCache + Apple/SourcePackages + key: ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-${{ inputs.scheme }}- + - name: Build + shell: bash working-directory: Apple run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 @@ -30,6 +50,8 @@ runs: -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ -onlyUsePackageVersionsFromResolvedFile \ + -clonedSourcePackagesDirPath SourcePackages \ + -packageCachePath $PWD/PackageCache \ -skipPackagePluginValidation \ -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ From c8bdf1bcbe179503b9f2e0880c7ef022542ec9d5 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Tue, 9 May 2023 23:04:35 -0400 Subject: [PATCH 020/128] Remove unneeded Github Actions step Xcode will both use and create the cache during the build. --- .github/actions/build-for-testing/action.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index 8ebc86b..fb5dd8d 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -18,16 +18,6 @@ inputs: runs: using: composite steps: - - name: Resolve Swift Packages - shell: bash - working-directory: Apple - run: | - xcodebuild -resolvePackageDependencies \ - -scheme '${{ inputs.scheme }}' \ - -destination '${{ inputs.destination }}' \ - -onlyUsePackageVersionsFromResolvedFile \ - -clonedSourcePackagesDirPath SourcePackages \ - -packageCachePath $PWD/PackageCache - name: Cache Swift Packages uses: actions/cache@v3 with: From 101470d17c2a6a4e5f6d9772f5062defed9e9557 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 13 May 2023 12:20:10 -0400 Subject: [PATCH 021/128] Fix requesting VPN permission on macOS The protocol configuration is copied when set, so it should be set after it is configured --- Apple/App/Tunnel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift index e782eff..ae185a4 100644 --- a/Apple/App/Tunnel.swift +++ b/Apple/App/Tunnel.swift @@ -91,9 +91,9 @@ class Tunnel: ObservableObject { let manager = NETunnelProviderManager() let proto = NETunnelProviderProtocol() proto.providerBundleIdentifier = bundleIdentifier - manager.protocolConfiguration = proto - configure(manager, proto) + + manager.protocolConfiguration = proto try await manager.save() } } From 8007e88b537b211a4254fa832a3709a9a818c022 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 13 May 2023 12:24:11 -0400 Subject: [PATCH 022/128] Enable IPv4 configuration on macOS This enables getting and setting the IPv4 address on tun interfaces on macOS --- tun/src/unix/apple/mod.rs | 36 +++++++++++++++---- tun/src/unix/apple/sys.rs | 73 +++++++++++++++++++++++++++++++-------- 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 11a7350..2bf44ee 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,15 +1,16 @@ use fehler::throws; use libc::c_char; -use std::io::Error; -use std::net::Ipv4Addr; -use std::os::fd::AsRawFd; +use socket2::{Domain, SockAddr, Socket, Type}; +use std::net::{Ipv4Addr, SocketAddrV4}; +use std::os::fd::{AsRawFd, RawFd}; +use std::{io::Error, mem}; mod kern_control; mod sys; pub use super::queue::TunQueue; -use super::ifname_to_string; +use super::{ifname_to_string, string_to_ifname}; use kern_control::SysControlSocket; #[derive(Debug)] @@ -53,12 +54,33 @@ impl TunInterface { } #[throws] - pub fn set_ipv4_addr(&self, _addr: Ipv4Addr) { - todo!() + fn ifreq(&self) -> sys::ifreq { + let mut iff: sys::ifreq = unsafe { mem::zeroed() }; + iff.ifr_name = string_to_ifname(&self.name()?); + iff + } + + #[throws] + pub fn set_ipv4_addr(&self, addr: Ipv4Addr) { + let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); + + let mut iff = self.ifreq()?; + iff.ifr_ifru.ifru_addr = unsafe { *addr.as_ptr() }; + + self.perform(|fd| unsafe { sys::if_set_addr(fd, &iff) })?; } #[throws] pub fn ipv4_addr(&self) -> Ipv4Addr { - todo!() + let mut iff = self.ifreq()?; + self.perform(|fd| unsafe { sys::if_get_addr(fd, &mut iff) })?; + let addr = unsafe { *(&iff.ifr_ifru.ifru_addr as *const _ as *const sys::sockaddr_in) }; + Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)) + } + + #[throws] + fn perform(&self, perform: impl FnOnce(RawFd) -> Result) -> R { + let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?; + perform(socket.as_raw_fd())? } } diff --git a/tun/src/unix/apple/sys.rs b/tun/src/unix/apple/sys.rs index ff909ed..949af2c 100644 --- a/tun/src/unix/apple/sys.rs +++ b/tun/src/unix/apple/sys.rs @@ -1,35 +1,80 @@ -pub use libc::{c_void, socklen_t, SYSPROTO_CONTROL, IFNAMSIZ, sockaddr_ctl, AF_SYSTEM, AF_SYS_CONTROL}; -use nix::ioctl_readwrite; +use std::mem; + +use libc::{c_char, c_int, c_short, c_uint, c_ulong, sockaddr}; +pub use libc::{ + c_void, sockaddr_ctl, sockaddr_in, socklen_t, AF_SYSTEM, AF_SYS_CONTROL, IFNAMSIZ, + SYSPROTO_CONTROL, +}; +use nix::{ioctl_read_bad, ioctl_readwrite, ioctl_write_ptr_bad, request_code_write}; pub const UTUN_CONTROL_NAME: &str = "com.apple.net.utun_control"; pub const UTUN_OPT_IFNAME: libc::c_int = 2; pub const MAX_KCTL_NAME: usize = 96; - #[repr(C)] +#[repr(C)] +#[derive(Copy, Clone, Debug)] pub struct ctl_info { pub ctl_id: u32, pub ctl_name: [u8; MAX_KCTL_NAME], } #[repr(C)] -pub struct ifreq {} +#[derive(Copy, Clone, Debug)] +pub struct ifkpi { + pub ifk_module_id: c_uint, + pub ifk_type: c_uint, + pub ifk_ptr: *mut c_void, +} -ioctl_readwrite!(resolve_ctl_info, b'N', 3, ctl_info); +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct ifdevmtu { + pub ifdm_current: c_int, + pub ifdm_min: c_int, + pub ifdm_max: c_int, +} + +#[repr(C)] +pub union ifr_ifru { + pub ifru_addr: sockaddr, + pub ifru_dstaddr: sockaddr, + pub ifru_broadaddr: sockaddr, + pub ifru_flags: c_short, + pub ifru_metric: c_int, + pub ifru_mtu: c_int, + pub ifru_phys: c_int, + pub ifru_media: c_int, + pub ifru_intval: c_int, + pub ifru_data: *mut c_char, + pub ifru_devmtu: ifdevmtu, + pub ifru_kpi: ifkpi, + pub ifru_wake_flags: u32, + pub ifru_route_refcnt: u32, + pub ifru_cap: [c_int; 2], + pub ifru_functional_type: u32, +} + +#[repr(C)] +pub struct ifreq { + pub ifr_name: [c_char; IFNAMSIZ], + pub ifr_ifru: ifr_ifru, +} + +pub const SIOCSIFADDR: c_ulong = request_code_write!(b'i', 12, mem::size_of::()); -/// Copied from https://github.com/rust-lang/socket2/blob/61314a231f73964b3db969ef72c0e9479df320f3/src/sys/unix.rs#L168-L178 -/// getsockopt is not exposed by socket2 #[macro_export] macro_rules! syscall { - ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{ - #[allow(unused_unsafe)] - let res = unsafe { libc::$fn($($arg, )*) }; - if res == -1 { - Err(std::io::Error::last_os_error()) - } else { - Ok(res) + ($call: ident ( $($arg: expr),* $(,)* ) ) => {{ + match unsafe { ::libc::$call($($arg, )*) } { + -1 => Err(::std::io::Error::last_os_error()), + res => Ok(res), } }}; } pub use syscall; + +ioctl_readwrite!(resolve_ctl_info, b'N', 3, ctl_info); +ioctl_read_bad!(if_get_addr, libc::SIOCGIFADDR, ifreq); +ioctl_write_ptr_bad!(if_set_addr, SIOCSIFADDR, ifreq); From 2e91838f461c0cf15a8fe75c48a54ca6e1c3048d Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 13 May 2023 12:24:48 -0400 Subject: [PATCH 023/128] Run burrow as root on Unix platform This is needed because you need to be root in order to create a tun interface on macOS, and you need CAP_NET_ADMIN on Linux. --- .cargo/config.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..956cc38 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(unix)'] +runner = "sudo -E" From 727798a7da7f2544f0936141665287939b0ac677 Mon Sep 17 00:00:00 2001 From: JettChenT Date: Sun, 14 May 2023 16:08:54 +0800 Subject: [PATCH 024/128] TunInterface implementations for MacOS This adds TunInterface Implementations for MacOS. With reference to the XNU kernel source and the linux implementation --- tun/src/unix/apple/mod.rs | 65 +++++++++++++++++++++++++++++++++++++++ tun/src/unix/apple/sys.rs | 14 ++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 2bf44ee..f3ee207 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -83,4 +83,69 @@ impl TunInterface { let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?; perform(socket.as_raw_fd())? } + + #[throws] + pub fn mtu(&self) -> i32 { + let mut iff = self.ifreq()?; + self.perform(|fd| unsafe { sys::if_get_mtu(fd, &mut iff) })?; + let mtu = unsafe { iff.ifr_ifru.ifru_mtu }; + + mtu + } + + #[throws] + pub fn set_mtu(&self, mtu: i32) { + let mut iff = self.ifreq()?; + iff.ifr_ifru.ifru_mtu = mtu; + self.perform(|fd| unsafe { sys::if_set_mtu(fd, &iff) })?; + } + + #[throws] + pub fn netmask(&self) -> Ipv4Addr { + let mut iff = self.ifreq()?; + self.perform(|fd| unsafe { sys::if_get_netmask(fd, &mut iff) })?; + + let netmask = + unsafe { *(&iff.ifr_ifru.ifru_netmask as *const _ as *const sys::sockaddr_in) }; + + Ipv4Addr::from(u32::from_be(netmask.sin_addr.s_addr)) + } + + #[throws] + pub fn set_netmask(&self, addr: Ipv4Addr) { + let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); + + let mut iff = self.ifreq()?; + iff.ifr_ifru.ifru_netmask = unsafe { *addr.as_ptr() }; + + self.perform(|fd| unsafe { sys::if_set_netmask(fd, &iff) })?; + } +} + +mod test { + use super::*; + use std::net::Ipv4Addr; + + #[test] + fn mtu() { + let interf = TunInterface::new().unwrap(); + + interf.set_mtu(500).unwrap(); + + assert_eq!(interf.mtu().unwrap(), 500); + } + + #[test] + #[throws] + fn netmask() { + let interf = TunInterface::new()?; + + let netmask = Ipv4Addr::new(255, 0, 0, 0); + let addr = Ipv4Addr::new(192, 168, 1, 1); + + interf.set_ipv4_addr(addr)?; + interf.set_netmask(netmask)?; + + assert_eq!(interf.netmask()?, netmask); + } } diff --git a/tun/src/unix/apple/sys.rs b/tun/src/unix/apple/sys.rs index 949af2c..c0ea613 100644 --- a/tun/src/unix/apple/sys.rs +++ b/tun/src/unix/apple/sys.rs @@ -5,7 +5,10 @@ pub use libc::{ c_void, sockaddr_ctl, sockaddr_in, socklen_t, AF_SYSTEM, AF_SYS_CONTROL, IFNAMSIZ, SYSPROTO_CONTROL, }; -use nix::{ioctl_read_bad, ioctl_readwrite, ioctl_write_ptr_bad, request_code_write}; +use nix::{ + ioctl_read_bad, ioctl_readwrite, ioctl_write_ptr_bad, request_code_readwrite, + request_code_write, +}; pub const UTUN_CONTROL_NAME: &str = "com.apple.net.utun_control"; pub const UTUN_OPT_IFNAME: libc::c_int = 2; @@ -40,6 +43,7 @@ pub union ifr_ifru { pub ifru_addr: sockaddr, pub ifru_dstaddr: sockaddr, pub ifru_broadaddr: sockaddr, + pub ifru_netmask: sockaddr, pub ifru_flags: c_short, pub ifru_metric: c_int, pub ifru_mtu: c_int, @@ -62,6 +66,10 @@ pub struct ifreq { } pub const SIOCSIFADDR: c_ulong = request_code_write!(b'i', 12, mem::size_of::()); +pub const SIOCGIFMTU: c_ulong = request_code_readwrite!(b'i', 51, mem::size_of::()); +pub const SIOCSIFMTU: c_ulong = request_code_write!(b'i', 52, mem::size_of::()); +pub const SIOCGIFNETMASK: c_ulong = request_code_readwrite!(b'i', 37, mem::size_of::()); +pub const SIOCSIFNETMASK: c_ulong = request_code_write!(b'i', 22, mem::size_of::()); #[macro_export] macro_rules! syscall { @@ -77,4 +85,8 @@ pub use syscall; ioctl_readwrite!(resolve_ctl_info, b'N', 3, ctl_info); ioctl_read_bad!(if_get_addr, libc::SIOCGIFADDR, ifreq); +ioctl_read_bad!(if_get_mtu, SIOCGIFMTU, ifreq); +ioctl_read_bad!(if_get_netmask, SIOCGIFNETMASK, ifreq); ioctl_write_ptr_bad!(if_set_addr, SIOCSIFADDR, ifreq); +ioctl_write_ptr_bad!(if_set_mtu, SIOCSIFMTU, ifreq); +ioctl_write_ptr_bad!(if_set_netmask, SIOCSIFNETMASK, ifreq); From cdea9eba8c640084cd0ce2ba4287858e665fa4fa Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Thu, 25 May 2023 00:05:07 -0400 Subject: [PATCH 025/128] Run apt-get update in CI This fixes a build failure caused by an outdated package cache. --- .github/workflows/build-rust.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index c767eca..66c389c 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -1,7 +1,7 @@ name: Rust Build on: push: - branches: + branches: - main pull_request: branches: @@ -48,7 +48,9 @@ jobs: - name: Install Packages if: matrix.os == 'ubuntu-latest' shell: bash - run: sudo apt-get install -y ${{ join(matrix.packages, ' ') }} + run: | + sudo apt-get update + sudo apt-get install -y ${{ join(matrix.packages, ' ') }} - name: Install Rust uses: dtolnay/rust-toolchain@stable with: From 5baf86d9752df412376eecd0b7a06cf8b0a70fb0 Mon Sep 17 00:00:00 2001 From: JettChenT Date: Fri, 26 May 2023 10:53:14 +0800 Subject: [PATCH 026/128] add cfg test this adds cfg-test for test modules --- tun/src/unix/apple/mod.rs | 1 + tun/src/unix/linux/mod.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index f3ee207..4230c41 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -122,6 +122,7 @@ impl TunInterface { } } +#[cfg(test)] mod test { use super::*; use std::net::Ipv4Addr; diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 42a1754..4f6f882 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -120,6 +120,7 @@ impl TunInterface { } } +#[cfg(test)] mod test { use super::TunInterface; use std::net::Ipv4Addr; From 3c226c81cce79bfe75bcf51f218be8c95f86fb97 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Mon, 29 May 2023 05:24:24 -0400 Subject: [PATCH 027/128] Use fewer dependencies in Windows build script This removes the dependencies on the platform crate as well as the sha256 crate, opting for the sri crate instead to check file integrity. --- Cargo.lock | 723 ++++++++++++++++++++++++++--------------- tun/Cargo.toml | 13 +- tun/build.rs | 33 +- tun/src/windows/mod.rs | 19 +- 4 files changed, 484 insertions(+), 304 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0817761..e0b913e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "autocfg" @@ -34,21 +34,21 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" -version = "0.13.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" [[package]] name = "base64ct" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bindgen" -version = "0.61.0" +version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a022e58a142a46fea340d68012b9201c094e93ec3d033a944a24f8fd4a4f09a" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" dependencies = [ "bitflags", "cexpr", @@ -57,12 +57,13 @@ dependencies = [ "lazycell", "log", "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn", + "syn 2.0.15", "which", ] @@ -74,18 +75,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" [[package]] name = "burrow" @@ -103,15 +104,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "bzip2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ "bzip2-sys", "libc", @@ -130,9 +131,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.74" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" dependencies = [ "jobserver", ] @@ -163,9 +164,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.4.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" dependencies = [ "glob", "libc", @@ -190,15 +191,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -214,9 +215,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if", ] @@ -233,9 +234,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -244,24 +245,45 @@ dependencies = [ [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ "cfg-if", ] [[package]] -name = "fastrand" -version = "1.8.0" +name = "errno" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] @@ -283,14 +305,14 @@ checksum = "ccb5acb1045ebbfa222e2c50679e392a71dd77030b78fb0189f2d9c5974400f9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "miniz_oxide", @@ -328,36 +350,36 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-sink" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-task", @@ -367,9 +389,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -377,15 +399,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.15" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" dependencies = [ "bytes", "fnv", @@ -407,10 +429,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] -name = "hex-literal" -version = "0.3.4" +name = "hermit-abi" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" @@ -423,9 +451,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", @@ -457,9 +485,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.22" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes", "futures-channel", @@ -504,9 +532,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", @@ -522,31 +550,42 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.5.0" +name = "io-lifetimes" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "jobserver" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" dependencies = [ "wasm-bindgen", ] @@ -565,20 +604,26 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libloading" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", "winapi", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" + [[package]] name = "log" version = "0.4.17" @@ -596,18 +641,41 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] -name = "mime" -version = "0.3.16" +name = "miette" +version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "a236ff270093b0b67451bc50a509bd1bad302cb1d3c7d37d5efe931238581fa9" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -617,30 +685,30 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] name = "native-tls" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", @@ -656,42 +724,33 @@ dependencies = [ [[package]] name = "nix" -version = "0.25.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "autocfg", "bitflags", "cfg-if", "libc", "memoffset", "pin-utils", + "static_assertions", ] [[package]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "opaque-debug" @@ -701,9 +760,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.42" +version = "0.10.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" +checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" dependencies = [ "bitflags", "cfg-if", @@ -716,13 +775,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -733,11 +792,10 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.77" +version = "0.9.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a" +checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -793,30 +851,34 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] -name = "platforms" -version = "3.0.1" +name = "prettyplease" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ec293fd25f7fcfeb7c70129241419a62c6200a26a725f680aff07c91d0ed05" +checksum = "1ceca8aaf45b5c46ec7ed39fff75f57290368c1846d33d24a122ca81416ab058" +dependencies = [ + "proc-macro2", + "syn 2.0.15", +] [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] @@ -829,42 +891,33 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "reqwest" -version = "0.11.12" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" dependencies = [ "base64", "bytes", @@ -904,26 +957,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "ryu" -version = "1.0.11" +name = "rustix" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] name = "security-framework" -version = "2.7.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "ca2855b3715770894e67cbfa3df957790aa0c9edc3bf06efa1a84d77fa0839d1" dependencies = [ "bitflags", "core-foundation", @@ -934,9 +1000,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", @@ -944,15 +1010,15 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.147" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -971,6 +1037,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha1" version = "0.10.5" @@ -1001,23 +1078,45 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] +[[package]] +name = "ssri" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5327a6eb28e137e180380169adeae3ac6128438ca1e8a8dc80118f3d1812cbd" +dependencies = [ + "base64", + "digest", + "hex", + "miette", + "sha-1", + "sha2", + "thiserror", + "xxhash-rust", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "subtle" version = "2.4.1" @@ -1026,9 +1125,20 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", @@ -1037,46 +1147,52 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "libc", "redox_syscall", - "remove_dir_all", - "winapi", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", ] [[package]] name = "time" -version = "0.3.16" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ - "itoa", - "libc", - "num_threads", "serde", "time-core", - "time-macros", ] [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" - -[[package]] -name = "time-macros" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" -dependencies = [ - "time-core", -] +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "tinyvec" @@ -1089,43 +1205,42 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.21.2" +version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "pin-project-lite", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "tokio-native-tls" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", @@ -1133,9 +1248,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -1164,18 +1279,18 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tun" @@ -1184,16 +1299,15 @@ dependencies = [ "anyhow", "bindgen", "fehler", - "hex-literal", "libc", "libloading", "nix", - "platforms", "reqwest", - "sha2", "socket2", + "ssri", "tokio", "widestring", + "windows", "zip", ] @@ -1206,21 +1320,21 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-normalization" @@ -1231,6 +1345,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "url" version = "2.3.1" @@ -1272,9 +1392,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1282,24 +1402,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.15", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" dependencies = [ "cfg-if", "js-sys", @@ -1309,9 +1429,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1319,28 +1439,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" dependencies = [ "js-sys", "wasm-bindgen", @@ -1348,9 +1468,9 @@ dependencies = [ [[package]] name = "which" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", "libc", @@ -1386,16 +1506,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.36.1" +name = "windows" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows-targets 0.48.0", ] [[package]] @@ -1404,86 +1520,146 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winreg" @@ -1495,10 +1671,16 @@ dependencies = [ ] [[package]] -name = "zip" -version = "0.6.3" +name = "xxhash-rust" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537ce7411d25e54e8ae21a7ce0b15840e7bfcff15b51d697ec3266cc76bdf080" +checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" + +[[package]] +name = "zip" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e92305c174683d78035cbf1b70e18db6329cc0f1b9cae0a52ca90bf5bfe7125" dependencies = [ "aes", "byteorder", @@ -1535,10 +1717,11 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.1+zstd.1.5.2" +version = "2.0.8+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" dependencies = [ "cc", "libc", + "pkg-config", ] diff --git a/tun/Cargo.toml b/tun/Cargo.toml index f203870..f054eb8 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -6,20 +6,19 @@ edition = "2021" [dependencies] libc = "0.2" fehler = "1.0" -nix = { version = "0.25", features = ["ioctl"] } +nix = { version = "0.26", features = ["ioctl"] } socket2 = "0.4" -tokio = { version = "1.21", features = [] } +tokio = { version = "1.28", features = [] } [target.'cfg(windows)'.dependencies] libloading = "0.7" widestring = "1.0" +windows = { version = "0.48", features = ["Win32_Foundation", "Win32_NetworkManagement_IpHelper"] } [target.'cfg(windows)'.build-dependencies] anyhow = "1.0" -bindgen = "0.61" -hex-literal = "0.3" -platforms = "3.0" +bindgen = "0.65" reqwest = { version = "0.11", features = ["native-tls"] } -sha2 = "0.10" -tokio = { version = "1.21", features = ["rt"] } +ssri = { version = "9.0", default-features = false } +tokio = { version = "1.28", features = ["rt"] } zip = { version = "0.6", features = ["deflate"] } diff --git a/tun/build.rs b/tun/build.rs index 6ad98c3..dd3ea28 100644 --- a/tun/build.rs +++ b/tun/build.rs @@ -7,10 +7,11 @@ async fn main() -> anyhow::Result<()> { .await? .bytes() .await?; - assert_content_hash( - &buf, - hex_literal::hex!("07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51"), - ); + + ssri::IntegrityChecker::new("sha256-B8JWGF1u42UuCfpVwLZz4mJLVl4CxLkJHHnKfS8k71E=".parse()?) + .chain(&buf) + .result()?; + let mut archive = zip::ZipArchive::new(Cursor::new(buf))?; let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); @@ -46,13 +47,14 @@ async fn main() -> anyhow::Result<()> { bindings.write_to_file(out_dir.join("wintun.rs"))?; let mut library = Vec::new(); - let platform = platforms::Platform::find(&std::env::var("TARGET")?).unwrap(); - let arch = match platform.target_arch { - platforms::target::Arch::Arm => "arm", - platforms::Arch::AArch64 => "arm64", - platforms::Arch::X86 => "x86", - platforms::Arch::X86_64 => "amd64", - arch => panic!("{} is not a supported architecture", arch), + let target = std::env::var("TARGET")?; + let arch = match target.split("-").next() { + Some("i686") => "x86", + Some("x86_64") => "amd64", + Some("aarch64") => "arm64", + Some("thumbv7a") => "arm", + Some(a) => panic!("{} is not a supported architecture", a), + None => unreachable!(), }; archive .by_name(&format!("wintun/bin/{}/wintun.dll", arch))? @@ -68,12 +70,3 @@ async fn main() -> anyhow::Result<()> { fn main() { println!("cargo:rerun-if-changed=build.rs"); } - -#[cfg(windows)] -fn assert_content_hash(content: &[u8], hash: [u8; 32]) { - use sha2::digest::Update; - use sha2::Digest; - - let computed = sha2::Sha256::new().chain(content).finalize(); - assert_eq!(computed.as_slice(), &hash[..]); -} diff --git a/tun/src/windows/mod.rs b/tun/src/windows/mod.rs index 6c831d1..92f8092 100644 --- a/tun/src/windows/mod.rs +++ b/tun/src/windows/mod.rs @@ -1,7 +1,8 @@ -use std::io::Result; +use fehler::throws; +use std::io::Error; use std::ptr; use widestring::{u16cstr, U16CString}; - +use windows::Win32::Foundation::GetLastError; mod queue; pub use queue::TunQueue; @@ -13,16 +14,20 @@ pub struct TunInterface { } impl TunInterface { - pub fn new() -> Result { - let name = U16CString::from(u16cstr!("ConradNet")); + #[throws] + pub fn new() -> TunInterface { + let name = U16CString::from(u16cstr!("Burrow")); let wintun = sys::wintun::default(); let handle = unsafe { wintun.WintunCreateAdapter(name.as_ptr(), name.as_ptr(), ptr::null()) }; - Ok(TunInterface { + if handle.is_null() { + unsafe { GetLastError() }.ok()? + } + TunInterface { wintun, handle, - name: String::from("ConradNet"), - }) + name: String::from("Burrow"), + } } pub fn name(&self) -> String { From bf36a822a0d09a2226e598995105c1e7f0a7e9e3 Mon Sep 17 00:00:00 2001 From: sporeball Date: Sat, 20 May 2023 08:46:15 -0700 Subject: [PATCH 028/128] Create initial GETTING_STARTED.md This is the very first draft --- docs/GETTING_STARTED.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/GETTING_STARTED.md diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 0000000..108d6bf --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,22 @@ +# Getting Started + +## Linux +- Clone the repository. +- Install Rust using [rustup](https://rustup.rs). +- Open the project folder in [Visual Studio Code](https://code.visualstudio.com). + +At this point, there are two options to open a new tunnel. + +### Method 1: Run & Debug +- Install the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) VS Code extension. +- In VS Code, press F5 to run & debug Burrow.\ +This flow does not work yet (panics). + +### Method 2: Cargo +- In VS Code, press Ctrl+\` to open the terminal. +- Type `cargo run`. +- Burrow should open a new tunnel (`Ok("tun0")`), then exit. + +## macOS + +## Windows From e51f9eb4fd71063b701b09f42d50e4c1ce1d8599 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Fri, 26 May 2023 22:06:27 -0400 Subject: [PATCH 029/128] Update Getting Started document This adds Windows and macOS instructions among other improvements. --- docs/GETTING_STARTED.md | 118 +++++++++++++++++++++++++++++++++++----- docs/extensions.png | Bin 0 -> 46237 bytes docs/xcode.png | Bin 0 -> 208000 bytes 3 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 docs/extensions.png create mode 100644 docs/xcode.png diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 108d6bf..e43680d 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -1,22 +1,110 @@ # Getting Started -## Linux -- Clone the repository. -- Install Rust using [rustup](https://rustup.rs). -- Open the project folder in [Visual Studio Code](https://code.visualstudio.com). -At this point, there are two options to open a new tunnel. +## Dependencies -### Method 1: Run & Debug -- Install the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) VS Code extension. -- In VS Code, press F5 to run & debug Burrow.\ -This flow does not work yet (panics). +Before you can start working on Burrow, you'll need to install some dependencies. They are different for each platform: -### Method 2: Cargo -- In VS Code, press Ctrl+\` to open the terminal. -- Type `cargo run`. -- Burrow should open a new tunnel (`Ok("tun0")`), then exit. +
+ Linux -## macOS + 1. Install **rustup** using the instructions on the [website](https://rustup.rs/): + ```bash + $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` -## Windows + 2. Install **Visual Studio Code** from the [website](https://code.visualstudio.com/#alt-downloads), [Snap Store](https://snapcraft.io/code), or your package manager of choice. +
+ +
+ macOS + + 1. Install **rustup** using the instructions on the [website](https://rustup.rs/): + ```bash + $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + + 2. Download and install **Visual Studio Code** from the [website](https://code.visualstudio.com/#alt-downloads), or by using brew: + ``` + brew install --cask visual-studio-code + ``` + + 3. Download and Install **Xcode** from the [App Store](https://apps.apple.com/us/app/xcode/id497799835) or the [Apple Developer](https://developer.apple.com/downloads) website. + +
+ + +
+ Windows + + 1. Download **Visual Studio** community edition from the [website](https://visualstudio.microsoft.com/vs/). Install the components for "Desktop Development with C++" + + 2. Install [**Visual Studio Code**](https://apps.microsoft.com/store/detail/visual-studio-code/XP9KHM4BK9FZ7Q), [**PowerShell**](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D) and [**Windows Terminal**](https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701) from the Microsoft Store + + 3. Open Windows Terminal and use [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) to install **git**, **LLVM** and **rustup**: + ```posh + winget install Git.Git + winget install LLVM.LLVM + winget install Rustlang.Rustup + ``` + + 4. Install Rust using rustup: + ```posh + rustup toolchain install stable-msvc + ``` +
+ +## Building + +1. Clone the repository: +``` +git clone git@github.com:hackclub/burrow.git +``` + +2. Open the `burrow` folder in Visual Studio Code: +``` +code burrow +``` + +3. Install the Visual Studio Code extensions [**rust-analyzer**](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) and [**CodeLLDB**](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb). You should get a suggestion to do this automatically: + + + +4. Compile burrow in Visual Studio Code by selecting `Terminal -> Run Build Task` + +## Running + + +
+ Command Line + + You can run burrow on the command line with cargo: + + ``` + cargo run + ``` + + Cargo will ask for your password because burrow needs permission in order to create a tunnel. +
+ +
+ Visual Studio Code + + You can debug the Rust program inside of Visual Studio using the **Run and Debug** tab. + + **_This does not work fully yet_**. Visual Studio Code does not have a way to debug programs with administrative privileges. +
+ +
+ iOS or macOS + + You can run the Burrow app on iOS or macOS using **Xcode**. + + You will need to be logged in with your Apple ID, and it should be a part of **The Hack Foundation** team: + + + + If your Apple ID is not a part of The Hack Foundation team, ask the Slack channel for assistance. + + You should now be able to run the app by opening `Apple/Burrow.xcodeproj` in Xcode, selecting the **App** scheme and clicking **Run**. +
diff --git a/docs/extensions.png b/docs/extensions.png new file mode 100644 index 0000000000000000000000000000000000000000..ad94ee463f1d60e4d478a41940060036f10ae95a GIT binary patch literal 46237 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 z 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 literal 0 HcmV?d00001 From b94356dfb799961a6239fc0a6322a1bc2af91958 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Fri, 26 May 2023 22:04:34 -0400 Subject: [PATCH 030/128] Set clippy as the default build task Also configure CodeLLDB as a recommended extension. --- .vscode/extensions.json | 3 ++- .vscode/settings.json | 12 ++++++++++-- .vscode/tasks.json | 12 +++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c5c0dd4..e40eae1 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ "rust-lang.rust-analyzer", + "vadimcn.vscode-lldb", ] -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 55c07df..649e121 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,10 +6,18 @@ "files.trimTrailingWhitespace": true, "editor.suggest.preview": true, "editor.acceptSuggestionOnEnter": "on", - "rust-analyzer.checkOnSave.command": "clippy", "rust-analyzer.restartServerOnConfigChange": true, "rust-analyzer.cargo.features": "all", + "rust-analyzer.check.overrideCommand": [ + "cargo", + "clippy", + "--fix", + "--workspace", + "--message-format=json", + "--all-targets", + "--allow-dirty" + ], "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer", }, -} +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index eb2a68a..3eb24be 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,7 +3,7 @@ "tasks": [ { "type": "cargo", - "command": "check", + "command": "clippy", "problemMatcher": [ "$rustc" ], @@ -11,7 +11,13 @@ "kind": "build", "isDefault": true }, - "label": "Check" - } + "label": "rust: cargo clippy", + "args": [ + "--workspace", + "--all-targets", + "--all-features", + "--all" + ] + }, ] } \ No newline at end of file From cc30fcd34c91b572a14cce7613f399e7968b6533 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 20 May 2023 05:07:14 -1000 Subject: [PATCH 031/128] =?UTF-8?q?=E2=9C=A8=20Create=20set=5Fipv6=5Faddr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new method for setting an ipv6 address on an interface --- tun/src/unix/linux/mod.rs | 26 +++++++++++++++++++++++--- tun/src/unix/linux/sys.rs | 2 ++ tun/tests/configure.rs | 18 +++++++++++++++--- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 4f6f882..20eabaa 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -4,10 +4,12 @@ use socket2::{Domain, SockAddr, Socket, Type}; use std::fs::OpenOptions; use std::io::Error; use std::mem; -use std::net::{Ipv4Addr, SocketAddrV4}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; use std::os::fd::RawFd; use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; +use libc::in6_ifreq; + use super::{ifname_to_string, string_to_ifname}; mod sys; @@ -51,6 +53,13 @@ impl TunInterface { iff } + #[throws] + fn in6_ifreq(&self) -> in6_ifreq { + let mut iff: in6_ifreq = unsafe { mem::zeroed() }; + iff.ifr6_ifindex = self.index()?; + iff + } + #[throws] pub fn index(&self) -> i32 { let mut iff = self.ifreq()?; @@ -61,10 +70,8 @@ impl TunInterface { #[throws] pub fn set_ipv4_addr(&self, addr: Ipv4Addr) { let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); - let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_addr = unsafe { *addr.as_ptr() }; - self.perform(|fd| unsafe { sys::if_set_addr(fd, &iff) })?; } @@ -93,6 +100,13 @@ impl TunInterface { Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)) } + #[throws] + pub fn set_ipv6_addr(&self, addr: Ipv6Addr) { + let mut iff = self.in6_ifreq()?; + iff.ifr6_addr.s6_addr = addr.octets(); + self.perform6(|fd| unsafe { sys::if_set_addr6(fd, &iff) })?; + } + #[throws] pub fn mtu(&self) -> i32 { let mut iff = self.ifreq()?; @@ -118,6 +132,12 @@ impl TunInterface { let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?; perform(socket.as_raw_fd())? } + + #[throws] + fn perform6(&self, perform: impl FnOnce(RawFd) -> Result) -> R { + let socket = Socket::new(Domain::IPV6, Type::DGRAM, None)?; + perform(socket.as_raw_fd())? + } } #[cfg(test)] diff --git a/tun/src/unix/linux/sys.rs b/tun/src/unix/linux/sys.rs index 8bac12d..61dd50e 100644 --- a/tun/src/unix/linux/sys.rs +++ b/tun/src/unix/linux/sys.rs @@ -4,6 +4,7 @@ use std::mem::size_of; pub use libc::ifreq; pub use libc::sockaddr; pub use libc::sockaddr_in; +pub use libc::sockaddr_in6; ioctl_write_ptr_bad!( tun_set_iff, @@ -21,5 +22,6 @@ ioctl_read_bad!(if_get_mtu, libc::SIOCGIFMTU, libc::ifreq); ioctl_read_bad!(if_get_netmask, libc::SIOCGIFNETMASK, libc::ifreq); ioctl_write_ptr_bad!(if_set_addr, libc::SIOCSIFADDR, libc::ifreq); +ioctl_write_ptr_bad!(if_set_addr6, libc::SIOCSIFADDR, libc::in6_ifreq); ioctl_write_ptr_bad!(if_set_mtu, libc::SIOCSIFMTU, libc::ifreq); ioctl_write_ptr_bad!(if_set_netmask, libc::SIOCSIFNETMASK, libc::ifreq); diff --git a/tun/tests/configure.rs b/tun/tests/configure.rs index 29674b6..05e5ef6 100644 --- a/tun/tests/configure.rs +++ b/tun/tests/configure.rs @@ -1,7 +1,7 @@ use fehler::throws; -use tun::TunInterface; use std::io::Error; -use std::net::Ipv4Addr; +use std::net::{Ipv4Addr, Ipv6Addr}; +use tun::TunInterface; #[test] #[throws] @@ -19,4 +19,16 @@ fn test_set_get_ipv4() { let result = tun.ipv4_addr()?; assert_eq!(addr, result); -} \ No newline at end of file +} + +#[test] +#[throws] +fn test_set_get_ipv6() { + let tun = TunInterface::new()?; + + let addr = Ipv6Addr::new(1, 1, 1, 1, 1, 1, 1, 1); + tun.set_ipv6_addr(addr)?; + + // let result = tun.ipv6_addr()?; + // assert_eq!(addr, result); +} From a502e2132cb947a0c7dc2828a9dc2c613eb967ec Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Mon, 5 Jun 2023 03:51:41 -0400 Subject: [PATCH 032/128] Only run build script when necessary The build script should only run if one of the generated files is changed. --- tun/build.rs | 147 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 110 insertions(+), 37 deletions(-) diff --git a/tun/build.rs b/tun/build.rs index dd3ea28..5569cc4 100644 --- a/tun/build.rs +++ b/tun/build.rs @@ -1,41 +1,97 @@ +#[cfg(not(windows))] +fn main() { + println!("cargo:rerun-if-changed=build.rs"); +} + #[cfg(windows)] #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { - use std::io::{Cursor, Read}; + let out_dir = std::env::var("OUT_DIR")?; + let path = std::path::PathBuf::from(out_dir); + generate(&path).await?; - let buf = reqwest::get("https://www.wintun.net/builds/wintun-0.14.1.zip") - .await? - .bytes() - .await?; + println!("cargo:rerun-if-changed=build.rs"); + Ok(()) +} - ssri::IntegrityChecker::new("sha256-B8JWGF1u42UuCfpVwLZz4mJLVl4CxLkJHHnKfS8k71E=".parse()?) - .chain(&buf) +#[cfg(windows)] +async fn generate(out_dir: &std::path::Path) -> anyhow::Result<()> { + use std::{fs::File, io::Write}; + + use anyhow::Context; + + let bindings_path = out_dir.join("wintun.rs"); + let binary_path = out_dir.join("wintun.dll"); + println!("cargo:rerun-if-changed={}", bindings_path.to_str().unwrap()); + println!("cargo:rerun-if-changed={}", binary_path.to_str().unwrap()); + + if let (Ok(..), Ok(..)) = (File::open(&bindings_path), File::open(&binary_path)) { + return Ok(()); + }; + + let archive = download(out_dir) + .await + .context("Failed to download wintun")?; + + let (bindings, binary) = parse(archive).context("Failed to parse wintun archive")?; + + bindings + .write_to_file(bindings_path) + .context("Failed to write bindings")?; + + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(binary_path) + .context("Failed to write binary")?; + file.write_all(&binary)?; + + Ok(()) +} + +#[cfg(windows)] +async fn download(directory: &std::path::Path) -> anyhow::Result { + use std::{io::Write, str::FromStr}; + + let path = directory.join(WINTUN_FILENAME); + let mut file = match std::fs::OpenOptions::new().read(true).open(&path) { + Ok(existing) => return Ok(existing), + Err(_e) => std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(path)?, + }; + + let mut url = reqwest::Url::from_str("https://www.wintun.net/builds")?; + url.path_segments_mut().unwrap().push(WINTUN_FILENAME); + + let body = reqwest::get(url).await?.bytes().await?; + + ssri::IntegrityChecker::new(WINTUN_INTEGRITY.parse()?) + .chain(&body) .result()?; - let mut archive = zip::ZipArchive::new(Cursor::new(buf))?; + file.set_len(0)?; + file.write_all(&body)?; - let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); + Ok(file) +} + +#[cfg(windows)] +fn parse(file: std::fs::File) -> anyhow::Result<(bindgen::Bindings, Vec)> { + use anyhow::Context; + use std::io::Read; + + let reader = std::io::BufReader::new(file); + let mut archive = zip::ZipArchive::new(reader)?; let mut header = String::new(); archive .by_name("wintun/include/wintun.h")? .read_to_string(&mut header)?; - header.push_str( - "WINTUN_CLOSE_ADAPTER_FUNC WintunCloseAdapter; - WINTUN_OPEN_ADAPTER_FUNC WintunOpenAdapter; - WINTUN_GET_ADAPTER_LUID_FUNC WintunGetAdapterLUID; - WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC WintunGetRunningDriverVersion; - WINTUN_DELETE_DRIVER_FUNC WintunDeleteDriver; - WINTUN_SET_LOGGER_FUNC WintunSetLogger; - WINTUN_START_SESSION_FUNC WintunStartSession; - WINTUN_END_SESSION_FUNC WintunEndSession; - WINTUN_CREATE_ADAPTER_FUNC WintunCreateAdapter; - WINTUN_GET_READ_WAIT_EVENT_FUNC WintunGetReadWaitEvent; - WINTUN_RECEIVE_PACKET_FUNC WintunReceivePacket; - WINTUN_RELEASE_RECEIVE_PACKET_FUNC WintunReleaseReceivePacket; - WINTUN_ALLOCATE_SEND_PACKET_FUNC WintunAllocateSendPacket; - WINTUN_SEND_PACKET_FUNC WintunSendPacket;", - ); + header.push_str(WINTUN_BINDINGS_PREAMBLE); + let bindings = bindgen::Builder::default() .header_contents("wintun.h", &header) .allowlist_function("Wintun.*") @@ -43,12 +99,12 @@ async fn main() -> anyhow::Result<()> { .dynamic_library_name("wintun") .dynamic_link_require_all(true) .generate() + .context("Failed to generate bindings from wintun archive") .unwrap(); - bindings.write_to_file(out_dir.join("wintun.rs"))?; - let mut library = Vec::new(); + let mut binary = Vec::new(); let target = std::env::var("TARGET")?; - let arch = match target.split("-").next() { + let arch = match target.split('-').next() { Some("i686") => "x86", Some("x86_64") => "amd64", Some("aarch64") => "arm64", @@ -58,15 +114,32 @@ async fn main() -> anyhow::Result<()> { }; archive .by_name(&format!("wintun/bin/{}/wintun.dll", arch))? - .read_to_end(&mut library)?; - std::fs::write(out_dir.join("wintun.dll"), library)?; + .read_to_end(&mut binary) + .context("Failed to read binary from wintun archive")?; - println!("cargo:rerun-if-changed=build.rs"); - - Ok(()) + Ok((bindings, binary)) } -#[cfg(not(windows))] -fn main() { - println!("cargo:rerun-if-changed=build.rs"); -} +#[cfg(windows)] +const WINTUN_FILENAME: &str = "wintun-0.14.1.zip"; + +#[cfg(windows)] +const WINTUN_INTEGRITY: &str = "sha256-B8JWGF1u42UuCfpVwLZz4mJLVl4CxLkJHHnKfS8k71E="; + +#[cfg(windows)] +const WINTUN_BINDINGS_PREAMBLE: &str = r#" +WINTUN_CLOSE_ADAPTER_FUNC WintunCloseAdapter; +WINTUN_OPEN_ADAPTER_FUNC WintunOpenAdapter; +WINTUN_GET_ADAPTER_LUID_FUNC WintunGetAdapterLUID; +WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC WintunGetRunningDriverVersion; +WINTUN_DELETE_DRIVER_FUNC WintunDeleteDriver; +WINTUN_SET_LOGGER_FUNC WintunSetLogger; +WINTUN_START_SESSION_FUNC WintunStartSession; +WINTUN_END_SESSION_FUNC WintunEndSession; +WINTUN_CREATE_ADAPTER_FUNC WintunCreateAdapter; +WINTUN_GET_READ_WAIT_EVENT_FUNC WintunGetReadWaitEvent; +WINTUN_RECEIVE_PACKET_FUNC WintunReceivePacket; +WINTUN_RELEASE_RECEIVE_PACKET_FUNC WintunReleaseReceivePacket; +WINTUN_ALLOCATE_SEND_PACKET_FUNC WintunAllocateSendPacket; +WINTUN_SEND_PACKET_FUNC WintunSendPacket; +"#; From 9dc10544b9ff62d8c014a601d76a0100bbecaca8 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Mon, 5 Jun 2023 03:53:51 -0400 Subject: [PATCH 033/128] Embed wintun inside of the Windows binary Burrow writes the driver to a temporary file and then loads it. --- Cargo.lock | 2 ++ tun/Cargo.toml | 2 ++ tun/src/windows/mod.rs | 25 +++++++++++++++---------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0b913e..c779b81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1299,12 +1299,14 @@ dependencies = [ "anyhow", "bindgen", "fehler", + "lazy_static", "libc", "libloading", "nix", "reqwest", "socket2", "ssri", + "tempfile", "tokio", "widestring", "windows", diff --git a/tun/Cargo.toml b/tun/Cargo.toml index f054eb8..e0a5e45 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -11,7 +11,9 @@ socket2 = "0.4" tokio = { version = "1.28", features = [] } [target.'cfg(windows)'.dependencies] +lazy_static = "1.4" libloading = "0.7" +tempfile = "3.5" widestring = "1.0" windows = { version = "0.48", features = ["Win32_Foundation", "Win32_NetworkManagement_IpHelper"] } diff --git a/tun/src/windows/mod.rs b/tun/src/windows/mod.rs index 92f8092..6c0a19c 100644 --- a/tun/src/windows/mod.rs +++ b/tun/src/windows/mod.rs @@ -8,7 +8,6 @@ mod queue; pub use queue::TunQueue; pub struct TunInterface { - wintun: sys::wintun, handle: sys::WINTUN_ADAPTER_HANDLE, name: String, } @@ -17,14 +16,12 @@ impl TunInterface { #[throws] pub fn new() -> TunInterface { let name = U16CString::from(u16cstr!("Burrow")); - let wintun = sys::wintun::default(); let handle = - unsafe { wintun.WintunCreateAdapter(name.as_ptr(), name.as_ptr(), ptr::null()) }; + unsafe { sys::WINTUN.WintunCreateAdapter(name.as_ptr(), name.as_ptr(), ptr::null()) }; if handle.is_null() { unsafe { GetLastError() }.ok()? } TunInterface { - wintun, handle, name: String::from("Burrow"), } @@ -37,17 +34,25 @@ impl TunInterface { impl Drop for TunInterface { fn drop(&mut self) { - unsafe { self.wintun.WintunCloseAdapter(self.handle) } + unsafe { sys::WINTUN.WintunCloseAdapter(self.handle) } } } pub(crate) mod sys { - #![allow(dead_code, non_camel_case_types, non_snake_case)] + #![allow(clippy::all, dead_code, non_camel_case_types, non_snake_case)] include!(concat!(env!("OUT_DIR"), "/wintun.rs")); - impl Default for wintun { - fn default() -> Self { - unsafe { wintun::new(format!("{}/wintun.dll", env!("OUT_DIR"))).unwrap() } - } + const WINTUN_BINARY: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/wintun.dll")); + + lazy_static::lazy_static! { + pub static ref WINTUN: wintun = { + use std::io::Write; + + let mut temp_file = tempfile::NamedTempFile::new().unwrap(); + temp_file.write_all(&WINTUN_BINARY).unwrap(); + let (_, path) = temp_file.keep().unwrap(); + + unsafe { wintun::new(&path) }.unwrap() + }; } } From 9aa1951575f1a7fe0732d555fe88140f53becc6b Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Mon, 5 Jun 2023 01:06:37 -0700 Subject: [PATCH 034/128] Update CODEOWNERS It should include Cara and Ben now --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 20c920e..ebfc124 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @conradev +* @conradev @ma1ted @Muirrum \ No newline at end of file From 82c4d218d7b505a97cf0e8357c0f208021fabc41 Mon Sep 17 00:00:00 2001 From: JettChenT Date: Mon, 29 May 2023 10:36:10 +0800 Subject: [PATCH 035/128] Add read and write functions for TunInterface This adds read and write functionality for TunInterface. --- Cargo.lock | 1 + tun/Cargo.toml | 1 + tun/src/unix/apple/mod.rs | 39 +++++++++++++++++++++++++-- tun/src/unix/linux/mod.rs | 13 ++++++++- tun/src/unix/mod.rs | 55 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c779b81..4edb2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1298,6 +1298,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bindgen", + "byteorder", "fehler", "lazy_static", "libc", diff --git a/tun/Cargo.toml b/tun/Cargo.toml index e0a5e45..33446d1 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -9,6 +9,7 @@ fehler = "1.0" nix = { version = "0.26", features = ["ioctl"] } socket2 = "0.4" tokio = { version = "1.28", features = [] } +byteorder = "1.4" [target.'cfg(windows)'.dependencies] lazy_static = "1.4" diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 4230c41..a2a5674 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,9 +1,14 @@ +use byteorder::{ByteOrder, NetworkEndian}; use fehler::throws; -use libc::c_char; +use libc::{c_char, iovec, writev, AF_INET, AF_INET6}; use socket2::{Domain, SockAddr, Socket, Type}; +use std::io::{IoSlice, Write}; use std::net::{Ipv4Addr, SocketAddrV4}; use std::os::fd::{AsRawFd, RawFd}; -use std::{io::Error, mem}; +use std::{ + io::{Error, Read}, + mem, +}; mod kern_control; mod sys; @@ -122,6 +127,36 @@ impl TunInterface { } } +impl Write for TunInterface { + #[throws] + fn write(&mut self, buf: &[u8]) -> usize { + use std::io::ErrorKind; + let proto = match buf[0] >> 4 { + 6 => Ok(AF_INET6), + 4 => Ok(AF_INET), + _ => Err(Error::new(ErrorKind::InvalidInput, "Invalid IP version")), + }?; + let mut pbuf = [0; 4]; + NetworkEndian::write_i32(&mut pbuf, proto); + + let bufs = [IoSlice::new(&pbuf), IoSlice::new(buf)]; + let bytes_written: isize = unsafe { + writev( + self.as_raw_fd(), + bufs.as_ptr() as *const iovec, + bufs.len() as i32, + ) + }; + bytes_written + .try_into() + .map_err(|_| Error::new(ErrorKind::Other, "Conversion error"))? + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 20eabaa..8467731 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -2,7 +2,7 @@ use fehler::throws; use socket2::{Domain, SockAddr, Socket, Type}; use std::fs::OpenOptions; -use std::io::Error; +use std::io::{Error, Write}; use std::mem; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; use std::os::fd::RawFd; @@ -140,6 +140,17 @@ impl TunInterface { } } +impl Write for TunInterface { + #[throws] + fn write(&mut self, buf: &[u8]) -> usize { + self.socket.send(buf)? + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + #[cfg(test)] mod test { use super::TunInterface; diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs index 6ee3bf4..28b4ec8 100644 --- a/tun/src/unix/mod.rs +++ b/tun/src/unix/mod.rs @@ -1,4 +1,7 @@ -use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}; +use std::{ + io::{Error, Read}, + os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}, +}; mod queue; @@ -10,6 +13,7 @@ mod imp; #[path = "linux/mod.rs"] mod imp; +use fehler::throws; pub use imp::TunInterface; pub use queue::TunQueue; @@ -32,6 +36,19 @@ impl IntoRawFd for TunInterface { self.socket.into_raw_fd() } } + +impl TunInterface { + // #[throws] + // pub fn write(&self, buf: &[u8]) -> usize { + // self.socket.send(buf)? + // } + + #[throws] + pub fn read(&mut self, buf: &mut [u8]) -> usize { + self.socket.read(buf)? + } +} + pub fn ifname_to_string(buf: [libc::c_char; libc::IFNAMSIZ]) -> String { // TODO: Switch to `CStr::from_bytes_until_nul` when stabilized unsafe { @@ -48,3 +65,39 @@ pub fn string_to_ifname(name: &str) -> [libc::c_char; libc::IFNAMSIZ] { buf[..len].copy_from_slice(unsafe { &*(name.as_bytes() as *const _ as *const [libc::c_char]) }); buf } + +#[cfg(test)] +mod test { + + use super::*; + use std::io::Write; + use std::io::{self, BufRead}; + use std::net::Ipv4Addr; + + #[throws] + #[test] + fn tst_read() { + // This test is interactive, you need to send a packet to any server through 192.168.1.10 + // EG. `sudo route add 8.8.8.8 192.168.1.10`, + //`dig @8.8.8.8 hackclub.com` + let mut tun = TunInterface::new()?; + println!("tun name: {:?}", tun.name()?); + tun.set_ipv4_addr(Ipv4Addr::from([192, 168, 1, 10]))?; + println!("tun ip: {:?}", tun.ipv4_addr()?); + println!("Waiting for a packet..."); + let buf = &mut [0u8; 1500]; + let res = tun.read(buf); + println!("Received!"); + assert!(res.is_ok()); + } + + #[test] + #[throws] + fn write_packets() { + let mut tun = TunInterface::new()?; + let mut buf = [0u8; 1500]; + buf[0] = 6 << 4; + let bytes_written = tun.write(&buf)?; + assert_eq!(bytes_written, 1504); + } +} From d65b9a4c687c415b71888469eb7d30e428fe3029 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Tue, 6 Jun 2023 15:05:42 +0000 Subject: [PATCH 036/128] Begin CLI Work Uses the Parser module + it sets up a help & version. --- Cargo.lock | 108 ++++++++++++++++++++++++++++++++++++++ burrow/Cargo.toml | 1 + burrow/src/main.rs | 10 ++++ tun/src/unix/linux/mod.rs | 2 +- 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4edb2ec..53cbe29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,55 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" version = "1.0.71" @@ -92,6 +141,7 @@ checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" name = "burrow" version = "0.1.0" dependencies = [ + "clap", "tokio", "tun", ] @@ -173,6 +223,40 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -566,6 +650,18 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1117,6 +1213,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" @@ -1365,6 +1467,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index a984553..c47bc98 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -9,3 +9,4 @@ crate-type = ["lib", "staticlib"] [dependencies] tokio = { version = "1.21", features = ["rt", "macros"] } tun = { version = "0.1", path = "../tun" } +clap = "4.3.2" diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 8dae842..091f9d7 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -1,6 +1,15 @@ +use clap::Parser; use tokio::io::Result; use tun::TunInterface; +#[derive(Parser)] +#[command(name = "Burrow")] +#[command(author = "Hack Club ")] +#[command(version = "0.1")] +#[command(about = "Burrow is a tool for burrowing through firewalls, built by teenagers at Hack Club.", long_about = None)] + +struct Cli {} + async fn try_main() -> Result<()> { let iface = TunInterface::new()?; println!("{:?}", iface.name()); @@ -10,5 +19,6 @@ async fn try_main() -> Result<()> { #[tokio::main(flavor = "current_thread")] async fn main() { + let cli = Cli::parse(); try_main().await.unwrap(); } diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 8467731..07cf80b 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -4,7 +4,7 @@ use socket2::{Domain, SockAddr, Socket, Type}; use std::fs::OpenOptions; use std::io::{Error, Write}; use std::mem; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}; use std::os::fd::RawFd; use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; From 008ea9ec655bac0a70d7a5af46e5b235ae569db8 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Tue, 6 Jun 2023 15:15:13 +0000 Subject: [PATCH 037/128] Enable derive on Clap This let's us use the derive feature. --- Cargo.lock | 20 ++++++++++++++++++++ burrow/Cargo.toml | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 53cbe29..8677d43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,6 +230,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" dependencies = [ "clap_builder", + "clap_derive", + "once_cell", ] [[package]] @@ -245,6 +247,18 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "clap_lex" version = "0.5.0" @@ -512,6 +526,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.1" diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index c47bc98..8880c8b 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -9,4 +9,4 @@ crate-type = ["lib", "staticlib"] [dependencies] tokio = { version = "1.21", features = ["rt", "macros"] } tun = { version = "0.1", path = "../tun" } -clap = "4.3.2" +clap = { version = "4.3.2", features = ["derive"] } From e8704a2560cf23feabe6bd67100ea82c9613668f Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Tue, 6 Jun 2023 15:50:22 +0000 Subject: [PATCH 038/128] Add Start Command This uses the subcommands feature, it was requested by Conrad. --- burrow/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 091f9d7..4e025bc 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -19,6 +19,6 @@ async fn try_main() -> Result<()> { #[tokio::main(flavor = "current_thread")] async fn main() { - let cli = Cli::parse(); + let _cli = Cli::parse(); try_main().await.unwrap(); } From a73228cee0ca87049957d0853f38677739d5db05 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Tue, 6 Jun 2023 15:50:22 +0000 Subject: [PATCH 039/128] Add Start Command Command that will start burrow. --- burrow/src/main.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 4e025bc..7ef7530 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -1,4 +1,4 @@ -use clap::Parser; +use clap::{Args, Parser, Subcommand}; use tokio::io::Result; use tun::TunInterface; @@ -8,7 +8,19 @@ use tun::TunInterface; #[command(version = "0.1")] #[command(about = "Burrow is a tool for burrowing through firewalls, built by teenagers at Hack Club.", long_about = None)] -struct Cli {} +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Adds files to myapp + Start(StartArgs), +} + +#[derive(Args)] +struct StartArgs {} async fn try_main() -> Result<()> { let iface = TunInterface::new()?; @@ -19,6 +31,10 @@ async fn try_main() -> Result<()> { #[tokio::main(flavor = "current_thread")] async fn main() { - let _cli = Cli::parse(); - try_main().await.unwrap(); + let cli = Cli::parse(); + match &cli.command { + Commands::Start(..) => { + try_main().await.unwrap(); + } + } } From f2af721debd711f788870236ef9e0d8026a873f8 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Tue, 6 Jun 2023 20:51:06 +0200 Subject: [PATCH 040/128] Update tun/src/unix/linux/mod.rs This fixes an accidental change I made, sorry! --- tun/src/unix/linux/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 07cf80b..8467731 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -4,7 +4,7 @@ use socket2::{Domain, SockAddr, Socket, Type}; use std::fs::OpenOptions; use std::io::{Error, Write}; use std::mem; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; use std::os::fd::RawFd; use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; From 706024617dd61a7d297436a5ceb48a0f11f43ad3 Mon Sep 17 00:00:00 2001 From: Andromeda <58833781+AndromedaHelix@users.noreply.github.com> Date: Fri, 12 May 2023 11:40:46 -0600 Subject: [PATCH 041/128] Update README.md Fixed git lint, implemented recommended changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a89463..f19a7fb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![License](https://img.shields.io/github/license/hackclub/burrow) ![Apple Build Status](https://img.shields.io/github/actions/workflow/status/hackclub/burrow/build-apple.yml?branch=main&label=macos%2C%20ios&logo=Apple) ![Crate Build Status](https://img.shields.io/github/actions/workflow/status/hackclub/burrow/build-rust.yml?branch=main&label=crate&logo=Rust) -Burrow is a tool for burrowing through firewalls, built by teenagers at [Hack Club](https://hackclub.com/). +Burrow ia a best-in-class tool for burrowing through firewalls. It is built by teenagers at [Hack Club](https://hackclub.com/). At its core, `burrow` is a command line utility written in Rust that can open virtual interfaces and direct traffic through them. From ee066a2d36a9e44ed9ad51c91b64cf2f47af6513 Mon Sep 17 00:00:00 2001 From: Andromeda <58833781+AndromedaHelix@users.noreply.github.com> Date: Fri, 12 May 2023 11:57:20 -0600 Subject: [PATCH 042/128] Update README.md Fixed git lint, implemented recommended changes --- README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f19a7fb..72fcaba 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,24 @@ Burrow ia a best-in-class tool for burrowing through firewalls. It is built by teenagers at [Hack Club](https://hackclub.com/). -At its core, `burrow` is a command line utility written in Rust that can open virtual interfaces and direct traffic through them. +It works through the app that allows for a quick and easy setup to open virtual interfaces and direct traffic through them. You can also use burrow directly from the command line. -You can use it directly from the command line, or you can use it wrapped inside of an app. Currently, there are apps for iOS and macOS that you can use to start the tunnel. +## Contributing + +Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! + +The project structure is divided in the following folders: + +[] To do, add folder directories + +## Installation + +To start burrowing, download the latest release build in the release section. + +## Hack Club + +Hack Club is a global community of high-school hackers from all around the world! Start your hack club by visiting the [Hack Club Page](https://hackclub.com/) + +## License + +Burrow is open source and licensed under the [GNU General Public License v3.0](./LICENSE.md) From a4f8b61f53ad1489399dd31aa4f32757f521d674 Mon Sep 17 00:00:00 2001 From: Andromeda <58833781+AndromedaHelix@users.noreply.github.com> Date: Sat, 13 May 2023 08:49:28 -0600 Subject: [PATCH 043/128] Update README.md Fixed git lint, implemented recommended changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72fcaba..fea4fdb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![License](https://img.shields.io/github/license/hackclub/burrow) ![Apple Build Status](https://img.shields.io/github/actions/workflow/status/hackclub/burrow/build-apple.yml?branch=main&label=macos%2C%20ios&logo=Apple) ![Crate Build Status](https://img.shields.io/github/actions/workflow/status/hackclub/burrow/build-rust.yml?branch=main&label=crate&logo=Rust) -Burrow ia a best-in-class tool for burrowing through firewalls. It is built by teenagers at [Hack Club](https://hackclub.com/). +Burrow is a best-in-class tool for burrowing through firewalls. It is built by teenagers at [Hack Club](https://hackclub.com/). It works through the app that allows for a quick and easy setup to open virtual interfaces and direct traffic through them. You can also use burrow directly from the command line. From 8221c224fddd942290097aadbfd59defab12685b Mon Sep 17 00:00:00 2001 From: Andromeda <58833781+AndromedaHelix@users.noreply.github.com> Date: Fri, 2 Jun 2023 20:07:49 -0600 Subject: [PATCH 044/128] Update README.md Fixed git lint, implemented recommended changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fea4fdb..d2ada1f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![License](https://img.shields.io/github/license/hackclub/burrow) ![Apple Build Status](https://img.shields.io/github/actions/workflow/status/hackclub/burrow/build-apple.yml?branch=main&label=macos%2C%20ios&logo=Apple) ![Crate Build Status](https://img.shields.io/github/actions/workflow/status/hackclub/burrow/build-rust.yml?branch=main&label=crate&logo=Rust) -Burrow is a best-in-class tool for burrowing through firewalls. It is built by teenagers at [Hack Club](https://hackclub.com/). +Burrow is an open source tool for burrowing through firewalls. It is built by teenagers at [Hack Club](https://hackclub.com/). It works through the app that allows for a quick and easy setup to open virtual interfaces and direct traffic through them. You can also use burrow directly from the command line. From a030a12beafed323226035ef6ef3f1b09ba43e6d Mon Sep 17 00:00:00 2001 From: Andromeda <58833781+AndromedaHelix@users.noreply.github.com> Date: Sat, 3 Jun 2023 16:18:25 -0600 Subject: [PATCH 045/128] Update README.md Co-authored-by: Cara Salter --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2ada1f..37b4443 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![License](https://img.shields.io/github/license/hackclub/burrow) ![Apple Build Status](https://img.shields.io/github/actions/workflow/status/hackclub/burrow/build-apple.yml?branch=main&label=macos%2C%20ios&logo=Apple) ![Crate Build Status](https://img.shields.io/github/actions/workflow/status/hackclub/burrow/build-rust.yml?branch=main&label=crate&logo=Rust) -Burrow is an open source tool for burrowing through firewalls. It is built by teenagers at [Hack Club](https://hackclub.com/). +Burrow is an open source tool for burrowing through firewalls, built by teenagers at [Hack Club](https://hackclub.com/). It works through the app that allows for a quick and easy setup to open virtual interfaces and direct traffic through them. You can also use burrow directly from the command line. From a99fd3608137acbd35caadc781f9c2eb8f93672c Mon Sep 17 00:00:00 2001 From: Andromeda <58833781+AndromedaHelix@users.noreply.github.com> Date: Sat, 3 Jun 2023 21:51:22 -0600 Subject: [PATCH 046/128] Update README.md Fixed git lint, implemented recommended changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37b4443..1141f4d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Burrow is an open source tool for burrowing through firewalls, built by teenagers at [Hack Club](https://hackclub.com/). -It works through the app that allows for a quick and easy setup to open virtual interfaces and direct traffic through them. You can also use burrow directly from the command line. +`burrow` provides a simple command-line tool to open virtual interfaces and direct traffic through them ## Contributing From a73599c268b6610ca8a3a756bad0417c42143e34 Mon Sep 17 00:00:00 2001 From: Andromeda <58833781+AndromedaHelix@users.noreply.github.com> Date: Tue, 6 Jun 2023 13:13:37 -0600 Subject: [PATCH 047/128] Update README.md Co-authored-by: Cara Salter --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 1141f4d..e5f72de 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,11 @@ Burrow is an open source tool for burrowing through firewalls, built by teenager ## Contributing -Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! +Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! For more information on how to contribute, please see [CONTRIBUTING.md] The project structure is divided in the following folders: [] To do, add folder directories - ## Installation To start burrowing, download the latest release build in the release section. From 4cc13ac2d3f0c360ea1097419503053db315fcef Mon Sep 17 00:00:00 2001 From: Andromeda <58833781+AndromedaHelix@users.noreply.github.com> Date: Tue, 6 Jun 2023 13:48:33 -0600 Subject: [PATCH 048/128] Update README.md Fixed git lint, implemented recommended changes --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e5f72de..b6c5164 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,16 @@ Burrow is fully open source, you can fork the repo and start contributing easily The project structure is divided in the following folders: -[] To do, add folder directories +``` +Apple/ # Xcode project for burrow on macOS and iOS +burrow/ # Higher-level API library for tun and tun-async +tun/ # Low-level interface to OS networking + src/ + unix/ # macOS and Linux code + windows/ # Windows networking code +tun-async/ # Async interface to tun +``` + ## Installation To start burrowing, download the latest release build in the release section. From 45fc74a3ba898f45f5988d27be48935c3e8065f2 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Tue, 6 Jun 2023 18:28:44 -0400 Subject: [PATCH 049/128] Add period to end of line Improves consistency and grammatical correctness --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6c5164..05fffae 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Burrow is an open source tool for burrowing through firewalls, built by teenagers at [Hack Club](https://hackclub.com/). -`burrow` provides a simple command-line tool to open virtual interfaces and direct traffic through them +`burrow` provides a simple command-line tool to open virtual interfaces and direct traffic through them. ## Contributing From 6bd8051c78f075019311d800c5c34175fd931844 Mon Sep 17 00:00:00 2001 From: Malted Date: Sat, 10 Jun 2023 16:57:42 +0100 Subject: [PATCH 050/128] =?UTF-8?q?=F0=9F=94=A7=20Add=20cargo=20release=20?= =?UTF-8?q?aliases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the alias field with bb and rr defined as release aliases --- .cargo/config.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.cargo/config.toml b/.cargo/config.toml index 956cc38..302ce48 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,6 @@ [target.'cfg(unix)'] runner = "sudo -E" + +[alias] # command aliases +rr = "run --release" +bb = "build --release" From 40cc0ba049edd1ad49e10c8b6ed046f0ab48613a Mon Sep 17 00:00:00 2001 From: Malted Date: Sat, 10 Jun 2023 17:25:08 +0100 Subject: [PATCH 051/128] =?UTF-8?q?=F0=9F=9B=82=20Check=20for=20required?= =?UTF-8?q?=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Linux, checks for the `CAP_NET_ADMIN` capability. On macOS, checks for root. --- Cargo.lock | 12 ++++++++++++ burrow/Cargo.toml | 6 ++++++ burrow/src/ensureroot.rs | 35 +++++++++++++++++++++++++++++++++++ burrow/src/lib.rs | 4 +--- burrow/src/main.rs | 4 ++++ tun/src/unix/apple/mod.rs | 2 +- tun/src/unix/mod.rs | 2 +- 7 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 burrow/src/ensureroot.rs diff --git a/Cargo.lock b/Cargo.lock index 8677d43..a011c61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,9 @@ checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" name = "burrow" version = "0.1.0" dependencies = [ + "caps", "clap", + "nix", "tokio", "tun", ] @@ -179,6 +181,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "caps" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" +dependencies = [ + "libc", + "thiserror", +] + [[package]] name = "cc" version = "1.0.79" diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 8880c8b..92059ba 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,3 +10,9 @@ crate-type = ["lib", "staticlib"] tokio = { version = "1.21", features = ["rt", "macros"] } tun = { version = "0.1", path = "../tun" } clap = { version = "4.3.2", features = ["derive"] } + +[target.'cfg(target_os = "linux")'.dependencies] +caps = "0.5.5" + +[target.'cfg(target_os = "macos")'.dependencies] +nix = { version = "0.26.2" } diff --git a/burrow/src/ensureroot.rs b/burrow/src/ensureroot.rs new file mode 100644 index 0000000..8648268 --- /dev/null +++ b/burrow/src/ensureroot.rs @@ -0,0 +1,35 @@ +// Check capabilities on Linux +#[cfg(target_os = "linux")] +pub fn ensure_root() { + use caps::{has_cap, CapSet, Capability}; + + let cap_net_admin = Capability::CAP_NET_ADMIN; + if let Ok(has_cap) = has_cap(None, CapSet::Effective, cap_net_admin) { + if !has_cap { + eprintln!( + "This action needs the CAP_NET_ADMIN permission. Did you mean to run it as root?" + ); + std::process::exit(77); + } + } else { + eprintln!("Failed to check capabilities. Please file a bug report!"); + std::process::exit(71); + } +} + +// Check for root user on macOS +#[cfg(target_os = "macos")] +pub fn ensure_root() { + use nix::unistd::Uid; + + let current_uid = Uid::current(); + if !current_uid.is_root() { + eprintln!("This action must be run as root!"); + std::process::exit(77); + } +} + +#[cfg(target_family = "windows")] +pub fn ensure_root() { + todo!() +} diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index 7d83484..6abc28f 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -1,3 +1 @@ -pub fn hello_world() { - println!("Hello, world!"); -} +pub mod ensureroot; diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 7ef7530..256f292 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -23,6 +23,8 @@ enum Commands { struct StartArgs {} async fn try_main() -> Result<()> { + burrow::ensureroot::ensure_root(); + let iface = TunInterface::new()?; println!("{:?}", iface.name()); @@ -31,6 +33,8 @@ async fn try_main() -> Result<()> { #[tokio::main(flavor = "current_thread")] async fn main() { + println!("Platform: {}", std::env::consts::OS); + let cli = Cli::parse(); match &cli.command { Commands::Start(..) => { diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index a2a5674..a88078d 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -6,7 +6,7 @@ use std::io::{IoSlice, Write}; use std::net::{Ipv4Addr, SocketAddrV4}; use std::os::fd::{AsRawFd, RawFd}; use std::{ - io::{Error, Read}, + io::{Error}, mem, }; diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs index 28b4ec8..8f194b7 100644 --- a/tun/src/unix/mod.rs +++ b/tun/src/unix/mod.rs @@ -71,7 +71,7 @@ mod test { use super::*; use std::io::Write; - use std::io::{self, BufRead}; + use std::net::Ipv4Addr; #[throws] From ba22ebc79f3fdc5540cded6639d1b454613b6edb Mon Sep 17 00:00:00 2001 From: Malted Date: Sat, 10 Jun 2023 17:45:06 +0100 Subject: [PATCH 052/128] :bug: Include function on all Darwin platforms Builds the relevant ensure root function on all Apple devices --- burrow/src/ensureroot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/burrow/src/ensureroot.rs b/burrow/src/ensureroot.rs index 8648268..8c1d33e 100644 --- a/burrow/src/ensureroot.rs +++ b/burrow/src/ensureroot.rs @@ -18,7 +18,7 @@ pub fn ensure_root() { } // Check for root user on macOS -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] pub fn ensure_root() { use nix::unistd::Uid; From 1ec7ea7d3c8d37be84aaaab7516b9af68fce00f4 Mon Sep 17 00:00:00 2001 From: Malted Date: Sat, 10 Jun 2023 17:56:12 +0100 Subject: [PATCH 053/128] :bug: Update the target for the nix crate I'm a silly little goose and forger :skull: it before --- burrow/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 92059ba..fddaddf 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -14,5 +14,5 @@ clap = { version = "4.3.2", features = ["derive"] } [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5.5" -[target.'cfg(target_os = "macos")'.dependencies] +[target.'cfg(target_vendor = "apple")'.dependencies] nix = { version = "0.26.2" } From f3d086effc74e32733c551de62d3f056c26d6fee Mon Sep 17 00:00:00 2001 From: Malted Date: Sat, 10 Jun 2023 18:11:54 +0100 Subject: [PATCH 054/128] :memo: Add long description to clap help Replaces the None value with a descriptive str --- burrow/src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 256f292..40c54e6 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -6,7 +6,12 @@ use tun::TunInterface; #[command(name = "Burrow")] #[command(author = "Hack Club ")] #[command(version = "0.1")] -#[command(about = "Burrow is a tool for burrowing through firewalls, built by teenagers at Hack Club.", long_about = None)] +#[command( + about = "Burrow is a tool for burrowing through firewalls, built by teenagers at Hack Club.", + long_about = "Burrow is a 🚀 blazingly fast 🚀 tool designed to penetrate unnecessarily restrictive firewalls, providing teenagers worldwide with secure, less-filtered, and safe access to the internet! +It's being built by teenagers from Hack Club, in public! Check it out: https://github.com/hackclub/burrow +Spotted a bug? Please open an issue! https://github.com/hackclub/burrow/issues/new" +)] struct Cli { #[command(subcommand)] @@ -15,7 +20,7 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Adds files to myapp + /// Start Burrow Start(StartArgs), } From efee4afc8d84e417a27e683aec38071a51fa17dc Mon Sep 17 00:00:00 2001 From: Soumya Kushwaha Date: Mon, 17 Apr 2023 09:07:33 +0530 Subject: [PATCH 055/128] Add contributors section to README This highlights people's contributions to Burrow. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 05fffae..936bdfb 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,9 @@ Hack Club is a global community of high-school hackers from all around the world ## License Burrow is open source and licensed under the [GNU General Public License v3.0](./LICENSE.md) + +## Contributors + + + + From e1b03fd0a8dddc4442abbf4889638457fe4cfee4 Mon Sep 17 00:00:00 2001 From: Dev380 <49997896+Dev380@users.noreply.github.com> Date: Tue, 13 Jun 2023 09:07:16 -0400 Subject: [PATCH 056/128] Delete empty queue.rs The queue.rs seems to be empty with no apparent purpose even as a stub so this commit removes it. --- tun/src/queue.rs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tun/src/queue.rs diff --git a/tun/src/queue.rs b/tun/src/queue.rs deleted file mode 100644 index 8b13789..0000000 --- a/tun/src/queue.rs +++ /dev/null @@ -1 +0,0 @@ - From 923bc9511de07ab314b3536fa19098064d395938 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Fri, 23 Jun 2023 16:10:54 -0400 Subject: [PATCH 057/128] Ignore body is missing errors It seems excessive to require an extended description to each commit, such as the extended description being given here. --- .gitlint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlint b/.gitlint index b185b1c..a72a004 100644 --- a/.gitlint +++ b/.gitlint @@ -1,5 +1,5 @@ [general] -ignore=body-changed-file-mention +ignore=body-changed-file-mention,body-is-missing ignore-merge-commits=false ignore-fixup-commits=false ignore-squash-commits=false From 543854228487a600925fb19102a073a55f6c618c Mon Sep 17 00:00:00 2001 From: SerenityUX Date: Fri, 23 Jun 2023 20:19:10 -0400 Subject: [PATCH 058/128] Added initial menu bar to macOS --- Apple/App/BurrowApp.swift | 42 ++++++++++++++-- Apple/App/Menu/MenuView.swift | 69 ++++++++++++++++++++++++++ Apple/App/Tunnel.swift | 2 +- Apple/App/TunnelView.swift | 55 ++++++++++---------- Apple/Burrow.xcodeproj/project.pbxproj | 12 +++++ 5 files changed, 149 insertions(+), 31 deletions(-) create mode 100644 Apple/App/Menu/MenuView.swift diff --git a/Apple/App/BurrowApp.swift b/Apple/App/BurrowApp.swift index 50a7231..59907f8 100644 --- a/Apple/App/BurrowApp.swift +++ b/Apple/App/BurrowApp.swift @@ -4,14 +4,50 @@ import SwiftUI @main @MainActor struct BurrowApp: App { + //To connect to the App Delegate + @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate + + + var body: some Scene { + WindowGroup { + TunnelView() + } + } +} + + +@MainActor +class AppDelegate: NSObject, NSApplicationDelegate { + static let tunnel = Tunnel { manager, proto in proto.serverAddress = "hackclub.com" manager.localizedDescription = "Burrow" } - var body: some Scene { - WindowGroup { - TunnelView(tunnel: Self.tunnel) + var statusItem: NSStatusItem? + var popOver = NSPopover() + func applicationDidFinishLaunching(_ notification: Notification) { + let menuView = MenuView(tunnel: AppDelegate.tunnel) + // Creating apopOver + popOver.behavior = .transient + popOver.animates = true + popOver.contentViewController = NSViewController() + popOver.contentViewController?.view = NSHostingView(rootView: menuView) + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + // Safe Check if status Button is Available or not... + if let menuButton = statusItem?.button { + let icon = "network.badge.shield.half.filled" + menuButton.image = NSImage(systemSymbolName: icon, accessibilityDescription: nil) + menuButton.action = #selector(menuButtonToggle) + } + } + @objc func menuButtonToggle() { + + + if let menuButton = statusItem?.button { + + self.popOver.show(relativeTo: menuButton.bounds, of: menuButton, preferredEdge: NSRectEdge.minY) } } } + diff --git a/Apple/App/Menu/MenuView.swift b/Apple/App/Menu/MenuView.swift new file mode 100644 index 0000000..62abd66 --- /dev/null +++ b/Apple/App/Menu/MenuView.swift @@ -0,0 +1,69 @@ +// +// MenuView.swift +// App +// +// Created by Thomas Stubblefield on 5/13/23. +// + +import SwiftUI + +struct MenuView: View { + @State private var isToggled = false + @ObservedObject var tunnel: Tunnel + + private func start() { + + do { + try tunnel.start() + } catch { + print(error) + } + } + + private func stop() { + tunnel.stop() + } + + private func configure() { + Task { try await tunnel.configure() } + } + + var body: some View { + VStack { + HStack { + Text("Burrow") + .fontWeight(.bold) + + Spacer() + Toggle("", isOn: $isToggled) + .toggleStyle(SwitchToggleStyle(tint: .blue)) + .onChange(of: isToggled) { value in + if value { + start() + } else { + stop() + } + print("Toggle value: \(value)") + } + } + Divider() + switch tunnel.status { + case .permissionRequired: + VStack(alignment: .leading) { + Text("Burrow requires additional permissions to function optimally on your machine. Please grant the necessary permissions to ensure smooth operation.") + .font(.caption) + .truncationMode(.tail) + + Button("Grant Permissions", action: configure) + } + default: + + Text("Burrow is equipped with the necessary permissions to operate seamlessly on your device.") + .font(.caption) + } + } + .frame(width: 250) + .padding(16) + .task { await tunnel.update() } + } +} diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift index ae185a4..e8bff22 100644 --- a/Apple/App/Tunnel.swift +++ b/Apple/App/Tunnel.swift @@ -92,7 +92,7 @@ class Tunnel: ObservableObject { let proto = NETunnelProviderProtocol() proto.providerBundleIdentifier = bundleIdentifier configure(manager, proto) - + manager.protocolConfiguration = proto try await manager.save() } diff --git a/Apple/App/TunnelView.swift b/Apple/App/TunnelView.swift index 6692a04..e3b9e28 100644 --- a/Apple/App/TunnelView.swift +++ b/Apple/App/TunnelView.swift @@ -1,35 +1,36 @@ import SwiftUI struct TunnelView: View { - @ObservedObject var tunnel: Tunnel +// @ObservedObject var tunnel: Tunnel var body: some View { - VStack { - Text(verbatim: tunnel.status.description) - switch tunnel.status { - case .connected: - Button("Disconnect", action: stop) - case .permissionRequired: - Button("Allow", action: configure) - case .disconnected: - Button("Start", action: start) - default: - EmptyView() - } - } - .task { await tunnel.update() } - .padding() + EmptyView() +// VStack { +// Text(verbatim: tunnel.status.description) +// switch tunnel.status { +// case .connected: +// Button("Disconnect", action: stop) +// case .permissionRequired: +// Button("Allow", action: configure) +// case .disconnected: +// Button("Start", action: start) +// default: +// EmptyView() +// } +// } +// .task { await tunnel.update() } +// .padding() } - private func start() { - try? tunnel.start() - } - - private func stop() { - tunnel.stop() - } - - private func configure() { - Task { try await tunnel.configure() } - } +// private func start() { +// try? tunnel.start() +// } +// +// private func stop() { +// tunnel.stop() +// } +// +// private func configure() { +// Task { try await tunnel.configure() } +// } } diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 56b64e4..b5be4d1 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuView.swift */; }; D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; @@ -44,6 +45,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 43AA26D72A10004900F14CE6 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = ""; }; D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -90,6 +92,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 43AA26D62A0FFFD000F14CE6 /* Menu */ = { + isa = PBXGroup; + children = ( + 43AA26D72A10004900F14CE6 /* MenuView.swift */, + ); + path = Menu; + sourceTree = ""; + }; D020F63C29E4A1FF002790F6 /* Configuration */ = { isa = PBXGroup; children = ( @@ -137,6 +147,7 @@ D05B9F7429E39EEC008CB1F9 /* App */ = { isa = PBXGroup; children = ( + 43AA26D62A0FFFD000F14CE6 /* Menu */, D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */, D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */, D0B98FC629FDC5B5004E7149 /* Tunnel.swift */, @@ -299,6 +310,7 @@ buildActionMask = 2147483647; files = ( D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */, + 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */, D05B9F7829E39EEC008CB1F9 /* TunnelView.swift in Sources */, D0BCC5FF2A086E1C00AD070D /* Status.swift in Sources */, D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */, From 32e4e9d1d708a5bbec08f57384f611e31af0ad2b Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Fri, 23 Jun 2023 18:22:26 -0400 Subject: [PATCH 059/128] Refactored MenuView into MenuItemToggleView This change fixes some subtle state bugs in the switch handling by making isOn a direct function of the Tunnel. --- Apple/App/AppDelegate.swift | 51 ++++++++++++++ Apple/App/BurrowApp.swift | 48 +++---------- Apple/App/Menu/MenuView.swift | 98 ++++++++++++-------------- Apple/Burrow.xcodeproj/project.pbxproj | 4 ++ 4 files changed, 108 insertions(+), 93 deletions(-) create mode 100644 Apple/App/AppDelegate.swift diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift new file mode 100644 index 0000000..f42b52f --- /dev/null +++ b/Apple/App/AppDelegate.swift @@ -0,0 +1,51 @@ +#if os(macOS) +import AppKit +import SwiftUI + +@MainActor +class AppDelegate: NSObject, NSApplicationDelegate { + private let quitItem: NSMenuItem = { + let quitItem = NSMenuItem( + title: "Quit Burrow", + action: #selector(NSApplication.terminate(_:)), + keyEquivalent: "q" + ) + quitItem.target = NSApplication.shared + quitItem.keyEquivalentModifierMask = .command + return quitItem + }() + + private let toggleItem: NSMenuItem = { + let toggleView = NSHostingView(rootView: MenuItemToggleView(tunnel: BurrowApp.tunnel)) + toggleView.frame.size = CGSize(width: 300, height: 32) + toggleView.autoresizingMask = [.width] + + let toggleItem = NSMenuItem() + toggleItem.view = toggleView + return toggleItem + }() + + private lazy var menu: NSMenu = { + let menu = NSMenu() + menu.items = [ + toggleItem, + .separator(), + quitItem + ] + return menu + }() + + private lazy var statusItem: NSStatusItem = { + let statusBar = NSStatusBar.system + let statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength) + if let button = statusItem.button { + button.image = NSImage(systemSymbolName: "network.badge.shield.half.filled", accessibilityDescription: nil) + } + return statusItem + }() + + func applicationDidFinishLaunching(_ notification: Notification) { + statusItem.menu = menu + } +} +#endif diff --git a/Apple/App/BurrowApp.swift b/Apple/App/BurrowApp.swift index 59907f8..6d798fb 100644 --- a/Apple/App/BurrowApp.swift +++ b/Apple/App/BurrowApp.swift @@ -1,12 +1,17 @@ -import NetworkExtension import SwiftUI @main @MainActor struct BurrowApp: App { - //To connect to the App Delegate - @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate + static let tunnel = Tunnel { manager, proto in + proto.serverAddress = "hackclub.com" + manager.localizedDescription = "Burrow" + } + #if os(macOS) + @NSApplicationDelegateAdaptor(AppDelegate.self) + var delegate + #endif var body: some Scene { WindowGroup { @@ -14,40 +19,3 @@ struct BurrowApp: App { } } } - - -@MainActor -class AppDelegate: NSObject, NSApplicationDelegate { - - static let tunnel = Tunnel { manager, proto in - proto.serverAddress = "hackclub.com" - manager.localizedDescription = "Burrow" - } - - var statusItem: NSStatusItem? - var popOver = NSPopover() - func applicationDidFinishLaunching(_ notification: Notification) { - let menuView = MenuView(tunnel: AppDelegate.tunnel) - // Creating apopOver - popOver.behavior = .transient - popOver.animates = true - popOver.contentViewController = NSViewController() - popOver.contentViewController?.view = NSHostingView(rootView: menuView) - statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - // Safe Check if status Button is Available or not... - if let menuButton = statusItem?.button { - let icon = "network.badge.shield.half.filled" - menuButton.image = NSImage(systemSymbolName: icon, accessibilityDescription: nil) - menuButton.action = #selector(menuButtonToggle) - } - } - @objc func menuButtonToggle() { - - - if let menuButton = statusItem?.button { - - self.popOver.show(relativeTo: menuButton.bounds, of: menuButton, preferredEdge: NSRectEdge.minY) - } - } -} - diff --git a/Apple/App/Menu/MenuView.swift b/Apple/App/Menu/MenuView.swift index 62abd66..9d8fb31 100644 --- a/Apple/App/Menu/MenuView.swift +++ b/Apple/App/Menu/MenuView.swift @@ -7,63 +7,55 @@ import SwiftUI -struct MenuView: View { - @State private var isToggled = false +struct MenuItemToggleView: View { @ObservedObject var tunnel: Tunnel - private func start() { - - do { - try tunnel.start() - } catch { - print(error) - } - } - - private func stop() { - tunnel.stop() - } - - private func configure() { - Task { try await tunnel.configure() } - } - var body: some View { - VStack { - HStack { - Text("Burrow") - .fontWeight(.bold) - - Spacer() - Toggle("", isOn: $isToggled) - .toggleStyle(SwitchToggleStyle(tint: .blue)) - .onChange(of: isToggled) { value in - if value { - start() - } else { - stop() - } - print("Toggle value: \(value)") - } - } - Divider() - switch tunnel.status { - case .permissionRequired: - VStack(alignment: .leading) { - Text("Burrow requires additional permissions to function optimally on your machine. Please grant the necessary permissions to ensure smooth operation.") - .font(.caption) - .truncationMode(.tail) - - Button("Grant Permissions", action: configure) - } - default: - - Text("Burrow is equipped with the necessary permissions to operate seamlessly on your device.") - .font(.caption) - } + HStack { + Text("Burrow") + .font(.headline) + Spacer() + Toggle("Burrow", isOn: tunnel.isOn) + .labelsHidden() + .disabled(tunnel.isDisabled) + .toggleStyle(.switch) } - .frame(width: 250) - .padding(16) + .padding(.horizontal, 4) + .padding(10) + .frame(minWidth: 300, minHeight: 32, maxHeight: 32) .task { await tunnel.update() } } } + +extension Tunnel { + var isDisabled: Bool { + switch self.status { + case .disconnected, .permissionRequired, .connected: + return false + case .unknown, .disabled, .connecting, .reasserting, .disconnecting, .invalid, .configurationReadWriteFailed: + return true + } + } + + var isOn: Binding { + Binding { + switch self.status { + case .unknown, .disabled, .disconnecting, .disconnected, .invalid, .permissionRequired, .configurationReadWriteFailed: + return false + case .connecting, .reasserting, .connected: + return true + } + } set: { newValue in + switch (self.status, newValue) { + case (.permissionRequired, true): + Task { try await self.configure() } + case (.disconnected, true): + try? self.start() + case (.connected, false): + self.stop() + default: + return + } + } + } +} diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index b5be4d1..f9c7454 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuView.swift */; }; + D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; }; D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; @@ -46,6 +47,7 @@ /* Begin PBXFileReference section */ 43AA26D72A10004900F14CE6 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; + D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = ""; }; D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -149,6 +151,7 @@ children = ( 43AA26D62A0FFFD000F14CE6 /* Menu */, D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */, + D00AA8962A4669BC005C8102 /* AppDelegate.swift */, D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */, D0B98FC629FDC5B5004E7149 /* Tunnel.swift */, D0BCC5FE2A086E1C00AD070D /* Status.swift */, @@ -313,6 +316,7 @@ 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */, D05B9F7829E39EEC008CB1F9 /* TunnelView.swift in Sources */, D0BCC5FF2A086E1C00AD070D /* Status.swift in Sources */, + D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */, D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */, D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */, ); From da065b503f2d741ed091b52a1e2400fd34d76b7c Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Wed, 21 Jun 2023 15:46:39 +0000 Subject: [PATCH 060/128] Log "Set" Actions in Tun (Linux) I've used the log library and its info method. I've also added but not used env-logger. --- Cargo.lock | 58 +++++++++++++++++++++++++++++++++++---- Cargo.toml | 6 +--- burrow/Cargo.toml | 2 ++ tun/Cargo.toml | 6 +++- tun/src/unix/linux/mod.rs | 47 +++++++++++++++++++------------ 5 files changed, 90 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a011c61..59691cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.3.2" @@ -143,6 +152,8 @@ version = "0.1.0" dependencies = [ "caps", "clap", + "env_logger", + "log", "nix", "tokio", "tun", @@ -368,6 +379,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.3.1" @@ -599,6 +623,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.26" @@ -754,12 +784,9 @@ checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "memchr" @@ -1032,6 +1059,8 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -1292,6 +1321,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.40" @@ -1437,6 +1475,7 @@ dependencies = [ "lazy_static", "libc", "libloading", + "log", "nix", "reqwest", "socket2", @@ -1642,6 +1681,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 8afa305..bb03a92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,2 @@ [workspace] -members = [ - "burrow", - "tun-async", - "tun" -] +members = ["burrow", "tun-async", "tun"] diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index fddaddf..c9dd71f 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,6 +10,8 @@ crate-type = ["lib", "staticlib"] tokio = { version = "1.21", features = ["rt", "macros"] } tun = { version = "0.1", path = "../tun" } clap = { version = "4.3.2", features = ["derive"] } +env_logger = "0.10" +log = "0.4" [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5.5" diff --git a/tun/Cargo.toml b/tun/Cargo.toml index 33446d1..71579a3 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -10,13 +10,17 @@ nix = { version = "0.26", features = ["ioctl"] } socket2 = "0.4" tokio = { version = "1.28", features = [] } byteorder = "1.4" +log = "0.4" [target.'cfg(windows)'.dependencies] lazy_static = "1.4" libloading = "0.7" tempfile = "3.5" widestring = "1.0" -windows = { version = "0.48", features = ["Win32_Foundation", "Win32_NetworkManagement_IpHelper"] } +windows = { version = "0.48", features = [ + "Win32_Foundation", + "Win32_NetworkManagement_IpHelper", +] } [target.'cfg(windows)'.build-dependencies] anyhow = "1.0" diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 8467731..242e7c9 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -4,10 +4,12 @@ use socket2::{Domain, SockAddr, Socket, Type}; use std::fs::OpenOptions; use std::io::{Error, Write}; use std::mem; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}; use std::os::fd::RawFd; use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; +use log::info; + use libc::in6_ifreq; use super::{ifname_to_string, string_to_ifname}; @@ -73,23 +75,7 @@ impl TunInterface { let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_addr = unsafe { *addr.as_ptr() }; self.perform(|fd| unsafe { sys::if_set_addr(fd, &iff) })?; - } - - #[throws] - pub fn set_mtu(&self, mtu: i32) { - let mut iff = self.ifreq()?; - iff.ifr_ifru.ifru_mtu = mtu; - self.perform(|fd| unsafe { sys::if_set_mtu(fd, &iff) })?; - } - - #[throws] - pub fn set_netmask(&self, addr: Ipv4Addr) { - let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); - - let mut iff = self.ifreq()?; - iff.ifr_ifru.ifru_netmask = unsafe { *addr.as_ptr() }; - - self.perform(|fd| unsafe { sys::if_set_netmask(fd, &iff) })?; + info!("ipv4_addr_set: {:?} (fd: {:?})", addr, self.as_raw_fd()) } #[throws] @@ -105,6 +91,15 @@ impl TunInterface { let mut iff = self.in6_ifreq()?; iff.ifr6_addr.s6_addr = addr.octets(); self.perform6(|fd| unsafe { sys::if_set_addr6(fd, &iff) })?; + info!("ipv6_addr_set: {:?} (fd: {:?})", addr, self.as_raw_fd()) + } + + #[throws] + pub fn set_mtu(&self, mtu: i32) { + let mut iff = self.ifreq()?; + iff.ifr_ifru.ifru_mtu = mtu; + self.perform(|fd| unsafe { sys::if_set_mtu(fd, &iff) })?; + info!("mtu_set: {:?} (fd: {:?})", mtu, self.as_raw_fd()) } #[throws] @@ -116,6 +111,22 @@ impl TunInterface { mtu } + #[throws] + pub fn set_netmask(&self, addr: Ipv4Addr) { + let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); + + let mut iff = self.ifreq()?; + iff.ifr_ifru.ifru_netmask = unsafe { *addr.as_ptr() }; + + self.perform(|fd| unsafe { sys::if_set_netmask(fd, &iff) })?; + + info!( + "netmask_set: {:?} (fd: {:?})", + unsafe { iff.ifr_ifru.ifru_netmask }, + self.as_raw_fd() + ) + } + #[throws] pub fn netmask(&self) -> Ipv4Addr { let mut iff = self.ifreq()?; From 84f1d91d5ceb45e63a186e9af521e8c181f09812 Mon Sep 17 00:00:00 2001 From: dav Date: Sat, 24 Jun 2023 09:24:19 -0700 Subject: [PATCH 061/128] Implement TunInterfaceOptions --- tun/src/lib.rs | 3 +++ tun/src/options.rs | 38 ++++++++++++++++++++++++++++++++++++++ tun/src/unix/apple/mod.rs | 7 ++++++- tun/src/unix/linux/mod.rs | 27 ++++++++++++++++++++++----- tun/src/unix/mod.rs | 4 +++- tun/src/windows/mod.rs | 13 +++++++++++-- 6 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 tun/src/options.rs diff --git a/tun/src/lib.rs b/tun/src/lib.rs index 7e86059..99f74fc 100644 --- a/tun/src/lib.rs +++ b/tun/src/lib.rs @@ -6,4 +6,7 @@ mod imp; #[path = "unix/mod.rs"] pub(crate) mod imp; +mod options; + pub use imp::{TunInterface, TunQueue}; +pub use options::TunInterfaceOptions; diff --git a/tun/src/options.rs b/tun/src/options.rs new file mode 100644 index 0000000..7b7d478 --- /dev/null +++ b/tun/src/options.rs @@ -0,0 +1,38 @@ +use fehler::throws; +use std::io::Error; + +use super::TunInterface; + +#[derive(Default)] +pub struct TunInterfaceOptions { + /// (Windows + Linux) Name the tun interface. + pub(crate) name: Option, + /// (Linux) Don't include packet information. + pub(crate) no_pi: Option<()>, + /// (Linux) Avoid opening an existing persistant device. + pub(crate) tun_excl: Option<()>, +} + +impl TunInterfaceOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn name(mut self, name: &str) -> Self { + self.name = Some(name.to_owned()); + self + } + + pub fn no_pi(mut self, enable: bool) { + self.no_pi = enable.then_some(()); + } + + pub fn tun_excl(mut self, enable: bool) { + self.tun_excl = enable.then_some(()); + } + + #[throws] + pub fn open(self) -> TunInterface { + TunInterface::new_with_options(self)? + } +} diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index a88078d..4ed002c 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -15,7 +15,7 @@ mod sys; pub use super::queue::TunQueue; -use super::{ifname_to_string, string_to_ifname}; +use super::{ifname_to_string, string_to_ifname, TunInterfaceOptions}; use kern_control::SysControlSocket; #[derive(Debug)] @@ -26,6 +26,11 @@ pub struct TunInterface { impl TunInterface { #[throws] pub fn new() -> TunInterface { + Self::new_with_options(TunInterfaceOptions::new())? + } + + #[throws] + pub fn new_with_options(_: TunInterfaceOptions) -> TunInterface { TunInterface::connect(0)? } diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 242e7c9..af2693d 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -12,7 +12,7 @@ use log::info; use libc::in6_ifreq; -use super::{ifname_to_string, string_to_ifname}; +use super::{ifname_to_string, string_to_ifname, TunInterfaceOptions}; mod sys; @@ -24,16 +24,33 @@ pub struct TunInterface { impl TunInterface { #[throws] pub fn new() -> TunInterface { + Self::new_with_options(TunInterfaceOptions::new())? + } + + #[throws] + pub(crate) fn new_with_options(options: TunInterfaceOptions) -> TunInterface { let file = OpenOptions::new() .read(true) .write(true) .open("/dev/net/tun")?; + let mut flags = libc::IFF_TUN as i16; + + if options.no_pi.is_some() { + flags |= libc::IFF_NO_PI as i16; + } + if options.tun_excl.is_some() { + flags |= libc::IFF_TUN_EXCL as i16; + } + + let name = options + .name + .map(|name| string_to_ifname(&name)) + .unwrap_or([0; libc::IFNAMSIZ]); + let iff = libc::ifreq { - ifr_name: [0; libc::IFNAMSIZ], - ifr_ifru: libc::__c_anonymous_ifr_ifru { - ifru_flags: (libc::IFF_TUN | libc::IFF_TUN_EXCL | libc::IFF_NO_PI) as i16, - }, + ifr_name: name, + ifr_ifru: libc::__c_anonymous_ifr_ifru { ifru_flags: flags }, }; unsafe { sys::tun_set_iff(file.as_raw_fd(), &iff)? }; diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs index 8f194b7..e4960da 100644 --- a/tun/src/unix/mod.rs +++ b/tun/src/unix/mod.rs @@ -3,6 +3,8 @@ use std::{ os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}, }; +use super::TunInterfaceOptions; + mod queue; #[cfg(target_vendor = "apple")] @@ -71,7 +73,7 @@ mod test { use super::*; use std::io::Write; - + use std::net::Ipv4Addr; #[throws] diff --git a/tun/src/windows/mod.rs b/tun/src/windows/mod.rs index 6c0a19c..d38e7dc 100644 --- a/tun/src/windows/mod.rs +++ b/tun/src/windows/mod.rs @@ -5,6 +5,8 @@ use widestring::{u16cstr, U16CString}; use windows::Win32::Foundation::GetLastError; mod queue; +use super::TunInterfaceOptions; + pub use queue::TunQueue; pub struct TunInterface { @@ -15,7 +17,14 @@ pub struct TunInterface { impl TunInterface { #[throws] pub fn new() -> TunInterface { - let name = U16CString::from(u16cstr!("Burrow")); + Self::new_with_options(TunInterfaceOptions::new())? + } + + #[throws] + pub(crate) fn new_with_options(options: TunInterfaceOptions) -> TunInterface { + let name_owned = options.name.unwrap_or("Burrow".to_owned()); + let name = U16CString::from_str(&name_owned).unwrap(); + let handle = unsafe { sys::WINTUN.WintunCreateAdapter(name.as_ptr(), name.as_ptr(), ptr::null()) }; if handle.is_null() { @@ -23,7 +32,7 @@ impl TunInterface { } TunInterface { handle, - name: String::from("Burrow"), + name: name_owned, } } From f20f56062c733e2474fb363e285847c62c468c29 Mon Sep 17 00:00:00 2001 From: dav Date: Sat, 24 Jun 2023 09:41:02 -0700 Subject: [PATCH 062/128] Fix unused import warning for Windows --- tun/src/windows/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tun/src/windows/mod.rs b/tun/src/windows/mod.rs index d38e7dc..757f629 100644 --- a/tun/src/windows/mod.rs +++ b/tun/src/windows/mod.rs @@ -1,7 +1,7 @@ use fehler::throws; use std::io::Error; use std::ptr; -use widestring::{u16cstr, U16CString}; +use widestring::U16CString; use windows::Win32::Foundation::GetLastError; mod queue; From d3882bd0088188aef8d4139247acc4d80c7fa19a Mon Sep 17 00:00:00 2001 From: dav Date: Wed, 28 Jun 2023 12:23:33 -0700 Subject: [PATCH 063/128] TunInterfaceOptions -> TunOptions --- tun/src/lib.rs | 2 +- tun/src/options.rs | 4 ++-- tun/src/unix/apple/mod.rs | 6 +++--- tun/src/unix/linux/mod.rs | 6 +++--- tun/src/unix/mod.rs | 2 +- tun/src/windows/mod.rs | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tun/src/lib.rs b/tun/src/lib.rs index 99f74fc..c05fca9 100644 --- a/tun/src/lib.rs +++ b/tun/src/lib.rs @@ -9,4 +9,4 @@ pub(crate) mod imp; mod options; pub use imp::{TunInterface, TunQueue}; -pub use options::TunInterfaceOptions; +pub use options::TunOptions; diff --git a/tun/src/options.rs b/tun/src/options.rs index 7b7d478..4c81a83 100644 --- a/tun/src/options.rs +++ b/tun/src/options.rs @@ -4,7 +4,7 @@ use std::io::Error; use super::TunInterface; #[derive(Default)] -pub struct TunInterfaceOptions { +pub struct TunOptions { /// (Windows + Linux) Name the tun interface. pub(crate) name: Option, /// (Linux) Don't include packet information. @@ -13,7 +13,7 @@ pub struct TunInterfaceOptions { pub(crate) tun_excl: Option<()>, } -impl TunInterfaceOptions { +impl TunOptions { pub fn new() -> Self { Self::default() } diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 4ed002c..a82ceb9 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -15,7 +15,7 @@ mod sys; pub use super::queue::TunQueue; -use super::{ifname_to_string, string_to_ifname, TunInterfaceOptions}; +use super::{ifname_to_string, string_to_ifname, TunOptions}; use kern_control::SysControlSocket; #[derive(Debug)] @@ -26,11 +26,11 @@ pub struct TunInterface { impl TunInterface { #[throws] pub fn new() -> TunInterface { - Self::new_with_options(TunInterfaceOptions::new())? + Self::new_with_options(TunOptions::new())? } #[throws] - pub fn new_with_options(_: TunInterfaceOptions) -> TunInterface { + pub fn new_with_options(_: TunOptions) -> TunInterface { TunInterface::connect(0)? } diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index af2693d..cd3e4c0 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -12,7 +12,7 @@ use log::info; use libc::in6_ifreq; -use super::{ifname_to_string, string_to_ifname, TunInterfaceOptions}; +use super::{ifname_to_string, string_to_ifname, TunOptions}; mod sys; @@ -24,11 +24,11 @@ pub struct TunInterface { impl TunInterface { #[throws] pub fn new() -> TunInterface { - Self::new_with_options(TunInterfaceOptions::new())? + Self::new_with_options(TunOptions::new())? } #[throws] - pub(crate) fn new_with_options(options: TunInterfaceOptions) -> TunInterface { + pub(crate) fn new_with_options(options: TunOptions) -> TunInterface { let file = OpenOptions::new() .read(true) .write(true) diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs index e4960da..550df4d 100644 --- a/tun/src/unix/mod.rs +++ b/tun/src/unix/mod.rs @@ -3,7 +3,7 @@ use std::{ os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}, }; -use super::TunInterfaceOptions; +use super::TunOptions; mod queue; diff --git a/tun/src/windows/mod.rs b/tun/src/windows/mod.rs index 757f629..c7b1ba5 100644 --- a/tun/src/windows/mod.rs +++ b/tun/src/windows/mod.rs @@ -5,7 +5,7 @@ use widestring::U16CString; use windows::Win32::Foundation::GetLastError; mod queue; -use super::TunInterfaceOptions; +use super::TunOptions; pub use queue::TunQueue; @@ -17,11 +17,11 @@ pub struct TunInterface { impl TunInterface { #[throws] pub fn new() -> TunInterface { - Self::new_with_options(TunInterfaceOptions::new())? + Self::new_with_options(TunOptions::new())? } #[throws] - pub(crate) fn new_with_options(options: TunInterfaceOptions) -> TunInterface { + pub(crate) fn new_with_options(options: TunOptions) -> TunInterface { let name_owned = options.name.unwrap_or("Burrow".to_owned()); let name = U16CString::from_str(&name_owned).unwrap(); From beae8c0f791cee5cdb7f9a114b3c0510eb5a6b26 Mon Sep 17 00:00:00 2001 From: JettChenT Date: Mon, 19 Jun 2023 22:45:25 +0800 Subject: [PATCH 064/128] Add Read and Write for Async TunInterface Those features are implemented using AsyncFD. While write doesn't require a mutable reference to self, read does. Make Async Tun a feature remove async tun from workspace rename write/read to send/recv --- .vscode/settings.json | 2 +- Cargo.lock | 62 +++++++++++++++++++++++++++++++++------ Cargo.toml | 2 +- tun-async/Cargo.toml | 9 ------ tun-async/src/lib.rs | 14 --------- tun/Cargo.toml | 8 +++++ tun/src/lib.rs | 4 +++ tun/src/tokio/mod.rs | 58 ++++++++++++++++++++++++++++++++++++ tun/src/unix/apple/mod.rs | 15 ++-------- tun/src/unix/linux/mod.rs | 8 +---- tun/src/unix/mod.rs | 14 +++------ tun/tests/configure.rs | 3 +- 12 files changed, 135 insertions(+), 64 deletions(-) delete mode 100644 tun-async/Cargo.toml delete mode 100644 tun-async/src/lib.rs create mode 100644 tun/src/tokio/mod.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 649e121..887fb70 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,5 +19,5 @@ ], "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer", - }, + } } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 59691cf..146bbc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,6 +482,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -489,6 +504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -497,6 +513,34 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -515,10 +559,16 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -1383,9 +1433,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.1" +version = "1.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" dependencies = [ "autocfg", "bytes", @@ -1472,6 +1522,7 @@ dependencies = [ "bindgen", "byteorder", "fehler", + "futures", "lazy_static", "libc", "libloading", @@ -1487,13 +1538,6 @@ dependencies = [ "zip", ] -[[package]] -name = "tun-async" -version = "0.1.0" -dependencies = [ - "tun", -] - [[package]] name = "typenum" version = "1.16.0" diff --git a/Cargo.toml b/Cargo.toml index bb03a92..fcb83f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["burrow", "tun-async", "tun"] +members = ["burrow", "tun"] diff --git a/tun-async/Cargo.toml b/tun-async/Cargo.toml deleted file mode 100644 index e011ca4..0000000 --- a/tun-async/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "tun-async" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -tun = { version = "0.1", path = "../tun" } diff --git a/tun-async/src/lib.rs b/tun-async/src/lib.rs deleted file mode 100644 index 7d12d9a..0000000 --- a/tun-async/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/tun/Cargo.toml b/tun/Cargo.toml index 71579a3..79b2735 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -12,6 +12,14 @@ tokio = { version = "1.28", features = [] } byteorder = "1.4" log = "0.4" +futures = { version = "0.3.28", optional = true } + +[features] +tokio = ["tokio/net", "dep:futures"] + +[target.'cfg(feature = "tokio")'.dev-dependencies] +tokio = { features = ["rt", "macros"] } + [target.'cfg(windows)'.dependencies] lazy_static = "1.4" libloading = "0.7" diff --git a/tun/src/lib.rs b/tun/src/lib.rs index c05fca9..f0c0a6e 100644 --- a/tun/src/lib.rs +++ b/tun/src/lib.rs @@ -8,5 +8,9 @@ pub(crate) mod imp; mod options; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +#[cfg(feature = "tokio")] +pub mod tokio; + pub use imp::{TunInterface, TunQueue}; pub use options::TunOptions; diff --git a/tun/src/tokio/mod.rs b/tun/src/tokio/mod.rs new file mode 100644 index 0000000..674dfe6 --- /dev/null +++ b/tun/src/tokio/mod.rs @@ -0,0 +1,58 @@ +use std::io; +use tokio::io::unix::AsyncFd; + +pub struct TunInterface { + inner: AsyncFd, +} + +impl TunInterface { + pub fn new(tun: crate::TunInterface) -> io::Result { + Ok(Self { + inner: AsyncFd::new(tun)?, + }) + } + + pub async fn write(&self, buf: &[u8]) -> io::Result { + loop { + let mut guard = self.inner.writable().await?; + match guard.try_io(|inner| inner.get_ref().send(buf)) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } + + pub async fn read(&mut self, buf: &mut [u8]) -> io::Result { + loop { + let mut guard = self.inner.readable_mut().await?; + match guard.try_io(|inner| (*inner).get_mut().recv(buf)) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } +} + +#[cfg(test)] +mod tests { + use std::net::Ipv4Addr; + + use super::*; + #[tokio::test] + async fn test_create() { + let tun = crate::TunInterface::new().unwrap(); + let _async_tun = TunInterface::new(tun).unwrap(); + } + + #[tokio::test] + async fn test_write() { + let tun = crate::TunInterface::new().unwrap(); + tun.set_ipv4_addr(Ipv4Addr::from([192, 168, 1, 10])) + .unwrap(); + let async_tun = TunInterface::new(tun).unwrap(); + let mut buf = [0u8; 1500]; + buf[0] = 6 << 4; + let bytes_written = async_tun.write(&buf).await.unwrap(); + assert!(bytes_written > 0); + } +} diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index a82ceb9..2da4644 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -2,13 +2,10 @@ use byteorder::{ByteOrder, NetworkEndian}; use fehler::throws; use libc::{c_char, iovec, writev, AF_INET, AF_INET6}; use socket2::{Domain, SockAddr, Socket, Type}; -use std::io::{IoSlice, Write}; +use std::io::IoSlice; use std::net::{Ipv4Addr, SocketAddrV4}; use std::os::fd::{AsRawFd, RawFd}; -use std::{ - io::{Error}, - mem, -}; +use std::{io::Error, mem}; mod kern_control; mod sys; @@ -130,11 +127,9 @@ impl TunInterface { self.perform(|fd| unsafe { sys::if_set_netmask(fd, &iff) })?; } -} -impl Write for TunInterface { #[throws] - fn write(&mut self, buf: &[u8]) -> usize { + pub fn send(&self, buf: &[u8]) -> usize { use std::io::ErrorKind; let proto = match buf[0] >> 4 { 6 => Ok(AF_INET6), @@ -156,10 +151,6 @@ impl Write for TunInterface { .try_into() .map_err(|_| Error::new(ErrorKind::Other, "Conversion error"))? } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } } #[cfg(test)] diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index cd3e4c0..abc1ccd 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -166,17 +166,11 @@ impl TunInterface { let socket = Socket::new(Domain::IPV6, Type::DGRAM, None)?; perform(socket.as_raw_fd())? } -} -impl Write for TunInterface { #[throws] - fn write(&mut self, buf: &[u8]) -> usize { + pub fn send(&self, buf: &[u8]) -> usize { self.socket.send(buf)? } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } } #[cfg(test)] diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs index 550df4d..af67d39 100644 --- a/tun/src/unix/mod.rs +++ b/tun/src/unix/mod.rs @@ -40,13 +40,8 @@ impl IntoRawFd for TunInterface { } impl TunInterface { - // #[throws] - // pub fn write(&self, buf: &[u8]) -> usize { - // self.socket.send(buf)? - // } - #[throws] - pub fn read(&mut self, buf: &mut [u8]) -> usize { + pub fn recv(&mut self, buf: &mut [u8]) -> usize { self.socket.read(buf)? } } @@ -72,7 +67,6 @@ pub fn string_to_ifname(name: &str) -> [libc::c_char; libc::IFNAMSIZ] { mod test { use super::*; - use std::io::Write; use std::net::Ipv4Addr; @@ -88,7 +82,7 @@ mod test { println!("tun ip: {:?}", tun.ipv4_addr()?); println!("Waiting for a packet..."); let buf = &mut [0u8; 1500]; - let res = tun.read(buf); + let res = tun.recv(buf); println!("Received!"); assert!(res.is_ok()); } @@ -96,10 +90,10 @@ mod test { #[test] #[throws] fn write_packets() { - let mut tun = TunInterface::new()?; + let tun = TunInterface::new()?; let mut buf = [0u8; 1500]; buf[0] = 6 << 4; - let bytes_written = tun.write(&buf)?; + let bytes_written = tun.send(&buf)?; assert_eq!(bytes_written, 1504); } } diff --git a/tun/tests/configure.rs b/tun/tests/configure.rs index 05e5ef6..48ddd96 100644 --- a/tun/tests/configure.rs +++ b/tun/tests/configure.rs @@ -1,6 +1,6 @@ use fehler::throws; use std::io::Error; -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::net::{Ipv4Addr}; use tun::TunInterface; #[test] @@ -23,6 +23,7 @@ fn test_set_get_ipv4() { #[test] #[throws] +#[cfg(target_os = "linux")] fn test_set_get_ipv6() { let tun = TunInterface::new()?; From 9eb661ebd5cfec59211977d0ad47137392e70a73 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Thu, 29 Jun 2023 20:26:06 +0000 Subject: [PATCH 065/128] Log when TunInterface is configured This uses info from the log package. --- tun/src/unix/apple/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 2da4644..b96be9b 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,6 +1,7 @@ use byteorder::{ByteOrder, NetworkEndian}; use fehler::throws; use libc::{c_char, iovec, writev, AF_INET, AF_INET6}; +use log::info; use socket2::{Domain, SockAddr, Socket, Type}; use std::io::IoSlice; use std::net::{Ipv4Addr, SocketAddrV4}; @@ -70,11 +71,10 @@ impl TunInterface { #[throws] pub fn set_ipv4_addr(&self, addr: Ipv4Addr) { let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); - let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_addr = unsafe { *addr.as_ptr() }; - self.perform(|fd| unsafe { sys::if_set_addr(fd, &iff) })?; + info!("ipv4_addr_set: {:?} (fd: {:?})", addr, self.as_raw_fd()) } #[throws] @@ -105,6 +105,7 @@ impl TunInterface { let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_mtu = mtu; self.perform(|fd| unsafe { sys::if_set_mtu(fd, &iff) })?; + info!("mtu_set: {:?} (fd: {:?})", mtu, self.as_raw_fd()) } #[throws] @@ -121,11 +122,14 @@ impl TunInterface { #[throws] pub fn set_netmask(&self, addr: Ipv4Addr) { let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); - let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_netmask = unsafe { *addr.as_ptr() }; - self.perform(|fd| unsafe { sys::if_set_netmask(fd, &iff) })?; + info!( + "netmask_set: {:?} (fd: {:?})", + unsafe { iff.ifr_ifru.ifru_netmask }, + self.as_raw_fd() + ) } #[throws] From 631bbc8fb34a19ca2c74a8b504321533a7806838 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Mon, 3 Jul 2023 12:29:25 -0400 Subject: [PATCH 066/128] Expand description about Hack Club This description is more inline with our copy in other places --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 936bdfb..f48264f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ To start burrowing, download the latest release build in the release section. ## Hack Club -Hack Club is a global community of high-school hackers from all around the world! Start your hack club by visiting the [Hack Club Page](https://hackclub.com/) +Hack Club is a global community of high-school hackers from all around the world! Start your own Hack Club, attend an upcoming hackathon or join our online community at [hackclub.com](https://hackclub.com/). ## License From a2e93278c12351fd4c9e24d1615027aeca003ed3 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Tue, 4 Jul 2023 18:18:31 -0400 Subject: [PATCH 067/128] Create Dockerfile Also adds a Github workflow to publish a Docker image --- .dockerignore | 5 ++ .github/workflows/build-docker.yml | 45 ++++++++++++++++++ Dockerfile | 75 ++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/build-docker.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e855853 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +# Xcode +Apple/ + +# Rust +target/ diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml new file mode 100644 index 0000000..1ce7a9a --- /dev/null +++ b/.github/workflows/build-docker.yml @@ -0,0 +1,45 @@ +name: Build Docker +on: + push: + branches: + - main + pull_request: + branches: + - "*" +jobs: + build: + name: Build Docker Image + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Setup QEMU + uses: docker/setup-qemu-action@v2 + with: + platforms: arm64 + - name: Setup BuildKit + uses: docker/setup-buildx-action@v2 + - name: Authenticate + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha + type=raw,value=latest,enable={{is_default_branch}} + - name: Build and Push + uses: docker/build-push-action@v4 + with: + platforms: ${{ github.event_name != 'pull_request' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b1500bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,75 @@ +FROM docker.io/library/rust:1.70.0-slim-bookworm AS builder + +ARG TARGETPLATFORM +ARG LLVM_VERSION=16 + +ENV KEYRINGS /etc/apt/keyrings + +RUN set -eux && \ + mkdir -p $KEYRINGS && \ + apt-get update && \ + apt-get install --no-install-recommends -y gpg curl musl-dev && \ + curl --proto '=https' --tlsv1.2 -sSf https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor --output $KEYRINGS/llvm.gpg && \ + echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-$LLVM_VERSION main" > /etc/apt/sources.list.d/llvm.list && \ + apt-get update && \ + apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION && \ + ln -s clang-$LLVM_VERSION /usr/bin/clang && \ + ln -s clang /usr/bin/clang++ && \ + ln -s lld-$LLVM_VERSION /usr/bin/ld.lld && \ + ln -s clang-$LLVM_VERSION /usr/bin/clang-cl && \ + ln -s llvm-ar-$LLVM_VERSION /usr/bin/llvm-lib && \ + ln -s lld-link-$LLVM_VERSION /usr/bin/lld-link && \ + update-alternatives --install /usr/bin/cc cc /usr/bin/clang 100 && \ + update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++ 100 && \ + apt-get remove -y --auto-remove && \ + rm -rf /var/lib/apt/lists/* + +RUN case $TARGETPLATFORM in \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ + *) exit 1 ;; \ + esac && \ + rustup target add $LLVM_TARGET + +ENV CC_x86_64_unknown_linux_musl=clang-$LLVM_VERSION \ + AR_x86_64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ + CC_aarch64_unknown_linux_musl=clang-$LLVM_VERSION \ + AR_aarch64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ + CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/x86_64-linux-musl -L/lib/x86_64-linux-musl -C linker=rust-lld" \ + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/aarch64-linux-musl -L/lib/aarch64-linux-musl -C linker=rust-lld" \ + CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse + +COPY . . + +RUN case $TARGETPLATFORM in \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ + *) exit 1 ;; \ + esac && \ + cargo install --path burrow --target $LLVM_TARGET + +WORKDIR /tmp/rootfs + +RUN set -eux && \ + mkdir -p ./bin ./etc ./tmp ./data && \ + mv /usr/local/cargo/bin/burrow ./bin/burrow && \ + echo 'burrow:x:10001:10001::/tmp:/sbin/nologin' > ./etc/passwd && \ + echo 'burrow:x:10001:' > ./etc/group && \ + chown -R 10001:10001 ./tmp ./data && \ + chmod 0777 ./tmp + +FROM scratch as runtime +LABEL \ + # https://github.com/opencontainers/image-spec/blob/master/annotations.md + org.opencontainers.image.title="burrow" \ + org.opencontainers.image.description="Burrow is an open source tool for burrowing through firewalls, built by teenagers at Hack Club." \ + org.opencontainers.image.url="https://github.com/hackclub/burrow" \ + org.opencontainers.image.source="https://github.com/hackclub/burrow" \ + org.opencontainers.image.vendor="hackclub" \ + org.opencontainers.image.licenses="GPL-3.0" + +USER 10001:10001 +COPY --from=builder /tmp/rootfs / +WORKDIR /data + +ENTRYPOINT ["/bin/burrow"] From 1907b115454c5485394fb055d911be6fa5d86d59 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Thu, 29 Jun 2023 18:18:55 +0000 Subject: [PATCH 068/128] Move tests into a separate directory Also run these tests on Github Actions as part of the PR request flow. --- .github/workflows/build-rust.yml | 3 +++ tun/src/unix/apple/mod.rs | 29 ------------------------- tun/src/unix/linux/mod.rs | 29 ------------------------- tun/src/unix/mod.rs | 37 +------------------------------- tun/tests/configure.rs | 29 ++++++++++++++++++++++++- tun/tests/packets.rs | 36 +++++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 95 deletions(-) create mode 100644 tun/tests/packets.rs diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 66c389c..7955d62 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -60,3 +60,6 @@ jobs: - name: Build shell: bash run: cargo build --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} + - name: Post-Build Tests + shell: bash + run: cargo test diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index b96be9b..427f5e8 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -156,32 +156,3 @@ impl TunInterface { .map_err(|_| Error::new(ErrorKind::Other, "Conversion error"))? } } - -#[cfg(test)] -mod test { - use super::*; - use std::net::Ipv4Addr; - - #[test] - fn mtu() { - let interf = TunInterface::new().unwrap(); - - interf.set_mtu(500).unwrap(); - - assert_eq!(interf.mtu().unwrap(), 500); - } - - #[test] - #[throws] - fn netmask() { - let interf = TunInterface::new()?; - - let netmask = Ipv4Addr::new(255, 0, 0, 0); - let addr = Ipv4Addr::new(192, 168, 1, 1); - - interf.set_ipv4_addr(addr)?; - interf.set_netmask(netmask)?; - - assert_eq!(interf.netmask()?, netmask); - } -} diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index abc1ccd..7b8b05d 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -172,32 +172,3 @@ impl TunInterface { self.socket.send(buf)? } } - -#[cfg(test)] -mod test { - use super::TunInterface; - use std::net::Ipv4Addr; - - #[test] - fn mtu() { - let interf = TunInterface::new().unwrap(); - - interf.set_mtu(500).unwrap(); - - assert_eq!(interf.mtu().unwrap(), 500); - } - - #[test] - #[throws] - fn netmask() { - let interf = TunInterface::new()?; - - let netmask = Ipv4Addr::new(255, 0, 0, 0); - let addr = Ipv4Addr::new(192, 168, 1, 1); - - interf.set_ipv4_addr(addr)?; - interf.set_netmask(netmask)?; - - assert_eq!(interf.netmask()?, netmask); - } -} diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs index af67d39..1defbbd 100644 --- a/tun/src/unix/mod.rs +++ b/tun/src/unix/mod.rs @@ -61,39 +61,4 @@ pub fn string_to_ifname(name: &str) -> [libc::c_char; libc::IFNAMSIZ] { let len = name.len().min(buf.len()); buf[..len].copy_from_slice(unsafe { &*(name.as_bytes() as *const _ as *const [libc::c_char]) }); buf -} - -#[cfg(test)] -mod test { - - use super::*; - - use std::net::Ipv4Addr; - - #[throws] - #[test] - fn tst_read() { - // This test is interactive, you need to send a packet to any server through 192.168.1.10 - // EG. `sudo route add 8.8.8.8 192.168.1.10`, - //`dig @8.8.8.8 hackclub.com` - let mut tun = TunInterface::new()?; - println!("tun name: {:?}", tun.name()?); - tun.set_ipv4_addr(Ipv4Addr::from([192, 168, 1, 10]))?; - println!("tun ip: {:?}", tun.ipv4_addr()?); - println!("Waiting for a packet..."); - let buf = &mut [0u8; 1500]; - let res = tun.recv(buf); - println!("Received!"); - assert!(res.is_ok()); - } - - #[test] - #[throws] - fn write_packets() { - let tun = TunInterface::new()?; - let mut buf = [0u8; 1500]; - buf[0] = 6 << 4; - let bytes_written = tun.send(&buf)?; - assert_eq!(bytes_written, 1504); - } -} +} \ No newline at end of file diff --git a/tun/tests/configure.rs b/tun/tests/configure.rs index 48ddd96..1d762d7 100644 --- a/tun/tests/configure.rs +++ b/tun/tests/configure.rs @@ -11,6 +11,7 @@ fn test_create() { #[test] #[throws] +#[cfg(not(target_os = "windows"))] fn test_set_get_ipv4() { let tun = TunInterface::new()?; @@ -23,7 +24,7 @@ fn test_set_get_ipv4() { #[test] #[throws] -#[cfg(target_os = "linux")] +#[cfg(not(any(target_os = "windows", target_vendor = "apple")))] fn test_set_get_ipv6() { let tun = TunInterface::new()?; @@ -33,3 +34,29 @@ fn test_set_get_ipv6() { // let result = tun.ipv6_addr()?; // assert_eq!(addr, result); } + +#[test] +#[throws] +#[cfg(not(target_os = "windows"))] +fn test_set_get_mtu() { + let interf = TunInterface::new()?; + + interf.set_mtu(500)?; + + assert_eq!(interf.mtu().unwrap(), 500); +} + +#[test] +#[throws] +#[cfg(not(target_os = "windows"))] +fn test_set_get_netmask() { + let interf = TunInterface::new()?; + + let netmask = Ipv4Addr::new(255, 0, 0, 0); + let addr = Ipv4Addr::new(192, 168, 1, 1); + + interf.set_ipv4_addr(addr)?; + interf.set_netmask(netmask)?; + + assert_eq!(interf.netmask()?, netmask); +} diff --git a/tun/tests/packets.rs b/tun/tests/packets.rs new file mode 100644 index 0000000..836ac30 --- /dev/null +++ b/tun/tests/packets.rs @@ -0,0 +1,36 @@ +use fehler::throws; +use std::io::Error; +use std::io::Write; +use std::net::Ipv4Addr; +use tun::TunInterface; + +#[throws] +#[test] +#[ignore = "requires interactivity"] +#[cfg(not(target_os = "windows"))] +fn tst_read() { + // This test is interactive, you need to send a packet to any server through 192.168.1.10 + // EG. `sudo route add 8.8.8.8 192.168.1.10`, + //`dig @8.8.8.8 hackclub.com` + let mut tun = TunInterface::new()?; + println!("tun name: {:?}", tun.name()?); + tun.set_ipv4_addr(Ipv4Addr::from([192, 168, 1, 10]))?; + println!("tun ip: {:?}", tun.ipv4_addr()?); + println!("Waiting for a packet..."); + let buf = &mut [0u8; 1500]; + let res = tun.read(buf); + println!("Received!"); + assert!(res.is_ok()); +} + +#[test] +#[throws] +#[ignore = "requires interactivity"] +#[cfg(not(target_os = "windows"))] +fn write_packets() { + let mut tun = TunInterface::new()?; + let mut buf = [0u8; 1500]; + buf[0] = 6 << 4; + let bytes_written = tun.write(&buf)?; + assert_eq!(bytes_written, 1504); +} From 17af03089332afdd3b3f26d9ffa576c2f9d40467 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 5 Aug 2023 08:32:54 -0700 Subject: [PATCH 069/128] Run tests on Github Actions --- .github/workflows/build-rust.yml | 15 +++++++++------ .vscode/settings.json | 9 --------- tun/src/tokio/mod.rs | 24 ------------------------ tun/tests/configure.rs | 4 +++- tun/tests/packets.rs | 8 ++++---- tun/tests/tokio.rs | 22 ++++++++++++++++++++++ 6 files changed, 38 insertions(+), 44 deletions(-) create mode 100644 tun/tests/tokio.rs diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 7955d62..f56e26b 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -17,21 +17,24 @@ jobs: platform: Linux packages: - gcc-aarch64-linux-gnu - targets: + test-targets: - x86_64-unknown-linux-gnu + targets: - aarch64-unknown-linux-gnu - os: macos-12 platform: macOS - targets: + test-targets: - x86_64-apple-darwin + targets: - aarch64-apple-darwin - aarch64-apple-ios - aarch64-apple-ios-sim - x86_64-apple-ios - os: windows-2022 platform: Windows - targets: + test-targets: - x86_64-pc-windows-msvc + targets: - aarch64-pc-windows-msvc runs-on: ${{ matrix.os }} env: @@ -59,7 +62,7 @@ jobs: targets: ${{ join(matrix.targets, ', ') }} - name: Build shell: bash - run: cargo build --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} - - name: Post-Build Tests + run: cargo build --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} --target ${{ join(matrix.test-targets, ' --target ') }} + - name: Test shell: bash - run: cargo test + run: cargo test --verbose --workspace --all-features --target ${{ join(matrix.test-targets, ' --target ') }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 887fb70..4718093 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,15 +8,6 @@ "editor.acceptSuggestionOnEnter": "on", "rust-analyzer.restartServerOnConfigChange": true, "rust-analyzer.cargo.features": "all", - "rust-analyzer.check.overrideCommand": [ - "cargo", - "clippy", - "--fix", - "--workspace", - "--message-format=json", - "--all-targets", - "--allow-dirty" - ], "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer", } diff --git a/tun/src/tokio/mod.rs b/tun/src/tokio/mod.rs index 674dfe6..ae99b73 100644 --- a/tun/src/tokio/mod.rs +++ b/tun/src/tokio/mod.rs @@ -32,27 +32,3 @@ impl TunInterface { } } } - -#[cfg(test)] -mod tests { - use std::net::Ipv4Addr; - - use super::*; - #[tokio::test] - async fn test_create() { - let tun = crate::TunInterface::new().unwrap(); - let _async_tun = TunInterface::new(tun).unwrap(); - } - - #[tokio::test] - async fn test_write() { - let tun = crate::TunInterface::new().unwrap(); - tun.set_ipv4_addr(Ipv4Addr::from([192, 168, 1, 10])) - .unwrap(); - let async_tun = TunInterface::new(tun).unwrap(); - let mut buf = [0u8; 1500]; - buf[0] = 6 << 4; - let bytes_written = async_tun.write(&buf).await.unwrap(); - assert!(bytes_written > 0); - } -} diff --git a/tun/tests/configure.rs b/tun/tests/configure.rs index 1d762d7..35f9726 100644 --- a/tun/tests/configure.rs +++ b/tun/tests/configure.rs @@ -1,6 +1,6 @@ use fehler::throws; use std::io::Error; -use std::net::{Ipv4Addr}; +use std::net::Ipv4Addr; use tun::TunInterface; #[test] @@ -26,6 +26,8 @@ fn test_set_get_ipv4() { #[throws] #[cfg(not(any(target_os = "windows", target_vendor = "apple")))] fn test_set_get_ipv6() { + use std::net::Ipv6Addr; + let tun = TunInterface::new()?; let addr = Ipv6Addr::new(1, 1, 1, 1, 1, 1, 1, 1); diff --git a/tun/tests/packets.rs b/tun/tests/packets.rs index 836ac30..b160893 100644 --- a/tun/tests/packets.rs +++ b/tun/tests/packets.rs @@ -1,6 +1,6 @@ use fehler::throws; use std::io::Error; -use std::io::Write; + use std::net::Ipv4Addr; use tun::TunInterface; @@ -18,7 +18,7 @@ fn tst_read() { println!("tun ip: {:?}", tun.ipv4_addr()?); println!("Waiting for a packet..."); let buf = &mut [0u8; 1500]; - let res = tun.read(buf); + let res = tun.recv(buf); println!("Received!"); assert!(res.is_ok()); } @@ -28,9 +28,9 @@ fn tst_read() { #[ignore = "requires interactivity"] #[cfg(not(target_os = "windows"))] fn write_packets() { - let mut tun = TunInterface::new()?; + let tun = TunInterface::new()?; let mut buf = [0u8; 1500]; buf[0] = 6 << 4; - let bytes_written = tun.write(&buf)?; + let bytes_written = tun.send(&buf)?; assert_eq!(bytes_written, 1504); } diff --git a/tun/tests/tokio.rs b/tun/tests/tokio.rs new file mode 100644 index 0000000..0a8a276 --- /dev/null +++ b/tun/tests/tokio.rs @@ -0,0 +1,22 @@ +use std::net::Ipv4Addr; + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn test_create() { + let tun = tun::TunInterface::new().unwrap(); + let async_tun = tun::tokio::TunInterface::new(tun).unwrap(); +} + +#[tokio::test] +#[ignore = "requires interactivity"] +#[cfg(all(feature = "tokio", not(target_os = "windows")))] +async fn test_write() { + let tun = tun::TunInterface::new().unwrap(); + tun.set_ipv4_addr(Ipv4Addr::from([192, 168, 1, 10])) + .unwrap(); + let async_tun = tun::tokio::TunInterface::new(tun).unwrap(); + let mut buf = [0u8; 1500]; + buf[0] = 6 << 4; + let bytes_written = async_tun.write(&buf).await.unwrap(); + assert!(bytes_written > 0); +} From d821f3d03c709e8d161ac9635187fb69f793a445 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Tue, 8 Aug 2023 10:57:04 -0400 Subject: [PATCH 070/128] Only run Tokio tests on non-Windows platforms --- tun/tests/tokio.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tun/tests/tokio.rs b/tun/tests/tokio.rs index 0a8a276..e745c27 100644 --- a/tun/tests/tokio.rs +++ b/tun/tests/tokio.rs @@ -1,7 +1,7 @@ use std::net::Ipv4Addr; #[tokio::test] -#[cfg(feature = "tokio")] +#[cfg(all(feature = "tokio", not(target_os = "windows")))] async fn test_create() { let tun = tun::TunInterface::new().unwrap(); let async_tun = tun::tokio::TunInterface::new(tun).unwrap(); From 3ef13b09a3613882aa9041b7e29cba0b7e9ba31b Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sun, 6 Aug 2023 11:51:50 +0800 Subject: [PATCH 071/128] Add C Swift Bindings This adds C Swift bindings for burrow via compiling burrow with a matching header file. --- .../PacketTunnelProvider.swift | 15 +++++++ Apple/NetworkExtension/libburrow/libburrow.h | 2 +- burrow/src/lib.rs | 31 ++++++++++++++ burrow/src/main.rs | 41 +++++++++++++++++-- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index ff08261..5ca4e93 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -1,7 +1,22 @@ +import libburrow import NetworkExtension +import OSLog class PacketTunnelProvider: NEPacketTunnelProvider { + let logger = Logger(subsystem: "com.hackclub.burrow", category: "General") override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { + let fild = libburrow.retrieve() + if fild == -1 { + // Not sure if this is the right way to return an error + logger.error("Failed to retrieve file descriptor for burrow.") + let err = NSError( + domain: "com.hackclub.burrow", + code: 1_010, + userInfo: [NSLocalizedDescriptionKey: "Failed to find TunInterface"] + ) + completionHandler(err) + } + logger.info("fd: \(fild)") completionHandler(nil) } diff --git a/Apple/NetworkExtension/libburrow/libburrow.h b/Apple/NetworkExtension/libburrow/libburrow.h index 8b13789..1057c90 100644 --- a/Apple/NetworkExtension/libburrow/libburrow.h +++ b/Apple/NetworkExtension/libburrow/libburrow.h @@ -1 +1 @@ - +int retrieve(); diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index 6abc28f..687d306 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -1 +1,32 @@ pub mod ensureroot; + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +use std::{ + mem, + os::fd::{AsRawFd, FromRawFd}, +}; + +use tun::TunInterface; + +// TODO Separate start and retrieve functions + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +#[no_mangle] +pub extern "C" fn retrieve() -> i32 { + let iface2 = (1..100) + .filter_map(|i| { + let iface = unsafe { TunInterface::from_raw_fd(i) }; + match iface.name() { + Ok(_name) => Some(iface), + Err(_) => { + mem::forget(iface); + None + } + } + }) + .next(); + match iface2 { + Some(iface) => iface.as_raw_fd(), + None => -1, + } +} diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 40c54e6..4dace79 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -1,5 +1,11 @@ +use std::mem; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +use std::os::fd::FromRawFd; + use clap::{Args, Parser, Subcommand}; use tokio::io::Result; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +use burrow::retrieve; use tun::TunInterface; #[derive(Parser)] @@ -22,17 +28,41 @@ struct Cli { enum Commands { /// Start Burrow Start(StartArgs), + /// Retrieve the file descriptor of the tun interface + Retrieve(RetrieveArgs), } #[derive(Args)] struct StartArgs {} -async fn try_main() -> Result<()> { - burrow::ensureroot::ensure_root(); +#[derive(Args)] +struct RetrieveArgs {} +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_start() -> Result<()> { + burrow::ensureroot::ensure_root(); let iface = TunInterface::new()?; println!("{:?}", iface.name()); + let iface2 = retrieve(); + println!("{}", iface2); + Ok(()) +} +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_retrieve() -> Result<()> { + burrow::ensureroot::ensure_root(); + let iface2 = retrieve(); + println!("{}", iface2); + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] +async fn try_start() -> Result<()> { + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] +async fn try_retrieve() -> Result<()> { Ok(()) } @@ -43,7 +73,12 @@ async fn main() { let cli = Cli::parse(); match &cli.command { Commands::Start(..) => { - try_main().await.unwrap(); + try_start().await.unwrap(); + println!("FINISHED"); + } + Commands::Retrieve(..) => { + try_retrieve().await.unwrap(); + println!("FINISHED"); } } } From 22e41203fbae1457fefd7872801554d2996de023 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sat, 12 Aug 2023 12:05:57 +0800 Subject: [PATCH 072/128] Update project structure in readme Async tun is now located at tun/src/tokio . --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f48264f..7492039 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Apple/ # Xcode project for burrow on macOS and iOS burrow/ # Higher-level API library for tun and tun-async tun/ # Low-level interface to OS networking src/ + tokio/ # Async/Tokio code unix/ # macOS and Linux code windows/ # Windows networking code -tun-async/ # Async interface to tun ``` ## Installation From c8df4b860dd9ed11114a5bcfc1edbf82c197a6df Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Mon, 3 Jul 2023 17:29:52 +0000 Subject: [PATCH 073/128] Set/get broadcast address in TunInterface Modelled after TunInterface's IPV4 logic. Uses SIOCGIFBRDADDR & SIOCSIFBRDADDR. View https://man7.org/linux/man-pages/man7/netdevice.7.html. --- tun/src/unix/linux/mod.rs | 22 ++++++++++++++++++++++ tun/src/unix/linux/sys.rs | 2 ++ tun/tests/configure.rs | 15 +++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 7b8b05d..2b3d3a7 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -103,6 +103,28 @@ impl TunInterface { Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)) } + #[throws] + pub fn set_broadcast_addr(&self, addr: Ipv4Addr) { + let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); + let mut iff = self.ifreq()?; + iff.ifr_ifru.ifru_broadaddr = unsafe { *addr.as_ptr() }; + self.perform(|fd| unsafe { sys::if_set_brdaddr(fd, &iff) })?; + info!( + "broadcast_addr_set: {:?} (fd: {:?})", + addr, + self.as_raw_fd() + ) + } + + #[throws] + pub fn broadcast_addr(&self) -> Ipv4Addr { + let mut iff = self.ifreq()?; + self.perform(|fd| unsafe { sys::if_get_brdaddr(fd, &mut iff) })?; + let addr = + unsafe { *(&iff.ifr_ifru.ifru_broadaddr as *const _ as *const sys::sockaddr_in) }; + Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)) + } + #[throws] pub fn set_ipv6_addr(&self, addr: Ipv6Addr) { let mut iff = self.in6_ifreq()?; diff --git a/tun/src/unix/linux/sys.rs b/tun/src/unix/linux/sys.rs index 61dd50e..8d8725b 100644 --- a/tun/src/unix/linux/sys.rs +++ b/tun/src/unix/linux/sys.rs @@ -18,10 +18,12 @@ ioctl_read_bad!( ); ioctl_read_bad!(if_get_index, libc::SIOCGIFINDEX, libc::ifreq); ioctl_read_bad!(if_get_addr, libc::SIOCGIFADDR, libc::ifreq); +ioctl_read_bad!(if_get_brdaddr, libc::SIOCGIFBRDADDR, libc::ifreq); ioctl_read_bad!(if_get_mtu, libc::SIOCGIFMTU, libc::ifreq); ioctl_read_bad!(if_get_netmask, libc::SIOCGIFNETMASK, libc::ifreq); ioctl_write_ptr_bad!(if_set_addr, libc::SIOCSIFADDR, libc::ifreq); ioctl_write_ptr_bad!(if_set_addr6, libc::SIOCSIFADDR, libc::in6_ifreq); +ioctl_write_ptr_bad!(if_set_brdaddr, libc::SIOCSIFBRDADDR, libc::ifreq); ioctl_write_ptr_bad!(if_set_mtu, libc::SIOCSIFMTU, libc::ifreq); ioctl_write_ptr_bad!(if_set_netmask, libc::SIOCSIFNETMASK, libc::ifreq); diff --git a/tun/tests/configure.rs b/tun/tests/configure.rs index 35f9726..0f1199d 100644 --- a/tun/tests/configure.rs +++ b/tun/tests/configure.rs @@ -9,6 +9,21 @@ fn test_create() { TunInterface::new()?; } +#[test] +#[throws] +#[cfg(not(any(target_os = "windows", target_vendor = "apple")))] +fn test_set_get_broadcast_addr() { + let tun = TunInterface::new()?; + let addr = Ipv4Addr::new(10, 0, 0, 1); + tun.set_ipv4_addr(addr)?; + + let broadcast_addr = Ipv4Addr::new(255, 255, 255, 0); + tun.set_broadcast_addr(broadcast_addr)?; + let result = tun.broadcast_addr()?; + + assert_eq!(broadcast_addr, result); +} + #[test] #[throws] #[cfg(not(target_os = "windows"))] From f869cbdb53289ccb801444261c41e0bbde761780 Mon Sep 17 00:00:00 2001 From: dav Date: Sat, 1 Jul 2023 09:44:13 -0700 Subject: [PATCH 074/128] Implement sending commands via Unix sockets --- Cargo.lock | 426 +++++++++++++++++++------------ burrow/Cargo.toml | 7 +- burrow/src/daemon/command.rs | 13 + burrow/src/daemon/instance.rs | 40 +++ burrow/src/daemon/mod.rs | 19 ++ burrow/src/daemon/net/mod.rs | 29 +++ burrow/src/daemon/net/systemd.rs | 16 ++ burrow/src/daemon/net/unix.rs | 102 ++++++++ burrow/src/daemon/net/windows.rs | 17 ++ burrow/src/main.rs | 41 ++- systemd/burrow.service | 9 + systemd/burrow.socket | 8 + tun/Cargo.toml | 2 + tun/src/options.rs | 3 +- 14 files changed, 555 insertions(+), 177 deletions(-) create mode 100644 burrow/src/daemon/command.rs create mode 100644 burrow/src/daemon/instance.rs create mode 100644 burrow/src/daemon/mod.rs create mode 100644 burrow/src/daemon/net/mod.rs create mode 100644 burrow/src/daemon/net/systemd.rs create mode 100644 burrow/src/daemon/net/unix.rs create mode 100644 burrow/src/daemon/net/windows.rs create mode 100644 systemd/burrow.service create mode 100644 systemd/burrow.socket diff --git a/Cargo.lock b/Cargo.lock index 146bbc5..fd1eee6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -10,14 +19,13 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.7.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", "cpufeatures", - "opaque-debug", ] [[package]] @@ -46,15 +54,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" dependencies = [ "utf8parse", ] @@ -91,10 +99,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] -name = "base64" -version = "0.21.0" +name = "backtrace" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "base64ct" @@ -108,7 +131,7 @@ version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "lazy_static", @@ -121,7 +144,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.15", + "syn 2.0.22", "which", ] @@ -131,6 +154,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + [[package]] name = "block-buffer" version = "0.10.4" @@ -142,9 +171,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.2" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "burrow" @@ -153,8 +182,11 @@ dependencies = [ "caps", "clap", "env_logger", + "libsystemd", "log", "nix", + "serde", + "serde_json", "tokio", "tun", ] @@ -228,11 +260,12 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cipher" -version = "0.3.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "generic-array", + "crypto-common", + "inout", ] [[package]] @@ -248,9 +281,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.2" +version = "4.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" +checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a" dependencies = [ "clap_builder", "clap_derive", @@ -259,13 +292,12 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.1" +version = "4.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" +checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d" dependencies = [ "anstream", "anstyle", - "bitflags", "clap_lex", "strsim", ] @@ -279,7 +311,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] @@ -318,9 +350,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] @@ -336,9 +368,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] @@ -355,9 +387,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -475,9 +507,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -538,7 +570,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] @@ -581,6 +613,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + [[package]] name = "glob" version = "0.3.1" @@ -589,9 +627,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -681,9 +719,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -718,9 +756,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -736,6 +774,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -747,9 +794,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", @@ -758,19 +805,18 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.7.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" dependencies = [ "hermit-abi", - "io-lifetimes", - "rustix", + "rustix 0.38.1", "windows-sys 0.48.0", ] @@ -791,9 +837,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.62" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -812,9 +858,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libloading" @@ -827,10 +873,34 @@ dependencies = [ ] [[package]] -name = "linux-raw-sys" -version = "0.3.7" +name = "libsystemd" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" +checksum = "88b9597a67aa1c81a6624603e6bd0bcefb9e0f94c9c54970ec53771082104b4e" +dependencies = [ + "hmac", + "libc", + "log", + "nix", + "nom", + "once_cell", + "serde", + "sha2", + "thiserror", + "uuid", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "log" @@ -873,7 +943,7 @@ checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] @@ -899,14 +969,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -933,7 +1002,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", "memoffset", @@ -952,24 +1021,27 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.17.1" +name = "object" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] [[package]] -name = "opaque-debug" -version = "0.3.0" +name = "once_cell" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl" -version = "0.10.52" +version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -986,7 +1058,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] @@ -997,9 +1069,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.87" +version = "0.9.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" dependencies = [ "cc", "libc", @@ -1038,9 +1110,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project-lite" @@ -1062,28 +1134,28 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "prettyplease" -version = "0.2.4" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ceca8aaf45b5c46ec7ed39fff75f57290368c1846d33d24a122ca81416ab058" +checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282" dependencies = [ "proc-macro2", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" dependencies = [ "proc-macro2", ] @@ -1100,14 +1172,14 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -1116,15 +1188,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "reqwest" -version = "0.11.17" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ "base64", "bytes", @@ -1157,6 +1229,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1165,15 +1243,28 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "62f25693a73057a1b4cb56179dd3c7ea21a7c6c5ee7d85781f5749b46f34b79c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc6396159432b5c8490d4e301d8c705f61860b8b6c863bf79942ce5401968f3" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", "windows-sys 0.48.0", ] @@ -1194,11 +1285,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2855b3715770894e67cbfa3df957790aa0c9edc3bf06efa1a84d77fa0839d1" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -1217,15 +1308,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.22", +] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" dependencies = [ "itoa", "ryu", @@ -1268,9 +1373,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", @@ -1332,9 +1437,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -1349,9 +1454,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" dependencies = [ "proc-macro2", "quote", @@ -1360,15 +1465,16 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if", "fastrand", "redox_syscall", - "rustix", - "windows-sys 0.45.0", + "rustix 0.37.21", + "windows-sys 0.48.0", ] [[package]] @@ -1397,14 +1503,14 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] name = "time" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "serde", "time-core", @@ -1433,11 +1539,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.2" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -1455,7 +1562,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] @@ -1529,6 +1636,7 @@ dependencies = [ "log", "nix", "reqwest", + "serde", "socket2", "ssri", "tempfile", @@ -1552,9 +1660,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -1573,9 +1681,9 @@ checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", @@ -1588,6 +1696,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" +dependencies = [ + "serde", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -1602,11 +1719,10 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -1618,9 +1734,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1628,24 +1744,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -1655,9 +1771,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1665,28 +1781,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "web-sys" -version = "0.3.62" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -1746,7 +1862,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-targets", ] [[package]] @@ -1764,44 +1880,20 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", @@ -1913,9 +2005,9 @@ checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" [[package]] name = "zip" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e92305c174683d78035cbf1b70e18db6329cc0f1b9cae0a52ca90bf5bfe7125" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ "aes", "byteorder", diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index c9dd71f..9f0c193 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -7,14 +7,17 @@ edition = "2021" crate-type = ["lib", "staticlib"] [dependencies] -tokio = { version = "1.21", features = ["rt", "macros"] } -tun = { version = "0.1", path = "../tun" } +tokio = { version = "1.21", features = ["rt", "sync", "io-util", "macros"] } +tun = { version = "0.1", path = "../tun", features = ["serde"] } clap = { version = "4.3.2", features = ["derive"] } env_logger = "0.10" log = "0.4" +serde = { version = "1", features = ["derive"] } +serde_json = "1" [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5.5" +libsystemd = "0.6" [target.'cfg(target_vendor = "apple")'.dependencies] nix = { version = "0.26.2" } diff --git a/burrow/src/daemon/command.rs b/burrow/src/daemon/command.rs new file mode 100644 index 0000000..d786a99 --- /dev/null +++ b/burrow/src/daemon/command.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; +use tun::TunOptions; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DaemonCommand { + Start(DaemonStartOptions), + Stop, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct DaemonStartOptions { + pub(super) tun: TunOptions, +} diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs new file mode 100644 index 0000000..d1849d0 --- /dev/null +++ b/burrow/src/daemon/instance.rs @@ -0,0 +1,40 @@ +use super::*; + +pub struct DaemonInstance { + rx: mpsc::Receiver, + tun_interface: Option, +} + +impl DaemonInstance { + pub fn new(rx: mpsc::Receiver) -> Self { + Self { + rx, + tun_interface: None, + } + } + + pub async fn run(&mut self) -> Result<()> { + while let Some(command) = self.rx.recv().await { + match command { + DaemonCommand::Start(options) => { + if self.tun_interface.is_none() { + self.tun_interface = Some(options.tun.open()?); + eprintln!("Daemon starting tun interface."); + } else { + eprintln!("Got start, but tun interface already up."); + } + } + DaemonCommand::Stop => { + if self.tun_interface.is_some() { + self.tun_interface = None; + eprintln!("Daemon stopping tun interface."); + } else { + eprintln!("Got stop, but tun interface is not up.") + } + } + } + } + + Ok(()) + } +} diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs new file mode 100644 index 0000000..5fcf8ee --- /dev/null +++ b/burrow/src/daemon/mod.rs @@ -0,0 +1,19 @@ +use super::*; +use tokio::sync::mpsc; + +mod command; +mod instance; +mod net; + +use instance::DaemonInstance; +use net::listen; + +pub use command::{DaemonCommand, DaemonStartOptions}; +pub use net::DaemonClient; + +pub async fn daemon_main() -> Result<()> { + let (tx, rx) = mpsc::channel(2); + let mut inst = DaemonInstance::new(rx); + + tokio::try_join!(inst.run(), listen(tx)).map(|_| ()) +} diff --git a/burrow/src/daemon/net/mod.rs b/burrow/src/daemon/net/mod.rs new file mode 100644 index 0000000..d8cc5fa --- /dev/null +++ b/burrow/src/daemon/net/mod.rs @@ -0,0 +1,29 @@ +use super::*; +use serde::{Deserialize, Serialize}; + +#[cfg(target_family = "unix")] +mod unix; +#[cfg(all(target_family = "unix", not(target_os = "linux")))] +pub use unix::{listen, DaemonClient}; + +#[cfg(target_os = "linux")] +mod systemd; +#[cfg(target_os = "linux")] +pub use systemd::{listen, DaemonClient}; + +#[cfg(target_os = "windows")] +mod windows; +#[cfg(target_os = "windows")] +pub use windows::{listen, DaemonClient}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct DaemonRequest { + pub id: u32, + pub command: DaemonCommand, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct DaemonResponse { + // Error types can't be serialized, so this is the second best option. + result: std::result::Result<(), String>, +} diff --git a/burrow/src/daemon/net/systemd.rs b/burrow/src/daemon/net/systemd.rs new file mode 100644 index 0000000..f67888e --- /dev/null +++ b/burrow/src/daemon/net/systemd.rs @@ -0,0 +1,16 @@ +use super::*; +use std::os::fd::IntoRawFd; + +pub async fn listen(cmd_tx: mpsc::Sender) -> Result<()> { + if !libsystemd::daemon::booted() || listen_with_systemd(cmd_tx.clone()).await.is_err() { + unix::listen(cmd_tx).await?; + } + Ok(()) +} + +async fn listen_with_systemd(cmd_tx: mpsc::Sender) -> Result<()> { + let fds = libsystemd::activation::receive_descriptors(false).unwrap(); + super::unix::listen_with_optional_fd(cmd_tx, Some(fds[0].clone().into_raw_fd())).await +} + +pub type DaemonClient = unix::DaemonClient; diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs new file mode 100644 index 0000000..a2837a3 --- /dev/null +++ b/burrow/src/daemon/net/unix.rs @@ -0,0 +1,102 @@ +use super::*; +use std::{ + os::fd::{FromRawFd, RawFd}, + os::unix::net::UnixListener as StdUnixListener, + path::Path, +}; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::{UnixListener, UnixStream}, +}; + +const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; + +pub async fn listen(cmd_tx: mpsc::Sender) -> Result<()> { + listen_with_optional_fd(cmd_tx, None).await +} + +pub(crate) async fn listen_with_optional_fd( + cmd_tx: mpsc::Sender, + raw_fd: Option, +) -> Result<()> { + let path = Path::new(UNIX_SOCKET_PATH); + + let listener = if let Some(raw_fd) = raw_fd { + let listener = unsafe { StdUnixListener::from_raw_fd(raw_fd) }; + listener.set_nonblocking(true)?; + UnixListener::from_std(listener) + } else { + UnixListener::bind(path) + }; + let listener = if let Ok(listener) = listener { + listener + } else { + // Won't help all that much, if we use the async version of fs. + std::fs::remove_file(path)?; + UnixListener::bind(path)? + }; + loop { + let (stream, _) = listener.accept().await?; + let cmd_tx = cmd_tx.clone(); + + // I'm pretty sure we won't need to manually join / shut this down, + // `lines` will return Err during dropping, and this task should exit gracefully. + tokio::task::spawn(async { + let cmd_tx = cmd_tx; + let mut stream = stream; + let (mut read_stream, mut write_stream) = stream.split(); + let buf_reader = BufReader::new(&mut read_stream); + let mut lines = buf_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + let mut res = DaemonResponse { result: Ok(()) }; + let command = match serde_json::from_str::(&line) { + Ok(req) => Some(req.command), + Err(e) => { + res.result = Err(format!("{}", e)); + None + } + }; + let mut res = serde_json::to_string(&res).unwrap(); + res.push('\n'); + + write_stream.write_all(res.as_bytes()).await.unwrap(); + + // I want this to come at the very end so that we always send a reponse back. + if let Some(command) = command { + cmd_tx.send(command).await.unwrap(); + } + } + }); + } +} + +pub struct DaemonClient { + connection: UnixStream, +} + +impl DaemonClient { + pub async fn new() -> Result { + Self::new_with_path(UNIX_SOCKET_PATH).await + } + + pub async fn new_with_path(path: &str) -> Result { + let path = Path::new(path); + let connection = UnixStream::connect(path).await?; + + Ok(Self { connection }) + } + + pub async fn send_command(&mut self, command: DaemonCommand) -> Result<()> { + let mut command = serde_json::to_string(&DaemonRequest { id: 0, command })?; + command.push('\n'); + + self.connection.write_all(command.as_bytes()).await?; + let buf_reader = BufReader::new(&mut self.connection); + let mut lines = buf_reader.lines(); + // This unwrap *should* never cause issues. + let response = lines.next_line().await?.unwrap(); + let res: DaemonResponse = serde_json::from_str(&response)?; + res.result.unwrap(); + Ok(()) + } +} diff --git a/burrow/src/daemon/net/windows.rs b/burrow/src/daemon/net/windows.rs new file mode 100644 index 0000000..c858613 --- /dev/null +++ b/burrow/src/daemon/net/windows.rs @@ -0,0 +1,17 @@ +use super::*; + +pub async fn listen(_: mpsc::Sender) -> Result<()> { + unimplemented!("This platform does not currently support daemon mode.") +} + +pub struct DaemonClient; + +impl DaemonClient { + pub async fn new() -> Result { + unimplemented!("This platform does not currently support daemon mode.") + } + + pub async fn send_command(&mut self, _: DaemonCommand) -> Result<()> { + unimplemented!("This platform does not currently support daemon mode.") + } +} diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 4dace79..7793be2 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -8,6 +8,10 @@ use tokio::io::Result; use burrow::retrieve; use tun::TunInterface; +mod daemon; + +use daemon::{DaemonClient, DaemonCommand, DaemonStartOptions}; + #[derive(Parser)] #[command(name = "Burrow")] #[command(author = "Hack Club ")] @@ -30,6 +34,10 @@ enum Commands { Start(StartArgs), /// Retrieve the file descriptor of the tun interface Retrieve(RetrieveArgs), + /// Stop Burrow daemon + Stop, + /// Start Burrow daemon + Daemon(DaemonArgs), } #[derive(Args)] @@ -38,14 +46,15 @@ struct StartArgs {} #[derive(Args)] struct RetrieveArgs {} +#[derive(Args)] +struct DaemonArgs {} + #[cfg(any(target_os = "linux", target_vendor = "apple"))] async fn try_start() -> Result<()> { - burrow::ensureroot::ensure_root(); - let iface = TunInterface::new()?; - println!("{:?}", iface.name()); - let iface2 = retrieve(); - println!("{}", iface2); - Ok(()) + let mut client = DaemonClient::new().await?; + client + .send_command(DaemonCommand::Start(DaemonStartOptions::default())) + .await } #[cfg(any(target_os = "linux", target_vendor = "apple"))] @@ -56,6 +65,13 @@ async fn try_retrieve() -> Result<()> { Ok(()) } +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_stop() -> Result<()> { + let mut client = DaemonClient::new().await?; + client.send_command(DaemonCommand::Stop).await?; + Ok(()) +} + #[cfg(not(any(target_os = "linux", target_vendor = "apple")))] async fn try_start() -> Result<()> { Ok(()) @@ -66,8 +82,13 @@ async fn try_retrieve() -> Result<()> { Ok(()) } +#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] +async fn try_stop() -> Result<()> { + Ok(()) +} + #[tokio::main(flavor = "current_thread")] -async fn main() { +async fn main() -> Result<()> { println!("Platform: {}", std::env::consts::OS); let cli = Cli::parse(); @@ -80,5 +101,11 @@ async fn main() { try_retrieve().await.unwrap(); println!("FINISHED"); } + Commands::Stop => { + try_stop().await.unwrap(); + } + Commands::Daemon(_) => daemon::daemon_main().await?, } + + Ok(()) } diff --git a/systemd/burrow.service b/systemd/burrow.service new file mode 100644 index 0000000..496b307 --- /dev/null +++ b/systemd/burrow.service @@ -0,0 +1,9 @@ +[Unit] +Description=Burrow +After=burrow.socket + +[Service] +ExecStart=/usr/local/bin/burrow daemon + +[Install] +WantedBy=multi-user.target diff --git a/systemd/burrow.socket b/systemd/burrow.socket new file mode 100644 index 0000000..c5da49d --- /dev/null +++ b/systemd/burrow.socket @@ -0,0 +1,8 @@ +[Unit] +Description=Burrow Socket + +[Socket] +ListenStream=/run/burrow.sock + +[Install] +WantedBy=sockets.target diff --git a/tun/Cargo.toml b/tun/Cargo.toml index 79b2735..c0151fc 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -11,10 +11,12 @@ socket2 = "0.4" tokio = { version = "1.28", features = [] } byteorder = "1.4" log = "0.4" +serde = { version = "1", features = ["derive"], optional = true } futures = { version = "0.3.28", optional = true } [features] +serde = ["dep:serde"] tokio = ["tokio/net", "dep:futures"] [target.'cfg(feature = "tokio")'.dev-dependencies] diff --git a/tun/src/options.rs b/tun/src/options.rs index 4c81a83..e766be8 100644 --- a/tun/src/options.rs +++ b/tun/src/options.rs @@ -3,7 +3,8 @@ use std::io::Error; use super::TunInterface; -#[derive(Default)] +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TunOptions { /// (Windows + Linux) Name the tun interface. pub(crate) name: Option, From 60cfd95789f635d3622b8ab06178fd92b9500e93 Mon Sep 17 00:00:00 2001 From: dav Date: Sat, 26 Aug 2023 11:33:58 -0700 Subject: [PATCH 075/128] Add rust build caching to ci --- .github/workflows/build-rust.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index f56e26b..0993ec2 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -60,6 +60,8 @@ jobs: toolchain: stable components: rustfmt targets: ${{ join(matrix.targets, ', ') }} + - name: Setup Rust Cache + uses: Swatinem/rust-cache@v2 - name: Build shell: bash run: cargo build --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} --target ${{ join(matrix.test-targets, ' --target ') }} From e643d9dd41d010432c5ba211ab2081c13aac0726 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sun, 27 Aug 2023 11:43:17 +0800 Subject: [PATCH 076/128] Switch logging to use tracing instead of log Tracing has support for intervals and a great os_log integration. --- Cargo.lock | 181 +++++++++++++++++++++++++++++++++++++- burrow/Cargo.toml | 8 +- burrow/src/ensureroot.rs | 5 ++ burrow/src/lib.rs | 1 + burrow/src/main.rs | 42 ++++++++- tun/Cargo.toml | 1 + tun/src/lib.rs | 2 + tun/src/tokio/mod.rs | 5 ++ tun/src/unix/apple/mod.rs | 18 +++- tun/src/unix/linux/mod.rs | 24 ++++- tun/src/unix/mod.rs | 4 + tun/src/unix/queue.rs | 3 + tun/src/windows/mod.rs | 10 +++ tun/src/windows/queue.rs | 1 + 14 files changed, 297 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd1eee6..f7cf03a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,28 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", + "which", +] + [[package]] name = "bindgen" version = "0.65.1" @@ -179,6 +201,7 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" name = "burrow" version = "0.1.0" dependencies = [ + "anyhow", "caps", "clap", "env_logger", @@ -188,6 +211,11 @@ dependencies = [ "serde", "serde_json", "tokio", + "tracing", + "tracing-journald", + "tracing-log", + "tracing-oslog", + "tracing-subscriber", "tun", ] @@ -902,6 +930,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.19" @@ -1020,6 +1058,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "object" version = "0.31.1" @@ -1079,6 +1127,35 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -1283,6 +1360,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "security-framework" version = "2.9.1" @@ -1382,6 +1465,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.1.0" @@ -1397,6 +1489,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + [[package]] name = "socket2" version = "0.4.9" @@ -1506,6 +1604,16 @@ dependencies = [ "syn 2.0.22", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.22" @@ -1603,9 +1711,21 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.22", +] + [[package]] name = "tracing-core" version = "0.1.31" @@ -1613,6 +1733,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-journald" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba316a74e8fc3c3896a850dba2375928a9fa171b085ecddfc7c054d39970f3fd" +dependencies = [ + "libc", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-oslog" +version = "0.1.2" +source = "git+https://github.com/Stormshield-robinc/tracing-oslog#c4d21a95e70cdd62b1cea04fc4f8be1c547cad6c" +dependencies = [ + "bindgen 0.64.0", + "cc", + "cfg-if", + "fnv", + "once_cell", + "parking_lot", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -1626,7 +1798,7 @@ name = "tun" version = "0.1.0" dependencies = [ "anyhow", - "bindgen", + "bindgen 0.65.1", "byteorder", "fehler", "futures", @@ -1641,6 +1813,7 @@ dependencies = [ "ssri", "tempfile", "tokio", + "tracing", "widestring", "windows", "zip", @@ -1705,6 +1878,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 9f0c193..c9d0e67 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -7,9 +7,15 @@ edition = "2021" crate-type = ["lib", "staticlib"] [dependencies] -tokio = { version = "1.21", features = ["rt", "sync", "io-util", "macros"] } +anyhow = "1.0" +tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util"] } tun = { version = "0.1", path = "../tun", features = ["serde"] } clap = { version = "4.3.2", features = ["derive"] } +tracing = "0.1" +tracing-log = "0.1" +tracing-journald = "0.3" +tracing-oslog = {git = "https://github.com/Stormshield-robinc/tracing-oslog"} +tracing-subscriber = "0.3" env_logger = "0.10" log = "0.4" serde = { version = "1", features = ["derive"] } diff --git a/burrow/src/ensureroot.rs b/burrow/src/ensureroot.rs index 8c1d33e..b7c0757 100644 --- a/burrow/src/ensureroot.rs +++ b/burrow/src/ensureroot.rs @@ -1,5 +1,8 @@ +use tracing::instrument; + // Check capabilities on Linux #[cfg(target_os = "linux")] +#[instrument] pub fn ensure_root() { use caps::{has_cap, CapSet, Capability}; @@ -19,6 +22,7 @@ pub fn ensure_root() { // Check for root user on macOS #[cfg(target_vendor = "apple")] +#[instrument] pub fn ensure_root() { use nix::unistd::Uid; @@ -30,6 +34,7 @@ pub fn ensure_root() { } #[cfg(target_family = "windows")] +#[instrument] pub fn ensure_root() { todo!() } diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index 687d306..1032e97 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -1,3 +1,4 @@ +#![deny(missing_debug_implementations)] pub mod ensureroot; #[cfg(any(target_os = "linux", target_vendor = "apple"))] diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 7793be2..1f70b1c 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -1,8 +1,14 @@ +use anyhow::Context; use std::mem; #[cfg(any(target_os = "linux", target_vendor = "apple"))] use std::os::fd::FromRawFd; use clap::{Args, Parser, Subcommand}; +use tracing::instrument; + +use tracing_log::LogTracer; +use tracing_oslog::OsLogger; +use tracing_subscriber::{prelude::*, FmtSubscriber}; use tokio::io::Result; #[cfg(any(target_os = "linux", target_vendor = "apple"))] use burrow::retrieve; @@ -58,10 +64,21 @@ async fn try_start() -> Result<()> { } #[cfg(any(target_os = "linux", target_vendor = "apple"))] +#[instrument] async fn try_retrieve() -> Result<()> { + LogTracer::init().context("Failed to initialize LogTracer").unwrap(); + + if cfg!(target_os = "linux") || cfg!(target_vendor = "apple") { + let maybe_layer = system_log().unwrap(); + if let Some(layer) = maybe_layer { + let logger = layer.with_subscriber(FmtSubscriber::new()); + tracing::subscriber::set_global_default(logger).context("Failed to set the global tracing subscriber").unwrap(); + } + } + burrow::ensureroot::ensure_root(); let iface2 = retrieve(); - println!("{}", iface2); + tracing::info!("{}", iface2); Ok(()) } @@ -89,17 +106,17 @@ async fn try_stop() -> Result<()> { #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { - println!("Platform: {}", std::env::consts::OS); + tracing::info!("Platform: {}", std::env::consts::OS); let cli = Cli::parse(); match &cli.command { Commands::Start(..) => { try_start().await.unwrap(); - println!("FINISHED"); + tracing::info!("FINISHED"); } Commands::Retrieve(..) => { try_retrieve().await.unwrap(); - println!("FINISHED"); + tracing::info!("FINISHED"); } Commands::Stop => { try_stop().await.unwrap(); @@ -109,3 +126,20 @@ async fn main() -> Result<()> { Ok(()) } + +#[cfg(target_os = "linux")] +fn system_log() -> anyhow::Result> { + let maybe_journald = tracing_journald::layer(); + match maybe_journald { + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + tracing::trace!("journald not found"); + Ok(None) + }, + _ => Ok(Some(maybe_journald?)) + } +} + +#[cfg(target_vendor = "apple")] +fn system_log() -> anyhow::Result> { + Ok(Some(OsLogger::new("com.hackclub.burrow", "default"))) +} diff --git a/tun/Cargo.toml b/tun/Cargo.toml index c0151fc..2979c18 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -10,6 +10,7 @@ nix = { version = "0.26", features = ["ioctl"] } socket2 = "0.4" tokio = { version = "1.28", features = [] } byteorder = "1.4" +tracing = "0.1" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } diff --git a/tun/src/lib.rs b/tun/src/lib.rs index f0c0a6e..151c10d 100644 --- a/tun/src/lib.rs +++ b/tun/src/lib.rs @@ -1,3 +1,5 @@ +#![deny(missing_debug_implementations)] + #[cfg(target_os = "windows")] #[path = "windows/mod.rs"] mod imp; diff --git a/tun/src/tokio/mod.rs b/tun/src/tokio/mod.rs index ae99b73..7828279 100644 --- a/tun/src/tokio/mod.rs +++ b/tun/src/tokio/mod.rs @@ -1,17 +1,21 @@ use std::io; use tokio::io::unix::AsyncFd; +use tracing::instrument; +#[derive(Debug)] pub struct TunInterface { inner: AsyncFd, } impl TunInterface { + #[instrument] pub fn new(tun: crate::TunInterface) -> io::Result { Ok(Self { inner: AsyncFd::new(tun)?, }) } + #[instrument] pub async fn write(&self, buf: &[u8]) -> io::Result { loop { let mut guard = self.inner.writable().await?; @@ -22,6 +26,7 @@ impl TunInterface { } } + #[instrument] pub async fn read(&mut self, buf: &mut [u8]) -> io::Result { loop { let mut guard = self.inner.readable_mut().await?; diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 427f5e8..f4fd1e2 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,12 +1,13 @@ use byteorder::{ByteOrder, NetworkEndian}; use fehler::throws; use libc::{c_char, iovec, writev, AF_INET, AF_INET6}; -use log::info; +use tracing::info; use socket2::{Domain, SockAddr, Socket, Type}; use std::io::IoSlice; use std::net::{Ipv4Addr, SocketAddrV4}; use std::os::fd::{AsRawFd, RawFd}; use std::{io::Error, mem}; +use tracing::instrument; mod kern_control; mod sys; @@ -23,16 +24,19 @@ pub struct TunInterface { impl TunInterface { #[throws] + #[instrument] pub fn new() -> TunInterface { Self::new_with_options(TunOptions::new())? } #[throws] + #[instrument] pub fn new_with_options(_: TunOptions) -> TunInterface { TunInterface::connect(0)? } #[throws] + #[instrument] fn connect(index: u32) -> TunInterface { use socket2::{Domain, Protocol, Socket, Type}; @@ -48,6 +52,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn name(&self) -> String { let mut buf = [0 as c_char; sys::IFNAMSIZ]; let mut len = buf.len() as sys::socklen_t; @@ -62,6 +67,7 @@ impl TunInterface { } #[throws] + #[instrument] fn ifreq(&self) -> sys::ifreq { let mut iff: sys::ifreq = unsafe { mem::zeroed() }; iff.ifr_name = string_to_ifname(&self.name()?); @@ -69,6 +75,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn set_ipv4_addr(&self, addr: Ipv4Addr) { let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); let mut iff = self.ifreq()?; @@ -78,6 +85,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn ipv4_addr(&self) -> Ipv4Addr { let mut iff = self.ifreq()?; self.perform(|fd| unsafe { sys::if_get_addr(fd, &mut iff) })?; @@ -87,11 +95,15 @@ impl TunInterface { #[throws] fn perform(&self, perform: impl FnOnce(RawFd) -> Result) -> R { + let span = tracing::info_span!("perform", fd = self.as_raw_fd()); + let _enter = span.enter(); + let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?; perform(socket.as_raw_fd())? } #[throws] + #[instrument] pub fn mtu(&self) -> i32 { let mut iff = self.ifreq()?; self.perform(|fd| unsafe { sys::if_get_mtu(fd, &mut iff) })?; @@ -101,6 +113,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn set_mtu(&self, mtu: i32) { let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_mtu = mtu; @@ -109,6 +122,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn netmask(&self) -> Ipv4Addr { let mut iff = self.ifreq()?; self.perform(|fd| unsafe { sys::if_get_netmask(fd, &mut iff) })?; @@ -120,6 +134,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn set_netmask(&self, addr: Ipv4Addr) { let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); let mut iff = self.ifreq()?; @@ -133,6 +148,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn send(&self, buf: &[u8]) -> usize { use std::io::ErrorKind; let proto = match buf[0] >> 4 { diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 2b3d3a7..75bb9d2 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -8,7 +8,7 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}; use std::os::fd::RawFd; use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; -use log::info; +use tracing::{info, instrument}; use libc::in6_ifreq; @@ -23,11 +23,13 @@ pub struct TunInterface { impl TunInterface { #[throws] + #[instrument] pub fn new() -> TunInterface { Self::new_with_options(TunOptions::new())? } #[throws] + #[instrument] pub(crate) fn new_with_options(options: TunOptions) -> TunInterface { let file = OpenOptions::new() .read(true) @@ -59,6 +61,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn name(&self) -> String { let mut iff = unsafe { mem::zeroed() }; unsafe { sys::tun_get_iff(self.socket.as_raw_fd(), &mut iff)? }; @@ -66,6 +69,7 @@ impl TunInterface { } #[throws] + #[instrument] fn ifreq(&self) -> sys::ifreq { let mut iff: sys::ifreq = unsafe { mem::zeroed() }; iff.ifr_name = string_to_ifname(&self.name()?); @@ -73,6 +77,7 @@ impl TunInterface { } #[throws] + #[instrument] fn in6_ifreq(&self) -> in6_ifreq { let mut iff: in6_ifreq = unsafe { mem::zeroed() }; iff.ifr6_ifindex = self.index()?; @@ -80,6 +85,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn index(&self) -> i32 { let mut iff = self.ifreq()?; self.perform(|fd| unsafe { sys::if_get_index(fd, &mut iff) })?; @@ -87,6 +93,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn set_ipv4_addr(&self, addr: Ipv4Addr) { let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); let mut iff = self.ifreq()?; @@ -96,6 +103,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn ipv4_addr(&self) -> Ipv4Addr { let mut iff = self.ifreq()?; self.perform(|fd| unsafe { sys::if_get_addr(fd, &mut iff) })?; @@ -104,6 +112,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn set_broadcast_addr(&self, addr: Ipv4Addr) { let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); let mut iff = self.ifreq()?; @@ -117,6 +126,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn broadcast_addr(&self) -> Ipv4Addr { let mut iff = self.ifreq()?; self.perform(|fd| unsafe { sys::if_get_brdaddr(fd, &mut iff) })?; @@ -126,6 +136,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn set_ipv6_addr(&self, addr: Ipv6Addr) { let mut iff = self.in6_ifreq()?; iff.ifr6_addr.s6_addr = addr.octets(); @@ -134,6 +145,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn set_mtu(&self, mtu: i32) { let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_mtu = mtu; @@ -142,6 +154,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn mtu(&self) -> i32 { let mut iff = self.ifreq()?; self.perform(|fd| unsafe { sys::if_get_mtu(fd, &mut iff) })?; @@ -151,6 +164,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn set_netmask(&self, addr: Ipv4Addr) { let addr = SockAddr::from(SocketAddrV4::new(addr, 0)); @@ -167,6 +181,7 @@ impl TunInterface { } #[throws] + #[instrument] pub fn netmask(&self) -> Ipv4Addr { let mut iff = self.ifreq()?; self.perform(|fd| unsafe { sys::if_get_netmask(fd, &mut iff) })?; @@ -179,17 +194,24 @@ impl TunInterface { #[throws] fn perform(&self, perform: impl FnOnce(RawFd) -> Result) -> R { + let span = tracing::info_span!("perform"); + let _enter = span.enter(); + let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?; perform(socket.as_raw_fd())? } #[throws] fn perform6(&self, perform: impl FnOnce(RawFd) -> Result) -> R { + let span = tracing::info_span!("perform"); + let _enter = span.enter(); + let socket = Socket::new(Domain::IPV6, Type::DGRAM, None)?; perform(socket.as_raw_fd())? } #[throws] + #[instrument] pub fn send(&self, buf: &[u8]) -> usize { self.socket.send(buf)? } diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs index 1defbbd..9da4204 100644 --- a/tun/src/unix/mod.rs +++ b/tun/src/unix/mod.rs @@ -2,6 +2,7 @@ use std::{ io::{Error, Read}, os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}, }; +use tracing::instrument; use super::TunOptions; @@ -41,11 +42,13 @@ impl IntoRawFd for TunInterface { impl TunInterface { #[throws] + #[instrument] pub fn recv(&mut self, buf: &mut [u8]) -> usize { self.socket.read(buf)? } } +#[instrument] pub fn ifname_to_string(buf: [libc::c_char; libc::IFNAMSIZ]) -> String { // TODO: Switch to `CStr::from_bytes_until_nul` when stabilized unsafe { @@ -56,6 +59,7 @@ pub fn ifname_to_string(buf: [libc::c_char; libc::IFNAMSIZ]) -> String { } } +#[instrument] pub fn string_to_ifname(name: &str) -> [libc::c_char; libc::IFNAMSIZ] { let mut buf = [0 as libc::c_char; libc::IFNAMSIZ]; let len = name.len().min(buf.len()); diff --git a/tun/src/unix/queue.rs b/tun/src/unix/queue.rs index 1288a3b..923f926 100644 --- a/tun/src/unix/queue.rs +++ b/tun/src/unix/queue.rs @@ -5,15 +5,18 @@ use std::{ mem::MaybeUninit, os::unix::io::{AsRawFd, IntoRawFd, RawFd}, }; +use tracing::instrument; use crate::TunInterface; +#[derive(Debug)] pub struct TunQueue { socket: socket2::Socket, } impl TunQueue { #[throws] + #[instrument] pub fn recv(&self, buf: &mut [MaybeUninit]) -> usize { self.socket.recv(buf)? } diff --git a/tun/src/windows/mod.rs b/tun/src/windows/mod.rs index c7b1ba5..bae75c0 100644 --- a/tun/src/windows/mod.rs +++ b/tun/src/windows/mod.rs @@ -1,3 +1,4 @@ +use std::fmt::Debug; use fehler::throws; use std::io::Error; use std::ptr; @@ -14,6 +15,15 @@ pub struct TunInterface { name: String, } +impl Debug for TunInterface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TunInterface") + .field("handle", &"SYS_WINTUN_ADAPTER_HANDLE".to_string()) + .field("name", &self.name) + .finish() + } +} + impl TunInterface { #[throws] pub fn new() -> TunInterface { diff --git a/tun/src/windows/queue.rs b/tun/src/windows/queue.rs index 609da89..8fa9e19 100644 --- a/tun/src/windows/queue.rs +++ b/tun/src/windows/queue.rs @@ -1 +1,2 @@ +#[derive(Debug)] pub struct TunQueue; From 26d97c6a1115ed4e25f1815a232098fe646643c7 Mon Sep 17 00:00:00 2001 From: dav Date: Tue, 3 Oct 2023 22:26:15 -0700 Subject: [PATCH 077/128] Super Basic RPM Install Config --- burrow/Cargo.toml | 12 ++++++++++++ package/rpm/post_install | 2 ++ package/rpm/pre_uninstall | 3 +++ systemd/burrow.service | 2 +- 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 package/rpm/post_install create mode 100644 package/rpm/pre_uninstall diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index c9d0e67..63da36c 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -2,6 +2,9 @@ name = "burrow" version = "0.1.0" edition = "2021" +description = "" +license = "GPL-3.0-or-later" +license-file = "../LICENSE.md" [lib] crate-type = ["lib", "staticlib"] @@ -27,3 +30,12 @@ libsystemd = "0.6" [target.'cfg(target_vendor = "apple")'.dependencies] nix = { version = "0.26.2" } + +[package.metadata.generate-rpm] +assets = [ + { source = "target/release/burrow", dest = "/usr/bin/burrow", mode = "755" }, + { source = "systemd/burrow.service", dest = "/etc/systemd/system/burrow.service", mode = "644" }, + { source = "systemd/burrow.socket", dest = "/etc/systemd/system/burrow.socket", mode = "644" }, +] +post_install_script = "../package/rpm/post_install" +pre_uninstall_script = "../package/rpm/pre_uninstall" diff --git a/package/rpm/post_install b/package/rpm/post_install new file mode 100644 index 0000000..751c190 --- /dev/null +++ b/package/rpm/post_install @@ -0,0 +1,2 @@ +systemctl daemon-reload +systemctl enable burrow diff --git a/package/rpm/pre_uninstall b/package/rpm/pre_uninstall new file mode 100644 index 0000000..e0fef26 --- /dev/null +++ b/package/rpm/pre_uninstall @@ -0,0 +1,3 @@ +systemctl disable burrow.service > /dev/null 2>&1 +systemctl stop burrow.service > /dev/null 2>&1 +systemctl daemon-reload diff --git a/systemd/burrow.service b/systemd/burrow.service index 496b307..8d35355 100644 --- a/systemd/burrow.service +++ b/systemd/burrow.service @@ -3,7 +3,7 @@ Description=Burrow After=burrow.socket [Service] -ExecStart=/usr/local/bin/burrow daemon +ExecStart=/usr/bin/burrow daemon [Install] WantedBy=multi-user.target From fb5cff0d5a3a395e0f7aaacd3deb0cf2a760865b Mon Sep 17 00:00:00 2001 From: dav Date: Tue, 3 Oct 2023 22:36:44 -0700 Subject: [PATCH 078/128] Add Github Workflow for RPM Builds --- .github/workflows/build-rpm.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/build-rpm.yml diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml new file mode 100644 index 0000000..08e3ae1 --- /dev/null +++ b/.github/workflows/build-rpm.yml @@ -0,0 +1,19 @@ +name: Build RPM +on: + push: + branches: [ "main" ] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + - name: Install + run: cargo install cargo-generate-rpm + - name: Build + run: + cargo build --release | + strip -s target/release/burrow + - name: Build RPM + run: cargo cargo-generate-rpm -p burrow + From 391802b8cad78a50f582d9ce339d7387607f3c75 Mon Sep 17 00:00:00 2001 From: dav Date: Tue, 3 Oct 2023 22:39:08 -0700 Subject: [PATCH 079/128] Build RPM for Pull Requests too --- .github/workflows/build-rpm.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml index 08e3ae1..c1671fd 100644 --- a/.github/workflows/build-rpm.yml +++ b/.github/workflows/build-rpm.yml @@ -2,6 +2,9 @@ name: Build RPM on: push: branches: [ "main" ] + pull_request: + branches: + - "*" jobs: build: runs-on: ubuntu-latest From 6a77df3e2049345a08bd10d9c976aead4c0cf34d Mon Sep 17 00:00:00 2001 From: dav Date: Fri, 6 Oct 2023 21:55:18 -0700 Subject: [PATCH 080/128] Remove license-file field --- burrow/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 63da36c..4fe2288 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2021" description = "" license = "GPL-3.0-or-later" -license-file = "../LICENSE.md" [lib] crate-type = ["lib", "staticlib"] From 674deb08ba2f140b0a9c85ff2e6507941b168a8f Mon Sep 17 00:00:00 2001 From: dav Date: Fri, 6 Oct 2023 21:56:55 -0700 Subject: [PATCH 081/128] Fix RPM CI --- .github/workflows/build-rpm.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml index c1671fd..c2c6406 100644 --- a/.github/workflows/build-rpm.yml +++ b/.github/workflows/build-rpm.yml @@ -14,8 +14,8 @@ jobs: - name: Install run: cargo install cargo-generate-rpm - name: Build - run: - cargo build --release | + run: | + cargo build --release strip -s target/release/burrow - name: Build RPM run: cargo cargo-generate-rpm -p burrow From c059512b8b7321a527c40e2363fe36d30971a8f7 Mon Sep 17 00:00:00 2001 From: dav Date: Fri, 6 Oct 2023 22:04:05 -0700 Subject: [PATCH 082/128] RPM CI Should be cargo generate-rpm --- .github/workflows/build-rpm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml index c2c6406..9d11874 100644 --- a/.github/workflows/build-rpm.yml +++ b/.github/workflows/build-rpm.yml @@ -18,5 +18,5 @@ jobs: cargo build --release strip -s target/release/burrow - name: Build RPM - run: cargo cargo-generate-rpm -p burrow + run: cargo generate-rpm -p burrow From 6368ca7f7474b372ed4ee7bc24664dcd49b29679 Mon Sep 17 00:00:00 2001 From: dav Date: Sat, 7 Oct 2023 23:34:15 -0700 Subject: [PATCH 083/128] Add Burrow Crate Description --- burrow/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 4fe2288..ddce4b1 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -2,7 +2,7 @@ name = "burrow" version = "0.1.0" edition = "2021" -description = "" +description = "Burrow is an open source tool for burrowing through firewalls, built by teenagers at Hack Club." license = "GPL-3.0-or-later" [lib] From c9f104e5234b170ab323b9bf7b943b63d1ac6fc4 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sun, 1 Oct 2023 11:49:00 +0800 Subject: [PATCH 084/128] Generate NetworkSettings with IPC This generates and applies NetworkSettings object with unix socket IPC. - domain socket, json-rpc based communication - switches to anyhow for burrow crate - adds support for starting daemons on macos --- .gitignore | 3 + Apple/Burrow.xcodeproj/project.pbxproj | 8 + Apple/NetworkExtension/BurrowIpc.swift | 142 ++++++++++++ Apple/NetworkExtension/DataTypes.swift | 40 ++++ .../NetworkExtension-macOS.entitlements | 2 + .../PacketTunnelProvider.swift | 63 +++-- Apple/NetworkExtension/libburrow/libburrow.h | 3 +- Cargo.lock | 217 ++++++++++++++++-- burrow/Cargo.toml | 11 +- burrow/src/apple.rs | 15 ++ burrow/src/daemon/command.rs | 23 +- burrow/src/daemon/instance.rs | 66 ++++-- burrow/src/daemon/mod.rs | 15 +- burrow/src/daemon/net/apple.rs | 24 ++ burrow/src/daemon/net/mod.rs | 12 +- burrow/src/daemon/net/systemd.rs | 12 +- burrow/src/daemon/net/unix.rs | 99 ++++++-- burrow/src/daemon/net/windows.rs | 2 +- burrow/src/daemon/response.rs | 109 +++++++++ ...ommand__daemoncommand_serialization-2.snap | 5 + ...ommand__daemoncommand_serialization-3.snap | 5 + ...ommand__daemoncommand_serialization-4.snap | 5 + ..._command__daemoncommand_serialization.snap | 5 + ...n__response__response_serialization-2.snap | 5 + ...n__response__response_serialization-3.snap | 5 + ...n__response__response_serialization-4.snap | 5 + ...mon__response__response_serialization.snap | 5 + burrow/src/lib.rs | 11 + burrow/src/main.rs | 104 +++++++-- tun/Cargo.toml | 3 +- tun/src/options.rs | 2 +- 31 files changed, 909 insertions(+), 117 deletions(-) create mode 100644 Apple/NetworkExtension/BurrowIpc.swift create mode 100644 Apple/NetworkExtension/DataTypes.swift create mode 100644 burrow/src/apple.rs create mode 100644 burrow/src/daemon/net/apple.rs create mode 100644 burrow/src/daemon/response.rs create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-2.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-3.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization.snap diff --git a/.gitignore b/.gitignore index 102ee0d..11c6ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ xcuserdata # Rust target/ + +.DS_STORE +.idea/ \ No newline at end of file diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index f9c7454..7548f3e 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; + 0B46E8E02AC918CA00BA2A3C /* BurrowIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* BurrowIpc.swift */; }; 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuView.swift */; }; D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; }; D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; @@ -46,6 +48,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; + 0B46E8DF2AC918CA00BA2A3C /* BurrowIpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowIpc.swift; sourceTree = ""; }; 43AA26D72A10004900F14CE6 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; @@ -122,6 +126,8 @@ D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */, D020F66629E4A95D002790F6 /* NetworkExtension-macOS.entitlements */, D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */, + 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, + 0B46E8DF2AC918CA00BA2A3C /* BurrowIpc.swift */, D0B98FD729FDDB57004E7149 /* libburrow */, ); path = NetworkExtension; @@ -304,6 +310,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */, + 0B46E8E02AC918CA00BA2A3C /* BurrowIpc.swift in Sources */, D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Apple/NetworkExtension/BurrowIpc.swift b/Apple/NetworkExtension/BurrowIpc.swift new file mode 100644 index 0000000..0992bfc --- /dev/null +++ b/Apple/NetworkExtension/BurrowIpc.swift @@ -0,0 +1,142 @@ +import Foundation +import Network +import os + +final class LineProtocol: NWProtocolFramerImplementation { + static let definition = NWProtocolFramer.Definition(implementation: LineProtocol.self) + static let label = "Lines" + init(framer: NWProtocolFramer.Instance) { } + func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { .ready } + func stop(framer: NWProtocolFramer.Instance) -> Bool { true } + func wakeup(framer: NWProtocolFramer.Instance) { } + func cleanup(framer: NWProtocolFramer.Instance) { } + func lines(from buffer: UnsafeMutableRawBufferPointer?) -> (lines: [Data], size: Int)? { + guard let buffer = buffer else { return nil } + let lines = buffer + .split(separator: 10) + guard !lines.isEmpty else { return nil } + let size = lines + .lazy + .map(\.count) + .reduce(0, +) + lines.count + let strings = lines + .lazy + .map { Data($0) } + return (lines: Array(strings), size: size) + } + func handleInput(framer: NWProtocolFramer.Instance) -> Int { + var result: [Data] = [] + framer.parseInput(minimumIncompleteLength: 1, maximumLength: 16_000) { buffer, _ in + guard let (lines, size) = lines(from: buffer) else { + return 0 + } + result = lines + return size + } + for line in result { + framer.deliverInput(data: line, message: .init(instance: framer), isComplete: true) + } + return 0 + } + func handleOutput( + framer: NWProtocolFramer.Instance, + message: NWProtocolFramer.Message, + messageLength: Int, + isComplete: Bool + ) { + do { + try framer.writeOutputNoCopy(length: messageLength) + } catch { + } + } +} + +extension NWConnection { + func receiveMessage() async throws -> (Data?, NWConnection.ContentContext?, Bool) { + try await withUnsafeThrowingContinuation { continuation in + receiveMessage { completeContent, contentContext, isComplete, error in + if let error = error { + continuation.resume(throwing: error) + } + continuation.resume(returning: (completeContent, contentContext, isComplete)) + } + } + } +} + +final class BurrowIpc { + let connection: NWConnection + private var generator = SystemRandomNumberGenerator() + private var continuations: [UInt: UnsafeContinuation] = [:] + private var logger: Logger + init(logger: Logger) { + let params = NWParameters.tcp + params.defaultProtocolStack + .applicationProtocols + .insert(NWProtocolFramer.Options(definition: LineProtocol.definition), at: 0) + let connection = NWConnection(to: .unix(path: "burrow.sock"), using: params) + connection.start(queue: .global()) + self.connection = connection + self.logger = logger + } + func send(_ request: T) async throws -> U { + let data: Data = try await withUnsafeThrowingContinuation { continuation in + let id: UInt = generator.next(upperBound: UInt.max) + continuations[id] = continuation + var copy = request + copy.id = id + do { + var data = try JSONEncoder().encode(request) + data.append(contentsOf: [10]) + let completion: NWConnection.SendCompletion = .contentProcessed { error in + guard let error = error else { return } + continuation.resume(throwing: error) + } + connection.send(content: data, completion: completion) + } catch { + continuation.resume(throwing: error) + return + } + } + return try JSONDecoder().decode(Response.self, from: data).result + } + func send_raw(_ request: Data) async throws -> Data { + try await withCheckedThrowingContinuation { continuation in + let comp: NWConnection.SendCompletion = .contentProcessed {error in + if let error = error { + continuation.resume(with: .failure(error)) + } else { + continuation.resume(with: .success(request)) + } + } + self.connection.send(content: request, completion: comp) + } + } + + func receive_raw() async throws -> Data { + let (completeContent, _, _) = try await connection.receiveMessage() + self.logger.info("Received raw message response") + guard let data = completeContent else { + throw BurrowError.resultIsNone + } + return data + } + + func request(_ request: Request, type: U.Type) async throws -> U { + do { + var data: Data = try JSONEncoder().encode(request) + data.append(contentsOf: [10]) + try await send_raw(data) + self.logger.debug("message sent") + let receivedData = try await receive_raw() + self.logger.info("Received result: \(String(decoding: receivedData, as: UTF8.self))") + return try self.parse_response(receivedData) + } catch { + throw error + } + } + + func parse_response(_ response: Data) throws -> U { + try JSONDecoder().decode(U.self, from: response) + } +} diff --git a/Apple/NetworkExtension/DataTypes.swift b/Apple/NetworkExtension/DataTypes.swift new file mode 100644 index 0000000..b228d77 --- /dev/null +++ b/Apple/NetworkExtension/DataTypes.swift @@ -0,0 +1,40 @@ +import Foundation + +enum BurrowError: Error { + case addrDoesntExist + case resultIsError + case cantParseResult + case resultIsNone +} + +protocol Request: Codable { + var id: UInt { get set } + var command: String { get set } +} + +struct BurrowRequest: Request { + var id: UInt + var command: String +} + +struct Response: Decodable where T: Decodable { + var id: UInt + var result: T +} + +// swiftlint:disable identifier_name +struct BurrowResult: Codable where T: Codable { + var Ok: T? + var Err: String? +} + +struct ServerConfigData: Codable { + struct InternalConfig: Codable { + let address: String? + let name: String? + let mtu: Int32? + } + let ServerConfig: InternalConfig +} + +// swiftlint:enable identifier_name diff --git a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements index 02ee960..c3d6dc2 100644 --- a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements +++ b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements @@ -2,6 +2,8 @@ + com.apple.security.network.client + com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 5ca4e93..d3a3e5d 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -1,39 +1,66 @@ import libburrow import NetworkExtension -import OSLog +import os class PacketTunnelProvider: NEPacketTunnelProvider { - let logger = Logger(subsystem: "com.hackclub.burrow", category: "General") + let logger = Logger(subsystem: "com.hackclub.burrow", category: "frontend") + var client: BurrowIpc? + var osInitialized = false override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { - let fild = libburrow.retrieve() - if fild == -1 { - // Not sure if this is the right way to return an error - logger.error("Failed to retrieve file descriptor for burrow.") - let err = NSError( - domain: "com.hackclub.burrow", - code: 1_010, - userInfo: [NSLocalizedDescriptionKey: "Failed to find TunInterface"] - ) - completionHandler(err) + logger.log("Starting tunnel") + if !osInitialized { + libburrow.initialize_oslog() + osInitialized = true + } + libburrow.start_srv() + client = BurrowIpc(logger: logger) + logger.info("Started server") + Task { + do { + let command = BurrowRequest(id: 0, command: "ServerConfig") + guard let data = try await client?.request(command, type: Response>.self) + else { + throw BurrowError.cantParseResult + } + let encoded = try JSONEncoder().encode(data.result) + self.logger.log("Received final data: \(String(decoding: encoded, as: UTF8.self))") + guard let serverconfig = data.result.Ok else { + throw BurrowError.resultIsError + } + guard let tunNs = self.generateTunSettings(from: serverconfig) else { + throw BurrowError.addrDoesntExist + } + try await self.setTunnelNetworkSettings(tunNs) + self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") + completionHandler(nil) + } catch { + self.logger.error("An error occurred: \(error)") + completionHandler(error) + } } - logger.info("fd: \(fild)") - completionHandler(nil) } - + private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { + let cfig = from.ServerConfig + guard let addr = cfig.address else { + return nil + } + // Using a makeshift remote tunnel address + var nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") + nst.ipv4Settings = NEIPv4Settings(addresses: [addr], subnetMasks: ["255.255.255.0"]) + logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") + return nst + } override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { completionHandler() } - override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { if let handler = completionHandler { handler(messageData) } } - override func sleep(completionHandler: @escaping () -> Void) { completionHandler() } - override func wake() { } } diff --git a/Apple/NetworkExtension/libburrow/libburrow.h b/Apple/NetworkExtension/libburrow/libburrow.h index 1057c90..32d1d3b 100644 --- a/Apple/NetworkExtension/libburrow/libburrow.h +++ b/Apple/NetworkExtension/libburrow/libburrow.h @@ -1 +1,2 @@ -int retrieve(); +void start_srv(); +void initialize_oslog(); diff --git a/Cargo.lock b/Cargo.lock index f7cf03a..6716320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,16 +39,15 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] @@ -78,9 +77,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -92,6 +91,17 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -202,12 +212,15 @@ name = "burrow" version = "0.1.0" dependencies = [ "anyhow", + "async-channel", "caps", "clap", "env_logger", + "insta", "libsystemd", "log", "nix", + "schemars", "serde", "serde_json", "tokio", @@ -309,20 +322,19 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.10" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.10" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" dependencies = [ "anstream", "anstyle", @@ -332,9 +344,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.2" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck", "proc-macro2", @@ -354,6 +366,27 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -424,12 +457,24 @@ dependencies = [ "subtle", ] +[[package]] +name = "dyn-clone" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" + [[package]] name = "either" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -473,6 +518,12 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "1.9.0" @@ -811,6 +862,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "insta" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e02c584f4595792d09509a94cdb92a3cef7592b1eb2d9877ee6f527062d0ea" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", + "yaml-rust", +] + [[package]] name = "instant" version = "0.1.12" @@ -918,6 +983,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -946,6 +1017,15 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.5.0" @@ -1068,6 +1148,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.31.1" @@ -1153,7 +1243,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.1", ] [[package]] @@ -1260,9 +1350,24 @@ checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.2", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.7.2" @@ -1360,6 +1465,30 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "schemars" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1409,6 +1538,17 @@ dependencies = [ "syn 2.0.22", ] +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "serde_json" version = "1.0.99" @@ -1480,6 +1620,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + [[package]] name = "slab" version = "0.4.8" @@ -1656,6 +1802,7 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", "pin-project-lite", "socket2", "tokio-macros", @@ -1779,10 +1926,14 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -1808,6 +1959,7 @@ dependencies = [ "log", "nix", "reqwest", + "schemars", "serde", "socket2", "ssri", @@ -2041,7 +2193,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", ] [[package]] @@ -2059,13 +2211,37 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -2182,6 +2358,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index ddce4b1..ab19e5c 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,18 +10,20 @@ crate-type = ["lib", "staticlib"] [dependencies] anyhow = "1.0" -tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util"] } -tun = { version = "0.1", path = "../tun", features = ["serde"] } +tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util", "rt-multi-thread"] } +tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } clap = { version = "4.3.2", features = ["derive"] } tracing = "0.1" tracing-log = "0.1" tracing-journald = "0.3" tracing-oslog = {git = "https://github.com/Stormshield-robinc/tracing-oslog"} -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3" , features = ["std", "env-filter"]} env_logger = "0.10" log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" +async-channel = "1.9" +schemars = "0.8" [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5.5" @@ -30,6 +32,9 @@ libsystemd = "0.6" [target.'cfg(target_vendor = "apple")'.dependencies] nix = { version = "0.26.2" } +[dev-dependencies] +insta = { version = "1.32.0", features = ["yaml"] } + [package.metadata.generate-rpm] assets = [ { source = "target/release/burrow", dest = "/usr/bin/burrow", mode = "755" }, diff --git a/burrow/src/apple.rs b/burrow/src/apple.rs new file mode 100644 index 0000000..0a96877 --- /dev/null +++ b/burrow/src/apple.rs @@ -0,0 +1,15 @@ +use tracing::{debug, Subscriber}; +use tracing::instrument::WithSubscriber; +use tracing_oslog::OsLogger; +use tracing_subscriber::FmtSubscriber; +use tracing_subscriber::layer::SubscriberExt; + +pub use crate::daemon::start_srv; + +#[no_mangle] +pub extern "C" fn initialize_oslog() { + let collector = tracing_subscriber::registry() + .with(OsLogger::new("com.hackclub.burrow", "backend")); + tracing::subscriber::set_global_default(collector).unwrap(); + debug!("Initialized oslog tracing in libburrow rust FFI"); +} \ No newline at end of file diff --git a/burrow/src/daemon/command.rs b/burrow/src/daemon/command.rs index d786a99..a5a1f30 100644 --- a/burrow/src/daemon/command.rs +++ b/burrow/src/daemon/command.rs @@ -1,13 +1,32 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tun::TunOptions; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub enum DaemonCommand { Start(DaemonStartOptions), + ServerInfo, + ServerConfig, Stop, } -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct DaemonStartOptions { pub(super) tun: TunOptions, } + +#[test] +fn test_daemoncommand_serialization() { + insta::assert_snapshot!( + serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap() + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonCommand::ServerInfo).unwrap() + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonCommand::Stop).unwrap() + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonCommand::ServerConfig).unwrap() + ) +} \ No newline at end of file diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index d1849d0..db9e1ac 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -1,40 +1,70 @@ +use tracing::{debug, info, warn}; +use DaemonResponse; +use crate::daemon::response::{DaemonResponseData, ServerConfig, ServerInfo}; use super::*; pub struct DaemonInstance { - rx: mpsc::Receiver, + rx: async_channel::Receiver, + sx: async_channel::Sender, tun_interface: Option, } impl DaemonInstance { - pub fn new(rx: mpsc::Receiver) -> Self { + pub fn new(rx: async_channel::Receiver, sx: async_channel::Sender) -> Self { Self { rx, + sx, tun_interface: None, } } - pub async fn run(&mut self) -> Result<()> { - while let Some(command) = self.rx.recv().await { - match command { - DaemonCommand::Start(options) => { - if self.tun_interface.is_none() { - self.tun_interface = Some(options.tun.open()?); - eprintln!("Daemon starting tun interface."); - } else { - eprintln!("Got start, but tun interface already up."); - } + async fn proc_command(&mut self, command: DaemonCommand) -> Result { + info!("Daemon got command: {:?}", command); + match command { + DaemonCommand::Start(st) => { + if self.tun_interface.is_none() { + debug!("Daemon attempting start tun interface."); + self.tun_interface = Some(st.tun.open()?); + info!("Daemon started tun interface"); + } else { + warn!("Got start, but tun interface already up."); } - DaemonCommand::Stop => { - if self.tun_interface.is_some() { - self.tun_interface = None; - eprintln!("Daemon stopping tun interface."); - } else { - eprintln!("Got stop, but tun interface is not up.") + Ok(DaemonResponseData::None) + } + DaemonCommand::ServerInfo => { + match &self.tun_interface { + None => {Ok(DaemonResponseData::None)} + Some(ti) => { + info!("{:?}", ti); + Ok( + DaemonResponseData::ServerInfo( + ServerInfo::try_from(ti)? + ) + ) } } } + DaemonCommand::Stop => { + if self.tun_interface.is_some() { + self.tun_interface = None; + info!("Daemon stopping tun interface."); + } else { + warn!("Got stop, but tun interface is not up.") + } + Ok(DaemonResponseData::None) + } + DaemonCommand::ServerConfig => { + Ok(DaemonResponseData::ServerConfig(ServerConfig::default())) + } } + } + pub async fn run(&mut self) -> Result<()> { + while let Ok(command) = self.rx.recv().await { + let response = self.proc_command(command).await; + info!("Daemon response: {:?}", response); + self.sx.send(DaemonResponse::new(response)).await?; + } Ok(()) } } diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs index 5fcf8ee..b59ad7f 100644 --- a/burrow/src/daemon/mod.rs +++ b/burrow/src/daemon/mod.rs @@ -4,6 +4,7 @@ use tokio::sync::mpsc; mod command; mod instance; mod net; +mod response; use instance::DaemonInstance; use net::listen; @@ -11,9 +12,15 @@ use net::listen; pub use command::{DaemonCommand, DaemonStartOptions}; pub use net::DaemonClient; -pub async fn daemon_main() -> Result<()> { - let (tx, rx) = mpsc::channel(2); - let mut inst = DaemonInstance::new(rx); +#[cfg(target_vendor = "apple")] +pub use net::start_srv; - tokio::try_join!(inst.run(), listen(tx)).map(|_| ()) +pub use response::{DaemonResponseData, DaemonResponse, ServerInfo}; + +pub async fn daemon_main() -> Result<()> { + let (commands_tx, commands_rx) = async_channel::unbounded(); + let (response_tx, response_rx) = async_channel::unbounded(); + let mut inst = DaemonInstance::new(commands_rx, response_tx); + + tokio::try_join!(inst.run(), listen(commands_tx, response_rx)).map(|_| ()) } diff --git a/burrow/src/daemon/net/apple.rs b/burrow/src/daemon/net/apple.rs new file mode 100644 index 0000000..e53bdaa --- /dev/null +++ b/burrow/src/daemon/net/apple.rs @@ -0,0 +1,24 @@ +use std::thread; +use tokio::runtime::Runtime; +use tracing::error; +use crate::daemon::{daemon_main, DaemonClient}; + +#[no_mangle] +pub extern "C" fn start_srv(){ + let _handle = thread::spawn(move || { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + if let Err(e) = daemon_main().await { + error!("Error when starting rpc server: {}", e); + } + }); + }); + let rt = Runtime::new().unwrap(); + rt.block_on(async { + loop { + if let Ok(_) = DaemonClient::new().await{ + break + } + } + }); +} \ No newline at end of file diff --git a/burrow/src/daemon/net/mod.rs b/burrow/src/daemon/net/mod.rs index d8cc5fa..b5e0736 100644 --- a/burrow/src/daemon/net/mod.rs +++ b/burrow/src/daemon/net/mod.rs @@ -13,17 +13,19 @@ pub use systemd::{listen, DaemonClient}; #[cfg(target_os = "windows")] mod windows; + #[cfg(target_os = "windows")] pub use windows::{listen, DaemonClient}; +#[cfg(target_vendor = "apple")] +mod apple; + +#[cfg(target_vendor = "apple")] +pub use apple::start_srv; + #[derive(Clone, Serialize, Deserialize)] pub struct DaemonRequest { pub id: u32, pub command: DaemonCommand, } -#[derive(Clone, Serialize, Deserialize)] -pub struct DaemonResponse { - // Error types can't be serialized, so this is the second best option. - result: std::result::Result<(), String>, -} diff --git a/burrow/src/daemon/net/systemd.rs b/burrow/src/daemon/net/systemd.rs index f67888e..446c259 100644 --- a/burrow/src/daemon/net/systemd.rs +++ b/burrow/src/daemon/net/systemd.rs @@ -1,16 +1,16 @@ use super::*; use std::os::fd::IntoRawFd; -pub async fn listen(cmd_tx: mpsc::Sender) -> Result<()> { - if !libsystemd::daemon::booted() || listen_with_systemd(cmd_tx.clone()).await.is_err() { - unix::listen(cmd_tx).await?; +pub async fn listen(cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver) -> Result<()> { + if !libsystemd::daemon::booted() || listen_with_systemd(cmd_tx.clone(), rsp_rx.clone()).await.is_err() { + unix::listen(cmd_tx, rsp_rx).await?; } Ok(()) } -async fn listen_with_systemd(cmd_tx: mpsc::Sender) -> Result<()> { - let fds = libsystemd::activation::receive_descriptors(false).unwrap(); - super::unix::listen_with_optional_fd(cmd_tx, Some(fds[0].clone().into_raw_fd())).await +async fn listen_with_systemd(cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver) -> Result<()> { + let fds = libsystemd::activation::receive_descriptors(false)?; + super::unix::listen_with_optional_fd(cmd_tx, rsp_rx,Some(fds[0].clone().into_raw_fd())).await } pub type DaemonClient = unix::DaemonClient; diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs index a2837a3..c193a7b 100644 --- a/burrow/src/daemon/net/unix.rs +++ b/burrow/src/daemon/net/unix.rs @@ -1,22 +1,51 @@ use super::*; -use std::{ - os::fd::{FromRawFd, RawFd}, - os::unix::net::UnixListener as StdUnixListener, - path::Path, -}; +use std::{ascii, io, os::fd::{FromRawFd, RawFd}, os::unix::net::UnixListener as StdUnixListener, path::Path}; +use std::hash::Hash; +use std::path::PathBuf; +use anyhow::anyhow; +use log::log; +use tracing::info; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{UnixListener, UnixStream}, }; +use tracing::debug; +#[cfg(not(target_vendor = "apple"))] const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; -pub async fn listen(cmd_tx: mpsc::Sender) -> Result<()> { - listen_with_optional_fd(cmd_tx, None).await +#[cfg(target_vendor = "apple")] +const UNIX_SOCKET_PATH: &str = "burrow.sock"; + +#[cfg(target_os = "macos")] +fn fetch_socket_path() -> Option{ + let tries = vec![ + "burrow.sock".to_string(), + format!("{}/Library/Containers/com.hackclub.burrow.network/Data/burrow.sock", + std::env::var("HOME").unwrap_or_default()) + .to_string(), + ]; + for path in tries{ + let path = PathBuf::from(path); + if path.exists(){ + return Some(path); + } + } + None +} + +#[cfg(not(target_os = "macos"))] +fn fetch_socket_path() -> Option{ + Some(Path::new(UNIX_SOCKET_PATH).to_path_buf()) +} + +pub async fn listen(cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver) -> Result<()> { + listen_with_optional_fd(cmd_tx, rsp_rx, None).await } pub(crate) async fn listen_with_optional_fd( - cmd_tx: mpsc::Sender, + cmd_tx: async_channel::Sender, + rsp_rx: async_channel::Receiver, raw_fd: Option, ) -> Result<()> { let path = Path::new(UNIX_SOCKET_PATH); @@ -32,7 +61,16 @@ pub(crate) async fn listen_with_optional_fd( listener } else { // Won't help all that much, if we use the async version of fs. - std::fs::remove_file(path)?; + if let Some(par) = path.parent(){ + std::fs::create_dir_all( + par + )?; + } + match std::fs::remove_file(path){ + Err(e) if e.kind()==io::ErrorKind::NotFound => {Ok(())} + stuff => stuff + }?; + info!("Relative path: {}", path.to_string_lossy()); UnixListener::bind(path)? }; loop { @@ -41,29 +79,35 @@ pub(crate) async fn listen_with_optional_fd( // I'm pretty sure we won't need to manually join / shut this down, // `lines` will return Err during dropping, and this task should exit gracefully. - tokio::task::spawn(async { + let rsp_rxc = rsp_rx.clone(); + tokio::task::spawn(async move { let cmd_tx = cmd_tx; let mut stream = stream; let (mut read_stream, mut write_stream) = stream.split(); let buf_reader = BufReader::new(&mut read_stream); let mut lines = buf_reader.lines(); while let Ok(Some(line)) = lines.next_line().await { - let mut res = DaemonResponse { result: Ok(()) }; - let command = match serde_json::from_str::(&line) { - Ok(req) => Some(req.command), + info!("Got line: {}", line); + debug!("Line raw data: {:?}", line.as_bytes()); + let mut res : DaemonResponse = DaemonResponseData::None.into(); + let req = match serde_json::from_str::(&line) { + Ok(req) => Some(req), Err(e) => { - res.result = Err(format!("{}", e)); + res.result = Err(e.to_string()); None } }; let mut res = serde_json::to_string(&res).unwrap(); res.push('\n'); - write_stream.write_all(res.as_bytes()).await.unwrap(); - // I want this to come at the very end so that we always send a reponse back. - if let Some(command) = command { - cmd_tx.send(command).await.unwrap(); + if let Some(req) = req { + cmd_tx.send(req.command).await.unwrap(); + let res = rsp_rxc.recv().await.unwrap().with_id(req.id); + let mut retres = serde_json::to_string(&res).unwrap(); + retres.push('\n'); + info!("Sending response: {}", retres); + write_stream.write_all(retres.as_bytes()).await.unwrap(); } } }); @@ -76,7 +120,12 @@ pub struct DaemonClient { impl DaemonClient { pub async fn new() -> Result { - Self::new_with_path(UNIX_SOCKET_PATH).await + let path = fetch_socket_path() + .ok_or(anyhow!("Failed to find socket path"))?; + // debug!("found path: {:?}", path); + let connection = UnixStream::connect(path).await?; + debug!("connected to socket"); + Ok(Self { connection }) } pub async fn new_with_path(path: &str) -> Result { @@ -86,17 +135,19 @@ impl DaemonClient { Ok(Self { connection }) } - pub async fn send_command(&mut self, command: DaemonCommand) -> Result<()> { + pub async fn send_command(&mut self, command: DaemonCommand) -> Result { let mut command = serde_json::to_string(&DaemonRequest { id: 0, command })?; command.push('\n'); self.connection.write_all(command.as_bytes()).await?; let buf_reader = BufReader::new(&mut self.connection); let mut lines = buf_reader.lines(); - // This unwrap *should* never cause issues. - let response = lines.next_line().await?.unwrap(); + let response = lines + .next_line() + .await? + .ok_or(anyhow!("Failed to read response"))?; + debug!("Got raw response: {}", response); let res: DaemonResponse = serde_json::from_str(&response)?; - res.result.unwrap(); - Ok(()) + Ok(res) } } diff --git a/burrow/src/daemon/net/windows.rs b/burrow/src/daemon/net/windows.rs index c858613..3f9d513 100644 --- a/burrow/src/daemon/net/windows.rs +++ b/burrow/src/daemon/net/windows.rs @@ -1,6 +1,6 @@ use super::*; -pub async fn listen(_: mpsc::Sender) -> Result<()> { +pub async fn listen(_cmd_tx: async_channel::Sender, _rsp_rx: async_channel::Receiver) -> Result<()> { unimplemented!("This platform does not currently support daemon mode.") } diff --git a/burrow/src/daemon/response.rs b/burrow/src/daemon/response.rs new file mode 100644 index 0000000..da47150 --- /dev/null +++ b/burrow/src/daemon/response.rs @@ -0,0 +1,109 @@ +use anyhow::anyhow; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use tun::TunInterface; + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +pub struct DaemonResponse { + // Error types can't be serialized, so this is the second best option. + pub result: Result, + pub id: u32 +} + +impl DaemonResponse{ + pub fn new(result: Result) -> Self{ + Self{ + result: result.map_err(|e| e.to_string()), + id: 0 + } + } +} + +impl Into for DaemonResponseData{ + fn into(self) -> DaemonResponse{ + DaemonResponse::new(Ok::(self)) + } +} + +impl DaemonResponse{ + pub fn with_id(self, id: u32) -> Self{ + Self { + id, + ..self + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct ServerInfo { + pub name: Option, + pub ip: Option, + pub mtu: Option +} + +impl TryFrom<&TunInterface> for ServerInfo{ + type Error = anyhow::Error; + + #[cfg(any(target_os="linux",target_vendor="apple"))] + fn try_from(server: &TunInterface) -> anyhow::Result { + Ok( + ServerInfo{ + name: server.name().ok(), + ip: server.ipv4_addr().ok().map(|ip| ip.to_string()), + mtu: server.mtu().ok() + } + ) + } + + #[cfg(not(any(target_os="linux",target_vendor="apple")))] + fn try_from(server: &TunInterface) -> anyhow::Result { + Err(anyhow!("Not implemented in this platform")) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct ServerConfig { + pub address: Option, + pub name: Option, + pub mtu: Option +} + +impl Default for ServerConfig { + fn default() -> Self { + Self{ + address: Some("10.0.0.1".to_string()), // Dummy remote address + name: None, + mtu: None + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub enum DaemonResponseData{ + ServerInfo(ServerInfo), + ServerConfig(ServerConfig), + None +} + +#[test] +fn test_response_serialization() -> anyhow::Result<()>{ + insta::assert_snapshot!( + serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::None)))? + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerInfo(ServerInfo{ + name: Some("burrow".to_string()), + ip: None, + mtu: Some(1500) + }))))? + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonResponse::new(Err::("error".to_string())))? + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig( + ServerConfig::default() + ))))? + ); + Ok(()) +} \ No newline at end of file diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap new file mode 100644 index 0000000..80b9e24 --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/command.rs +expression: "serde_json::to_string(&DaemonCommand::ServerInfo).unwrap()" +--- +"ServerInfo" diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap new file mode 100644 index 0000000..8dc1b8b --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/command.rs +expression: "serde_json::to_string(&DaemonCommand::Stop).unwrap()" +--- +"Stop" diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap new file mode 100644 index 0000000..9334ece --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/command.rs +expression: "serde_json::to_string(&DaemonCommand::ServerConfig).unwrap()" +--- +"ServerConfig" diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap new file mode 100644 index 0000000..2f8af66 --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/command.rs +expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap()" +--- +{"Start":{"tun":{"name":null,"no_pi":null,"tun_excl":null}}} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-2.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-2.snap new file mode 100644 index 0000000..3787cd1 --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-2.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerInfo(ServerInfo {\n name: Some(\"burrow\".to_string()),\n ip: None,\n mtu: Some(1500),\n }))))?" +--- +{"result":{"Ok":{"ServerInfo":{"name":"burrow","ip":null,"mtu":1500}}},"id":0} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-3.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-3.snap new file mode 100644 index 0000000..4ef9575 --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-3.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Err::(\"error\".to_string())))?" +--- +{"result":{"Err":"error"},"id":0} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap new file mode 100644 index 0000000..95f9e7b --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig(ServerConfig::default()))))?" +--- +{"result":{"Ok":{"ServerConfig":{"address":"10.0.0.1","name":null,"mtu":null}}},"id":0} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization.snap new file mode 100644 index 0000000..647d01c --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::None)))?" +--- +{"result":{"Ok":"None"},"id":0} diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index 1032e97..f66c4ac 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -1,6 +1,8 @@ #![deny(missing_debug_implementations)] pub mod ensureroot; +use anyhow::Result; + #[cfg(any(target_os = "linux", target_vendor = "apple"))] use std::{ mem, @@ -11,6 +13,15 @@ use tun::TunInterface; // TODO Separate start and retrieve functions +mod daemon; +pub use daemon::{DaemonCommand, DaemonResponseData, DaemonStartOptions, DaemonResponse, ServerInfo}; + +#[cfg(target_vendor = "apple")] +mod apple; + +#[cfg(target_vendor = "apple")] +pub use apple::*; + #[cfg(any(target_os = "linux", target_vendor = "apple"))] #[no_mangle] pub extern "C" fn retrieve() -> i32 { diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 1f70b1c..f89ee39 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -4,12 +4,12 @@ use std::mem; use std::os::fd::FromRawFd; use clap::{Args, Parser, Subcommand}; -use tracing::instrument; +use tracing::{instrument, Level}; use tracing_log::LogTracer; use tracing_oslog::OsLogger; -use tracing_subscriber::{prelude::*, FmtSubscriber}; -use tokio::io::Result; +use tracing_subscriber::{prelude::*, FmtSubscriber, EnvFilter}; +use anyhow::Result; #[cfg(any(target_os = "linux", target_vendor = "apple"))] use burrow::retrieve; use tun::TunInterface; @@ -17,6 +17,7 @@ use tun::TunInterface; mod daemon; use daemon::{DaemonClient, DaemonCommand, DaemonStartOptions}; +use crate::daemon::DaemonResponseData; #[derive(Parser)] #[command(name = "Burrow")] @@ -44,6 +45,10 @@ enum Commands { Stop, /// Start Burrow daemon Daemon(DaemonArgs), + /// Server Info + ServerInfo, + /// Server config + ServerConfig, } #[derive(Args)] @@ -61,27 +66,38 @@ async fn try_start() -> Result<()> { client .send_command(DaemonCommand::Start(DaemonStartOptions::default())) .await + .map(|_| ()) } #[cfg(any(target_os = "linux", target_vendor = "apple"))] #[instrument] async fn try_retrieve() -> Result<()> { - LogTracer::init().context("Failed to initialize LogTracer").unwrap(); - - if cfg!(target_os = "linux") || cfg!(target_vendor = "apple") { - let maybe_layer = system_log().unwrap(); - if let Some(layer) = maybe_layer { - let logger = layer.with_subscriber(FmtSubscriber::new()); - tracing::subscriber::set_global_default(logger).context("Failed to set the global tracing subscriber").unwrap(); - } - } - burrow::ensureroot::ensure_root(); let iface2 = retrieve(); tracing::info!("{}", iface2); Ok(()) } +async fn initialize_tracing() -> Result<()> { + LogTracer::init().context("Failed to initialize LogTracer")?; + + #[cfg(any(target_os = "linux", target_vendor = "apple"))] + { + let maybe_layer = system_log()?; + if let Some(layer) = maybe_layer { + let logger = layer.with_subscriber( + FmtSubscriber::builder() + .with_line_number(true) + .with_env_filter(EnvFilter::from_default_env()) + .finish() + ); + tracing::subscriber::set_global_default(logger).context("Failed to set the global tracing subscriber")?; + } + } + + Ok(()) +} + #[cfg(any(target_os = "linux", target_vendor = "apple"))] async fn try_stop() -> Result<()> { let mut client = DaemonClient::new().await?; @@ -89,6 +105,44 @@ async fn try_stop() -> Result<()> { Ok(()) } +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_serverinfo() -> Result<()>{ + let mut client = DaemonClient::new().await?; + let res = client.send_command(DaemonCommand::ServerInfo).await?; + match res.result { + Ok(DaemonResponseData::ServerInfo(si)) => { + println!("Got Result! {:?}", si); + } + Ok(DaemonResponseData::None) => { + println!("Server not started.") + } + Ok(res) => {println!("Unexpected Response: {:?}", res)} + Err(e) => { + println!("Error when retrieving from server: {}", e) + } + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_serverconfig() -> Result<()>{ + let mut client = DaemonClient::new().await?; + let res = client.send_command(DaemonCommand::ServerConfig).await?; + match res.result { + Ok(DaemonResponseData::ServerConfig(cfig)) => { + println!("Got Result! {:?}", cfig); + } + Ok(DaemonResponseData::None) => { + println!("Server not started.") + } + Ok(res) => {println!("Unexpected Response: {:?}", res)} + Err(e) => { + println!("Error when retrieving from server: {}", e) + } + } + Ok(()) +} + #[cfg(not(any(target_os = "linux", target_vendor = "apple")))] async fn try_start() -> Result<()> { Ok(()) @@ -104,24 +158,40 @@ async fn try_stop() -> Result<()> { Ok(()) } +#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] +async fn try_serverinfo() -> Result<()> { + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] +async fn try_serverconfig() -> Result<()> { + Ok(()) +} #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { + initialize_tracing().await?; tracing::info!("Platform: {}", std::env::consts::OS); let cli = Cli::parse(); match &cli.command { Commands::Start(..) => { - try_start().await.unwrap(); + try_start().await?; tracing::info!("FINISHED"); } Commands::Retrieve(..) => { - try_retrieve().await.unwrap(); + try_retrieve().await?; tracing::info!("FINISHED"); } Commands::Stop => { - try_stop().await.unwrap(); + try_stop().await?; } Commands::Daemon(_) => daemon::daemon_main().await?, + Commands::ServerInfo => { + try_serverinfo().await? + } + Commands::ServerConfig => { + try_serverconfig().await? + } } Ok(()) @@ -141,5 +211,5 @@ fn system_log() -> anyhow::Result> { #[cfg(target_vendor = "apple")] fn system_log() -> anyhow::Result> { - Ok(Some(OsLogger::new("com.hackclub.burrow", "default"))) + Ok(Some(OsLogger::new("com.hackclub.burrow", "burrow-cli"))) } diff --git a/tun/Cargo.toml b/tun/Cargo.toml index 2979c18..b95c1bf 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -13,11 +13,12 @@ byteorder = "1.4" tracing = "0.1" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } +schemars = { version = "0.8", optional = true } futures = { version = "0.3.28", optional = true } [features] -serde = ["dep:serde"] +serde = ["dep:serde", "dep:schemars"] tokio = ["tokio/net", "dep:futures"] [target.'cfg(feature = "tokio")'.dev-dependencies] diff --git a/tun/src/options.rs b/tun/src/options.rs index e766be8..e74afe3 100644 --- a/tun/src/options.rs +++ b/tun/src/options.rs @@ -4,7 +4,7 @@ use std::io::Error; use super::TunInterface; #[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema))] pub struct TunOptions { /// (Windows + Linux) Name the tun interface. pub(crate) name: Option, From 759311e4f4c8fc476218e7046eb68ae895ef8632 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Mon, 16 Oct 2023 11:44:38 +0800 Subject: [PATCH 085/128] remove continuation from BurrowIPC This removes usage of continuation from BurrowIPC by moving it to NWConnections. --- Apple/NetworkExtension/BurrowIpc.swift | 51 ++++++++----------- .../PacketTunnelProvider.swift | 2 +- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/Apple/NetworkExtension/BurrowIpc.swift b/Apple/NetworkExtension/BurrowIpc.swift index 0992bfc..7f18679 100644 --- a/Apple/NetworkExtension/BurrowIpc.swift +++ b/Apple/NetworkExtension/BurrowIpc.swift @@ -26,7 +26,7 @@ final class LineProtocol: NWProtocolFramerImplementation { } func handleInput(framer: NWProtocolFramer.Instance) -> Int { var result: [Data] = [] - framer.parseInput(minimumIncompleteLength: 1, maximumLength: 16_000) { buffer, _ in + _ = framer.parseInput(minimumIncompleteLength: 1, maximumLength: 16_000) { buffer, _ in guard let (lines, size) = lines(from: buffer) else { return 0 } @@ -62,12 +62,23 @@ extension NWConnection { } } } + func send_raw(_ request: Data) async throws -> Data { + try await withCheckedThrowingContinuation { continuation in + let comp: NWConnection.SendCompletion = .contentProcessed {error in + if let error = error { + continuation.resume(with: .failure(error)) + } else { + continuation.resume(with: .success(request)) + } + } + self.send(content: request, completion: comp) + } + } } final class BurrowIpc { let connection: NWConnection private var generator = SystemRandomNumberGenerator() - private var continuations: [UInt: UnsafeContinuation] = [:] private var logger: Logger init(logger: Logger) { let params = NWParameters.tcp @@ -80,36 +91,16 @@ final class BurrowIpc { self.logger = logger } func send(_ request: T) async throws -> U { - let data: Data = try await withUnsafeThrowingContinuation { continuation in + do { let id: UInt = generator.next(upperBound: UInt.max) - continuations[id] = continuation var copy = request copy.id = id - do { - var data = try JSONEncoder().encode(request) - data.append(contentsOf: [10]) - let completion: NWConnection.SendCompletion = .contentProcessed { error in - guard let error = error else { return } - continuation.resume(throwing: error) - } - connection.send(content: data, completion: completion) - } catch { - continuation.resume(throwing: error) - return - } - } - return try JSONDecoder().decode(Response.self, from: data).result - } - func send_raw(_ request: Data) async throws -> Data { - try await withCheckedThrowingContinuation { continuation in - let comp: NWConnection.SendCompletion = .contentProcessed {error in - if let error = error { - continuation.resume(with: .failure(error)) - } else { - continuation.resume(with: .success(request)) - } - } - self.connection.send(content: request, completion: comp) + var data = try JSONEncoder().encode(request) + data.append(contentsOf: [10]) + _ = try await self.connection.send_raw(data) + return try JSONDecoder().decode(Response.self, from: data).result + } catch { + throw error } } @@ -126,7 +117,7 @@ final class BurrowIpc { do { var data: Data = try JSONEncoder().encode(request) data.append(contentsOf: [10]) - try await send_raw(data) + _ = try await self.connection.send_raw(data) self.logger.debug("message sent") let receivedData = try await receive_raw() self.logger.info("Received result: \(String(decoding: receivedData, as: UTF8.self))") diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index d3a3e5d..4b72115 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -45,7 +45,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return nil } // Using a makeshift remote tunnel address - var nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") + let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") nst.ipv4Settings = NEIPv4Settings(addresses: [addr], subnetMasks: ["255.255.255.0"]) logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") return nst From 91a15ec9088cffa4997c6134e7715de7fc7b15ee Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Mon, 25 Sep 2023 09:47:31 -0400 Subject: [PATCH 086/128] Remove myself from Apple PRs I do not have the required experience with Swift/iOS/macOS development to be an approving reviewer for Apple PRs --- .github/CODEOWNERS | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ebfc124..39b0b89 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,3 @@ -* @conradev @ma1ted @Muirrum \ No newline at end of file +* @conradev @ma1ted +burrow/ @Muirrum +tun/ @Muirrum From ff819af752e7feb8bdb9f8e8bfef594ba94f710e Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Mon, 25 Sep 2023 09:48:29 -0400 Subject: [PATCH 087/128] Update CODEOWNERS Update ben's GitHub username @ma1ted -> \@malted --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 39b0b89..02a2448 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ -* @conradev @ma1ted +* @conradev @malted burrow/ @Muirrum tun/ @Muirrum From f31133f4dcfbc1a7f6060cf27cb57dd4c195f254 Mon Sep 17 00:00:00 2001 From: Jasper Mayone Date: Sat, 18 Nov 2023 13:03:19 -0500 Subject: [PATCH 088/128] update codeowners --- .github/CODEOWNERS | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 02a2448..fe0babf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,5 @@ -* @conradev @malted +* @conradev @malted @JettChenT @jdogcoder burrow/ @Muirrum tun/ @Muirrum +Apple/ @jdogcoder +burrow-gtk/ @davnotdev \ No newline at end of file From f1d7a9849161c74975dbdd13442a27a4200f87f7 Mon Sep 17 00:00:00 2001 From: reesericci Date: Sat, 1 Jul 2023 12:25:56 -0500 Subject: [PATCH 089/128] Initialized burrow-gtk project --- .gitignore | 2 +- burrow-gtk/Cargo.lock | 897 ++++++++++++++++++ burrow-gtk/Cargo.toml | 13 + burrow-gtk/README.md | 3 + burrow-gtk/com.hackclub.Burrow.json | 51 + .../data/com.hackclub.Burrow.appdata.xml.in | 8 + .../data/com.hackclub.Burrow.desktop.in | 8 + .../data/com.hackclub.Burrow.gschema.xml | 5 + .../scalable/apps/com.hackclub.Burrow.svg | 130 +++ .../apps/com.hackclub.Burrow-symbolic.svg | 1 + burrow-gtk/data/icons/meson.build | 13 + burrow-gtk/data/meson.build | 39 + burrow-gtk/meson.build | 20 + burrow-gtk/po/LINGUAS | 0 burrow-gtk/po/POTFILES | 4 + burrow-gtk/po/meson.build | 1 + burrow-gtk/src/application.rs | 92 ++ burrow-gtk/src/burrow-gtk.gresource.xml | 7 + burrow-gtk/src/config.rs | 4 + burrow-gtk/src/config.rs.in | 4 + burrow-gtk/src/gtk/help-overlay.blp | 24 + burrow-gtk/src/main.rs | 35 + burrow-gtk/src/meson.build | 67 ++ burrow-gtk/src/window.blp | 48 + burrow-gtk/src/window.rs | 51 + .../subprojects/blueprint-compiler.wrap | 8 + 26 files changed, 1534 insertions(+), 1 deletion(-) create mode 100644 burrow-gtk/Cargo.lock create mode 100644 burrow-gtk/Cargo.toml create mode 100644 burrow-gtk/README.md create mode 100644 burrow-gtk/com.hackclub.Burrow.json create mode 100644 burrow-gtk/data/com.hackclub.Burrow.appdata.xml.in create mode 100644 burrow-gtk/data/com.hackclub.Burrow.desktop.in create mode 100644 burrow-gtk/data/com.hackclub.Burrow.gschema.xml create mode 100644 burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.Burrow.svg create mode 100644 burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.Burrow-symbolic.svg create mode 100644 burrow-gtk/data/icons/meson.build create mode 100644 burrow-gtk/data/meson.build create mode 100644 burrow-gtk/meson.build create mode 100644 burrow-gtk/po/LINGUAS create mode 100644 burrow-gtk/po/POTFILES create mode 100644 burrow-gtk/po/meson.build create mode 100644 burrow-gtk/src/application.rs create mode 100644 burrow-gtk/src/burrow-gtk.gresource.xml create mode 100644 burrow-gtk/src/config.rs create mode 100644 burrow-gtk/src/config.rs.in create mode 100644 burrow-gtk/src/gtk/help-overlay.blp create mode 100644 burrow-gtk/src/main.rs create mode 100644 burrow-gtk/src/meson.build create mode 100644 burrow-gtk/src/window.blp create mode 100644 burrow-gtk/src/window.rs create mode 100644 burrow-gtk/subprojects/blueprint-compiler.wrap diff --git a/.gitignore b/.gitignore index 11c6ec9..dc886ed 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ xcuserdata target/ .DS_STORE -.idea/ \ No newline at end of file +.idea/ diff --git a/burrow-gtk/Cargo.lock b/burrow-gtk/Cargo.lock new file mode 100644 index 0000000..4e87651 --- /dev/null +++ b/burrow-gtk/Cargo.lock @@ -0,0 +1,897 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "burrow-gtk" +version = "0.1.0" +dependencies = [ + "gettext-rs", + "gtk4", + "libadwaita", +] + +[[package]] +name = "cairo-rs" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3603c4028a5e368d09b51c8b624b9a46edcd7c3778284077a6125af73c9f0a" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "691d0c66b1fb4881be80a760cb8fe76ea97218312f9dfe2c9cc0f496ca279cb1" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-expr" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "215c0072ecc28f92eeb0eea38ba63ddfcb65c2828c46311d646f1a3ff5f9841c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.22", +] + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695d6bc846438c5708b07007537b9274d883373dd30858ca881d7d71b5540717" +dependencies = [ + "bitflags", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9285ec3c113c66d7d0ab5676599176f1f42f4944ca1b581852215bf5694870cb" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc92aa1608c089c49393d014c38ac0390d01e4841e1fedaa75dbcef77aaed64" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gettext-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" +dependencies = [ + "cc", + "temp-dir", +] + +[[package]] +name = "gio" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" +dependencies = [ + "anyhow", + "heck", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def4bb01265b59ed548b05455040d272d989b3012c42d4c1bbd39083cb9b40d9" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1856fc817e6a6675e36cea0bd9a3afe296f5d9709d1e2d3182803ac77f0ab21d" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07a84fb4dcf1323d29435aa85e2f5f58bef564342bef06775ec7bd0da1f01b0" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28a32a04cd75cef14a0983f8b0c669e0fe152a0a7725accdeb594e2c764c88b" +dependencies = [ + "bitflags", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "once_cell", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a4d6b61570f76d3ee542d984da443b1cd69b6105264c61afec3abed08c2500f" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "gtk4-sys" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f8283f707b07e019e76c7f2934bdd4180c277e08aa93f4c0d8dd07b7a34e22f" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libadwaita" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c4efd2020a4fcedbad2c4a97de97bf6045e5dc49d61d5a5d0cfd753db60700" +dependencies = [ + "bitflags", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "gtk4", + "libadwaita-sys", + "libc", + "once_cell", + "pango", +] + +[[package]] +name = "libadwaita-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0727b85b4fe2b1bed5ac90df6343de15cbf8118bfb96d7c3cc1512681a4b34ac" +dependencies = [ + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "pango" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35be456fc620e61f62dff7ff70fbd54dcbaf0a4b920c0f16de1107c47d921d48" +dependencies = [ + "bitflags", + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da69f9f3850b0d8990d462f8c709561975e95f689c1cdf0fecdebde78b35195" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac" + +[[package]] +name = "temp-dir" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.22", +] + +[[package]] +name = "toml" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winnow" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +dependencies = [ + "memchr", +] diff --git a/burrow-gtk/Cargo.toml b/burrow-gtk/Cargo.toml new file mode 100644 index 0000000..7243745 --- /dev/null +++ b/burrow-gtk/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "burrow-gtk" +version = "0.1.0" +edition = "2021" + +[dependencies] +gettext-rs = { version = "0.7", features = ["gettext-system"] } +gtk = { version = "0.6", package = "gtk4" } + +[dependencies.adw] +package = "libadwaita" +version = "0.3" +features = ["v1_2"] diff --git a/burrow-gtk/README.md b/burrow-gtk/README.md new file mode 100644 index 0000000..93c1d95 --- /dev/null +++ b/burrow-gtk/README.md @@ -0,0 +1,3 @@ +# burrow-gtk + +A description of this project. diff --git a/burrow-gtk/com.hackclub.Burrow.json b/burrow-gtk/com.hackclub.Burrow.json new file mode 100644 index 0000000..081096b --- /dev/null +++ b/burrow-gtk/com.hackclub.Burrow.json @@ -0,0 +1,51 @@ +{ + "app-id" : "com.hackclub.Burrow", + "runtime" : "org.gnome.Platform", + "runtime-version" : "44", + "sdk" : "org.gnome.Sdk", + "sdk-extensions" : [ + "org.freedesktop.Sdk.Extension.rust-stable" + ], + "command" : "burrow-gtk", + "finish-args" : [ + "--share=network", + "--share=ipc", + "--socket=fallback-x11", + "--device=dri", + "--socket=wayland" + ], + "build-options" : { + "append-path" : "/usr/lib/sdk/rust-stable/bin", + "build-args" : [ + "--share=network" + ], + "env" : { + "RUST_BACKTRACE" : "1", + "RUST_LOG" : "burrow-gtk=debug" + } + }, + "cleanup" : [ + "/include", + "/lib/pkgconfig", + "/man", + "/share/doc", + "/share/gtk-doc", + "/share/man", + "/share/pkgconfig", + "*.la", + "*.a" + ], + "modules" : [ + { + "name" : "burrow-gtk", + "builddir" : true, + "buildsystem" : "meson", + "sources" : [ + { + "type" : "git", + "url" : "file:///var/home/reesericci/Code" + } + ] + } + ] +} diff --git a/burrow-gtk/data/com.hackclub.Burrow.appdata.xml.in b/burrow-gtk/data/com.hackclub.Burrow.appdata.xml.in new file mode 100644 index 0000000..cbee29f --- /dev/null +++ b/burrow-gtk/data/com.hackclub.Burrow.appdata.xml.in @@ -0,0 +1,8 @@ + + + com.hackclub.Burrow.desktop + GPL-3.0-or-later + +

No description

+
+
diff --git a/burrow-gtk/data/com.hackclub.Burrow.desktop.in b/burrow-gtk/data/com.hackclub.Burrow.desktop.in new file mode 100644 index 0000000..024d14e --- /dev/null +++ b/burrow-gtk/data/com.hackclub.Burrow.desktop.in @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=burrow-gtk +Exec=burrow-gtk +Icon=com.hackclub.Burrow +Terminal=false +Type=Application +Categories=GTK;Network +StartupNotify=true diff --git a/burrow-gtk/data/com.hackclub.Burrow.gschema.xml b/burrow-gtk/data/com.hackclub.Burrow.gschema.xml new file mode 100644 index 0000000..7121dfb --- /dev/null +++ b/burrow-gtk/data/com.hackclub.Burrow.gschema.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.Burrow.svg b/burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.Burrow.svg new file mode 100644 index 0000000..a74c4df --- /dev/null +++ b/burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.Burrow.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + application-x-executable + + + + + + + + + + + + + + + + diff --git a/burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.Burrow-symbolic.svg b/burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.Burrow-symbolic.svg new file mode 100644 index 0000000..0444828 --- /dev/null +++ b/burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.Burrow-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/burrow-gtk/data/icons/meson.build b/burrow-gtk/data/icons/meson.build new file mode 100644 index 0000000..4ee9744 --- /dev/null +++ b/burrow-gtk/data/icons/meson.build @@ -0,0 +1,13 @@ +application_id = 'com.hackclub.Burrow' + +scalable_dir = join_paths('hicolor', 'scalable', 'apps') +install_data( + join_paths(scalable_dir, ('@0@.svg').format(application_id)), + install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) +) + +symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') +install_data( + join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)), + install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir) +) diff --git a/burrow-gtk/data/meson.build b/burrow-gtk/data/meson.build new file mode 100644 index 0000000..4cca4a4 --- /dev/null +++ b/burrow-gtk/data/meson.build @@ -0,0 +1,39 @@ +desktop_file = i18n.merge_file( + input: 'com.hackclub.Burrow.desktop.in', + output: 'com.hackclub.Burrow.desktop', + type: 'desktop', + po_dir: '../po', + install: true, + install_dir: join_paths(get_option('datadir'), 'applications') +) + +desktop_utils = find_program('desktop-file-validate', required: false) +if desktop_utils.found() + test('Validate desktop file', desktop_utils, args: [desktop_file]) +endif + +appstream_file = i18n.merge_file( + input: 'com.hackclub.Burrow.appdata.xml.in', + output: 'com.hackclub.Burrow.appdata.xml', + po_dir: '../po', + install: true, + install_dir: join_paths(get_option('datadir'), 'appdata') +) + +appstream_util = find_program('appstream-util', required: false) +if appstream_util.found() + test('Validate appstream file', appstream_util, args: ['validate', appstream_file]) +endif + +install_data('com.hackclub.Burrow.gschema.xml', + install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') +) + +compile_schemas = find_program('glib-compile-schemas', required: false) +if compile_schemas.found() + test('Validate schema file', + compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()]) +endif + +subdir('icons') diff --git a/burrow-gtk/meson.build b/burrow-gtk/meson.build new file mode 100644 index 0000000..3e9a026 --- /dev/null +++ b/burrow-gtk/meson.build @@ -0,0 +1,20 @@ +project('burrow-gtk', 'rust', + version: '0.1.0', + meson_version: '>= 0.62.0', + default_options: [ 'warning_level=2', 'werror=false', ], +) + +i18n = import('i18n') +gnome = import('gnome') + + + +subdir('data') +subdir('src') +subdir('po') + +gnome.post_install( + glib_compile_schemas: true, + gtk_update_icon_cache: true, + update_desktop_database: true, +) diff --git a/burrow-gtk/po/LINGUAS b/burrow-gtk/po/LINGUAS new file mode 100644 index 0000000..e69de29 diff --git a/burrow-gtk/po/POTFILES b/burrow-gtk/po/POTFILES new file mode 100644 index 0000000..d1acb5a --- /dev/null +++ b/burrow-gtk/po/POTFILES @@ -0,0 +1,4 @@ +data/com.hackclub.Burrow.desktop.in +data/com.hackclub.Burrow.appdata.xml.in +data/com.hackclub.Burrow.gschema.xml +src/window.ui diff --git a/burrow-gtk/po/meson.build b/burrow-gtk/po/meson.build new file mode 100644 index 0000000..4b239a8 --- /dev/null +++ b/burrow-gtk/po/meson.build @@ -0,0 +1 @@ +i18n.gettext('burrow-gtk', preset: 'glib') diff --git a/burrow-gtk/src/application.rs b/burrow-gtk/src/application.rs new file mode 100644 index 0000000..f511bae --- /dev/null +++ b/burrow-gtk/src/application.rs @@ -0,0 +1,92 @@ +use gtk::prelude::*; +use adw::subclass::prelude::*; +use gtk::{gio, glib}; + +use crate::config::VERSION; +use crate::BurrowGtkWindow; + +mod imp { + use super::*; + + #[derive(Debug, Default)] + pub struct BurrowGtkApplication {} + + #[glib::object_subclass] + impl ObjectSubclass for BurrowGtkApplication { + const NAME: &'static str = "BurrowGtkApplication"; + type Type = super::BurrowGtkApplication; + type ParentType = adw::Application; + } + + impl ObjectImpl for BurrowGtkApplication { + fn constructed(&self) { + self.parent_constructed(); + let obj = self.obj(); + obj.setup_gactions(); + obj.set_accels_for_action("app.quit", &["q"]); + } + } + + impl ApplicationImpl for BurrowGtkApplication { + // We connect to the activate callback to create a window when the application + // has been launched. Additionally, this callback notifies us when the user + // tries to launch a "second instance" of the application. When they try + // to do that, we'll just present any existing window. + fn activate(&self) { + let application = self.obj(); + // Get the current window or create one if necessary + let window = if let Some(window) = application.active_window() { + window + } else { + let window = BurrowGtkWindow::new(&*application); + window.upcast() + }; + + // Ask the window manager/compositor to present the window + window.present(); + } + } + + impl GtkApplicationImpl for BurrowGtkApplication {} + impl AdwApplicationImpl for BurrowGtkApplication {} +} + +glib::wrapper! { + pub struct BurrowGtkApplication(ObjectSubclass) + @extends gio::Application, gtk::Application, adw::Application, + @implements gio::ActionGroup, gio::ActionMap; +} + +impl BurrowGtkApplication { + pub fn new(application_id: &str, flags: &gio::ApplicationFlags) -> Self { + glib::Object::builder() + .property("application-id", application_id) + .property("flags", flags) + .build() + } + + fn setup_gactions(&self) { + let quit_action = gio::ActionEntry::builder("quit") + .activate(move |app: &Self, _, _| app.quit()) + .build(); + let about_action = gio::ActionEntry::builder("about") + .activate(move |app: &Self, _, _| app.show_about()) + .build(); + self.add_action_entries([quit_action, about_action]); + } + + fn show_about(&self) { + let window = self.active_window().unwrap(); + let about = adw::AboutWindow::builder() + .transient_for(&window) + .application_name("burrow-gtk") + .application_icon("com.hackclub.Burrow") + .developer_name("Hack Club") + .version(VERSION) + .developers(vec!["Hack Club"]) + .copyright("© 2023 The Hack Foundation") + .build(); + + about.present(); + } +} diff --git a/burrow-gtk/src/burrow-gtk.gresource.xml b/burrow-gtk/src/burrow-gtk.gresource.xml new file mode 100644 index 0000000..23a5a82 --- /dev/null +++ b/burrow-gtk/src/burrow-gtk.gresource.xml @@ -0,0 +1,7 @@ + + + + window.ui + gtk/help-overlay.ui + + diff --git a/burrow-gtk/src/config.rs b/burrow-gtk/src/config.rs new file mode 100644 index 0000000..73ce81e --- /dev/null +++ b/burrow-gtk/src/config.rs @@ -0,0 +1,4 @@ +pub static VERSION: &str = "0.1.0"; +pub static GETTEXT_PACKAGE: &str = "burrow-gtk"; +pub static LOCALEDIR: &str = "/app/share/locale"; +pub static PKGDATADIR: &str = "/app/share/burrow-gtk"; diff --git a/burrow-gtk/src/config.rs.in b/burrow-gtk/src/config.rs.in new file mode 100644 index 0000000..1a24858 --- /dev/null +++ b/burrow-gtk/src/config.rs.in @@ -0,0 +1,4 @@ +pub static VERSION: &str = @VERSION@; +pub static GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; +pub static LOCALEDIR: &str = @LOCALEDIR@; +pub static PKGDATADIR: &str = @PKGDATADIR@; diff --git a/burrow-gtk/src/gtk/help-overlay.blp b/burrow-gtk/src/gtk/help-overlay.blp new file mode 100644 index 0000000..90ee78f --- /dev/null +++ b/burrow-gtk/src/gtk/help-overlay.blp @@ -0,0 +1,24 @@ +using Gtk 4.0; + +ShortcutsWindow help_overlay { + modal: true; + + ShortcutsSection { + section-name: "shortcuts"; + max-height: 10; + + ShortcutsGroup { + title: C_("shortcut window", "General"); + + ShortcutsShortcut { + title: C_("shortcut window", "Show Shortcuts"); + action-name: "win.show-help-overlay"; + } + + ShortcutsShortcut { + title: C_("shortcut window", "Quit"); + action-name: "app.quit"; + } + } + } +} diff --git a/burrow-gtk/src/main.rs b/burrow-gtk/src/main.rs new file mode 100644 index 0000000..6ccd23b --- /dev/null +++ b/burrow-gtk/src/main.rs @@ -0,0 +1,35 @@ +mod application; +mod config; +mod window; + +use self::application::BurrowGtkApplication; +use self::window::BurrowGtkWindow; + +use config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR}; +use gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain}; +use gtk::{gio, glib}; +use gtk::prelude::*; + +fn main() -> glib::ExitCode { + // Set up gettext translations + bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8") + .expect("Unable to set the text domain encoding"); + textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain"); + + // Load resources + let resources = gio::Resource::load(PKGDATADIR.to_owned() + "/burrow-gtk.gresource") + .expect("Could not load resources"); + gio::resources_register(&resources); + + // Create a new GtkApplication. The application manages our main loop, + // application windows, integration with the window manager/compositor, and + // desktop features such as file opening and single-instance applications. + let app = BurrowGtkApplication::new("com.hackclub.Burrow", &gio::ApplicationFlags::empty()); + + // Run the application. This function will block until the application + // exits. Upon return, we have our exit code to return to the shell. (This + // is the code you see when you do `echo $?` after running a command in a + // terminal. + app.run() +} diff --git a/burrow-gtk/src/meson.build b/burrow-gtk/src/meson.build new file mode 100644 index 0000000..15ec34b --- /dev/null +++ b/burrow-gtk/src/meson.build @@ -0,0 +1,67 @@ +pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) +gnome = import('gnome') + + +blueprints = custom_target('blueprints', + input: files( + 'gtk/help-overlay.blp', + 'window.blp', + ), + output: '.', + command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], +) + +gnome.compile_resources('burrow-gtk', + 'burrow-gtk.gresource.xml', + gresource_bundle: true, + install: true, + install_dir: pkgdatadir, + dependencies: blueprints +) + +conf = configuration_data() +conf.set_quoted('VERSION', meson.project_version()) +conf.set_quoted('GETTEXT_PACKAGE', 'burrow-gtk') +conf.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) +conf.set_quoted('PKGDATADIR', pkgdatadir) + +configure_file( + input: 'config.rs.in', + output: 'config.rs', + configuration: conf +) + +# Copy the config.rs output to the source directory. +run_command( + 'cp', + join_paths(meson.project_build_root(), 'src', 'config.rs'), + join_paths(meson.project_source_root(), 'src', 'config.rs'), + check: true +) + +cargo_bin = find_program('cargo') +cargo_opt = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ] +cargo_opt += [ '--target-dir', meson.project_build_root() / 'src' ] +cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ] + +if get_option('buildtype') == 'release' + cargo_options += [ '--release' ] + rust_target = 'release' +else + rust_target = 'debug' +endif + +cargo_build = custom_target( + 'cargo-build', + build_by_default: true, + build_always_stale: true, + output: meson.project_name(), + console: true, + install: true, + install_dir: get_option('bindir'), + command: [ + 'env', cargo_env, + cargo_bin, 'build', + cargo_opt, '&&', 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@', + ] +) diff --git a/burrow-gtk/src/window.blp b/burrow-gtk/src/window.blp new file mode 100644 index 0000000..6f3534a --- /dev/null +++ b/burrow-gtk/src/window.blp @@ -0,0 +1,48 @@ +using Gtk 4.0; +using Adw 1; + +template BurrowGtkWindow : Adw.ApplicationWindow { + default-width: 600; + default-height: 400; + + Box { + orientation: vertical; + + HeaderBar header_bar { + [end] + MenuButton { + icon-name: "open-menu-symbolic"; + menu-model: primary_menu; + } + } + + Label label { + label: "Burrow GNOME"; + + vexpand: true; + + styles [ + "title-1", + ] + } + } +} + +menu primary_menu { + section { + item { + label: _("_Preferences"); + action: "app.preferences"; + } + + item { + label: _("_Keyboard Shortcuts"); + action: "win.show-help-overlay"; + } + + item { + label: _("_About Burrow-gtk"); + action: "app.about"; + } + } +} diff --git a/burrow-gtk/src/window.rs b/burrow-gtk/src/window.rs new file mode 100644 index 0000000..4a51ef0 --- /dev/null +++ b/burrow-gtk/src/window.rs @@ -0,0 +1,51 @@ +use gtk::prelude::*; +use adw::subclass::prelude::*; +use gtk::{gio, glib}; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(resource = "/com/hackclub/Burrow/window.ui")] + pub struct BurrowGtkWindow { + // Template widgets + #[template_child] + pub header_bar: TemplateChild, + #[template_child] + pub label: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for BurrowGtkWindow { + const NAME: &'static str = "BurrowGtkWindow"; + type Type = super::BurrowGtkWindow; + type ParentType = adw::ApplicationWindow; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for BurrowGtkWindow {} + impl WidgetImpl for BurrowGtkWindow {} + impl WindowImpl for BurrowGtkWindow {} + impl ApplicationWindowImpl for BurrowGtkWindow {} + impl AdwApplicationWindowImpl for BurrowGtkWindow {} +} + +glib::wrapper! { + pub struct BurrowGtkWindow(ObjectSubclass) + @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow, @implements gio::ActionGroup, gio::ActionMap; +} + +impl BurrowGtkWindow { + pub fn new>(application: &P) -> Self { + glib::Object::builder() + .property("application", application) + .build() + } +} diff --git a/burrow-gtk/subprojects/blueprint-compiler.wrap b/burrow-gtk/subprojects/blueprint-compiler.wrap new file mode 100644 index 0000000..6e6ee32 --- /dev/null +++ b/burrow-gtk/subprojects/blueprint-compiler.wrap @@ -0,0 +1,8 @@ +[wrap-git] +directory = blueprint-compiler +url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +revision = main +depth = 1 + +[provide] +program_names = blueprint-compiler \ No newline at end of file From 60257b256a811674e83279a3e21e5359892987f7 Mon Sep 17 00:00:00 2001 From: dav Date: Sat, 1 Jul 2023 12:48:55 -0500 Subject: [PATCH 090/128] Intial GTK, swtich to Relm, basic Flatpak Build --- .github/workflows/build-flatpak.yml | 19 + .github/workflows/build-rpm.yml | 5 +- Cargo.toml | 1 + burrow-gtk/.gitignore | 1 + burrow-gtk/Cargo.lock | 2503 ++++++++++++++++- burrow-gtk/Cargo.toml | 13 +- burrow-gtk/README.md | 3 - ...ow.json => com.hackclub.burrow.devel.json} | 17 +- burrow-gtk/com.hackclub.burrow.json | 56 + ....in => com.hackclub.burrow.appdata.xml.in} | 2 +- ...ktop.in => com.hackclub.burrow.desktop.in} | 4 +- ...ma.xml => com.hackclub.burrow.gschema.xml} | 2 +- ...lub.Burrow.svg => com.hackclub.burrow.svg} | 0 ...c.svg => com.hackclub.burrow-symbolic.svg} | 0 burrow-gtk/data/icons/meson.build | 2 +- burrow-gtk/data/meson.build | 10 +- burrow-gtk/meson.build | 20 - burrow-gtk/src/application.rs | 92 - burrow-gtk/src/burrow-gtk.gresource.xml | 7 - burrow-gtk/src/config.rs | 4 - burrow-gtk/src/config.rs.in | 4 - burrow-gtk/src/gtk/help-overlay.blp | 24 - burrow-gtk/src/main.rs | 116 +- burrow-gtk/src/meson.build | 67 - burrow-gtk/src/window.blp | 48 - burrow-gtk/src/window.rs | 51 - .../subprojects/blueprint-compiler.wrap | 8 - burrow/src/daemon/net/unix.rs | 53 +- burrow/src/lib.rs | 4 +- 29 files changed, 2573 insertions(+), 563 deletions(-) create mode 100644 .github/workflows/build-flatpak.yml create mode 100644 burrow-gtk/.gitignore delete mode 100644 burrow-gtk/README.md rename burrow-gtk/{com.hackclub.Burrow.json => com.hackclub.burrow.devel.json} (71%) create mode 100644 burrow-gtk/com.hackclub.burrow.json rename burrow-gtk/data/{com.hackclub.Burrow.appdata.xml.in => com.hackclub.burrow.appdata.xml.in} (83%) rename burrow-gtk/data/{com.hackclub.Burrow.desktop.in => com.hackclub.burrow.desktop.in} (72%) rename burrow-gtk/data/{com.hackclub.Burrow.gschema.xml => com.hackclub.burrow.gschema.xml} (62%) rename burrow-gtk/data/icons/hicolor/scalable/apps/{com.hackclub.Burrow.svg => com.hackclub.burrow.svg} (100%) rename burrow-gtk/data/icons/hicolor/symbolic/apps/{com.hackclub.Burrow-symbolic.svg => com.hackclub.burrow-symbolic.svg} (100%) delete mode 100644 burrow-gtk/meson.build delete mode 100644 burrow-gtk/src/application.rs delete mode 100644 burrow-gtk/src/burrow-gtk.gresource.xml delete mode 100644 burrow-gtk/src/config.rs delete mode 100644 burrow-gtk/src/config.rs.in delete mode 100644 burrow-gtk/src/gtk/help-overlay.blp delete mode 100644 burrow-gtk/src/meson.build delete mode 100644 burrow-gtk/src/window.blp delete mode 100644 burrow-gtk/src/window.rs delete mode 100644 burrow-gtk/subprojects/blueprint-compiler.wrap diff --git a/.github/workflows/build-flatpak.yml b/.github/workflows/build-flatpak.yml new file mode 100644 index 0000000..8a70613 --- /dev/null +++ b/.github/workflows/build-flatpak.yml @@ -0,0 +1,19 @@ +on: + push: + branches: [main] + pull_request: +name: Build Flatpak +jobs: + flatpak: + name: Build Flatpak + runs-on: ubuntu-latest + container: + image: bilelmoussaoui/flatpak-github-actions:gnome-45 + options: --privileged + steps: + - uses: actions/checkout@v4 + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: Burrow.flatpak + manifest-path: burrow-gtk/com.hackclub.burrow.devel.json + cache-key: flatpak-builder-${{ github.sha }} diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml index 9d11874..fd5837c 100644 --- a/.github/workflows/build-rpm.yml +++ b/.github/workflows/build-rpm.yml @@ -7,13 +7,14 @@ on: - "*" jobs: build: + name: Build RPM runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 - - name: Install + - name: Install RPM run: cargo install cargo-generate-rpm - - name: Build + - name: Build RPM run: | cargo build --release strip -s target/release/burrow diff --git a/Cargo.toml b/Cargo.toml index fcb83f5..7a17276 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,3 @@ [workspace] members = ["burrow", "tun"] +exclude = ["burrow-gtk"] diff --git a/burrow-gtk/.gitignore b/burrow-gtk/.gitignore new file mode 100644 index 0000000..caeec17 --- /dev/null +++ b/burrow-gtk/.gitignore @@ -0,0 +1 @@ +.flatpak-builder diff --git a/burrow-gtk/Cargo.lock b/burrow-gtk/Cargo.lock index 4e87651..3d8f154 100644 --- a/burrow-gtk/Cargo.lock +++ b/burrow-gtk/Cargo.lock @@ -3,19 +3,115 @@ version = 3 [[package]] -name = "aho-corasick" +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] -name = "anyhow" -version = "1.0.71" +name = "anstream" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] [[package]] name = "autocfg" @@ -23,6 +119,78 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", + "which", +] + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.39", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -30,18 +198,91 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "block" -version = "0.1.6" +name = "bitflags" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "burrow" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-channel", + "caps", + "clap", + "env_logger", + "libsystemd", + "log", + "nix", + "schemars", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-journald", + "tracing-log", + "tracing-oslog", + "tracing-subscriber", + "tun", +] [[package]] name = "burrow-gtk" version = "0.1.0" dependencies = [ - "gettext-rs", - "gtk4", - "libadwaita", + "burrow", + "relm4", + "relm4-components", + "relm4-icons", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", ] [[package]] @@ -50,7 +291,7 @@ version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab3603c4028a5e368d09b51c8b624b9a46edcd7c3778284077a6125af73c9f0a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", "glib", "libc", @@ -70,26 +311,286 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.0.79" +name = "caps" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" +dependencies = [ + "libc", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-expr" -version = "0.15.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "215c0072ecc28f92eeb0eea38ba63ddfcb65c2828c46311d646f1a3ff5f9841c" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" dependencies = [ "smallvec", "target-lexicon", ] [[package]] -name = "equivalent" +name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "concurrent-queue" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fehler" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5729fe49ba028cd550747b6e62cd3d841beccab5390aa398538c31a2d983635" +dependencies = [ + "fehler-macros", +] + +[[package]] +name = "fehler-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccb5acb1045ebbfa222e2c50679e392a71dd77030b78fb0189f2d9c5974400f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] name = "field-offset" @@ -97,30 +598,105 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ - "memoffset", + "memoffset 0.9.0", "rustc_version", ] [[package]] -name = "futures-channel" -version = "0.3.28" +name = "flate2" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" dependencies = [ "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -129,36 +705,46 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] -name = "futures-task" -version = "0.3.28" +name = "futures-sink" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -170,7 +756,7 @@ version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "695d6bc846438c5708b07007537b9274d883373dd30858ca881d7d71b5540717" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -197,7 +783,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk-pixbuf", "gdk4-sys", @@ -225,32 +811,41 @@ dependencies = [ ] [[package]] -name = "gettext-rs" -version = "0.7.0" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "gettext-sys", - "locale_config", + "typenum", + "version_check", ] [[package]] -name = "gettext-sys" -version = "0.21.3" +name = "getrandom" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ - "cc", - "temp-dir", + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + [[package]] name = "gio" version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", @@ -283,7 +878,7 @@ version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-executor", @@ -325,6 +920,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "gobject-sys" version = "0.17.10" @@ -365,7 +966,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk4", "glib", @@ -397,7 +998,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b28a32a04cd75cef14a0983f8b0c669e0fe152a0a7725accdeb594e2c764c88b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "field-offset", "futures-channel", @@ -448,10 +1049,53 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.14.0" +name = "gvdb" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "a7139233c0ecb66f285c47a3c1c02b35c8d52a42ca4c7448d0163e5637bb4bd3" +dependencies = [ + "byteorder", + "flate2", + "lazy_static", + "memmap2", + "quick-xml", + "safe-transmute", + "serde", + "serde_json", + "walkdir", + "zvariant", +] + +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" [[package]] name = "heck" @@ -460,13 +1104,190 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] -name = "indexmap" -version = "2.0.0" +name = "hermit-abi" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "http" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95b9abcae896730d42b78e09c155ed4ddf82c07b4de772c64aee5b2d8b7c150" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.2", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", ] [[package]] @@ -476,13 +1297,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] -name = "libadwaita" -version = "0.3.1" +name = "lazycell" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c4efd2020a4fcedbad2c4a97de97bf6045e5dc49d61d5a5d0cfd753db60700" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libadwaita" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab9c0843f9f23ff25634df2743690c3a1faffe0a190e60c490878517eb81abf" dependencies = [ - "bitflags", - "futures-channel", + "bitflags 1.3.2", "gdk-pixbuf", "gdk4", "gio", @@ -490,15 +1316,14 @@ dependencies = [ "gtk4", "libadwaita-sys", "libc", - "once_cell", "pango", ] [[package]] name = "libadwaita-sys" -version = "0.3.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0727b85b4fe2b1bed5ac90df6343de15cbf8118bfb96d7c3cc1512681a4b34ac" +checksum = "4231cb2499a9f0c4cdfa4885414b33e39901ddcac61150bc0bb4ff8a57ede404" dependencies = [ "gdk4-sys", "gio-sys", @@ -512,37 +1337,92 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] -name = "locale_config" -version = "0.3.0" +name = "libloading" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ - "lazy_static", - "objc", - "objc-foundation", - "regex", + "cfg-if", "winapi", ] [[package]] -name = "malloc_buf" -version = "0.0.6" +name = "libsystemd" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +checksum = "88b9597a67aa1c81a6624603e6bd0bcefb9e0f94c9c54970ec53771082104b4e" +dependencies = [ + "hmac", + "libc", + "log", + "nix", + "nom", + "once_cell", + "serde", + "sha2", + "thiserror", + "uuid", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" dependencies = [ "libc", ] [[package]] -name = "memchr" -version = "2.5.0" +name = "memoffset" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] [[package]] name = "memoffset" @@ -554,32 +1434,137 @@ dependencies = [ ] [[package]] -name = "objc" -version = "0.2.7" +name = "miette" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" dependencies = [ - "malloc_buf", + "miette-derive", + "once_cell", + "thiserror", + "unicode-width", ] [[package]] -name = "objc-foundation" -version = "0.1.1" +name = "miette-derive" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ - "block", - "objc", - "objc_id", + "proc-macro2", + "quote", + "syn 2.0.39", ] [[package]] -name = "objc_id" -version = "0.1.1" +name = "mime" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ - "objc", + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", ] [[package]] @@ -588,13 +1573,63 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl" +version = "0.10.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pango" version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35be456fc620e61f62dff7ff70fbd54dcbaf0a4b920c0f16de1107c47d921d48" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gio", "glib", "libc", @@ -615,10 +1650,88 @@ dependencies = [ ] [[package]] -name = "pin-project-lite" -version = "0.2.9" +name = "parking_lot" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -632,6 +1745,22 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.39", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -639,7 +1768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] @@ -668,38 +1797,190 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] -name = "quote" -version = "1.0.29" +name = "quick-xml" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] -name = "regex" -version = "1.8.4" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", ] [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "relm4" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c16f3fad883034773b7f5af4d7e865532b8f3641e5a8bab2a34561a8d960d81" +dependencies = [ + "async-trait", + "flume", + "fragile", + "futures", + "gtk4", + "libadwaita", + "once_cell", + "relm4-macros", + "tokio", + "tracing", +] + +[[package]] +name = "relm4-components" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5485d72dc94c12a59c571d80cf9a545e5b9a2f0ebc90ea5fd234929a9376f66d" +dependencies = [ + "once_cell", + "relm4", + "tracker", +] + +[[package]] +name = "relm4-icons" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e28bcc718a587bcfa31b034e0b8f4efe5b70e945b7de9d7d154b45357a0dadc" +dependencies = [ + "gtk4", + "gvdb", +] + +[[package]] +name = "relm4-macros" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9340e2553c0a184a80a0bfa1dcf73c47f3d48933aa6be90724b202f9fbd24735" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" @@ -711,40 +1992,295 @@ dependencies = [ ] [[package]] -name = "semver" -version = "1.0.17" +name = "rustix" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "safe-transmute" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a01dab6acf992653be49205bdd549f32f17cb2803e8eacf1560bf97259aae8" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "schemars" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.164" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] [[package]] name = "serde_spanned" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" dependencies = [ "serde", ] [[package]] -name = "slab" -version = "0.4.8" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "ssri" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082" +dependencies = [ + "base64", + "digest", + "hex", + "miette", + "sha-1", + "sha2", + "thiserror", + "xxhash-rust", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -759,9 +2295,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.22" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -769,10 +2305,31 @@ dependencies = [ ] [[package]] -name = "system-deps" -version = "6.1.1" +name = "system-configuration" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" dependencies = [ "cfg-expr", "heck", @@ -783,64 +2340,186 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.8" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] -name = "temp-dir" -version = "0.1.11" +name = "tempfile" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2 0.5.5", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", ] [[package]] name = "toml" -version = "0.7.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.21.0", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.11" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", @@ -848,10 +2527,221 @@ dependencies = [ ] [[package]] -name = "unicode-ident" -version = "1.0.9" +name = "tower-service" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-journald" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba316a74e8fc3c3896a850dba2375928a9fa171b085ecddfc7c054d39970f3fd" +dependencies = [ + "libc", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-oslog" +version = "0.1.2" +source = "git+https://github.com/Stormshield-robinc/tracing-oslog#c4d21a95e70cdd62b1cea04fc4f8be1c547cad6c" +dependencies = [ + "bindgen 0.64.0", + "cc", + "cfg-if", + "fnv", + "once_cell", + "parking_lot", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff9636d15e370187f6bf55b79ce62ebf4221998bc0ba1774d7fa208b007f6bf8" +dependencies = [ + "tracker-macros", +] + +[[package]] +name = "tracker-macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca029746fbe0efda3298205de77bf759d7fef23ac97902641e0b49a623b0455f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tun" +version = "0.1.0" +dependencies = [ + "anyhow", + "bindgen 0.65.1", + "byteorder", + "fehler", + "futures", + "lazy_static", + "libc", + "libloading", + "log", + "nix", + "reqwest", + "schemars", + "serde", + "socket2 0.4.10", + "ssri", + "tempfile", + "tokio", + "tracing", + "widestring", + "windows", + "zip", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +dependencies = [ + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" @@ -865,6 +2755,125 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "winapi" version = "0.3.9" @@ -881,6 +2890,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -888,10 +2906,187 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "winnow" -version = "0.4.7" +name = "windows" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" dependencies = [ "memchr", ] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zvariant" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +dependencies = [ + "byteorder", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/burrow-gtk/Cargo.toml b/burrow-gtk/Cargo.toml index 7243745..4763320 100644 --- a/burrow-gtk/Cargo.toml +++ b/burrow-gtk/Cargo.toml @@ -3,11 +3,10 @@ name = "burrow-gtk" version = "0.1.0" edition = "2021" -[dependencies] -gettext-rs = { version = "0.7", features = ["gettext-system"] } -gtk = { version = "0.6", package = "gtk4" } +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies.adw] -package = "libadwaita" -version = "0.3" -features = ["v1_2"] +[dependencies] +relm4 = { version = "0.6.2", features = ["libadwaita"] } +relm4-components = "0.6.2" +relm4-icons = { version = "0.6.0", features = ["plus"] } +burrow = { version = "*", path = "../burrow/" } diff --git a/burrow-gtk/README.md b/burrow-gtk/README.md deleted file mode 100644 index 93c1d95..0000000 --- a/burrow-gtk/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# burrow-gtk - -A description of this project. diff --git a/burrow-gtk/com.hackclub.Burrow.json b/burrow-gtk/com.hackclub.burrow.devel.json similarity index 71% rename from burrow-gtk/com.hackclub.Burrow.json rename to burrow-gtk/com.hackclub.burrow.devel.json index 081096b..8b32b02 100644 --- a/burrow-gtk/com.hackclub.Burrow.json +++ b/burrow-gtk/com.hackclub.burrow.devel.json @@ -1,7 +1,7 @@ { - "app-id" : "com.hackclub.Burrow", + "app-id" : "com.hackclub.burrow-devel", "runtime" : "org.gnome.Platform", - "runtime-version" : "44", + "runtime-version" : "45", "sdk" : "org.gnome.Sdk", "sdk-extensions" : [ "org.freedesktop.Sdk.Extension.rust-stable" @@ -39,11 +39,16 @@ { "name" : "burrow-gtk", "builddir" : true, - "buildsystem" : "meson", + "subdir" : "burrow-gtk", + "buildsystem" : "simple", + "build-commands": [ + "cargo build", + "install -Dm755 -t /app/bin target/debug/burrow-gtk" + ], "sources" : [ - { - "type" : "git", - "url" : "file:///var/home/reesericci/Code" + { + "type": "dir", + "path": "../" } ] } diff --git a/burrow-gtk/com.hackclub.burrow.json b/burrow-gtk/com.hackclub.burrow.json new file mode 100644 index 0000000..831a236 --- /dev/null +++ b/burrow-gtk/com.hackclub.burrow.json @@ -0,0 +1,56 @@ +{ + "app-id" : "com.hackclub.burrow", + "runtime" : "org.gnome.Platform", + "runtime-version" : "45", + "sdk" : "org.gnome.Sdk", + "sdk-extensions" : [ + "org.freedesktop.Sdk.Extension.rust-stable" + ], + "command" : "burrow-gtk", + "finish-args" : [ + "--share=network", + "--share=ipc", + "--socket=fallback-x11", + "--device=dri", + "--socket=wayland" + ], + "build-options" : { + "append-path" : "/usr/lib/sdk/rust-stable/bin", + "build-args" : [ + "--share=network" + ], + "env" : { + "RUST_BACKTRACE" : "1", + "RUST_LOG" : "burrow-gtk=debug" + } + }, + "cleanup" : [ + "/include", + "/lib/pkgconfig", + "/man", + "/share/doc", + "/share/gtk-doc", + "/share/man", + "/share/pkgconfig", + "*.la", + "*.a" + ], + "modules" : [ + { + "name" : "burrow-gtk", + "builddir" : true, + "subdir" : "burrow-gtk", + "buildsystem" : "simple", + "build-commands": [ + "cargo build --release", + "install -Dm755 -t /app/bin target/release/burrow-gtk" + ], + "sources" : [ + { + "type": "dir", + "path": "../" + } + ] + } + ] +} diff --git a/burrow-gtk/data/com.hackclub.Burrow.appdata.xml.in b/burrow-gtk/data/com.hackclub.burrow.appdata.xml.in similarity index 83% rename from burrow-gtk/data/com.hackclub.Burrow.appdata.xml.in rename to burrow-gtk/data/com.hackclub.burrow.appdata.xml.in index cbee29f..7f8e86b 100644 --- a/burrow-gtk/data/com.hackclub.Burrow.appdata.xml.in +++ b/burrow-gtk/data/com.hackclub.burrow.appdata.xml.in @@ -1,6 +1,6 @@ - com.hackclub.Burrow.desktop + com.hackclub.burrow.desktop GPL-3.0-or-later

No description

diff --git a/burrow-gtk/data/com.hackclub.Burrow.desktop.in b/burrow-gtk/data/com.hackclub.burrow.desktop.in similarity index 72% rename from burrow-gtk/data/com.hackclub.Burrow.desktop.in rename to burrow-gtk/data/com.hackclub.burrow.desktop.in index 024d14e..91c463d 100644 --- a/burrow-gtk/data/com.hackclub.Burrow.desktop.in +++ b/burrow-gtk/data/com.hackclub.burrow.desktop.in @@ -1,7 +1,7 @@ [Desktop Entry] -Name=burrow-gtk +Name=Burrow Exec=burrow-gtk -Icon=com.hackclub.Burrow +Icon=com.hackclub.burrow Terminal=false Type=Application Categories=GTK;Network diff --git a/burrow-gtk/data/com.hackclub.Burrow.gschema.xml b/burrow-gtk/data/com.hackclub.burrow.gschema.xml similarity index 62% rename from burrow-gtk/data/com.hackclub.Burrow.gschema.xml rename to burrow-gtk/data/com.hackclub.burrow.gschema.xml index 7121dfb..d1bceef 100644 --- a/burrow-gtk/data/com.hackclub.Burrow.gschema.xml +++ b/burrow-gtk/data/com.hackclub.burrow.gschema.xml @@ -1,5 +1,5 @@ - + diff --git a/burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.Burrow.svg b/burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.burrow.svg similarity index 100% rename from burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.Burrow.svg rename to burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.burrow.svg diff --git a/burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.Burrow-symbolic.svg b/burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.burrow-symbolic.svg similarity index 100% rename from burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.Burrow-symbolic.svg rename to burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.burrow-symbolic.svg diff --git a/burrow-gtk/data/icons/meson.build b/burrow-gtk/data/icons/meson.build index 4ee9744..86f6480 100644 --- a/burrow-gtk/data/icons/meson.build +++ b/burrow-gtk/data/icons/meson.build @@ -1,4 +1,4 @@ -application_id = 'com.hackclub.Burrow' +application_id = 'com.hackclub.burrow' scalable_dir = join_paths('hicolor', 'scalable', 'apps') install_data( diff --git a/burrow-gtk/data/meson.build b/burrow-gtk/data/meson.build index 4cca4a4..fadf18f 100644 --- a/burrow-gtk/data/meson.build +++ b/burrow-gtk/data/meson.build @@ -1,6 +1,6 @@ desktop_file = i18n.merge_file( - input: 'com.hackclub.Burrow.desktop.in', - output: 'com.hackclub.Burrow.desktop', + input: 'com.hackclub.burrow.desktop.in', + output: 'com.hackclub.burrow.desktop', type: 'desktop', po_dir: '../po', install: true, @@ -13,8 +13,8 @@ if desktop_utils.found() endif appstream_file = i18n.merge_file( - input: 'com.hackclub.Burrow.appdata.xml.in', - output: 'com.hackclub.Burrow.appdata.xml', + input: 'com.hackclub.burrow.appdata.xml.in', + output: 'com.hackclub.burrow.appdata.xml', po_dir: '../po', install: true, install_dir: join_paths(get_option('datadir'), 'appdata') @@ -25,7 +25,7 @@ if appstream_util.found() test('Validate appstream file', appstream_util, args: ['validate', appstream_file]) endif -install_data('com.hackclub.Burrow.gschema.xml', +install_data('com.hackclub.burrow.gschema.xml', install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') ) diff --git a/burrow-gtk/meson.build b/burrow-gtk/meson.build deleted file mode 100644 index 3e9a026..0000000 --- a/burrow-gtk/meson.build +++ /dev/null @@ -1,20 +0,0 @@ -project('burrow-gtk', 'rust', - version: '0.1.0', - meson_version: '>= 0.62.0', - default_options: [ 'warning_level=2', 'werror=false', ], -) - -i18n = import('i18n') -gnome = import('gnome') - - - -subdir('data') -subdir('src') -subdir('po') - -gnome.post_install( - glib_compile_schemas: true, - gtk_update_icon_cache: true, - update_desktop_database: true, -) diff --git a/burrow-gtk/src/application.rs b/burrow-gtk/src/application.rs deleted file mode 100644 index f511bae..0000000 --- a/burrow-gtk/src/application.rs +++ /dev/null @@ -1,92 +0,0 @@ -use gtk::prelude::*; -use adw::subclass::prelude::*; -use gtk::{gio, glib}; - -use crate::config::VERSION; -use crate::BurrowGtkWindow; - -mod imp { - use super::*; - - #[derive(Debug, Default)] - pub struct BurrowGtkApplication {} - - #[glib::object_subclass] - impl ObjectSubclass for BurrowGtkApplication { - const NAME: &'static str = "BurrowGtkApplication"; - type Type = super::BurrowGtkApplication; - type ParentType = adw::Application; - } - - impl ObjectImpl for BurrowGtkApplication { - fn constructed(&self) { - self.parent_constructed(); - let obj = self.obj(); - obj.setup_gactions(); - obj.set_accels_for_action("app.quit", &["q"]); - } - } - - impl ApplicationImpl for BurrowGtkApplication { - // We connect to the activate callback to create a window when the application - // has been launched. Additionally, this callback notifies us when the user - // tries to launch a "second instance" of the application. When they try - // to do that, we'll just present any existing window. - fn activate(&self) { - let application = self.obj(); - // Get the current window or create one if necessary - let window = if let Some(window) = application.active_window() { - window - } else { - let window = BurrowGtkWindow::new(&*application); - window.upcast() - }; - - // Ask the window manager/compositor to present the window - window.present(); - } - } - - impl GtkApplicationImpl for BurrowGtkApplication {} - impl AdwApplicationImpl for BurrowGtkApplication {} -} - -glib::wrapper! { - pub struct BurrowGtkApplication(ObjectSubclass) - @extends gio::Application, gtk::Application, adw::Application, - @implements gio::ActionGroup, gio::ActionMap; -} - -impl BurrowGtkApplication { - pub fn new(application_id: &str, flags: &gio::ApplicationFlags) -> Self { - glib::Object::builder() - .property("application-id", application_id) - .property("flags", flags) - .build() - } - - fn setup_gactions(&self) { - let quit_action = gio::ActionEntry::builder("quit") - .activate(move |app: &Self, _, _| app.quit()) - .build(); - let about_action = gio::ActionEntry::builder("about") - .activate(move |app: &Self, _, _| app.show_about()) - .build(); - self.add_action_entries([quit_action, about_action]); - } - - fn show_about(&self) { - let window = self.active_window().unwrap(); - let about = adw::AboutWindow::builder() - .transient_for(&window) - .application_name("burrow-gtk") - .application_icon("com.hackclub.Burrow") - .developer_name("Hack Club") - .version(VERSION) - .developers(vec!["Hack Club"]) - .copyright("© 2023 The Hack Foundation") - .build(); - - about.present(); - } -} diff --git a/burrow-gtk/src/burrow-gtk.gresource.xml b/burrow-gtk/src/burrow-gtk.gresource.xml deleted file mode 100644 index 23a5a82..0000000 --- a/burrow-gtk/src/burrow-gtk.gresource.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - window.ui - gtk/help-overlay.ui - - diff --git a/burrow-gtk/src/config.rs b/burrow-gtk/src/config.rs deleted file mode 100644 index 73ce81e..0000000 --- a/burrow-gtk/src/config.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub static VERSION: &str = "0.1.0"; -pub static GETTEXT_PACKAGE: &str = "burrow-gtk"; -pub static LOCALEDIR: &str = "/app/share/locale"; -pub static PKGDATADIR: &str = "/app/share/burrow-gtk"; diff --git a/burrow-gtk/src/config.rs.in b/burrow-gtk/src/config.rs.in deleted file mode 100644 index 1a24858..0000000 --- a/burrow-gtk/src/config.rs.in +++ /dev/null @@ -1,4 +0,0 @@ -pub static VERSION: &str = @VERSION@; -pub static GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; -pub static LOCALEDIR: &str = @LOCALEDIR@; -pub static PKGDATADIR: &str = @PKGDATADIR@; diff --git a/burrow-gtk/src/gtk/help-overlay.blp b/burrow-gtk/src/gtk/help-overlay.blp deleted file mode 100644 index 90ee78f..0000000 --- a/burrow-gtk/src/gtk/help-overlay.blp +++ /dev/null @@ -1,24 +0,0 @@ -using Gtk 4.0; - -ShortcutsWindow help_overlay { - modal: true; - - ShortcutsSection { - section-name: "shortcuts"; - max-height: 10; - - ShortcutsGroup { - title: C_("shortcut window", "General"); - - ShortcutsShortcut { - title: C_("shortcut window", "Show Shortcuts"); - action-name: "win.show-help-overlay"; - } - - ShortcutsShortcut { - title: C_("shortcut window", "Quit"); - action-name: "app.quit"; - } - } - } -} diff --git a/burrow-gtk/src/main.rs b/burrow-gtk/src/main.rs index 6ccd23b..d91b6c2 100644 --- a/burrow-gtk/src/main.rs +++ b/burrow-gtk/src/main.rs @@ -1,35 +1,87 @@ -mod application; -mod config; -mod window; +use adw::prelude::*; +use burrow::{DaemonClient, DaemonCommand, DaemonStartOptions}; +use gtk::Align; +use relm4::{ + component::{AsyncComponent, AsyncComponentParts, AsyncComponentSender}, + prelude::*, +}; -use self::application::BurrowGtkApplication; -use self::window::BurrowGtkWindow; +struct App {} -use config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR}; -use gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain}; -use gtk::{gio, glib}; -use gtk::prelude::*; - -fn main() -> glib::ExitCode { - // Set up gettext translations - bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); - bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8") - .expect("Unable to set the text domain encoding"); - textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain"); - - // Load resources - let resources = gio::Resource::load(PKGDATADIR.to_owned() + "/burrow-gtk.gresource") - .expect("Could not load resources"); - gio::resources_register(&resources); - - // Create a new GtkApplication. The application manages our main loop, - // application windows, integration with the window manager/compositor, and - // desktop features such as file opening and single-instance applications. - let app = BurrowGtkApplication::new("com.hackclub.Burrow", &gio::ApplicationFlags::empty()); - - // Run the application. This function will block until the application - // exits. Upon return, we have our exit code to return to the shell. (This - // is the code you see when you do `echo $?` after running a command in a - // terminal. - app.run() +#[derive(Debug)] +enum Msg { + Start, + Stop, +} + +#[relm4::component(async)] +impl AsyncComponent for App { + type Init = (); + type Input = Msg; + type Output = (); + type CommandOutput = (); + + view! { + adw::Window { + set_title: Some("Simple app"), + set_default_size: (640, 480), + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + set_margin_all: 5, + set_valign: Align::Center, + + gtk::Label { + set_label: "Burrow GTK Switch", + }, + + gtk::Switch { + set_halign: Align::Center, + set_hexpand: false, + set_vexpand: false, + connect_active_notify => move |switch| + sender.input(if switch.is_active() { Msg::Start } else { Msg::Stop }) + }, + } + } + } + + async fn init( + _: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let model = App {}; + + let widgets = view_output!(); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + msg: Self::Input, + _sender: AsyncComponentSender, + _root: &Self::Root, + ) { + match msg { + Msg::Start => { + let mut client = DaemonClient::new().await.unwrap(); + client + .send_command(DaemonCommand::Start(DaemonStartOptions::default())) + .await + .unwrap(); + } + Msg::Stop => { + let mut client = DaemonClient::new().await.unwrap(); + client.send_command(DaemonCommand::Stop).await.unwrap(); + } + } + } +} + +fn main() { + let app = RelmApp::new("com.hackclub.burrow"); + app.run_async::(()); } diff --git a/burrow-gtk/src/meson.build b/burrow-gtk/src/meson.build deleted file mode 100644 index 15ec34b..0000000 --- a/burrow-gtk/src/meson.build +++ /dev/null @@ -1,67 +0,0 @@ -pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) -gnome = import('gnome') - - -blueprints = custom_target('blueprints', - input: files( - 'gtk/help-overlay.blp', - 'window.blp', - ), - output: '.', - command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], -) - -gnome.compile_resources('burrow-gtk', - 'burrow-gtk.gresource.xml', - gresource_bundle: true, - install: true, - install_dir: pkgdatadir, - dependencies: blueprints -) - -conf = configuration_data() -conf.set_quoted('VERSION', meson.project_version()) -conf.set_quoted('GETTEXT_PACKAGE', 'burrow-gtk') -conf.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) -conf.set_quoted('PKGDATADIR', pkgdatadir) - -configure_file( - input: 'config.rs.in', - output: 'config.rs', - configuration: conf -) - -# Copy the config.rs output to the source directory. -run_command( - 'cp', - join_paths(meson.project_build_root(), 'src', 'config.rs'), - join_paths(meson.project_source_root(), 'src', 'config.rs'), - check: true -) - -cargo_bin = find_program('cargo') -cargo_opt = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ] -cargo_opt += [ '--target-dir', meson.project_build_root() / 'src' ] -cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ] - -if get_option('buildtype') == 'release' - cargo_options += [ '--release' ] - rust_target = 'release' -else - rust_target = 'debug' -endif - -cargo_build = custom_target( - 'cargo-build', - build_by_default: true, - build_always_stale: true, - output: meson.project_name(), - console: true, - install: true, - install_dir: get_option('bindir'), - command: [ - 'env', cargo_env, - cargo_bin, 'build', - cargo_opt, '&&', 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@', - ] -) diff --git a/burrow-gtk/src/window.blp b/burrow-gtk/src/window.blp deleted file mode 100644 index 6f3534a..0000000 --- a/burrow-gtk/src/window.blp +++ /dev/null @@ -1,48 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template BurrowGtkWindow : Adw.ApplicationWindow { - default-width: 600; - default-height: 400; - - Box { - orientation: vertical; - - HeaderBar header_bar { - [end] - MenuButton { - icon-name: "open-menu-symbolic"; - menu-model: primary_menu; - } - } - - Label label { - label: "Burrow GNOME"; - - vexpand: true; - - styles [ - "title-1", - ] - } - } -} - -menu primary_menu { - section { - item { - label: _("_Preferences"); - action: "app.preferences"; - } - - item { - label: _("_Keyboard Shortcuts"); - action: "win.show-help-overlay"; - } - - item { - label: _("_About Burrow-gtk"); - action: "app.about"; - } - } -} diff --git a/burrow-gtk/src/window.rs b/burrow-gtk/src/window.rs deleted file mode 100644 index 4a51ef0..0000000 --- a/burrow-gtk/src/window.rs +++ /dev/null @@ -1,51 +0,0 @@ -use gtk::prelude::*; -use adw::subclass::prelude::*; -use gtk::{gio, glib}; - -mod imp { - use super::*; - - #[derive(Debug, Default, gtk::CompositeTemplate)] - #[template(resource = "/com/hackclub/Burrow/window.ui")] - pub struct BurrowGtkWindow { - // Template widgets - #[template_child] - pub header_bar: TemplateChild, - #[template_child] - pub label: TemplateChild, - } - - #[glib::object_subclass] - impl ObjectSubclass for BurrowGtkWindow { - const NAME: &'static str = "BurrowGtkWindow"; - type Type = super::BurrowGtkWindow; - type ParentType = adw::ApplicationWindow; - - fn class_init(klass: &mut Self::Class) { - klass.bind_template(); - } - - fn instance_init(obj: &glib::subclass::InitializingObject) { - obj.init_template(); - } - } - - impl ObjectImpl for BurrowGtkWindow {} - impl WidgetImpl for BurrowGtkWindow {} - impl WindowImpl for BurrowGtkWindow {} - impl ApplicationWindowImpl for BurrowGtkWindow {} - impl AdwApplicationWindowImpl for BurrowGtkWindow {} -} - -glib::wrapper! { - pub struct BurrowGtkWindow(ObjectSubclass) - @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow, @implements gio::ActionGroup, gio::ActionMap; -} - -impl BurrowGtkWindow { - pub fn new>(application: &P) -> Self { - glib::Object::builder() - .property("application", application) - .build() - } -} diff --git a/burrow-gtk/subprojects/blueprint-compiler.wrap b/burrow-gtk/subprojects/blueprint-compiler.wrap deleted file mode 100644 index 6e6ee32..0000000 --- a/burrow-gtk/subprojects/blueprint-compiler.wrap +++ /dev/null @@ -1,8 +0,0 @@ -[wrap-git] -directory = blueprint-compiler -url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git -revision = main -depth = 1 - -[provide] -program_names = blueprint-compiler \ No newline at end of file diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs index c193a7b..d152240 100644 --- a/burrow/src/daemon/net/unix.rs +++ b/burrow/src/daemon/net/unix.rs @@ -1,15 +1,20 @@ use super::*; -use std::{ascii, io, os::fd::{FromRawFd, RawFd}, os::unix::net::UnixListener as StdUnixListener, path::Path}; -use std::hash::Hash; -use std::path::PathBuf; use anyhow::anyhow; use log::log; -use tracing::info; +use std::hash::Hash; +use std::path::PathBuf; +use std::{ + ascii, io, + os::fd::{FromRawFd, RawFd}, + os::unix::net::UnixListener as StdUnixListener, + path::Path, +}; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{UnixListener, UnixStream}, }; use tracing::debug; +use tracing::info; #[cfg(not(target_vendor = "apple"))] const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; @@ -18,16 +23,18 @@ const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; const UNIX_SOCKET_PATH: &str = "burrow.sock"; #[cfg(target_os = "macos")] -fn fetch_socket_path() -> Option{ +fn fetch_socket_path() -> Option { let tries = vec![ "burrow.sock".to_string(), - format!("{}/Library/Containers/com.hackclub.burrow.network/Data/burrow.sock", - std::env::var("HOME").unwrap_or_default()) - .to_string(), + format!( + "{}/Library/Containers/com.hackclub.burrow.network/Data/burrow.sock", + std::env::var("HOME").unwrap_or_default() + ) + .to_string(), ]; - for path in tries{ + for path in tries { let path = PathBuf::from(path); - if path.exists(){ + if path.exists() { return Some(path); } } @@ -35,11 +42,14 @@ fn fetch_socket_path() -> Option{ } #[cfg(not(target_os = "macos"))] -fn fetch_socket_path() -> Option{ +fn fetch_socket_path() -> Option { Some(Path::new(UNIX_SOCKET_PATH).to_path_buf()) } -pub async fn listen(cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver) -> Result<()> { +pub async fn listen( + cmd_tx: async_channel::Sender, + rsp_rx: async_channel::Receiver, +) -> Result<()> { listen_with_optional_fd(cmd_tx, rsp_rx, None).await } @@ -61,14 +71,12 @@ pub(crate) async fn listen_with_optional_fd( listener } else { // Won't help all that much, if we use the async version of fs. - if let Some(par) = path.parent(){ - std::fs::create_dir_all( - par - )?; + if let Some(par) = path.parent() { + std::fs::create_dir_all(par)?; } - match std::fs::remove_file(path){ - Err(e) if e.kind()==io::ErrorKind::NotFound => {Ok(())} - stuff => stuff + match std::fs::remove_file(path) { + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + stuff => stuff, }?; info!("Relative path: {}", path.to_string_lossy()); UnixListener::bind(path)? @@ -89,7 +97,7 @@ pub(crate) async fn listen_with_optional_fd( while let Ok(Some(line)) = lines.next_line().await { info!("Got line: {}", line); debug!("Line raw data: {:?}", line.as_bytes()); - let mut res : DaemonResponse = DaemonResponseData::None.into(); + let mut res: DaemonResponse = DaemonResponseData::None.into(); let req = match serde_json::from_str::(&line) { Ok(req) => Some(req), Err(e) => { @@ -100,7 +108,6 @@ pub(crate) async fn listen_with_optional_fd( let mut res = serde_json::to_string(&res).unwrap(); res.push('\n'); - if let Some(req) = req { cmd_tx.send(req.command).await.unwrap(); let res = rsp_rxc.recv().await.unwrap().with_id(req.id); @@ -114,14 +121,14 @@ pub(crate) async fn listen_with_optional_fd( } } +#[derive(Debug)] pub struct DaemonClient { connection: UnixStream, } impl DaemonClient { pub async fn new() -> Result { - let path = fetch_socket_path() - .ok_or(anyhow!("Failed to find socket path"))?; + let path = fetch_socket_path().ok_or(anyhow!("Failed to find socket path"))?; // debug!("found path: {:?}", path); let connection = UnixStream::connect(path).await?; debug!("connected to socket"); diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index f66c4ac..ce6d637 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -14,7 +14,9 @@ use tun::TunInterface; // TODO Separate start and retrieve functions mod daemon; -pub use daemon::{DaemonCommand, DaemonResponseData, DaemonStartOptions, DaemonResponse, ServerInfo}; +pub use daemon::{ + DaemonClient, DaemonCommand, DaemonResponse, DaemonResponseData, DaemonStartOptions, ServerInfo, +}; #[cfg(target_vendor = "apple")] mod apple; From b008762a5be1d4feb24de026bb7487dfe4f56e39 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sun, 17 Dec 2023 01:20:56 +0800 Subject: [PATCH 091/128] Implement Wireguard Implements Wireguard --- .github/workflows/build-rust.yml | 4 + .rustfmt.toml | 12 + .vscode/settings.json | 3 + Apple/Burrow.xcodeproj/project.pbxproj | 1 - .../xcshareddata/swiftpm/Package.resolved | 77 -- Apple/NetworkExtension/BurrowIpc.swift | 2 +- Apple/NetworkExtension/DataTypes.swift | 35 +- .../NetworkExtension-macOS.entitlements | 2 + .../PacketTunnelProvider.swift | 63 +- Cargo.lock | 413 +++++++- Cargo.toml | 1 + Makefile | 18 + burrow/Cargo.toml | 21 +- burrow/src/apple.rs | 10 +- burrow/src/daemon/command.rs | 25 +- burrow/src/daemon/instance.rs | 93 +- burrow/src/daemon/mod.rs | 47 +- burrow/src/daemon/net/apple.rs | 24 +- burrow/src/daemon/net/mod.rs | 4 +- burrow/src/daemon/net/systemd.rs | 29 +- burrow/src/daemon/net/unix.rs | 40 +- burrow/src/daemon/net/windows.rs | 10 +- burrow/src/daemon/response.rs | 114 +-- ...ommand__daemoncommand_serialization-2.snap | 4 +- ...ommand__daemoncommand_serialization-3.snap | 4 +- ...ommand__daemoncommand_serialization-4.snap | 4 +- ...ommand__daemoncommand_serialization-5.snap | 5 + ..._command__daemoncommand_serialization.snap | 2 +- ...n__response__response_serialization-4.snap | 2 +- burrow/src/ensureroot.rs | 40 - burrow/src/lib.rs | 44 +- burrow/src/main.rs | 88 +- burrow/src/wireguard/config.rs | 112 +++ burrow/src/wireguard/iface.rs | 160 ++++ burrow/src/wireguard/mod.rs | 11 + burrow/src/wireguard/noise/errors.rs | 20 + burrow/src/wireguard/noise/handshake.rs | 900 ++++++++++++++++++ burrow/src/wireguard/noise/mod.rs | 634 ++++++++++++ burrow/src/wireguard/noise/rate_limiter.rs | 212 +++++ burrow/src/wireguard/noise/session.rs | 280 ++++++ burrow/src/wireguard/noise/timers.rs | 333 +++++++ burrow/src/wireguard/pcb.rs | 135 +++ burrow/src/wireguard/peer.rs | 22 + tun/Cargo.toml | 2 +- tun/build.rs | 5 +- tun/src/lib.rs | 6 +- tun/src/options.rs | 42 +- tun/src/tokio/mod.rs | 23 +- tun/src/unix/apple/kern_control.rs | 12 +- tun/src/unix/apple/mod.rs | 76 +- tun/src/unix/apple/sys.rs | 13 +- tun/src/unix/linux/mod.rs | 29 +- tun/src/unix/linux/sys.rs | 7 +- tun/src/unix/mod.rs | 32 +- tun/src/unix/queue.rs | 13 +- tun/src/windows/mod.rs | 14 +- tun/tests/configure.rs | 4 +- tun/tests/packets.rs | 11 +- tun/tests/tokio.rs | 4 +- 59 files changed, 3824 insertions(+), 529 deletions(-) create mode 100644 .rustfmt.toml delete mode 100644 Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Makefile create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-5.snap delete mode 100644 burrow/src/ensureroot.rs create mode 100644 burrow/src/wireguard/config.rs create mode 100755 burrow/src/wireguard/iface.rs create mode 100755 burrow/src/wireguard/mod.rs create mode 100755 burrow/src/wireguard/noise/errors.rs create mode 100755 burrow/src/wireguard/noise/handshake.rs create mode 100755 burrow/src/wireguard/noise/mod.rs create mode 100755 burrow/src/wireguard/noise/rate_limiter.rs create mode 100755 burrow/src/wireguard/noise/session.rs create mode 100755 burrow/src/wireguard/noise/timers.rs create mode 100755 burrow/src/wireguard/pcb.rs create mode 100755 burrow/src/wireguard/peer.rs diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 0993ec2..4c3782a 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -54,6 +54,10 @@ jobs: run: | sudo apt-get update sudo apt-get install -y ${{ join(matrix.packages, ' ') }} + - name: Install Windows Deps + if: matrix.os == 'windows-2022' + shell: bash + run: echo "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\Llvm\x64\bin" >> $GITHUB_PATH - name: Install Rust uses: dtolnay/rust-toolchain@stable with: diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..2a12e19 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,12 @@ +condense_wildcard_suffixes = true +format_macro_matchers = true +imports_layout = "HorizontalVertical" +imports_granularity = "Crate" +newline_style = "Unix" +overflow_delimited_expr = true +reorder_impl_items = true +group_imports = "StdExternalCrate" +trailing_semicolon = false +use_field_init_shorthand = true +use_try_shorthand = true +struct_lit_width = 30 diff --git a/.vscode/settings.json b/.vscode/settings.json index 4718093..5fbfc5c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,9 @@ "editor.acceptSuggestionOnEnter": "on", "rust-analyzer.restartServerOnConfigChange": true, "rust-analyzer.cargo.features": "all", + "rust-analyzer.rustfmt.extraArgs": [ + "+nightly" + ], "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer", } diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 7548f3e..a8ff620 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -251,7 +251,6 @@ ); mainGroup = D05B9F6929E39EEC008CB1F9; packageReferences = ( - D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */, ); productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */; projectDirPath = ""; diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 233bbf9..0000000 --- a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,77 +0,0 @@ -{ - "pins" : [ - { - "identity" : "collectionconcurrencykit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", - "state" : { - "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", - "version" : "0.2.0" - } - }, - { - "identity" : "sourcekitten", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/SourceKitten.git", - "state" : { - "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", - "version" : "0.34.1" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", - "version" : "1.2.2" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "013a48e2312e57b7b355db25bd3ea75282ebf274", - "version" : "0.50900.0-swift-DEVELOPMENT-SNAPSHOT-2023-02-06-a" - } - }, - { - "identity" : "swiftlint", - "kind" : "remoteSourceControl", - "location" : "https://github.com/realm/SwiftLint.git", - "state" : { - "revision" : "eb85125a5f293de3d3248af259980c98bc2b1faa", - "version" : "0.51.0" - } - }, - { - "identity" : "swiftytexttable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", - "state" : { - "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", - "version" : "0.9.0" - } - }, - { - "identity" : "swxmlhash", - "kind" : "remoteSourceControl", - "location" : "https://github.com/drmohundro/SWXMLHash.git", - "state" : { - "revision" : "4d0f62f561458cbe1f732171e625f03195151b60", - "version" : "7.0.1" - } - }, - { - "identity" : "yams", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/Yams.git", - "state" : { - "revision" : "f47ba4838c30dbd59998a4e4c87ab620ff959e8a", - "version" : "5.0.5" - } - } - ], - "version" : 2 -} diff --git a/Apple/NetworkExtension/BurrowIpc.swift b/Apple/NetworkExtension/BurrowIpc.swift index 7f18679..279cdf1 100644 --- a/Apple/NetworkExtension/BurrowIpc.swift +++ b/Apple/NetworkExtension/BurrowIpc.swift @@ -113,7 +113,7 @@ final class BurrowIpc { return data } - func request(_ request: Request, type: U.Type) async throws -> U { + func request(_ request: any Request, type: U.Type) async throws -> U { do { var data: Data = try JSONEncoder().encode(request) data.append(contentsOf: [10]) diff --git a/Apple/NetworkExtension/DataTypes.swift b/Apple/NetworkExtension/DataTypes.swift index b228d77..5d73805 100644 --- a/Apple/NetworkExtension/DataTypes.swift +++ b/Apple/NetworkExtension/DataTypes.swift @@ -1,5 +1,6 @@ import Foundation +// swiftlint:disable identifier_name enum BurrowError: Error { case addrDoesntExist case resultIsError @@ -7,22 +8,48 @@ enum BurrowError: Error { case resultIsNone } -protocol Request: Codable { +protocol Request: Codable where CommandT: Codable { + associatedtype CommandT var id: UInt { get set } - var command: String { get set } + var command: CommandT { get set } } -struct BurrowRequest: Request { +struct BurrowSingleCommand: Request { var id: UInt var command: String } +struct BurrowRequest: Request where T: Codable { + var id: UInt + var command: T +} + +struct BurrowStartRequest: Codable { + struct TunOptions: Codable { + let name: String? + let no_pi: Bool + let tun_excl: Bool + let tun_retrieve: Bool + let address: String? + } + struct StartOptions: Codable { + let tun: TunOptions + } + let Start: StartOptions +} + +func start_req_fd(id: UInt) -> BurrowRequest { + let command = BurrowStartRequest(Start: BurrowStartRequest.StartOptions( + tun: BurrowStartRequest.TunOptions(name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: nil) + )) + return BurrowRequest(id: id, command: command) +} + struct Response: Decodable where T: Decodable { var id: UInt var result: T } -// swiftlint:disable identifier_name struct BurrowResult: Codable where T: Codable { var Ok: T? var Err: String? diff --git a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements index c3d6dc2..edb3f26 100644 --- a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements +++ b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements @@ -4,6 +4,8 @@ com.apple.security.network.client + com.apple.security.network.server + com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 4b72115..19fa760 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -6,7 +6,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let logger = Logger(subsystem: "com.hackclub.burrow", category: "frontend") var client: BurrowIpc? var osInitialized = false - override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { + override func startTunnel(options: [String: NSObject]? = nil) async throws { logger.log("Starting tunnel") if !osInitialized { libburrow.initialize_oslog() @@ -15,28 +15,35 @@ class PacketTunnelProvider: NEPacketTunnelProvider { libburrow.start_srv() client = BurrowIpc(logger: logger) logger.info("Started server") - Task { - do { - let command = BurrowRequest(id: 0, command: "ServerConfig") - guard let data = try await client?.request(command, type: Response>.self) - else { - throw BurrowError.cantParseResult - } - let encoded = try JSONEncoder().encode(data.result) - self.logger.log("Received final data: \(String(decoding: encoded, as: UTF8.self))") - guard let serverconfig = data.result.Ok else { - throw BurrowError.resultIsError - } - guard let tunNs = self.generateTunSettings(from: serverconfig) else { - throw BurrowError.addrDoesntExist - } - try await self.setTunnelNetworkSettings(tunNs) - self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") - completionHandler(nil) - } catch { - self.logger.error("An error occurred: \(error)") - completionHandler(error) + do { + let command = BurrowSingleCommand(id: 0, command: "ServerConfig") + guard let data = try await client?.request(command, type: Response>.self) + else { + throw BurrowError.cantParseResult } + let encoded = try JSONEncoder().encode(data.result) + self.logger.log("Received final data: \(String(decoding: encoded, as: UTF8.self))") + guard let serverconfig = data.result.Ok else { + throw BurrowError.resultIsError + } + guard let tunNs = self.generateTunSettings(from: serverconfig) else { + throw BurrowError.addrDoesntExist + } + try await self.setTunnelNetworkSettings(tunNs) + self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") + + // let tunFd = self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int; + // self.logger.info("Found File Descriptor: \(tunFd)") + let startCommand = start_req_fd(id: 1) + guard let data = try await client?.request(startCommand, type: Response>.self) + else { + throw BurrowError.cantParseResult + } + let encodedStartRes = try JSONEncoder().encode(data.result) + self.logger.log("Received start server response: \(String(decoding: encodedStartRes, as: UTF8.self))") + } catch { + self.logger.error("An error occurred: \(error)") + throw error } } private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { @@ -50,16 +57,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider { logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") return nst } - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - completionHandler() + override func stopTunnel(with reason: NEProviderStopReason) async { } - override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - if let handler = completionHandler { - handler(messageData) - } + override func handleAppMessage(_ messageData: Data) async -> Data? { + messageData } - override func sleep(completionHandler: @escaping () -> Void) { - completionHandler() + override func sleep() async { } override func wake() { } diff --git a/Cargo.lock b/Cargo.lock index 6716320..cc8d3b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.3" @@ -39,15 +49,16 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is-terminal", "utf8parse", ] @@ -77,9 +88,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -92,14 +103,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] -name = "async-channel" -version = "1.9.0" +name = "arrayvec" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-channel" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", "event-listener", + "event-listener-strategy", "futures-core", + "pin-project-lite", ] [[package]] @@ -125,9 +144,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.2" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -176,7 +195,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.22", + "syn 2.0.39", "which", ] @@ -192,6 +211,15 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -211,15 +239,30 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" name = "burrow" version = "0.1.0" dependencies = [ + "aead", "anyhow", "async-channel", + "base64", + "blake2", "caps", + "chacha20poly1305", "clap", "env_logger", + "etherparse", + "fehler", + "futures", + "hmac", "insta", + "ip_network", + "ip_network_table", + "ipnet", "libsystemd", "log", "nix", + "parking_lot", + "rand", + "rand_core", + "ring", "schemars", "serde", "serde_json", @@ -230,6 +273,8 @@ dependencies = [ "tracing-oslog", "tracing-subscriber", "tun", + "uuid", + "x25519-dalek", ] [[package]] @@ -277,11 +322,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -299,6 +345,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "cipher" version = "0.4.4" @@ -307,6 +377,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -322,19 +393,20 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.4" +version = "4.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" +checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a" dependencies = [ "clap_builder", "clap_derive", + "once_cell", ] [[package]] name = "clap_builder" -version = "4.4.4" +version = "4.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" +checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d" dependencies = [ "anstream", "anstyle", @@ -344,14 +416,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -368,9 +440,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ "crossbeam-utils", ] @@ -443,9 +515,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "digest" version = "0.10.7" @@ -459,9 +559,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "either" @@ -519,10 +619,34 @@ dependencies = [ ] [[package]] -name = "event-listener" -version = "2.5.3" +name = "etherparse" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "bcb08c4aab4e2985045305551e67126b43f1b6b136bc4e1cd87fb0327877a611" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "event-listener" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener", + "pin-project-lite", +] [[package]] name = "fastrand" @@ -553,6 +677,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "fiat-crypto" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7" + [[package]] name = "flate2" version = "1.0.26" @@ -649,7 +779,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -692,6 +822,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.27.3" @@ -864,9 +1005,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.32.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e02c584f4595792d09509a94cdb92a3cef7592b1eb2d9877ee6f527062d0ea" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" dependencies = [ "console", "lazy_static", @@ -896,11 +1037,36 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "ip_network_table" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" +dependencies = [ + "ip_network", + "ip_network_table-deps-treebitmap", +] + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + [[package]] name = "ipnet" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +dependencies = [ + "serde", +] [[package]] name = "is-terminal" @@ -951,9 +1117,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libloading" @@ -1061,7 +1227,7 @@ checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -1173,6 +1339,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.55" @@ -1196,7 +1368,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -1223,6 +1395,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1283,9 +1461,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1299,6 +1477,29 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "platforms" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "prettyplease" version = "0.2.9" @@ -1306,32 +1507,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282" dependencies = [ "proc-macro2", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.29" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "redox_syscall" @@ -1411,6 +1636,20 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1423,6 +1662,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.21" @@ -1518,6 +1766,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + [[package]] name = "serde" version = "1.0.164" @@ -1535,7 +1789,7 @@ checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -1622,9 +1876,9 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "similar" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" [[package]] name = "slab" @@ -1651,6 +1905,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "ssri" version = "9.0.0" @@ -1698,9 +1958,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.22" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -1747,7 +2007,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -1817,7 +2077,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -1870,7 +2130,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -2004,6 +2264,22 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.0" @@ -2023,10 +2299,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ + "getrandom", "serde", ] @@ -2084,7 +2361,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", "wasm-bindgen-shared", ] @@ -2118,7 +2395,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2352,6 +2629,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "x25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "xxhash-rust" version = "0.8.6" @@ -2367,6 +2656,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 7a17276..44981a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ [workspace] members = ["burrow", "tun"] +resolver = "2" exclude = ["burrow-gtk"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2988e5c --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +tun_num := $(shell ifconfig | awk -F 'utun|[: ]' '/utun[0-9]/ {print $$2}' | tail -n 1) + +check: + @cargo check + +build: + @cargo run build + +daemon: + @RUST_BACKTRACE=1 RUST_LOG=debug cargo run daemon + +start: + @RUST_BACKTRACE=1 RUST_LOG=debug cargo run start + +test-dns: + @sudo route delete 8.8.8.8 + @sudo route add 8.8.8.8 -interface utun$(tun_num) + @dig @8.8.8.8 hackclub.com diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index ab19e5c..f263cc6 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["lib", "staticlib"] [dependencies] anyhow = "1.0" -tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util", "rt-multi-thread"] } +tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util", "rt-multi-thread", "time"] } tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } clap = { version = "4.3.2", features = ["derive"] } tracing = "0.1" @@ -22,8 +22,24 @@ env_logger = "0.10" log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" -async-channel = "1.9" +blake2 = "0.10.6" +chacha20poly1305 = "0.10.1" +rand = "0.8.5" +rand_core = "0.6.4" +aead = "0.5.2" +x25519-dalek = { version = "2.0.0", features = ["reusable_secrets", "static_secrets"] } +ring = "0.17.7" +parking_lot = "0.12.1" +hmac = "0.12" +ipnet = { version = "2.8.0", features = ["serde"] } +base64 = "0.21.4" +fehler = "1.0.0" +ip_network_table = "0.2.0" +ip_network = "0.4.0" +async-channel = "2.1.1" schemars = "0.8" +futures = "0.3.28" +uuid = { version = "1.6.1", features = ["v4"] } [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5.5" @@ -34,6 +50,7 @@ nix = { version = "0.26.2" } [dev-dependencies] insta = { version = "1.32.0", features = ["yaml"] } +etherparse = "0.12" [package.metadata.generate-rpm] assets = [ diff --git a/burrow/src/apple.rs b/burrow/src/apple.rs index 0a96877..571b413 100644 --- a/burrow/src/apple.rs +++ b/burrow/src/apple.rs @@ -1,15 +1,13 @@ -use tracing::{debug, Subscriber}; -use tracing::instrument::WithSubscriber; +use tracing::debug; use tracing_oslog::OsLogger; -use tracing_subscriber::FmtSubscriber; use tracing_subscriber::layer::SubscriberExt; pub use crate::daemon::start_srv; #[no_mangle] pub extern "C" fn initialize_oslog() { - let collector = tracing_subscriber::registry() - .with(OsLogger::new("com.hackclub.burrow", "backend")); + let collector = + tracing_subscriber::registry().with(OsLogger::new("com.hackclub.burrow", "backend")); tracing::subscriber::set_global_default(collector).unwrap(); debug!("Initialized oslog tracing in libburrow rust FFI"); -} \ No newline at end of file +} diff --git a/burrow/src/daemon/command.rs b/burrow/src/daemon/command.rs index a5a1f30..53b4108 100644 --- a/burrow/src/daemon/command.rs +++ b/burrow/src/daemon/command.rs @@ -12,21 +12,22 @@ pub enum DaemonCommand { #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct DaemonStartOptions { - pub(super) tun: TunOptions, + pub tun: TunOptions, } #[test] fn test_daemoncommand_serialization() { + insta::assert_snapshot!(serde_json::to_string(&DaemonCommand::Start( + DaemonStartOptions::default() + )) + .unwrap()); insta::assert_snapshot!( - serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap() + serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions { + tun: TunOptions { ..TunOptions::default() } + })) + .unwrap() ); - insta::assert_snapshot!( - serde_json::to_string(&DaemonCommand::ServerInfo).unwrap() - ); - insta::assert_snapshot!( - serde_json::to_string(&DaemonCommand::Stop).unwrap() - ); - insta::assert_snapshot!( - serde_json::to_string(&DaemonCommand::ServerConfig).unwrap() - ) -} \ No newline at end of file + insta::assert_snapshot!(serde_json::to_string(&DaemonCommand::ServerInfo).unwrap()); + insta::assert_snapshot!(serde_json::to_string(&DaemonCommand::Stop).unwrap()); + insta::assert_snapshot!(serde_json::to_string(&DaemonCommand::ServerConfig).unwrap()) +} diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index db9e1ac..34e9878 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -1,20 +1,43 @@ +use std::sync::Arc; + +use anyhow::Result; +use tokio::{sync::RwLock, task::JoinHandle}; use tracing::{debug, info, warn}; -use DaemonResponse; -use crate::daemon::response::{DaemonResponseData, ServerConfig, ServerInfo}; -use super::*; +use tun::tokio::TunInterface; + +use crate::{ + daemon::{ + command::DaemonCommand, + response::{DaemonResponse, DaemonResponseData, ServerConfig, ServerInfo}, + }, + wireguard::Interface, +}; + +enum RunState { + Running(JoinHandle>), + Idle, +} pub struct DaemonInstance { rx: async_channel::Receiver, sx: async_channel::Sender, - tun_interface: Option, + tun_interface: Option>>, + wg_interface: Arc>, + wg_state: RunState, } impl DaemonInstance { - pub fn new(rx: async_channel::Receiver, sx: async_channel::Sender) -> Self { + pub fn new( + rx: async_channel::Receiver, + sx: async_channel::Sender, + wg_interface: Arc>, + ) -> Self { Self { rx, sx, + wg_interface, tun_interface: None, + wg_state: RunState::Idle, } } @@ -22,28 +45,52 @@ impl DaemonInstance { info!("Daemon got command: {:?}", command); match command { DaemonCommand::Start(st) => { - if self.tun_interface.is_none() { - debug!("Daemon attempting start tun interface."); - self.tun_interface = Some(st.tun.open()?); - info!("Daemon started tun interface"); - } else { - warn!("Got start, but tun interface already up."); + match self.wg_state { + RunState::Running(_) => { + warn!("Got start, but tun interface already up."); + } + RunState::Idle => { + let tun_if = Arc::new(RwLock::new(st.tun.open()?)); + + debug!("Setting tun_interface"); + self.tun_interface = Some(tun_if.clone()); + debug!("tun_interface set: {:?}", self.tun_interface); + + debug!("Setting tun on wg_interface"); + self.wg_interface.write().await.set_tun(tun_if); + debug!("tun set on wg_interface"); + + debug!("Cloning wg_interface"); + let tmp_wg = self.wg_interface.clone(); + debug!("wg_interface cloned"); + + debug!("Spawning run task"); + let run_task = tokio::spawn(async move { + debug!("Running wg_interface"); + let twlock = tmp_wg.read().await; + debug!("wg_interface read lock acquired"); + twlock.run().await + }); + debug!("Run task spawned: {:?}", run_task); + + debug!("Setting wg_state to Running"); + self.wg_state = RunState::Running(run_task); + debug!("wg_state set to Running"); + + info!("Daemon started tun interface"); + } } Ok(DaemonResponseData::None) } - DaemonCommand::ServerInfo => { - match &self.tun_interface { - None => {Ok(DaemonResponseData::None)} - Some(ti) => { - info!("{:?}", ti); - Ok( - DaemonResponseData::ServerInfo( - ServerInfo::try_from(ti)? - ) - ) - } + DaemonCommand::ServerInfo => match &self.tun_interface { + None => Ok(DaemonResponseData::None), + Some(ti) => { + info!("{:?}", ti); + Ok(DaemonResponseData::ServerInfo(ServerInfo::try_from( + ti.read().await.inner.get_ref(), + )?)) } - } + }, DaemonCommand::Stop => { if self.tun_interface.is_some() { self.tun_interface = None; diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs index b59ad7f..5a35b28 100644 --- a/burrow/src/daemon/mod.rs +++ b/burrow/src/daemon/mod.rs @@ -1,26 +1,51 @@ -use super::*; -use tokio::sync::mpsc; +use std::sync::Arc; mod command; mod instance; mod net; mod response; -use instance::DaemonInstance; -use net::listen; - +use anyhow::Result; pub use command::{DaemonCommand, DaemonStartOptions}; -pub use net::DaemonClient; - +use instance::DaemonInstance; #[cfg(target_vendor = "apple")] pub use net::start_srv; +pub use net::DaemonClient; +pub use response::{DaemonResponse, DaemonResponseData, ServerInfo}; +use tokio::sync::{Notify, RwLock}; -pub use response::{DaemonResponseData, DaemonResponse, ServerInfo}; +use crate::{ + daemon::net::listen, + wireguard::{Config, Interface}, +}; -pub async fn daemon_main() -> Result<()> { +pub async fn daemon_main(notify_ready: Option>) -> Result<()> { let (commands_tx, commands_rx) = async_channel::unbounded(); let (response_tx, response_rx) = async_channel::unbounded(); - let mut inst = DaemonInstance::new(commands_rx, response_tx); - tokio::try_join!(inst.run(), listen(commands_tx, response_rx)).map(|_| ()) + let config = Config::default(); + let iface: Interface = config.try_into()?; + + let mut inst: DaemonInstance = + DaemonInstance::new(commands_rx, response_tx, Arc::new(RwLock::new(iface))); + + tracing::info!("Starting daemon jobs..."); + + let inst_job = tokio::spawn(async move { + let res = inst.run().await; + if let Err(e) = res { + tracing::error!("Error when running instance: {}", e); + } + }); + + let listen_job = tokio::spawn(async move { + let res = listen(commands_tx, response_rx, notify_ready).await; + if let Err(e) = res { + tracing::error!("Error when listening: {}", e); + } + }); + + tokio::try_join!(inst_job, listen_job) + .map(|_| ()) + .map_err(|e| e.into()) } diff --git a/burrow/src/daemon/net/apple.rs b/burrow/src/daemon/net/apple.rs index e53bdaa..143e913 100644 --- a/burrow/src/daemon/net/apple.rs +++ b/burrow/src/daemon/net/apple.rs @@ -1,24 +1,32 @@ +use std::sync::Arc; use std::thread; + use tokio::runtime::Runtime; -use tracing::error; +use tokio::sync::Notify; +use tracing::{error, info}; + use crate::daemon::{daemon_main, DaemonClient}; #[no_mangle] -pub extern "C" fn start_srv(){ +pub extern "C" fn start_srv() { + info!("Starting server"); + let start_notify = Arc::new(Notify::new()); + let start_recv = start_notify.clone(); let _handle = thread::spawn(move || { let rt = Runtime::new().unwrap(); rt.block_on(async { - if let Err(e) = daemon_main().await { + if let Err(e) = daemon_main(Some(start_notify.clone())).await { error!("Error when starting rpc server: {}", e); } }); + start_notify.notify_one(); }); let rt = Runtime::new().unwrap(); rt.block_on(async { - loop { - if let Ok(_) = DaemonClient::new().await{ - break - } + start_recv.notified().await; + match DaemonClient::new().await { + Ok(..) => info!("Server successfully started"), + Err(e) => error!("Could not connect to server: {}", e) } }); -} \ No newline at end of file +} diff --git a/burrow/src/daemon/net/mod.rs b/burrow/src/daemon/net/mod.rs index b5e0736..d369f40 100644 --- a/burrow/src/daemon/net/mod.rs +++ b/burrow/src/daemon/net/mod.rs @@ -1,6 +1,7 @@ -use super::*; use serde::{Deserialize, Serialize}; +use super::DaemonCommand; + #[cfg(target_family = "unix")] mod unix; #[cfg(all(target_family = "unix", not(target_os = "linux")))] @@ -28,4 +29,3 @@ pub struct DaemonRequest { pub id: u32, pub command: DaemonCommand, } - diff --git a/burrow/src/daemon/net/systemd.rs b/burrow/src/daemon/net/systemd.rs index 446c259..4534742 100644 --- a/burrow/src/daemon/net/systemd.rs +++ b/burrow/src/daemon/net/systemd.rs @@ -1,16 +1,33 @@ -use super::*; use std::os::fd::IntoRawFd; +use std::sync::Arc; -pub async fn listen(cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver) -> Result<()> { - if !libsystemd::daemon::booted() || listen_with_systemd(cmd_tx.clone(), rsp_rx.clone()).await.is_err() { - unix::listen(cmd_tx, rsp_rx).await?; +use anyhow::Result; +use tokio::sync::Notify; + +use super::*; +use crate::daemon::DaemonResponse; + +pub async fn listen( + cmd_tx: async_channel::Sender, + rsp_rx: async_channel::Receiver, + notify: Option> +) -> Result<()> { + if !libsystemd::daemon::booted() + || listen_with_systemd(cmd_tx.clone(), rsp_rx.clone()) + .await + .is_err() + { + unix::listen(cmd_tx, rsp_rx, notify).await?; } Ok(()) } -async fn listen_with_systemd(cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver) -> Result<()> { +async fn listen_with_systemd( + cmd_tx: async_channel::Sender, + rsp_rx: async_channel::Receiver, +) -> Result<()> { let fds = libsystemd::activation::receive_descriptors(false)?; - super::unix::listen_with_optional_fd(cmd_tx, rsp_rx,Some(fds[0].clone().into_raw_fd())).await + super::unix::listen_with_optional_fd(cmd_tx, rsp_rx, Some(fds[0].clone().into_raw_fd()), None).await } pub type DaemonClient = unix::DaemonClient; diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs index d152240..948bdff 100644 --- a/burrow/src/daemon/net/unix.rs +++ b/burrow/src/daemon/net/unix.rs @@ -1,20 +1,23 @@ -use super::*; -use anyhow::anyhow; -use log::log; -use std::hash::Hash; -use std::path::PathBuf; use std::{ - ascii, io, - os::fd::{FromRawFd, RawFd}, - os::unix::net::UnixListener as StdUnixListener, - path::Path, + io, + os::{ + fd::{FromRawFd, RawFd}, + unix::net::UnixListener as StdUnixListener, + }, + path::{Path, PathBuf}, }; +use std::sync::Arc; + +use anyhow::{anyhow, Result}; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{UnixListener, UnixStream}, }; -use tracing::debug; -use tracing::info; +use tracing::{debug, info}; + +use tokio::sync::Notify; +use super::*; +use crate::daemon::{DaemonCommand, DaemonResponse, DaemonResponseData}; #[cfg(not(target_vendor = "apple"))] const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; @@ -35,7 +38,7 @@ fn fetch_socket_path() -> Option { for path in tries { let path = PathBuf::from(path); if path.exists() { - return Some(path); + return Some(path) } } None @@ -49,14 +52,16 @@ fn fetch_socket_path() -> Option { pub async fn listen( cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, + notify: Option> ) -> Result<()> { - listen_with_optional_fd(cmd_tx, rsp_rx, None).await + listen_with_optional_fd(cmd_tx, rsp_rx, None, notify).await } pub(crate) async fn listen_with_optional_fd( cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, raw_fd: Option, + notify: Option> ) -> Result<()> { let path = Path::new(UNIX_SOCKET_PATH); @@ -81,12 +86,16 @@ pub(crate) async fn listen_with_optional_fd( info!("Relative path: {}", path.to_string_lossy()); UnixListener::bind(path)? }; + if let Some(notify) = notify { + notify.notify_one(); + } loop { let (stream, _) = listener.accept().await?; let cmd_tx = cmd_tx.clone(); // I'm pretty sure we won't need to manually join / shut this down, - // `lines` will return Err during dropping, and this task should exit gracefully. + // `lines` will return Err during dropping, and this task should exit + // gracefully. let rsp_rxc = rsp_rx.clone(); tokio::task::spawn(async move { let cmd_tx = cmd_tx; @@ -102,6 +111,7 @@ pub(crate) async fn listen_with_optional_fd( Ok(req) => Some(req), Err(e) => { res.result = Err(e.to_string()); + tracing::error!("Failed to parse request: {}", e); None } }; @@ -115,6 +125,8 @@ pub(crate) async fn listen_with_optional_fd( retres.push('\n'); info!("Sending response: {}", retres); write_stream.write_all(retres.as_bytes()).await.unwrap(); + } else { + write_stream.write_all(res.as_bytes()).await.unwrap(); } } }); diff --git a/burrow/src/daemon/net/windows.rs b/burrow/src/daemon/net/windows.rs index 3f9d513..c4a1d71 100644 --- a/burrow/src/daemon/net/windows.rs +++ b/burrow/src/daemon/net/windows.rs @@ -1,6 +1,12 @@ -use super::*; +use anyhow::Result; -pub async fn listen(_cmd_tx: async_channel::Sender, _rsp_rx: async_channel::Receiver) -> Result<()> { +use super::*; +use crate::daemon::DaemonResponse; + +pub async fn listen( + _cmd_tx: async_channel::Sender, + _rsp_rx: async_channel::Receiver, +) -> Result<()> { unimplemented!("This platform does not currently support daemon mode.") } diff --git a/burrow/src/daemon/response.rs b/burrow/src/daemon/response.rs index da47150..386da46 100644 --- a/burrow/src/daemon/response.rs +++ b/burrow/src/daemon/response.rs @@ -1,4 +1,3 @@ -use anyhow::anyhow; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tun::TunInterface; @@ -7,55 +6,50 @@ use tun::TunInterface; pub struct DaemonResponse { // Error types can't be serialized, so this is the second best option. pub result: Result, - pub id: u32 + pub id: u32, } -impl DaemonResponse{ - pub fn new(result: Result) -> Self{ - Self{ - result: result.map_err(|e| e.to_string()), - id: 0 - } - } -} - -impl Into for DaemonResponseData{ - fn into(self) -> DaemonResponse{ - DaemonResponse::new(Ok::(self)) - } -} - -impl DaemonResponse{ - pub fn with_id(self, id: u32) -> Self{ +impl DaemonResponse { + pub fn new(result: Result) -> Self { Self { - id, - ..self + result: result.map_err(|e| e.to_string()), + id: 0, } } } +impl From for DaemonResponse { + fn from(val: DaemonResponseData) -> Self { + DaemonResponse::new(Ok::(val)) + } +} + +impl DaemonResponse { + pub fn with_id(self, id: u32) -> Self { + Self { id, ..self } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct ServerInfo { pub name: Option, pub ip: Option, - pub mtu: Option + pub mtu: Option, } -impl TryFrom<&TunInterface> for ServerInfo{ +impl TryFrom<&TunInterface> for ServerInfo { type Error = anyhow::Error; - #[cfg(any(target_os="linux",target_vendor="apple"))] + #[cfg(any(target_os = "linux", target_vendor = "apple"))] fn try_from(server: &TunInterface) -> anyhow::Result { - Ok( - ServerInfo{ - name: server.name().ok(), - ip: server.ipv4_addr().ok().map(|ip| ip.to_string()), - mtu: server.mtu().ok() - } - ) + Ok(ServerInfo { + name: server.name().ok(), + ip: server.ipv4_addr().ok().map(|ip| ip.to_string()), + mtu: server.mtu().ok(), + }) } - #[cfg(not(any(target_os="linux",target_vendor="apple")))] + #[cfg(not(any(target_os = "linux", target_vendor = "apple")))] fn try_from(server: &TunInterface) -> anyhow::Result { Err(anyhow!("Not implemented in this platform")) } @@ -65,45 +59,55 @@ impl TryFrom<&TunInterface> for ServerInfo{ pub struct ServerConfig { pub address: Option, pub name: Option, - pub mtu: Option + pub mtu: Option, } impl Default for ServerConfig { fn default() -> Self { - Self{ - address: Some("10.0.0.1".to_string()), // Dummy remote address + Self { + address: Some("10.13.13.2".to_string()), // Dummy remote address name: None, - mtu: None + mtu: None, } } } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub enum DaemonResponseData{ +pub enum DaemonResponseData { ServerInfo(ServerInfo), ServerConfig(ServerConfig), - None + None, } #[test] -fn test_response_serialization() -> anyhow::Result<()>{ - insta::assert_snapshot!( - serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::None)))? - ); - insta::assert_snapshot!( - serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerInfo(ServerInfo{ +fn test_response_serialization() -> anyhow::Result<()> { + insta::assert_snapshot!(serde_json::to_string(&DaemonResponse::new(Ok::< + DaemonResponseData, + String, + >( + DaemonResponseData::None + )))?); + insta::assert_snapshot!(serde_json::to_string(&DaemonResponse::new(Ok::< + DaemonResponseData, + String, + >( + DaemonResponseData::ServerInfo(ServerInfo { name: Some("burrow".to_string()), ip: None, mtu: Some(1500) - }))))? - ); - insta::assert_snapshot!( - serde_json::to_string(&DaemonResponse::new(Err::("error".to_string())))? - ); - insta::assert_snapshot!( - serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig( - ServerConfig::default() - ))))? - ); + }) + )))?); + insta::assert_snapshot!(serde_json::to_string(&DaemonResponse::new(Err::< + DaemonResponseData, + String, + >( + "error".to_string() + )))?); + insta::assert_snapshot!(serde_json::to_string(&DaemonResponse::new(Ok::< + DaemonResponseData, + String, + >( + DaemonResponseData::ServerConfig(ServerConfig::default()) + )))?); Ok(()) -} \ No newline at end of file +} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap index 80b9e24..0eb9096 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap @@ -1,5 +1,5 @@ --- source: burrow/src/daemon/command.rs -expression: "serde_json::to_string(&DaemonCommand::ServerInfo).unwrap()" +expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions {\n tun: TunOptions { ..TunOptions::default() },\n })).unwrap()" --- -"ServerInfo" +{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":null}}} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap index 8dc1b8b..80b9e24 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap @@ -1,5 +1,5 @@ --- source: burrow/src/daemon/command.rs -expression: "serde_json::to_string(&DaemonCommand::Stop).unwrap()" +expression: "serde_json::to_string(&DaemonCommand::ServerInfo).unwrap()" --- -"Stop" +"ServerInfo" diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap index 9334ece..8dc1b8b 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap @@ -1,5 +1,5 @@ --- source: burrow/src/daemon/command.rs -expression: "serde_json::to_string(&DaemonCommand::ServerConfig).unwrap()" +expression: "serde_json::to_string(&DaemonCommand::Stop).unwrap()" --- -"ServerConfig" +"Stop" diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-5.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-5.snap new file mode 100644 index 0000000..9334ece --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-5.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/command.rs +expression: "serde_json::to_string(&DaemonCommand::ServerConfig).unwrap()" +--- +"ServerConfig" diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap index 2f8af66..bfd5117 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/command.rs expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap()" --- -{"Start":{"tun":{"name":null,"no_pi":null,"tun_excl":null}}} +{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":null}}} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap index 95f9e7b..9752ebc 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/response.rs expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig(ServerConfig::default()))))?" --- -{"result":{"Ok":{"ServerConfig":{"address":"10.0.0.1","name":null,"mtu":null}}},"id":0} +{"result":{"Ok":{"ServerConfig":{"address":"10.13.13.2","name":null,"mtu":null}}},"id":0} diff --git a/burrow/src/ensureroot.rs b/burrow/src/ensureroot.rs deleted file mode 100644 index b7c0757..0000000 --- a/burrow/src/ensureroot.rs +++ /dev/null @@ -1,40 +0,0 @@ -use tracing::instrument; - -// Check capabilities on Linux -#[cfg(target_os = "linux")] -#[instrument] -pub fn ensure_root() { - use caps::{has_cap, CapSet, Capability}; - - let cap_net_admin = Capability::CAP_NET_ADMIN; - if let Ok(has_cap) = has_cap(None, CapSet::Effective, cap_net_admin) { - if !has_cap { - eprintln!( - "This action needs the CAP_NET_ADMIN permission. Did you mean to run it as root?" - ); - std::process::exit(77); - } - } else { - eprintln!("Failed to check capabilities. Please file a bug report!"); - std::process::exit(71); - } -} - -// Check for root user on macOS -#[cfg(target_vendor = "apple")] -#[instrument] -pub fn ensure_root() { - use nix::unistd::Uid; - - let current_uid = Uid::current(); - if !current_uid.is_root() { - eprintln!("This action must be run as root!"); - std::process::exit(77); - } -} - -#[cfg(target_family = "windows")] -#[instrument] -pub fn ensure_root() { - todo!() -} diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index ce6d637..3dfc4ac 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -1,21 +1,16 @@ -#![deny(missing_debug_implementations)] -pub mod ensureroot; - -use anyhow::Result; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +pub mod wireguard; #[cfg(any(target_os = "linux", target_vendor = "apple"))] -use std::{ - mem, - os::fd::{AsRawFd, FromRawFd}, -}; - -use tun::TunInterface; - -// TODO Separate start and retrieve functions - mod daemon; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] pub use daemon::{ - DaemonClient, DaemonCommand, DaemonResponse, DaemonResponseData, DaemonStartOptions, ServerInfo, + DaemonClient, + DaemonCommand, + DaemonResponse, + DaemonResponseData, + DaemonStartOptions, + ServerInfo, }; #[cfg(target_vendor = "apple")] @@ -23,24 +18,3 @@ mod apple; #[cfg(target_vendor = "apple")] pub use apple::*; - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -#[no_mangle] -pub extern "C" fn retrieve() -> i32 { - let iface2 = (1..100) - .filter_map(|i| { - let iface = unsafe { TunInterface::from_raw_fd(i) }; - match iface.name() { - Ok(_name) => Some(iface), - Err(_) => { - mem::forget(iface); - None - } - } - }) - .next(); - match iface2 { - Some(iface) => iface.as_raw_fd(), - None => -1, - } -} diff --git a/burrow/src/main.rs b/burrow/src/main.rs index f89ee39..9a692d6 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -1,22 +1,22 @@ -use anyhow::Context; -use std::mem; -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -use std::os::fd::FromRawFd; - +use anyhow::{Context, Result}; use clap::{Args, Parser, Subcommand}; -use tracing::{instrument, Level}; - +use tracing::instrument; use tracing_log::LogTracer; use tracing_oslog::OsLogger; -use tracing_subscriber::{prelude::*, FmtSubscriber, EnvFilter}; -use anyhow::Result; +use tracing_subscriber::{prelude::*, EnvFilter, FmtSubscriber}; #[cfg(any(target_os = "linux", target_vendor = "apple"))] -use burrow::retrieve; use tun::TunInterface; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] mod daemon; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +mod wireguard; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] use daemon::{DaemonClient, DaemonCommand, DaemonStartOptions}; +use tun::TunOptions; + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] use crate::daemon::DaemonResponseData; #[derive(Parser)] @@ -64,17 +64,32 @@ struct DaemonArgs {} async fn try_start() -> Result<()> { let mut client = DaemonClient::new().await?; client - .send_command(DaemonCommand::Start(DaemonStartOptions::default())) + .send_command(DaemonCommand::Start(DaemonStartOptions { + tun: TunOptions::new().address("10.13.13.2"), + })) .await .map(|_| ()) } -#[cfg(any(target_os = "linux", target_vendor = "apple"))] +#[cfg(target_vendor = "apple")] #[instrument] async fn try_retrieve() -> Result<()> { - burrow::ensureroot::ensure_root(); - let iface2 = retrieve(); - tracing::info!("{}", iface2); + LogTracer::init() + .context("Failed to initialize LogTracer") + .unwrap(); + + if cfg!(target_os = "linux") || cfg!(target_vendor = "apple") { + let maybe_layer = system_log().unwrap(); + if let Some(layer) = maybe_layer { + let logger = layer.with_subscriber(FmtSubscriber::new()); + tracing::subscriber::set_global_default(logger) + .context("Failed to set the global tracing subscriber") + .unwrap(); + } + } + + let iface2 = TunInterface::retrieve().ok_or(anyhow::anyhow!("No interface found"))?; + tracing::info!("{:?}", iface2); Ok(()) } @@ -89,9 +104,10 @@ async fn initialize_tracing() -> Result<()> { FmtSubscriber::builder() .with_line_number(true) .with_env_filter(EnvFilter::from_default_env()) - .finish() + .finish(), ); - tracing::subscriber::set_global_default(logger).context("Failed to set the global tracing subscriber")?; + tracing::subscriber::set_global_default(logger) + .context("Failed to set the global tracing subscriber")?; } } @@ -106,7 +122,7 @@ async fn try_stop() -> Result<()> { } #[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_serverinfo() -> Result<()>{ +async fn try_serverinfo() -> Result<()> { let mut client = DaemonClient::new().await?; let res = client.send_command(DaemonCommand::ServerInfo).await?; match res.result { @@ -116,7 +132,9 @@ async fn try_serverinfo() -> Result<()>{ Ok(DaemonResponseData::None) => { println!("Server not started.") } - Ok(res) => {println!("Unexpected Response: {:?}", res)} + Ok(res) => { + println!("Unexpected Response: {:?}", res) + } Err(e) => { println!("Error when retrieving from server: {}", e) } @@ -125,7 +143,7 @@ async fn try_serverinfo() -> Result<()>{ } #[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_serverconfig() -> Result<()>{ +async fn try_serverconfig() -> Result<()> { let mut client = DaemonClient::new().await?; let res = client.send_command(DaemonCommand::ServerConfig).await?; match res.result { @@ -135,7 +153,9 @@ async fn try_serverconfig() -> Result<()>{ Ok(DaemonResponseData::None) => { println!("Server not started.") } - Ok(res) => {println!("Unexpected Response: {:?}", res)} + Ok(res) => { + println!("Unexpected Response: {:?}", res) + } Err(e) => { println!("Error when retrieving from server: {}", e) } @@ -148,7 +168,7 @@ async fn try_start() -> Result<()> { Ok(()) } -#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] +#[cfg(not(target_vendor = "apple"))] async fn try_retrieve() -> Result<()> { Ok(()) } @@ -167,6 +187,7 @@ async fn try_serverinfo() -> Result<()> { async fn try_serverconfig() -> Result<()> { Ok(()) } +#[cfg(any(target_os = "linux", target_vendor = "apple"))] #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { initialize_tracing().await?; @@ -185,31 +206,32 @@ async fn main() -> Result<()> { Commands::Stop => { try_stop().await?; } - Commands::Daemon(_) => daemon::daemon_main().await?, - Commands::ServerInfo => { - try_serverinfo().await? - } - Commands::ServerConfig => { - try_serverconfig().await? - } + Commands::Daemon(_) => daemon::daemon_main(None).await?, + Commands::ServerInfo => try_serverinfo().await?, + Commands::ServerConfig => try_serverconfig().await?, } Ok(()) } #[cfg(target_os = "linux")] -fn system_log() -> anyhow::Result> { +fn system_log() -> Result> { let maybe_journald = tracing_journald::layer(); match maybe_journald { Err(e) if e.kind() == std::io::ErrorKind::NotFound => { tracing::trace!("journald not found"); Ok(None) - }, - _ => Ok(Some(maybe_journald?)) + } + _ => Ok(Some(maybe_journald?)), } } #[cfg(target_vendor = "apple")] -fn system_log() -> anyhow::Result> { +fn system_log() -> Result> { Ok(Some(OsLogger::new("com.hackclub.burrow", "burrow-cli"))) } + +#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] +pub fn main() { + eprintln!("This platform is not supported currently.") +} diff --git a/burrow/src/wireguard/config.rs b/burrow/src/wireguard/config.rs new file mode 100644 index 0000000..d86486e --- /dev/null +++ b/burrow/src/wireguard/config.rs @@ -0,0 +1,112 @@ +use std::{net::ToSocketAddrs, str::FromStr}; + +use anyhow::{anyhow, Error, Result}; +use base64::{engine::general_purpose, Engine}; +use fehler::throws; +use ip_network::IpNetwork; +use x25519_dalek::{PublicKey, StaticSecret}; + +use crate::wireguard::{Interface as WgInterface, Peer as WgPeer}; + +#[throws] +fn parse_key(string: &str) -> [u8; 32] { + let value = general_purpose::STANDARD.decode(string)?; + let mut key = [0u8; 32]; + key.copy_from_slice(&value[..]); + key +} + +#[throws] +fn parse_secret_key(string: &str) -> StaticSecret { + let key = parse_key(string)?; + StaticSecret::from(key) +} + +#[throws] +fn parse_public_key(string: &str) -> PublicKey { + let key = parse_key(string)?; + PublicKey::from(key) +} + +/// A raw version of Peer Config that can be used later to reflect configuration files. +/// This should be later converted to a `WgPeer`. +/// Refers to https://github.com/pirate/wireguard-docs?tab=readme-ov-file#overview +pub struct Peer { + pub public_key: String, + pub preshared_key: Option, + pub allowed_ips: Vec, + pub endpoint: String, + pub persistent_keepalive: Option, + pub name: Option, +} + +pub struct Interface { + pub private_key: String, + pub address: String, + pub listen_port: u32, + pub dns: Vec, + pub mtu: Option, +} + +pub struct Config { + pub peers: Vec, + pub interface: Interface, // Support for multiple interfaces? +} + +impl TryFrom for WgInterface { + type Error = anyhow::Error; + + fn try_from(cfig: Config) -> Result { + let sk = parse_secret_key(&cfig.interface.private_key)?; + let wg_peers: Vec = cfig + .peers + .iter() + .map(|p| { + Ok(WgPeer { + private_key: sk.clone(), + public_key: parse_public_key(&p.public_key)?, + endpoint: p + .endpoint + .to_socket_addrs()? + .find(|sock| sock.is_ipv4()) + .ok_or(anyhow!("DNS Lookup Fails!"))?, + preshared_key: match &p.preshared_key { + None => Ok(None), + Some(k) => parse_key(k).map(Some), + }?, + allowed_ips: p + .allowed_ips + .iter() + .map(|ip_addr| { + IpNetwork::from_str(ip_addr) + .map_err(|e| anyhow!("Error parsing IP Network {}: {}", ip_addr, e)) + }) + .collect::>>()?, + }) + }) + .collect::>>()?; + WgInterface::new(wg_peers) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + interface: Interface { + private_key: "GNqIAOCRxjl/cicZyvkvpTklgQuUmGUIEkH7IXF/sEE=".into(), + address: "10.13.13.2/24".into(), + listen_port: 51820, + dns: Default::default(), + mtu: Default::default(), + }, + peers: vec![Peer { + endpoint: "wg.burrow.rs:51820".into(), + allowed_ips: vec!["8.8.8.8/32".into()], + public_key: "uy75leriJay0+oHLhRMpV+A5xAQ0hCJ+q7Ww81AOvT4=".into(), + preshared_key: Some("s7lx/mg+reVEMnGnqeyYOQkzD86n2+gYnx1M9ygi08k=".into()), + persistent_keepalive: Default::default(), + name: Default::default(), + }], + } + } +} diff --git a/burrow/src/wireguard/iface.rs b/burrow/src/wireguard/iface.rs new file mode 100755 index 0000000..281cc4a --- /dev/null +++ b/burrow/src/wireguard/iface.rs @@ -0,0 +1,160 @@ +use std::{net::IpAddr, sync::Arc}; + +use anyhow::Error; +use fehler::throws; +use futures::future::join_all; +use ip_network_table::IpNetworkTable; +use tokio::sync::RwLock; +use tracing::{debug, error}; +use tun::tokio::TunInterface; + +use super::{noise::Tunnel, Peer, PeerPcb}; + +struct IndexedPcbs { + pcbs: Vec>, + allowed_ips: IpNetworkTable, +} + +impl IndexedPcbs { + pub fn new() -> Self { + Self { + pcbs: vec![], + allowed_ips: IpNetworkTable::new(), + } + } + + pub fn insert(&mut self, pcb: PeerPcb) { + let idx: usize = self.pcbs.len(); + for allowed_ip in pcb.allowed_ips.iter() { + self.allowed_ips.insert(*allowed_ip, idx); + } + self.pcbs.insert(idx, Arc::new(pcb)); + } + + pub fn find(&self, addr: IpAddr) -> Option { + let (_, &idx) = self.allowed_ips.longest_match(addr)?; + Some(idx) + } +} + +impl FromIterator for IndexedPcbs { + fn from_iter>(iter: I) -> Self { + iter.into_iter().fold(Self::new(), |mut acc, pcb| { + acc.insert(pcb); + acc + }) + } +} + +pub struct Interface { + tun: Option>>, + pcbs: Arc, +} + +impl Interface { + #[throws] + pub fn new>(peers: I) -> Self { + let pcbs: IndexedPcbs = peers + .into_iter() + .map(PeerPcb::new) + .collect::>()?; + + let pcbs = Arc::new(pcbs); + Self { pcbs, tun: None } + } + + pub fn set_tun(&mut self, tun: Arc>) { + self.tun = Some(tun); + } + + pub async fn run(&self) -> anyhow::Result<()> { + let pcbs = self.pcbs.clone(); + let tun = self + .tun + .clone() + .ok_or(anyhow::anyhow!("tun interface does not exist"))?; + log::info!("Starting interface"); + + let outgoing = async move { + loop { + let mut buf = [0u8; 3000]; + + let src = { + let src = match tun.read().await.recv(&mut buf[..]).await { + Ok(len) => &buf[..len], + Err(e) => { + error!("Failed to read from interface: {}", e); + continue + } + }; + debug!("Read {} bytes from interface", src.len()); + src + }; + + let dst_addr = match Tunnel::dst_address(src) { + Some(addr) => addr, + None => { + debug!("No destination found"); + continue + } + }; + + debug!("Routing packet to {}", dst_addr); + + let Some(idx) = pcbs.find(dst_addr) else { + continue + }; + + debug!("Found peer:{}", idx); + + match pcbs.pcbs[idx].send(src).await { + Ok(..) => { + let addr = pcbs.pcbs[idx].endpoint; + debug!("Sent packet to peer {}", addr); + } + Err(e) => { + log::error!("Failed to send packet {}", e); + continue + } + }; + } + }; + + let mut tsks = vec![]; + let tun = self + .tun + .clone() + .ok_or(anyhow::anyhow!("tun interface does not exist"))?; + let outgoing = tokio::task::spawn(outgoing); + tsks.push(outgoing); + debug!("preparing to spawn read tasks"); + + { + let pcbs = &self.pcbs; + for i in 0..pcbs.pcbs.len() { + debug!("spawning read task for peer {}", i); + let pcb = pcbs.pcbs[i].clone(); + let tun = tun.clone(); + let tsk = async move { + if let Err(e) = pcb.open_if_closed().await { + log::error!("failed to open pcb: {}", e); + return + } + let r2 = pcb.run(tun).await; + if let Err(e) = r2 { + log::error!("failed to run pcb: {}", e); + } else { + debug!("pcb ran successfully"); + } + }; + debug!("task made.."); + tsks.push(tokio::spawn(tsk)); + } + debug!("spawned read tasks"); + } + debug!("preparing to join.."); + join_all(tsks).await; + debug!("joined!"); + Ok(()) + } +} diff --git a/burrow/src/wireguard/mod.rs b/burrow/src/wireguard/mod.rs new file mode 100755 index 0000000..b2e7b54 --- /dev/null +++ b/burrow/src/wireguard/mod.rs @@ -0,0 +1,11 @@ +mod config; +mod iface; +mod noise; +mod pcb; +mod peer; + +pub use config::Config; +pub use iface::Interface; +pub use pcb::PeerPcb; +pub use peer::Peer; +pub use x25519_dalek::{PublicKey, StaticSecret}; diff --git a/burrow/src/wireguard/noise/errors.rs b/burrow/src/wireguard/noise/errors.rs new file mode 100755 index 0000000..e196635 --- /dev/null +++ b/burrow/src/wireguard/noise/errors.rs @@ -0,0 +1,20 @@ +// Copyright (c) 2019 Cloudflare, Inc. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +#[derive(Debug)] +pub enum WireGuardError { + DestinationBufferTooSmall, + UnexpectedPacket, + WrongIndex, + WrongKey, + InvalidTai64nTimestamp, + WrongTai64nTimestamp, + InvalidMac, + InvalidAeadTag, + InvalidCounter, + DuplicateCounter, + InvalidPacket, + NoCurrentSession, + ConnectionExpired, + UnderLoad, +} diff --git a/burrow/src/wireguard/noise/handshake.rs b/burrow/src/wireguard/noise/handshake.rs new file mode 100755 index 0000000..2ec0c6a --- /dev/null +++ b/burrow/src/wireguard/noise/handshake.rs @@ -0,0 +1,900 @@ +// Copyright (c) 2019 Cloudflare, Inc. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +use std::{ + convert::TryInto, + time::{Duration, Instant, SystemTime}, +}; + +use aead::{Aead, Payload}; +use blake2::{ + digest::{FixedOutput, KeyInit}, + Blake2s256, + Blake2sMac, + Digest, +}; +use chacha20poly1305::XChaCha20Poly1305; +use rand_core::OsRng; +use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305}; + +use super::{ + errors::WireGuardError, + session::Session, + x25519, + HandshakeInit, + HandshakeResponse, + PacketCookieReply, +}; + +pub(crate) const LABEL_MAC1: &[u8; 8] = b"mac1----"; +pub(crate) const LABEL_COOKIE: &[u8; 8] = b"cookie--"; +const KEY_LEN: usize = 32; +const TIMESTAMP_LEN: usize = 12; + +// initiator.chaining_key = HASH(CONSTRUCTION) +const INITIAL_CHAIN_KEY: [u8; KEY_LEN] = [ + 96, 226, 109, 174, 243, 39, 239, 192, 46, 195, 53, 226, 160, 37, 210, 208, 22, 235, 66, 6, 248, + 114, 119, 245, 45, 56, 209, 152, 139, 120, 205, 54, +]; + +// initiator.chaining_hash = HASH(initiator.chaining_key || IDENTIFIER) +const INITIAL_CHAIN_HASH: [u8; KEY_LEN] = [ + 34, 17, 179, 97, 8, 26, 197, 102, 105, 18, 67, 219, 69, 138, 213, 50, 45, 156, 108, 102, 34, + 147, 232, 183, 14, 225, 156, 101, 186, 7, 158, 243, +]; + +#[inline] +pub(crate) fn b2s_hash(data1: &[u8], data2: &[u8]) -> [u8; 32] { + let mut hash = Blake2s256::new(); + hash.update(data1); + hash.update(data2); + hash.finalize().into() +} + +#[inline] +/// RFC 2401 HMAC+Blake2s, not to be confused with *keyed* Blake2s +pub(crate) fn b2s_hmac(key: &[u8], data1: &[u8]) -> [u8; 32] { + use blake2::digest::Update; + type HmacBlake2s = hmac::SimpleHmac; + let mut hmac = HmacBlake2s::new_from_slice(key).unwrap(); + hmac.update(data1); + hmac.finalize_fixed().into() +} + +#[inline] +/// Like b2s_hmac, but chain data1 and data2 together +pub(crate) fn b2s_hmac2(key: &[u8], data1: &[u8], data2: &[u8]) -> [u8; 32] { + use blake2::digest::Update; + type HmacBlake2s = hmac::SimpleHmac; + let mut hmac = HmacBlake2s::new_from_slice(key).unwrap(); + hmac.update(data1); + hmac.update(data2); + hmac.finalize_fixed().into() +} + +#[inline] +pub(crate) fn b2s_keyed_mac_16(key: &[u8], data1: &[u8]) -> [u8; 16] { + let mut hmac = Blake2sMac::new_from_slice(key).unwrap(); + blake2::digest::Update::update(&mut hmac, data1); + hmac.finalize_fixed().into() +} + +#[inline] +pub(crate) fn b2s_keyed_mac_16_2(key: &[u8], data1: &[u8], data2: &[u8]) -> [u8; 16] { + let mut hmac = Blake2sMac::new_from_slice(key).unwrap(); + blake2::digest::Update::update(&mut hmac, data1); + blake2::digest::Update::update(&mut hmac, data2); + hmac.finalize_fixed().into() +} + +pub(crate) fn b2s_mac_24(key: &[u8], data1: &[u8]) -> [u8; 24] { + let mut hmac = Blake2sMac::new_from_slice(key).unwrap(); + blake2::digest::Update::update(&mut hmac, data1); + hmac.finalize_fixed().into() +} + +#[inline] +/// This wrapper involves an extra copy and MAY BE SLOWER +fn aead_chacha20_seal(ciphertext: &mut [u8], key: &[u8], counter: u64, data: &[u8], aad: &[u8]) { + let mut nonce: [u8; 12] = [0; 12]; + nonce[4..12].copy_from_slice(&counter.to_le_bytes()); + + aead_chacha20_seal_inner(ciphertext, key, nonce, data, aad) +} + +#[inline] +fn aead_chacha20_seal_inner( + ciphertext: &mut [u8], + key: &[u8], + nonce: [u8; 12], + data: &[u8], + aad: &[u8], +) { + let key = LessSafeKey::new(UnboundKey::new(&CHACHA20_POLY1305, key).unwrap()); + + ciphertext[..data.len()].copy_from_slice(data); + + let tag = key + .seal_in_place_separate_tag( + Nonce::assume_unique_for_key(nonce), + Aad::from(aad), + &mut ciphertext[..data.len()], + ) + .unwrap(); + + ciphertext[data.len()..].copy_from_slice(tag.as_ref()); +} + +#[inline] +/// This wrapper involves an extra copy and MAY BE SLOWER +fn aead_chacha20_open( + buffer: &mut [u8], + key: &[u8], + counter: u64, + data: &[u8], + aad: &[u8], +) -> Result<(), WireGuardError> { + let mut nonce: [u8; 12] = [0; 12]; + nonce[4..].copy_from_slice(&counter.to_le_bytes()); + aead_chacha20_open_inner(buffer, key, nonce, data, aad) + .map_err(|_| WireGuardError::InvalidAeadTag)?; + Ok(()) +} + +#[inline] +fn aead_chacha20_open_inner( + buffer: &mut [u8], + key: &[u8], + nonce: [u8; 12], + data: &[u8], + aad: &[u8], +) -> Result<(), ring::error::Unspecified> { + let key = LessSafeKey::new(UnboundKey::new(&CHACHA20_POLY1305, key).unwrap()); + + let mut inner_buffer = data.to_owned(); + + let plaintext = key.open_in_place( + Nonce::assume_unique_for_key(nonce), + Aad::from(aad), + &mut inner_buffer, + )?; + + buffer.copy_from_slice(plaintext); + + Ok(()) +} + +#[derive(Debug)] +/// This struct represents a 12 byte [Tai64N](https://cr.yp.to/libtai/tai64.html) timestamp +struct Tai64N { + secs: u64, + nano: u32, +} + +#[derive(Debug)] +/// This struct computes a [Tai64N](https://cr.yp.to/libtai/tai64.html) timestamp from current system time +struct TimeStamper { + duration_at_start: Duration, + instant_at_start: Instant, +} + +impl TimeStamper { + /// Create a new TimeStamper + pub fn new() -> TimeStamper { + TimeStamper { + duration_at_start: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(), + instant_at_start: Instant::now(), + } + } + + /// Take time reading and generate a 12 byte timestamp + pub fn stamp(&self) -> [u8; 12] { + const TAI64_BASE: u64 = (1u64 << 62) + 37; + let mut ext_stamp = [0u8; 12]; + let stamp = Instant::now().duration_since(self.instant_at_start) + self.duration_at_start; + ext_stamp[0..8].copy_from_slice(&(stamp.as_secs() + TAI64_BASE).to_be_bytes()); + ext_stamp[8..12].copy_from_slice(&stamp.subsec_nanos().to_be_bytes()); + ext_stamp + } +} + +impl Tai64N { + /// A zeroed out timestamp + fn zero() -> Tai64N { + Tai64N { secs: 0, nano: 0 } + } + + /// Parse a timestamp from a 12 byte u8 slice + fn parse(buf: &[u8; 12]) -> Result { + if buf.len() < 12 { + return Err(WireGuardError::InvalidTai64nTimestamp) + } + + let (sec_bytes, nano_bytes) = buf.split_at(std::mem::size_of::()); + let secs = u64::from_be_bytes(sec_bytes.try_into().unwrap()); + let nano = u32::from_be_bytes(nano_bytes.try_into().unwrap()); + + // WireGuard does not actually expect tai64n timestamp, just monotonically + // increasing one if secs < (1u64 << 62) || secs >= (1u64 << 63) { + // return Err(WireGuardError::InvalidTai64nTimestamp); + //}; + // if nano >= 1_000_000_000 { + // return Err(WireGuardError::InvalidTai64nTimestamp); + //} + + Ok(Tai64N { secs, nano }) + } + + /// Check if this timestamp represents a time that is chronologically after + /// the time represented by the other timestamp + pub fn after(&self, other: &Tai64N) -> bool { + (self.secs > other.secs) || ((self.secs == other.secs) && (self.nano > other.nano)) + } +} + +/// Parameters used by the noise protocol +struct NoiseParams { + /// Our static public key + static_public: x25519::PublicKey, + /// Our static private key + static_private: x25519::StaticSecret, + /// Static public key of the other party + peer_static_public: x25519::PublicKey, + /// A shared key = DH(static_private, peer_static_public) + static_shared: x25519::SharedSecret, + /// A pre-computation of HASH("mac1----", peer_static_public) for this peer + sending_mac1_key: [u8; KEY_LEN], + /// An optional preshared key + preshared_key: Option<[u8; KEY_LEN]>, +} + +impl std::fmt::Debug for NoiseParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NoiseParams") + .field("static_public", &self.static_public) + .field("static_private", &"") + .field("peer_static_public", &self.peer_static_public) + .field("static_shared", &"") + .field("sending_mac1_key", &self.sending_mac1_key) + .field("preshared_key", &self.preshared_key) + .finish() + } +} + +struct HandshakeInitSentState { + local_index: u32, + hash: [u8; KEY_LEN], + chaining_key: [u8; KEY_LEN], + ephemeral_private: x25519::ReusableSecret, + time_sent: Instant, +} + +impl std::fmt::Debug for HandshakeInitSentState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HandshakeInitSentState") + .field("local_index", &self.local_index) + .field("hash", &self.hash) + .field("chaining_key", &self.chaining_key) + .field("ephemeral_private", &"") + .field("time_sent", &self.time_sent) + .finish() + } +} + +#[derive(Debug)] +enum HandshakeState { + /// No handshake in process + None, + /// We initiated the handshake + InitSent(HandshakeInitSentState), + /// Handshake initiated by peer + InitReceived { + hash: [u8; KEY_LEN], + chaining_key: [u8; KEY_LEN], + peer_ephemeral_public: x25519::PublicKey, + peer_index: u32, + }, + /// Handshake was established too long ago (implies no handshake is in + /// progress) + Expired, +} + +#[derive(Debug)] +pub struct Handshake { + params: NoiseParams, + /// Index of the next session + next_index: u32, + /// Allow to have two outgoing handshakes in flight, because sometimes we + /// may receive a delayed response to a handshake with bad networks + previous: HandshakeState, + /// Current handshake state + state: HandshakeState, + cookies: Cookies, + /// The timestamp of the last handshake we received + last_handshake_timestamp: Tai64N, + // TODO: make TimeStamper a singleton + stamper: TimeStamper, + pub(super) last_rtt: Option, +} + +#[derive(Default, Debug)] +struct Cookies { + last_mac1: Option<[u8; 16]>, + index: u32, + write_cookie: Option<[u8; 16]>, +} + +#[derive(Debug)] +pub struct HalfHandshake { + pub peer_index: u32, + pub peer_static_public: [u8; 32], +} + +pub fn parse_handshake_anon( + static_private: &x25519::StaticSecret, + static_public: &x25519::PublicKey, + packet: &HandshakeInit, +) -> Result { + let peer_index = packet.sender_idx; + // initiator.chaining_key = HASH(CONSTRUCTION) + let mut chaining_key = INITIAL_CHAIN_KEY; + // initiator.hash = HASH(HASH(initiator.chaining_key || IDENTIFIER) || + // responder.static_public) + let mut hash = INITIAL_CHAIN_HASH; + hash = b2s_hash(&hash, static_public.as_bytes()); + // msg.unencrypted_ephemeral = DH_PUBKEY(initiator.ephemeral_private) + let peer_ephemeral_public = x25519::PublicKey::from(*packet.unencrypted_ephemeral); + // initiator.hash = HASH(initiator.hash || msg.unencrypted_ephemeral) + hash = b2s_hash(&hash, peer_ephemeral_public.as_bytes()); + // temp = HMAC(initiator.chaining_key, msg.unencrypted_ephemeral) + // initiator.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac( + &b2s_hmac(&chaining_key, peer_ephemeral_public.as_bytes()), + &[0x01], + ); + // temp = HMAC(initiator.chaining_key, DH(initiator.ephemeral_private, + // responder.static_public)) + let ephemeral_shared = static_private.diffie_hellman(&peer_ephemeral_public); + let temp = b2s_hmac(&chaining_key, &ephemeral_shared.to_bytes()); + // initiator.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // key = HMAC(temp, initiator.chaining_key || 0x2) + let key = b2s_hmac2(&temp, &chaining_key, &[0x02]); + + let mut peer_static_public = [0u8; KEY_LEN]; + // msg.encrypted_static = AEAD(key, 0, initiator.static_public, initiator.hash) + aead_chacha20_open( + &mut peer_static_public, + &key, + 0, + packet.encrypted_static, + &hash, + )?; + + Ok(HalfHandshake { peer_index, peer_static_public }) +} + +impl NoiseParams { + /// New noise params struct from our secret key, peers public key, and + /// optional preshared key + fn new( + static_private: x25519::StaticSecret, + static_public: x25519::PublicKey, + peer_static_public: x25519::PublicKey, + preshared_key: Option<[u8; 32]>, + ) -> Result { + let static_shared = static_private.diffie_hellman(&peer_static_public); + + let initial_sending_mac_key = b2s_hash(LABEL_MAC1, peer_static_public.as_bytes()); + + Ok(NoiseParams { + static_public, + static_private, + peer_static_public, + static_shared, + sending_mac1_key: initial_sending_mac_key, + preshared_key, + }) + } + + /// Set a new private key + fn set_static_private( + &mut self, + static_private: x25519::StaticSecret, + static_public: x25519::PublicKey, + ) -> Result<(), WireGuardError> { + // Check that the public key indeed matches the private key + let check_key = x25519::PublicKey::from(&static_private); + assert_eq!(check_key.as_bytes(), static_public.as_bytes()); + + self.static_private = static_private; + self.static_public = static_public; + + self.static_shared = self.static_private.diffie_hellman(&self.peer_static_public); + Ok(()) + } +} + +impl Handshake { + pub(crate) fn new( + static_private: x25519::StaticSecret, + static_public: x25519::PublicKey, + peer_static_public: x25519::PublicKey, + global_idx: u32, + preshared_key: Option<[u8; 32]>, + ) -> Result { + let params = NoiseParams::new( + static_private, + static_public, + peer_static_public, + preshared_key, + )?; + + Ok(Handshake { + params, + next_index: global_idx, + previous: HandshakeState::None, + state: HandshakeState::None, + last_handshake_timestamp: Tai64N::zero(), + stamper: TimeStamper::new(), + cookies: Default::default(), + last_rtt: None, + }) + } + + pub(crate) fn is_in_progress(&self) -> bool { + !matches!(self.state, HandshakeState::None | HandshakeState::Expired) + } + + pub(crate) fn timer(&self) -> Option { + match self.state { + HandshakeState::InitSent(HandshakeInitSentState { time_sent, .. }) => Some(time_sent), + _ => None, + } + } + + pub(crate) fn set_expired(&mut self) { + self.previous = HandshakeState::Expired; + self.state = HandshakeState::Expired; + } + + pub(crate) fn is_expired(&self) -> bool { + matches!(self.state, HandshakeState::Expired) + } + + pub(crate) fn has_cookie(&self) -> bool { + self.cookies.write_cookie.is_some() + } + + pub(crate) fn clear_cookie(&mut self) { + self.cookies.write_cookie = None; + } + + // The index used is 24 bits for peer index, allowing for 16M active peers per + // server and 8 bits for cyclic session index + fn inc_index(&mut self) -> u32 { + let index = self.next_index; + let idx8 = index as u8; + self.next_index = (index & !0xff) | u32::from(idx8.wrapping_add(1)); + self.next_index + } + + pub(crate) fn set_static_private( + &mut self, + private_key: x25519::StaticSecret, + public_key: x25519::PublicKey, + ) -> Result<(), WireGuardError> { + self.params.set_static_private(private_key, public_key) + } + + pub(super) fn receive_handshake_initialization<'a>( + &mut self, + packet: HandshakeInit, + dst: &'a mut [u8], + ) -> Result<(&'a mut [u8], Session), WireGuardError> { + // initiator.chaining_key = HASH(CONSTRUCTION) + let mut chaining_key = INITIAL_CHAIN_KEY; + // initiator.hash = HASH(HASH(initiator.chaining_key || IDENTIFIER) || + // responder.static_public) + let mut hash = INITIAL_CHAIN_HASH; + hash = b2s_hash(&hash, self.params.static_public.as_bytes()); + // msg.sender_index = little_endian(initiator.sender_index) + let peer_index = packet.sender_idx; + // msg.unencrypted_ephemeral = DH_PUBKEY(initiator.ephemeral_private) + let peer_ephemeral_public = x25519::PublicKey::from(*packet.unencrypted_ephemeral); + // initiator.hash = HASH(initiator.hash || msg.unencrypted_ephemeral) + hash = b2s_hash(&hash, peer_ephemeral_public.as_bytes()); + // temp = HMAC(initiator.chaining_key, msg.unencrypted_ephemeral) + // initiator.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac( + &b2s_hmac(&chaining_key, peer_ephemeral_public.as_bytes()), + &[0x01], + ); + // temp = HMAC(initiator.chaining_key, DH(initiator.ephemeral_private, + // responder.static_public)) + let ephemeral_shared = self + .params + .static_private + .diffie_hellman(&peer_ephemeral_public); + let temp = b2s_hmac(&chaining_key, &ephemeral_shared.to_bytes()); + // initiator.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // key = HMAC(temp, initiator.chaining_key || 0x2) + let key = b2s_hmac2(&temp, &chaining_key, &[0x02]); + + let mut peer_static_public_decrypted = [0u8; KEY_LEN]; + // msg.encrypted_static = AEAD(key, 0, initiator.static_public, initiator.hash) + aead_chacha20_open( + &mut peer_static_public_decrypted, + &key, + 0, + packet.encrypted_static, + &hash, + )?; + + ring::constant_time::verify_slices_are_equal( + self.params.peer_static_public.as_bytes(), + &peer_static_public_decrypted, + ) + .map_err(|_| WireGuardError::WrongKey)?; + + // initiator.hash = HASH(initiator.hash || msg.encrypted_static) + hash = b2s_hash(&hash, packet.encrypted_static); + // temp = HMAC(initiator.chaining_key, DH(initiator.static_private, + // responder.static_public)) + let temp = b2s_hmac(&chaining_key, self.params.static_shared.as_bytes()); + // initiator.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // key = HMAC(temp, initiator.chaining_key || 0x2) + let key = b2s_hmac2(&temp, &chaining_key, &[0x02]); + // msg.encrypted_timestamp = AEAD(key, 0, TAI64N(), initiator.hash) + let mut timestamp = [0u8; TIMESTAMP_LEN]; + aead_chacha20_open(&mut timestamp, &key, 0, packet.encrypted_timestamp, &hash)?; + + let timestamp = Tai64N::parse(×tamp)?; + if !timestamp.after(&self.last_handshake_timestamp) { + // Possibly a replay + return Err(WireGuardError::WrongTai64nTimestamp) + } + self.last_handshake_timestamp = timestamp; + + // initiator.hash = HASH(initiator.hash || msg.encrypted_timestamp) + hash = b2s_hash(&hash, packet.encrypted_timestamp); + + self.previous = std::mem::replace(&mut self.state, HandshakeState::InitReceived { + chaining_key, + hash, + peer_ephemeral_public, + peer_index, + }); + + self.format_handshake_response(dst) + } + + pub(super) fn receive_handshake_response( + &mut self, + packet: HandshakeResponse, + ) -> Result { + // Check if there is a handshake awaiting a response and return the correct one + let (state, is_previous) = match (&self.state, &self.previous) { + (HandshakeState::InitSent(s), _) if s.local_index == packet.receiver_idx => (s, false), + (_, HandshakeState::InitSent(s)) if s.local_index == packet.receiver_idx => (s, true), + _ => return Err(WireGuardError::UnexpectedPacket), + }; + + let peer_index = packet.sender_idx; + let local_index = state.local_index; + + let unencrypted_ephemeral = x25519::PublicKey::from(*packet.unencrypted_ephemeral); + // msg.unencrypted_ephemeral = DH_PUBKEY(responder.ephemeral_private) + // responder.hash = HASH(responder.hash || msg.unencrypted_ephemeral) + let mut hash = b2s_hash(&state.hash, unencrypted_ephemeral.as_bytes()); + // temp = HMAC(responder.chaining_key, msg.unencrypted_ephemeral) + let temp = b2s_hmac(&state.chaining_key, unencrypted_ephemeral.as_bytes()); + // responder.chaining_key = HMAC(temp, 0x1) + let mut chaining_key = b2s_hmac(&temp, &[0x01]); + // temp = HMAC(responder.chaining_key, DH(responder.ephemeral_private, + // initiator.ephemeral_public)) + let ephemeral_shared = state + .ephemeral_private + .diffie_hellman(&unencrypted_ephemeral); + let temp = b2s_hmac(&chaining_key, &ephemeral_shared.to_bytes()); + // responder.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // temp = HMAC(responder.chaining_key, DH(responder.ephemeral_private, + // initiator.static_public)) + let temp = b2s_hmac( + &chaining_key, + &self + .params + .static_private + .diffie_hellman(&unencrypted_ephemeral) + .to_bytes(), + ); + // responder.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // temp = HMAC(responder.chaining_key, preshared_key) + let temp = b2s_hmac( + &chaining_key, + &self.params.preshared_key.unwrap_or([0u8; 32])[..], + ); + // responder.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // temp2 = HMAC(temp, responder.chaining_key || 0x2) + let temp2 = b2s_hmac2(&temp, &chaining_key, &[0x02]); + // key = HMAC(temp, temp2 || 0x3) + let key = b2s_hmac2(&temp, &temp2, &[0x03]); + // responder.hash = HASH(responder.hash || temp2) + hash = b2s_hash(&hash, &temp2); + // msg.encrypted_nothing = AEAD(key, 0, [empty], responder.hash) + aead_chacha20_open(&mut [], &key, 0, packet.encrypted_nothing, &hash)?; + + // responder.hash = HASH(responder.hash || msg.encrypted_nothing) + // hash = b2s_hash(hash, buf[ENC_NOTHING_OFF..ENC_NOTHING_OFF + + // ENC_NOTHING_SZ]); + + // Derive keys + // temp1 = HMAC(initiator.chaining_key, [empty]) + // temp2 = HMAC(temp1, 0x1) + // temp3 = HMAC(temp1, temp2 || 0x2) + // initiator.sending_key = temp2 + // initiator.receiving_key = temp3 + // initiator.sending_key_counter = 0 + // initiator.receiving_key_counter = 0 + let temp1 = b2s_hmac(&chaining_key, &[]); + let temp2 = b2s_hmac(&temp1, &[0x01]); + let temp3 = b2s_hmac2(&temp1, &temp2, &[0x02]); + + let rtt_time = Instant::now().duration_since(state.time_sent); + self.last_rtt = Some(rtt_time.as_millis() as u32); + + if is_previous { + self.previous = HandshakeState::None; + } else { + self.state = HandshakeState::None; + } + Ok(Session::new(local_index, peer_index, temp3, temp2)) + } + + pub(super) fn receive_cookie_reply( + &mut self, + packet: PacketCookieReply, + ) -> Result<(), WireGuardError> { + let mac1 = match self.cookies.last_mac1 { + Some(mac) => mac, + None => return Err(WireGuardError::UnexpectedPacket), + }; + + let local_index = self.cookies.index; + if packet.receiver_idx != local_index { + return Err(WireGuardError::WrongIndex) + } + // msg.encrypted_cookie = XAEAD(HASH(LABEL_COOKIE || responder.static_public), + // msg.nonce, cookie, last_received_msg.mac1) + let key = b2s_hash(LABEL_COOKIE, self.params.peer_static_public.as_bytes()); // TODO: pre-compute + + let payload = Payload { + aad: &mac1[0..16], + msg: packet.encrypted_cookie, + }; + let plaintext = XChaCha20Poly1305::new_from_slice(&key) + .unwrap() + .decrypt(packet.nonce.into(), payload) + .map_err(|_| WireGuardError::InvalidAeadTag)?; + + let cookie = plaintext + .try_into() + .map_err(|_| WireGuardError::InvalidPacket)?; + self.cookies.write_cookie = Some(cookie); + Ok(()) + } + + // Compute and append mac1 and mac2 to a handshake message + fn append_mac1_and_mac2<'a>( + &mut self, + local_index: u32, + dst: &'a mut [u8], + ) -> Result<&'a mut [u8], WireGuardError> { + let mac1_off = dst.len() - 32; + let mac2_off = dst.len() - 16; + + // msg.mac1 = MAC(HASH(LABEL_MAC1 || responder.static_public), + // msg[0:offsetof(msg.mac1)]) + let msg_mac1 = b2s_keyed_mac_16(&self.params.sending_mac1_key, &dst[..mac1_off]); + + dst[mac1_off..mac2_off].copy_from_slice(&msg_mac1[..]); + + // msg.mac2 = MAC(initiator.last_received_cookie, msg[0:offsetof(msg.mac2)]) + let msg_mac2: [u8; 16] = if let Some(cookie) = self.cookies.write_cookie { + b2s_keyed_mac_16(&cookie, &dst[..mac2_off]) + } else { + [0u8; 16] + }; + + dst[mac2_off..].copy_from_slice(&msg_mac2[..]); + + self.cookies.index = local_index; + self.cookies.last_mac1 = Some(msg_mac1); + Ok(dst) + } + + pub(super) fn format_handshake_initiation<'a>( + &mut self, + dst: &'a mut [u8], + ) -> Result<&'a mut [u8], WireGuardError> { + if dst.len() < super::HANDSHAKE_INIT_SZ { + return Err(WireGuardError::DestinationBufferTooSmall) + } + + let (message_type, rest) = dst.split_at_mut(4); + let (sender_index, rest) = rest.split_at_mut(4); + let (unencrypted_ephemeral, rest) = rest.split_at_mut(32); + let (encrypted_static, rest) = rest.split_at_mut(32 + 16); + let (encrypted_timestamp, _) = rest.split_at_mut(12 + 16); + + let local_index = self.inc_index(); + + // initiator.chaining_key = HASH(CONSTRUCTION) + let mut chaining_key = INITIAL_CHAIN_KEY; + // initiator.hash = HASH(HASH(initiator.chaining_key || IDENTIFIER) || + // responder.static_public) + let mut hash = INITIAL_CHAIN_HASH; + hash = b2s_hash(&hash, self.params.peer_static_public.as_bytes()); + // initiator.ephemeral_private = DH_GENERATE() + let ephemeral_private = x25519::ReusableSecret::random_from_rng(OsRng); + // msg.message_type = 1 + // msg.reserved_zero = { 0, 0, 0 } + message_type.copy_from_slice(&super::HANDSHAKE_INIT.to_le_bytes()); + // msg.sender_index = little_endian(initiator.sender_index) + sender_index.copy_from_slice(&local_index.to_le_bytes()); + // msg.unencrypted_ephemeral = DH_PUBKEY(initiator.ephemeral_private) + unencrypted_ephemeral + .copy_from_slice(x25519::PublicKey::from(&ephemeral_private).as_bytes()); + // initiator.hash = HASH(initiator.hash || msg.unencrypted_ephemeral) + hash = b2s_hash(&hash, unencrypted_ephemeral); + // temp = HMAC(initiator.chaining_key, msg.unencrypted_ephemeral) + // initiator.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&b2s_hmac(&chaining_key, unencrypted_ephemeral), &[0x01]); + // temp = HMAC(initiator.chaining_key, DH(initiator.ephemeral_private, + // responder.static_public)) + let ephemeral_shared = ephemeral_private.diffie_hellman(&self.params.peer_static_public); + let temp = b2s_hmac(&chaining_key, &ephemeral_shared.to_bytes()); + // initiator.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // key = HMAC(temp, initiator.chaining_key || 0x2) + let key = b2s_hmac2(&temp, &chaining_key, &[0x02]); + // msg.encrypted_static = AEAD(key, 0, initiator.static_public, initiator.hash) + aead_chacha20_seal( + encrypted_static, + &key, + 0, + self.params.static_public.as_bytes(), + &hash, + ); + // initiator.hash = HASH(initiator.hash || msg.encrypted_static) + hash = b2s_hash(&hash, encrypted_static); + // temp = HMAC(initiator.chaining_key, DH(initiator.static_private, + // responder.static_public)) + let temp = b2s_hmac(&chaining_key, self.params.static_shared.as_bytes()); + // initiator.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // key = HMAC(temp, initiator.chaining_key || 0x2) + let key = b2s_hmac2(&temp, &chaining_key, &[0x02]); + // msg.encrypted_timestamp = AEAD(key, 0, TAI64N(), initiator.hash) + let timestamp = self.stamper.stamp(); + aead_chacha20_seal(encrypted_timestamp, &key, 0, ×tamp, &hash); + // initiator.hash = HASH(initiator.hash || msg.encrypted_timestamp) + hash = b2s_hash(&hash, encrypted_timestamp); + + let time_now = Instant::now(); + self.previous = std::mem::replace( + &mut self.state, + HandshakeState::InitSent(HandshakeInitSentState { + local_index, + chaining_key, + hash, + ephemeral_private, + time_sent: time_now, + }), + ); + + self.append_mac1_and_mac2(local_index, &mut dst[..super::HANDSHAKE_INIT_SZ]) + } + + fn format_handshake_response<'a>( + &mut self, + dst: &'a mut [u8], + ) -> Result<(&'a mut [u8], Session), WireGuardError> { + if dst.len() < super::HANDSHAKE_RESP_SZ { + return Err(WireGuardError::DestinationBufferTooSmall) + } + + let state = std::mem::replace(&mut self.state, HandshakeState::None); + let (mut chaining_key, mut hash, peer_ephemeral_public, peer_index) = match state { + HandshakeState::InitReceived { + chaining_key, + hash, + peer_ephemeral_public, + peer_index, + } => (chaining_key, hash, peer_ephemeral_public, peer_index), + _ => { + panic!("Unexpected attempt to call send_handshake_response"); + } + }; + + let (message_type, rest) = dst.split_at_mut(4); + let (sender_index, rest) = rest.split_at_mut(4); + let (receiver_index, rest) = rest.split_at_mut(4); + let (unencrypted_ephemeral, rest) = rest.split_at_mut(32); + let (encrypted_nothing, _) = rest.split_at_mut(16); + + // responder.ephemeral_private = DH_GENERATE() + let ephemeral_private = x25519::ReusableSecret::random_from_rng(OsRng); + let local_index = self.inc_index(); + // msg.message_type = 2 + // msg.reserved_zero = { 0, 0, 0 } + message_type.copy_from_slice(&super::HANDSHAKE_RESP.to_le_bytes()); + // msg.sender_index = little_endian(responder.sender_index) + sender_index.copy_from_slice(&local_index.to_le_bytes()); + // msg.receiver_index = little_endian(initiator.sender_index) + receiver_index.copy_from_slice(&peer_index.to_le_bytes()); + // msg.unencrypted_ephemeral = DH_PUBKEY(initiator.ephemeral_private) + unencrypted_ephemeral + .copy_from_slice(x25519::PublicKey::from(&ephemeral_private).as_bytes()); + // responder.hash = HASH(responder.hash || msg.unencrypted_ephemeral) + hash = b2s_hash(&hash, unencrypted_ephemeral); + // temp = HMAC(responder.chaining_key, msg.unencrypted_ephemeral) + let temp = b2s_hmac(&chaining_key, unencrypted_ephemeral); + // responder.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // temp = HMAC(responder.chaining_key, DH(responder.ephemeral_private, + // initiator.ephemeral_public)) + let ephemeral_shared = ephemeral_private.diffie_hellman(&peer_ephemeral_public); + let temp = b2s_hmac(&chaining_key, &ephemeral_shared.to_bytes()); + // responder.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // temp = HMAC(responder.chaining_key, DH(responder.ephemeral_private, + // initiator.static_public)) + let temp = b2s_hmac( + &chaining_key, + &ephemeral_private + .diffie_hellman(&self.params.peer_static_public) + .to_bytes(), + ); + // responder.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // temp = HMAC(responder.chaining_key, preshared_key) + let temp = b2s_hmac( + &chaining_key, + &self.params.preshared_key.unwrap_or([0u8; 32])[..], + ); + // responder.chaining_key = HMAC(temp, 0x1) + chaining_key = b2s_hmac(&temp, &[0x01]); + // temp2 = HMAC(temp, responder.chaining_key || 0x2) + let temp2 = b2s_hmac2(&temp, &chaining_key, &[0x02]); + // key = HMAC(temp, temp2 || 0x3) + let key = b2s_hmac2(&temp, &temp2, &[0x03]); + // responder.hash = HASH(responder.hash || temp2) + hash = b2s_hash(&hash, &temp2); + // msg.encrypted_nothing = AEAD(key, 0, [empty], responder.hash) + aead_chacha20_seal(encrypted_nothing, &key, 0, &[], &hash); + + // Derive keys + // temp1 = HMAC(initiator.chaining_key, [empty]) + // temp2 = HMAC(temp1, 0x1) + // temp3 = HMAC(temp1, temp2 || 0x2) + // initiator.sending_key = temp2 + // initiator.receiving_key = temp3 + // initiator.sending_key_counter = 0 + // initiator.receiving_key_counter = 0 + let temp1 = b2s_hmac(&chaining_key, &[]); + let temp2 = b2s_hmac(&temp1, &[0x01]); + let temp3 = b2s_hmac2(&temp1, &temp2, &[0x02]); + + let dst = self.append_mac1_and_mac2(local_index, &mut dst[..super::HANDSHAKE_RESP_SZ])?; + + Ok((dst, Session::new(local_index, peer_index, temp2, temp3))) + } +} diff --git a/burrow/src/wireguard/noise/mod.rs b/burrow/src/wireguard/noise/mod.rs new file mode 100755 index 0000000..6ece759 --- /dev/null +++ b/burrow/src/wireguard/noise/mod.rs @@ -0,0 +1,634 @@ +// Copyright (c) 2019 Cloudflare, Inc. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +pub mod errors; +pub mod handshake; +pub mod rate_limiter; + +mod session; +mod timers; + +use std::{ + collections::VecDeque, + convert::{TryFrom, TryInto}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::Arc, + time::Duration, +}; + +use errors::WireGuardError; +use handshake::Handshake; +use rate_limiter::RateLimiter; +use timers::{TimerName, Timers}; + +/// The default value to use for rate limiting, when no other rate limiter is +/// defined +const PEER_HANDSHAKE_RATE_LIMIT: u64 = 10; + +const IPV4_MIN_HEADER_SIZE: usize = 20; +const IPV4_LEN_OFF: usize = 2; +const IPV4_SRC_IP_OFF: usize = 12; +const IPV4_DST_IP_OFF: usize = 16; +const IPV4_IP_SZ: usize = 4; + +const IPV6_MIN_HEADER_SIZE: usize = 40; +const IPV6_LEN_OFF: usize = 4; +const IPV6_SRC_IP_OFF: usize = 8; +const IPV6_DST_IP_OFF: usize = 24; +const IPV6_IP_SZ: usize = 16; + +const IP_LEN_SZ: usize = 2; + +const MAX_QUEUE_DEPTH: usize = 256; +/// number of sessions in the ring, better keep a PoT +const N_SESSIONS: usize = 8; + +pub mod x25519 { + pub use x25519_dalek::{ + EphemeralSecret, + PublicKey, + ReusableSecret, + SharedSecret, + StaticSecret, + }; +} + +#[derive(Debug)] +pub enum TunnResult<'a> { + Done, + Err(WireGuardError), + WriteToNetwork(&'a mut [u8]), + WriteToTunnelV4(&'a mut [u8], Ipv4Addr), + WriteToTunnelV6(&'a mut [u8], Ipv6Addr), +} + +impl<'a> From for TunnResult<'a> { + fn from(err: WireGuardError) -> TunnResult<'a> { + TunnResult::Err(err) + } +} + +/// Tunnel represents a point-to-point WireGuard connection +#[derive(Debug)] +pub struct Tunnel { + /// The handshake currently in progress + handshake: handshake::Handshake, + /// The N_SESSIONS most recent sessions, index is session id modulo + /// N_SESSIONS + sessions: [Option; N_SESSIONS], + /// Index of most recently used session + current: usize, + /// Queue to store blocked packets + packet_queue: VecDeque>, + /// Keeps tabs on the expiring timers + timers: timers::Timers, + tx_bytes: usize, + rx_bytes: usize, + rate_limiter: Arc, +} + +type MessageType = u32; +const HANDSHAKE_INIT: MessageType = 1; +const HANDSHAKE_RESP: MessageType = 2; +const COOKIE_REPLY: MessageType = 3; +const DATA: MessageType = 4; + +const HANDSHAKE_INIT_SZ: usize = 148; +const HANDSHAKE_RESP_SZ: usize = 92; +const COOKIE_REPLY_SZ: usize = 64; +const DATA_OVERHEAD_SZ: usize = 32; + +#[derive(Debug)] +pub struct HandshakeInit<'a> { + sender_idx: u32, + unencrypted_ephemeral: &'a [u8; 32], + encrypted_static: &'a [u8], + encrypted_timestamp: &'a [u8], +} + +#[derive(Debug)] +pub struct HandshakeResponse<'a> { + sender_idx: u32, + pub receiver_idx: u32, + unencrypted_ephemeral: &'a [u8; 32], + encrypted_nothing: &'a [u8], +} + +#[derive(Debug)] +pub struct PacketCookieReply<'a> { + pub receiver_idx: u32, + nonce: &'a [u8], + encrypted_cookie: &'a [u8], +} + +#[derive(Debug)] +pub struct PacketData<'a> { + pub receiver_idx: u32, + counter: u64, + encrypted_encapsulated_packet: &'a [u8], +} + +/// Describes a packet from network +#[derive(Debug)] +pub enum Packet<'a> { + HandshakeInit(HandshakeInit<'a>), + HandshakeResponse(HandshakeResponse<'a>), + CookieReply(PacketCookieReply<'a>), + Data(PacketData<'a>), +} + +impl Tunnel { + #[inline(always)] + pub fn parse_incoming_packet(src: &[u8]) -> Result { + if src.len() < 4 { + return Err(WireGuardError::InvalidPacket) + } + + // Checks the type, as well as the reserved zero fields + let packet_type = u32::from_le_bytes(src[0..4].try_into().unwrap()); + tracing::debug!("packet_type: {}", packet_type); + + Ok(match (packet_type, src.len()) { + (HANDSHAKE_INIT, HANDSHAKE_INIT_SZ) => Packet::HandshakeInit(HandshakeInit { + sender_idx: u32::from_le_bytes(src[4..8].try_into().unwrap()), + unencrypted_ephemeral: <&[u8; 32] as TryFrom<&[u8]>>::try_from(&src[8..40]) + .expect("length already checked above"), + encrypted_static: &src[40..88], + encrypted_timestamp: &src[88..116], + }), + (HANDSHAKE_RESP, HANDSHAKE_RESP_SZ) => Packet::HandshakeResponse(HandshakeResponse { + sender_idx: u32::from_le_bytes(src[4..8].try_into().unwrap()), + receiver_idx: u32::from_le_bytes(src[8..12].try_into().unwrap()), + unencrypted_ephemeral: <&[u8; 32] as TryFrom<&[u8]>>::try_from(&src[12..44]) + .expect("length already checked above"), + encrypted_nothing: &src[44..60], + }), + (COOKIE_REPLY, COOKIE_REPLY_SZ) => Packet::CookieReply(PacketCookieReply { + receiver_idx: u32::from_le_bytes(src[4..8].try_into().unwrap()), + nonce: &src[8..32], + encrypted_cookie: &src[32..64], + }), + (DATA, DATA_OVERHEAD_SZ..=std::usize::MAX) => Packet::Data(PacketData { + receiver_idx: u32::from_le_bytes(src[4..8].try_into().unwrap()), + counter: u64::from_le_bytes(src[8..16].try_into().unwrap()), + encrypted_encapsulated_packet: &src[16..], + }), + _ => return Err(WireGuardError::InvalidPacket), + }) + } + + pub fn is_expired(&self) -> bool { + self.handshake.is_expired() + } + + pub fn dst_address(packet: &[u8]) -> Option { + if packet.is_empty() { + return None + } + + match packet[0] >> 4 { + 4 if packet.len() >= IPV4_MIN_HEADER_SIZE => { + let addr_bytes: [u8; IPV4_IP_SZ] = packet + [IPV4_DST_IP_OFF..IPV4_DST_IP_OFF + IPV4_IP_SZ] + .try_into() + .unwrap(); + Some(IpAddr::from(addr_bytes)) + } + 6 if packet.len() >= IPV6_MIN_HEADER_SIZE => { + let addr_bytes: [u8; IPV6_IP_SZ] = packet + [IPV6_DST_IP_OFF..IPV6_DST_IP_OFF + IPV6_IP_SZ] + .try_into() + .unwrap(); + Some(IpAddr::from(addr_bytes)) + } + _ => None, + } + } + + pub fn src_address(packet: &[u8]) -> Option { + if packet.is_empty() { + return None + } + + match packet[0] >> 4 { + 4 if packet.len() >= IPV4_MIN_HEADER_SIZE => { + let addr_bytes: [u8; IPV4_IP_SZ] = packet + [IPV4_SRC_IP_OFF..IPV4_SRC_IP_OFF + IPV4_IP_SZ] + .try_into() + .unwrap(); + Some(IpAddr::from(addr_bytes)) + } + 6 if packet.len() >= IPV6_MIN_HEADER_SIZE => { + let addr_bytes: [u8; IPV6_IP_SZ] = packet + [IPV6_SRC_IP_OFF..IPV6_SRC_IP_OFF + IPV6_IP_SZ] + .try_into() + .unwrap(); + Some(IpAddr::from(addr_bytes)) + } + _ => None, + } + } + + /// Create a new tunnel using own private key and the peer public key + pub fn new( + static_private: x25519::StaticSecret, + peer_static_public: x25519::PublicKey, + preshared_key: Option<[u8; 32]>, + persistent_keepalive: Option, + index: u32, + rate_limiter: Option>, + ) -> Result { + let static_public = x25519::PublicKey::from(&static_private); + + let tunn = Tunnel { + handshake: Handshake::new( + static_private, + static_public, + peer_static_public, + index << 8, + preshared_key, + ) + .map_err(|_| "Invalid parameters")?, + sessions: Default::default(), + current: Default::default(), + tx_bytes: Default::default(), + rx_bytes: Default::default(), + + packet_queue: VecDeque::new(), + timers: Timers::new(persistent_keepalive, rate_limiter.is_none()), + + rate_limiter: rate_limiter.unwrap_or_else(|| { + Arc::new(RateLimiter::new(&static_public, PEER_HANDSHAKE_RATE_LIMIT)) + }), + }; + + Ok(tunn) + } + + /// Update the private key and clear existing sessions + pub fn set_static_private( + &mut self, + static_private: x25519::StaticSecret, + static_public: x25519::PublicKey, + rate_limiter: Option>, + ) -> Result<(), WireGuardError> { + self.timers.should_reset_rr = rate_limiter.is_none(); + self.rate_limiter = rate_limiter.unwrap_or_else(|| { + Arc::new(RateLimiter::new(&static_public, PEER_HANDSHAKE_RATE_LIMIT)) + }); + self.handshake + .set_static_private(static_private, static_public)?; + for s in &mut self.sessions { + *s = None; + } + Ok(()) + } + + /// Encapsulate a single packet from the tunnel interface. + /// Returns TunnResult. + /// + /// # Panics + /// Panics if dst buffer is too small. + /// Size of dst should be at least src.len() + 32, and no less than 148 + /// bytes. + pub fn encapsulate<'a>(&mut self, src: &[u8], dst: &'a mut [u8]) -> TunnResult<'a> { + let current = self.current; + if let Some(ref session) = self.sessions[current % N_SESSIONS] { + // Send the packet using an established session + let packet = session.format_packet_data(src, dst); + self.timer_tick(TimerName::TimeLastPacketSent); + // Exclude Keepalive packets from timer update. + if !src.is_empty() { + self.timer_tick(TimerName::TimeLastDataPacketSent); + } + self.tx_bytes += src.len(); + return TunnResult::WriteToNetwork(packet) + } + + // If there is no session, queue the packet for future retry + self.queue_packet(src); + // Initiate a new handshake if none is in progress + self.format_handshake_initiation(dst, false) + } + + /// Receives a UDP datagram from the network and parses it. + /// Returns TunnResult. + /// + /// If the result is of type TunnResult::WriteToNetwork, should repeat the + /// call with empty datagram, until TunnResult::Done is returned. If + /// batch processing packets, it is OK to defer until last + /// packet is processed. + pub fn decapsulate<'a>( + &mut self, + src_addr: Option, + datagram: &[u8], + dst: &'a mut [u8], + ) -> TunnResult<'a> { + if datagram.is_empty() { + // Indicates a repeated call + return self.send_queued_packet(dst) + } + + let mut cookie = [0u8; COOKIE_REPLY_SZ]; + let packet = match self + .rate_limiter + .verify_packet(src_addr, datagram, &mut cookie) + { + Ok(packet) => packet, + Err(TunnResult::WriteToNetwork(cookie)) => { + dst[..cookie.len()].copy_from_slice(cookie); + return TunnResult::WriteToNetwork(&mut dst[..cookie.len()]) + } + Err(TunnResult::Err(e)) => return TunnResult::Err(e), + _ => unreachable!(), + }; + + self.handle_verified_packet(packet, dst) + } + + pub(crate) fn handle_verified_packet<'a>( + &mut self, + packet: Packet, + dst: &'a mut [u8], + ) -> TunnResult<'a> { + match packet { + Packet::HandshakeInit(p) => self.handle_handshake_init(p, dst), + Packet::HandshakeResponse(p) => self.handle_handshake_response(p, dst), + Packet::CookieReply(p) => self.handle_cookie_reply(p), + Packet::Data(p) => self.handle_data(p, dst), + } + .unwrap_or_else(TunnResult::from) + } + + fn handle_handshake_init<'a>( + &mut self, + p: HandshakeInit, + dst: &'a mut [u8], + ) -> Result, WireGuardError> { + tracing::debug!( + message = "Received handshake_initiation", + remote_idx = p.sender_idx + ); + + let (packet, session) = self.handshake.receive_handshake_initialization(p, dst)?; + + // Store new session in ring buffer + let index = session.local_index(); + self.sessions[index % N_SESSIONS] = Some(session); + + self.timer_tick(TimerName::TimeLastPacketReceived); + self.timer_tick(TimerName::TimeLastPacketSent); + self.timer_tick_session_established(false, index); // New session established, we are not the initiator + + tracing::debug!(message = "Sending handshake_response", local_idx = index); + + Ok(TunnResult::WriteToNetwork(packet)) + } + + fn handle_handshake_response<'a>( + &mut self, + p: HandshakeResponse, + dst: &'a mut [u8], + ) -> Result, WireGuardError> { + tracing::debug!( + message = "Received handshake_response", + local_idx = p.receiver_idx, + remote_idx = p.sender_idx + ); + + let session = self.handshake.receive_handshake_response(p)?; + + let keepalive_packet = session.format_packet_data(&[], dst); + // Store new session in ring buffer + let l_idx = session.local_index(); + let index = l_idx % N_SESSIONS; + self.sessions[index] = Some(session); + + self.timer_tick(TimerName::TimeLastPacketReceived); + self.timer_tick_session_established(true, index); // New session established, we are the initiator + self.set_current_session(l_idx); + + tracing::debug!("Sending keepalive"); + + Ok(TunnResult::WriteToNetwork(keepalive_packet)) // Send a keepalive as + // a response + } + + fn handle_cookie_reply<'a>( + &mut self, + p: PacketCookieReply, + ) -> Result, WireGuardError> { + tracing::debug!( + message = "Received cookie_reply", + local_idx = p.receiver_idx + ); + + self.handshake.receive_cookie_reply(p)?; + self.timer_tick(TimerName::TimeLastPacketReceived); + self.timer_tick(TimerName::TimeCookieReceived); + + tracing::debug!("Did set cookie"); + + Ok(TunnResult::Done) + } + + /// Update the index of the currently used session, if needed + fn set_current_session(&mut self, new_idx: usize) { + let cur_idx = self.current; + if cur_idx == new_idx { + // There is nothing to do, already using this session, this is the common case + return + } + if self.sessions[cur_idx % N_SESSIONS].is_none() + || self.timers.session_timers[new_idx % N_SESSIONS] + >= self.timers.session_timers[cur_idx % N_SESSIONS] + { + self.current = new_idx; + tracing::debug!(message = "New session", session = new_idx); + } + } + + /// Decrypts a data packet, and stores the decapsulated packet in dst. + fn handle_data<'a>( + &mut self, + packet: PacketData, + dst: &'a mut [u8], + ) -> Result, WireGuardError> { + let r_idx = packet.receiver_idx as usize; + let idx = r_idx % N_SESSIONS; + + // Get the (probably) right session + let decapsulated_packet = { + let session = self.sessions[idx].as_ref(); + let session = session.ok_or_else(|| { + tracing::trace!(message = "No current session available", remote_idx = r_idx); + WireGuardError::NoCurrentSession + })?; + session.receive_packet_data(packet, dst)? + }; + + self.set_current_session(r_idx); + + self.timer_tick(TimerName::TimeLastPacketReceived); + + Ok(self.validate_decapsulated_packet(decapsulated_packet)) + } + + /// Formats a new handshake initiation message and store it in dst. If + /// force_resend is true will send a new handshake, even if a handshake + /// is already in progress (for example when a handshake times out) + pub fn format_handshake_initiation<'a>( + &mut self, + dst: &'a mut [u8], + force_resend: bool, + ) -> TunnResult<'a> { + if self.handshake.is_in_progress() && !force_resend { + return TunnResult::Done + } + + if self.handshake.is_expired() { + self.timers.clear(); + } + + let starting_new_handshake = !self.handshake.is_in_progress(); + + match self.handshake.format_handshake_initiation(dst) { + Ok(packet) => { + tracing::debug!("Sending handshake_initiation"); + + if starting_new_handshake { + self.timer_tick(TimerName::TimeLastHandshakeStarted); + } + self.timer_tick(TimerName::TimeLastPacketSent); + TunnResult::WriteToNetwork(packet) + } + Err(e) => TunnResult::Err(e), + } + } + + /// Check if an IP packet is v4 or v6, truncate to the length indicated by + /// the length field Returns the truncated packet and the source IP as + /// TunnResult + fn validate_decapsulated_packet<'a>(&mut self, packet: &'a mut [u8]) -> TunnResult<'a> { + let (computed_len, src_ip_address) = match packet.len() { + 0 => return TunnResult::Done, // This is keepalive, and not an error + _ if packet[0] >> 4 == 4 && packet.len() >= IPV4_MIN_HEADER_SIZE => { + let len_bytes: [u8; IP_LEN_SZ] = packet[IPV4_LEN_OFF..IPV4_LEN_OFF + IP_LEN_SZ] + .try_into() + .unwrap(); + let addr_bytes: [u8; IPV4_IP_SZ] = packet + [IPV4_SRC_IP_OFF..IPV4_SRC_IP_OFF + IPV4_IP_SZ] + .try_into() + .unwrap(); + ( + u16::from_be_bytes(len_bytes) as usize, + IpAddr::from(addr_bytes), + ) + } + _ if packet[0] >> 4 == 6 && packet.len() >= IPV6_MIN_HEADER_SIZE => { + let len_bytes: [u8; IP_LEN_SZ] = packet[IPV6_LEN_OFF..IPV6_LEN_OFF + IP_LEN_SZ] + .try_into() + .unwrap(); + let addr_bytes: [u8; IPV6_IP_SZ] = packet + [IPV6_SRC_IP_OFF..IPV6_SRC_IP_OFF + IPV6_IP_SZ] + .try_into() + .unwrap(); + ( + u16::from_be_bytes(len_bytes) as usize + IPV6_MIN_HEADER_SIZE, + IpAddr::from(addr_bytes), + ) + } + _ => return TunnResult::Err(WireGuardError::InvalidPacket), + }; + + if computed_len > packet.len() { + return TunnResult::Err(WireGuardError::InvalidPacket) + } + + self.timer_tick(TimerName::TimeLastDataPacketReceived); + self.rx_bytes += computed_len; + + match src_ip_address { + IpAddr::V4(addr) => TunnResult::WriteToTunnelV4(&mut packet[..computed_len], addr), + IpAddr::V6(addr) => TunnResult::WriteToTunnelV6(&mut packet[..computed_len], addr), + } + } + + /// Get a packet from the queue, and try to encapsulate it + fn send_queued_packet<'a>(&mut self, dst: &'a mut [u8]) -> TunnResult<'a> { + if let Some(packet) = self.dequeue_packet() { + match self.encapsulate(&packet, dst) { + TunnResult::Err(_) => { + // On error, return packet to the queue + self.requeue_packet(packet); + } + r => return r, + } + } + TunnResult::Done + } + + /// Push packet to the back of the queue + fn queue_packet(&mut self, packet: &[u8]) { + if self.packet_queue.len() < MAX_QUEUE_DEPTH { + // Drop if too many are already in queue + self.packet_queue.push_back(packet.to_vec()); + } + } + + /// Push packet to the front of the queue + fn requeue_packet(&mut self, packet: Vec) { + if self.packet_queue.len() < MAX_QUEUE_DEPTH { + // Drop if too many are already in queue + self.packet_queue.push_front(packet); + } + } + + fn dequeue_packet(&mut self) -> Option> { + self.packet_queue.pop_front() + } + + fn estimate_loss(&self) -> f32 { + let session_idx = self.current; + + let mut weight = 9.0; + let mut cur_avg = 0.0; + let mut total_weight = 0.0; + + for i in 0..N_SESSIONS { + if let Some(ref session) = self.sessions[(session_idx.wrapping_sub(i)) % N_SESSIONS] { + let (expected, received) = session.current_packet_cnt(); + + let loss = if expected == 0 { + 0.0 + } else { + 1.0 - received as f32 / expected as f32 + }; + + cur_avg += loss * weight; + total_weight += weight; + weight /= 3.0; + } + } + + if total_weight == 0.0 { + 0.0 + } else { + cur_avg / total_weight + } + } + + /// Return stats from the tunnel: + /// * Time since last handshake in seconds + /// * Data bytes sent + /// * Data bytes received + pub fn stats(&self) -> (Option, usize, usize, f32, Option) { + let time = self.time_since_last_handshake(); + let tx_bytes = self.tx_bytes; + let rx_bytes = self.rx_bytes; + let loss = self.estimate_loss(); + let rtt = self.handshake.last_rtt; + + (time, tx_bytes, rx_bytes, loss, rtt) + } +} diff --git a/burrow/src/wireguard/noise/rate_limiter.rs b/burrow/src/wireguard/noise/rate_limiter.rs new file mode 100755 index 0000000..ff19efd --- /dev/null +++ b/burrow/src/wireguard/noise/rate_limiter.rs @@ -0,0 +1,212 @@ +use std::{ + net::IpAddr, + sync::atomic::{AtomicU64, Ordering}, + time::Instant, +}; + +use aead::{generic_array::GenericArray, AeadInPlace, KeyInit}; +use chacha20poly1305::{Key, XChaCha20Poly1305}; +use parking_lot::Mutex; +use rand_core::{OsRng, RngCore}; +use ring::constant_time::verify_slices_are_equal; + +use super::{ + handshake::{ + b2s_hash, + b2s_keyed_mac_16, + b2s_keyed_mac_16_2, + b2s_mac_24, + LABEL_COOKIE, + LABEL_MAC1, + }, + HandshakeInit, + HandshakeResponse, + Packet, + TunnResult, + Tunnel, + WireGuardError, +}; + +const COOKIE_REFRESH: u64 = 128; // Use 128 and not 120 so the compiler can optimize out the division +const COOKIE_SIZE: usize = 16; +const COOKIE_NONCE_SIZE: usize = 24; + +/// How often should reset count in seconds +const RESET_PERIOD: u64 = 1; + +type Cookie = [u8; COOKIE_SIZE]; + +/// There are two places where WireGuard requires "randomness" for cookies +/// * The 24 byte nonce in the cookie massage - here the only goal is to avoid +/// nonce reuse +/// * A secret value that changes every two minutes +/// Because the main goal of the cookie is simply for a party to prove ownership +/// of an IP address we can relax the randomness definition a bit, in order to +/// avoid locking, because using less resources is the main goal of any DoS +/// prevention mechanism. In order to avoid locking and calls to rand we derive +/// pseudo random values using the AEAD and some counters. +#[derive(Debug)] +pub struct RateLimiter { + /// The key we use to derive the nonce + nonce_key: [u8; 32], + /// The key we use to derive the cookie + secret_key: [u8; 16], + start_time: Instant, + /// A single 64 bit counter (should suffice for many years) + nonce_ctr: AtomicU64, + mac1_key: [u8; 32], + cookie_key: Key, + limit: u64, + /// The counter since last reset + count: AtomicU64, + /// The time last reset was performed on this rate limiter + last_reset: Mutex, +} + +impl RateLimiter { + pub fn new(public_key: &super::x25519::PublicKey, limit: u64) -> Self { + let mut secret_key = [0u8; 16]; + OsRng.fill_bytes(&mut secret_key); + RateLimiter { + nonce_key: Self::rand_bytes(), + secret_key, + start_time: Instant::now(), + nonce_ctr: AtomicU64::new(0), + mac1_key: b2s_hash(LABEL_MAC1, public_key.as_bytes()), + cookie_key: b2s_hash(LABEL_COOKIE, public_key.as_bytes()).into(), + limit, + count: AtomicU64::new(0), + last_reset: Mutex::new(Instant::now()), + } + } + + fn rand_bytes() -> [u8; 32] { + let mut key = [0u8; 32]; + OsRng.fill_bytes(&mut key); + key + } + + /// Reset packet count (ideally should be called with a period of 1 second) + pub fn reset_count(&self) { + // The rate limiter is not very accurate, but at the scale we care about it + // doesn't matter much + let current_time = Instant::now(); + let mut last_reset_time = self.last_reset.lock(); + if current_time.duration_since(*last_reset_time).as_secs() >= RESET_PERIOD { + self.count.store(0, Ordering::SeqCst); + *last_reset_time = current_time; + } + } + + /// Compute the correct cookie value based on the current secret value and + /// the source IP + fn current_cookie(&self, addr: IpAddr) -> Cookie { + let mut addr_bytes = [0u8; 16]; + + match addr { + IpAddr::V4(a) => addr_bytes[..4].copy_from_slice(&a.octets()[..]), + IpAddr::V6(a) => addr_bytes[..].copy_from_slice(&a.octets()[..]), + } + + // The current cookie for a given IP is the + // MAC(responder.changing_secret_every_two_minutes, initiator.ip_address) + // First we derive the secret from the current time, the value of cur_counter + // would change with time. + let cur_counter = Instant::now().duration_since(self.start_time).as_secs() / COOKIE_REFRESH; + + // Next we derive the cookie + b2s_keyed_mac_16_2(&self.secret_key, &cur_counter.to_le_bytes(), &addr_bytes) + } + + fn nonce(&self) -> [u8; COOKIE_NONCE_SIZE] { + let ctr = self.nonce_ctr.fetch_add(1, Ordering::Relaxed); + + b2s_mac_24(&self.nonce_key, &ctr.to_le_bytes()) + } + + fn is_under_load(&self) -> bool { + self.count.fetch_add(1, Ordering::SeqCst) >= self.limit + } + + pub(crate) fn format_cookie_reply<'a>( + &self, + idx: u32, + cookie: Cookie, + mac1: &[u8], + dst: &'a mut [u8], + ) -> Result<&'a mut [u8], WireGuardError> { + if dst.len() < super::COOKIE_REPLY_SZ { + return Err(WireGuardError::DestinationBufferTooSmall) + } + + let (message_type, rest) = dst.split_at_mut(4); + let (receiver_index, rest) = rest.split_at_mut(4); + let (nonce, rest) = rest.split_at_mut(24); + let (encrypted_cookie, _) = rest.split_at_mut(16 + 16); + + // msg.message_type = 3 + // msg.reserved_zero = { 0, 0, 0 } + message_type.copy_from_slice(&super::COOKIE_REPLY.to_le_bytes()); + // msg.receiver_index = little_endian(initiator.sender_index) + receiver_index.copy_from_slice(&idx.to_le_bytes()); + nonce.copy_from_slice(&self.nonce()[..]); + + let cipher = XChaCha20Poly1305::new(&self.cookie_key); + + let iv = GenericArray::from_slice(nonce); + + encrypted_cookie[..16].copy_from_slice(&cookie); + let tag = cipher + .encrypt_in_place_detached(iv, mac1, &mut encrypted_cookie[..16]) + .map_err(|_| WireGuardError::DestinationBufferTooSmall)?; + + encrypted_cookie[16..].copy_from_slice(&tag); + + Ok(&mut dst[..super::COOKIE_REPLY_SZ]) + } + + /// Verify the MAC fields on the datagram, and apply rate limiting if needed + pub fn verify_packet<'a, 'b>( + &self, + src_addr: Option, + src: &'a [u8], + dst: &'b mut [u8], + ) -> Result, TunnResult<'b>> { + let packet = Tunnel::parse_incoming_packet(src)?; + tracing::debug!("packet: {:?}", packet); + + // Verify and rate limit handshake messages only + if let Packet::HandshakeInit(HandshakeInit { sender_idx, .. }) + | Packet::HandshakeResponse(HandshakeResponse { sender_idx, .. }) = packet + { + tracing::debug!("sender_idx: {}", sender_idx); + tracing::debug!("response: {:?}", packet); + let (msg, macs) = src.split_at(src.len() - 32); + let (mac1, mac2) = macs.split_at(16); + + let computed_mac1 = b2s_keyed_mac_16(&self.mac1_key, msg); + verify_slices_are_equal(&computed_mac1[..16], mac1) + .map_err(|_| TunnResult::Err(WireGuardError::InvalidMac))?; + + if self.is_under_load() { + let addr = match src_addr { + None => return Err(TunnResult::Err(WireGuardError::UnderLoad)), + Some(addr) => addr, + }; + + // Only given an address can we validate mac2 + let cookie = self.current_cookie(addr); + let computed_mac2 = b2s_keyed_mac_16_2(&cookie, msg, mac1); + + if verify_slices_are_equal(&computed_mac2[..16], mac2).is_err() { + let cookie_packet = self + .format_cookie_reply(sender_idx, cookie, mac1, dst) + .map_err(TunnResult::Err)?; + return Err(TunnResult::WriteToNetwork(cookie_packet)) + } + } + } + + Ok(packet) + } +} diff --git a/burrow/src/wireguard/noise/session.rs b/burrow/src/wireguard/noise/session.rs new file mode 100755 index 0000000..8988728 --- /dev/null +++ b/burrow/src/wireguard/noise/session.rs @@ -0,0 +1,280 @@ +// Copyright (c) 2019 Cloudflare, Inc. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +use std::sync::atomic::{AtomicUsize, Ordering}; + +use parking_lot::Mutex; +use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305}; + +use super::{errors::WireGuardError, PacketData}; + +pub struct Session { + pub(crate) receiving_index: u32, + sending_index: u32, + receiver: LessSafeKey, + sender: LessSafeKey, + sending_key_counter: AtomicUsize, + receiving_key_counter: Mutex, +} + +impl std::fmt::Debug for Session { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "Session: {}<- ->{}", + self.receiving_index, self.sending_index + ) + } +} + +/// Where encrypted data resides in a data packet +const DATA_OFFSET: usize = 16; +/// The overhead of the AEAD +const AEAD_SIZE: usize = 16; + +// Receiving buffer constants +const WORD_SIZE: u64 = 64; +const N_WORDS: u64 = 16; // Suffice to reorder 64*16 = 1024 packets; can be increased at will +const N_BITS: u64 = WORD_SIZE * N_WORDS; + +#[derive(Debug, Clone, Default)] +struct ReceivingKeyCounterValidator { + /// In order to avoid replays while allowing for some reordering of the + /// packets, we keep a bitmap of received packets, and the value of the + /// highest counter + next: u64, + /// Used to estimate packet loss + receive_cnt: u64, + bitmap: [u64; N_WORDS as usize], +} + +impl ReceivingKeyCounterValidator { + #[inline(always)] + fn set_bit(&mut self, idx: u64) { + let bit_idx = idx % N_BITS; + let word = (bit_idx / WORD_SIZE) as usize; + let bit = (bit_idx % WORD_SIZE) as usize; + self.bitmap[word] |= 1 << bit; + } + + #[inline(always)] + fn clear_bit(&mut self, idx: u64) { + let bit_idx = idx % N_BITS; + let word = (bit_idx / WORD_SIZE) as usize; + let bit = (bit_idx % WORD_SIZE) as usize; + self.bitmap[word] &= !(1u64 << bit); + } + + /// Clear the word that contains idx + #[inline(always)] + fn clear_word(&mut self, idx: u64) { + let bit_idx = idx % N_BITS; + let word = (bit_idx / WORD_SIZE) as usize; + self.bitmap[word] = 0; + } + + /// Returns true if bit is set, false otherwise + #[inline(always)] + fn check_bit(&self, idx: u64) -> bool { + let bit_idx = idx % N_BITS; + let word = (bit_idx / WORD_SIZE) as usize; + let bit = (bit_idx % WORD_SIZE) as usize; + ((self.bitmap[word] >> bit) & 1) == 1 + } + + /// Returns true if the counter was not yet received, and is not too far + /// back + #[inline(always)] + fn will_accept(&self, counter: u64) -> Result<(), WireGuardError> { + if counter >= self.next { + // As long as the counter is growing no replay took place for sure + return Ok(()) + } + if counter + N_BITS < self.next { + // Drop if too far back + return Err(WireGuardError::InvalidCounter) + } + if !self.check_bit(counter) { + Ok(()) + } else { + Err(WireGuardError::DuplicateCounter) + } + } + + /// Marks the counter as received, and returns true if it is still good (in + /// case during decryption something changed) + #[inline(always)] + fn mark_did_receive(&mut self, counter: u64) -> Result<(), WireGuardError> { + if counter + N_BITS < self.next { + // Drop if too far back + return Err(WireGuardError::InvalidCounter) + } + if counter == self.next { + // Usually the packets arrive in order, in that case we simply mark the bit and + // increment the counter + self.set_bit(counter); + self.next += 1; + return Ok(()) + } + if counter < self.next { + // A packet arrived out of order, check if it is valid, and mark + if self.check_bit(counter) { + return Err(WireGuardError::InvalidCounter) + } + self.set_bit(counter); + return Ok(()) + } + // Packets where dropped, or maybe reordered, skip them and mark unused + if counter - self.next >= N_BITS { + // Too far ahead, clear all the bits + for c in self.bitmap.iter_mut() { + *c = 0; + } + } else { + let mut i = self.next; + while i % WORD_SIZE != 0 && i < counter { + // Clear until i aligned to word size + self.clear_bit(i); + i += 1; + } + while i + WORD_SIZE < counter { + // Clear whole word at a time + self.clear_word(i); + i = (i + WORD_SIZE) & 0u64.wrapping_sub(WORD_SIZE); + } + while i < counter { + // Clear any remaining bits + self.clear_bit(i); + i += 1; + } + } + self.set_bit(counter); + self.next = counter + 1; + Ok(()) + } +} + +impl Session { + pub(super) fn new( + local_index: u32, + peer_index: u32, + receiving_key: [u8; 32], + sending_key: [u8; 32], + ) -> Session { + Session { + receiving_index: local_index, + sending_index: peer_index, + receiver: LessSafeKey::new( + UnboundKey::new(&CHACHA20_POLY1305, &receiving_key).unwrap(), + ), + sender: LessSafeKey::new(UnboundKey::new(&CHACHA20_POLY1305, &sending_key).unwrap()), + sending_key_counter: AtomicUsize::new(0), + receiving_key_counter: Mutex::new(Default::default()), + } + } + + pub(super) fn local_index(&self) -> usize { + self.receiving_index as usize + } + + /// Returns true if receiving counter is good to use + fn receiving_counter_quick_check(&self, counter: u64) -> Result<(), WireGuardError> { + let counter_validator = self.receiving_key_counter.lock(); + counter_validator.will_accept(counter) + } + + /// Returns true if receiving counter is good to use, and marks it as used { + fn receiving_counter_mark(&self, counter: u64) -> Result<(), WireGuardError> { + let mut counter_validator = self.receiving_key_counter.lock(); + let ret = counter_validator.mark_did_receive(counter); + if ret.is_ok() { + counter_validator.receive_cnt += 1; + } + ret + } + + /// src - an IP packet from the interface + /// dst - pre-allocated space to hold the encapsulating UDP packet to send + /// over the network returns the size of the formatted packet + pub(super) fn format_packet_data<'a>(&self, src: &[u8], dst: &'a mut [u8]) -> &'a mut [u8] { + if dst.len() < src.len() + super::DATA_OVERHEAD_SZ { + panic!("The destination buffer is too small"); + } + + let sending_key_counter = self.sending_key_counter.fetch_add(1, Ordering::Relaxed) as u64; + + let (message_type, rest) = dst.split_at_mut(4); + let (receiver_index, rest) = rest.split_at_mut(4); + let (counter, data) = rest.split_at_mut(8); + + message_type.copy_from_slice(&super::DATA.to_le_bytes()); + receiver_index.copy_from_slice(&self.sending_index.to_le_bytes()); + counter.copy_from_slice(&sending_key_counter.to_le_bytes()); + + // TODO: spec requires padding to 16 bytes, but actually works fine without it + let n = { + let mut nonce = [0u8; 12]; + nonce[4..12].copy_from_slice(&sending_key_counter.to_le_bytes()); + data[..src.len()].copy_from_slice(src); + self.sender + .seal_in_place_separate_tag( + Nonce::assume_unique_for_key(nonce), + Aad::from(&[]), + &mut data[..src.len()], + ) + .map(|tag| { + data[src.len()..src.len() + AEAD_SIZE].copy_from_slice(tag.as_ref()); + src.len() + AEAD_SIZE + }) + .unwrap() + }; + + &mut dst[..DATA_OFFSET + n] + } + + /// packet - a data packet we received from the network + /// dst - pre-allocated space to hold the encapsulated IP packet, to send to + /// the interface dst will always take less space than src + /// return the size of the encapsulated packet on success + pub(super) fn receive_packet_data<'a>( + &self, + packet: PacketData, + dst: &'a mut [u8], + ) -> Result<&'a mut [u8], WireGuardError> { + let ct_len = packet.encrypted_encapsulated_packet.len(); + if dst.len() < ct_len { + // This is a very incorrect use of the library, therefore panic and not error + panic!("The destination buffer is too small"); + } + if packet.receiver_idx != self.receiving_index { + return Err(WireGuardError::WrongIndex) + } + // Don't reuse counters, in case this is a replay attack we want to quickly + // check the counter without running expensive decryption + self.receiving_counter_quick_check(packet.counter)?; + + tracing::debug!("TAG C"); + let ret = { + let mut nonce = [0u8; 12]; + nonce[4..12].copy_from_slice(&packet.counter.to_le_bytes()); + dst[..ct_len].copy_from_slice(packet.encrypted_encapsulated_packet); + self.receiver + .open_in_place( + Nonce::assume_unique_for_key(nonce), + Aad::from(&[]), + &mut dst[..ct_len], + ) + .map_err(|_| WireGuardError::InvalidAeadTag)? + }; + + // After decryption is done, check counter again, and mark as received + self.receiving_counter_mark(packet.counter)?; + Ok(ret) + } + + /// Returns the estimated downstream packet loss for this session + pub(super) fn current_packet_cnt(&self) -> (u64, u64) { + let counter_validator = self.receiving_key_counter.lock(); + (counter_validator.next, counter_validator.receive_cnt) + } +} diff --git a/burrow/src/wireguard/noise/timers.rs b/burrow/src/wireguard/noise/timers.rs new file mode 100755 index 0000000..1d0cf1f --- /dev/null +++ b/burrow/src/wireguard/noise/timers.rs @@ -0,0 +1,333 @@ +// Copyright (c) 2019 Cloudflare, Inc. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +use std::{ + mem, + ops::{Index, IndexMut}, + time::{Duration, Instant}, +}; + +use super::{errors::WireGuardError, TunnResult, Tunnel}; + +// Some constants, represent time in seconds +// https://www.wireguard.com/papers/wireguard.pdf#page=14 +pub(crate) const REKEY_AFTER_TIME: Duration = Duration::from_secs(120); +const REJECT_AFTER_TIME: Duration = Duration::from_secs(180); +const REKEY_ATTEMPT_TIME: Duration = Duration::from_secs(90); +pub(crate) const REKEY_TIMEOUT: Duration = Duration::from_secs(5); +const KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(10); +const COOKIE_EXPIRATION_TIME: Duration = Duration::from_secs(120); + +#[derive(Debug)] +pub enum TimerName { + /// Current time, updated each call to `update_timers` + TimeCurrent, + /// Time when last handshake was completed + TimeSessionEstablished, + /// Time the last attempt for a new handshake began + TimeLastHandshakeStarted, + /// Time we last received and authenticated a packet + TimeLastPacketReceived, + /// Time we last send a packet + TimeLastPacketSent, + /// Time we last received and authenticated a DATA packet + TimeLastDataPacketReceived, + /// Time we last send a DATA packet + TimeLastDataPacketSent, + /// Time we last received a cookie + TimeCookieReceived, + /// Time we last sent persistent keepalive + TimePersistentKeepalive, + Top, +} + +use self::TimerName::*; + +#[derive(Debug)] +pub struct Timers { + /// Is the owner of the timer the initiator or the responder for the last + /// handshake? + is_initiator: bool, + /// Start time of the tunnel + time_started: Instant, + timers: [Duration; TimerName::Top as usize], + pub(super) session_timers: [Duration; super::N_SESSIONS], + /// Did we receive data without sending anything back? + want_keepalive: bool, + /// Did we send data without hearing back? + want_handshake: bool, + persistent_keepalive: usize, + /// Should this timer call reset rr function (if not a shared rr instance) + pub(super) should_reset_rr: bool, +} + +impl Timers { + pub(super) fn new(persistent_keepalive: Option, reset_rr: bool) -> Timers { + Timers { + is_initiator: false, + time_started: Instant::now(), + timers: Default::default(), + session_timers: Default::default(), + want_keepalive: Default::default(), + want_handshake: Default::default(), + persistent_keepalive: usize::from(persistent_keepalive.unwrap_or(0)), + should_reset_rr: reset_rr, + } + } + + fn is_initiator(&self) -> bool { + self.is_initiator + } + + // We don't really clear the timers, but we set them to the current time to + // so the reference time frame is the same + pub(super) fn clear(&mut self) { + let now = Instant::now().duration_since(self.time_started); + for t in &mut self.timers[..] { + *t = now; + } + self.want_handshake = false; + self.want_keepalive = false; + } +} + +impl Index for Timers { + type Output = Duration; + + fn index(&self, index: TimerName) -> &Duration { + &self.timers[index as usize] + } +} + +impl IndexMut for Timers { + fn index_mut(&mut self, index: TimerName) -> &mut Duration { + &mut self.timers[index as usize] + } +} + +impl Tunnel { + pub(super) fn timer_tick(&mut self, timer_name: TimerName) { + match timer_name { + TimeLastPacketReceived => { + self.timers.want_keepalive = true; + self.timers.want_handshake = false; + } + TimeLastPacketSent => { + self.timers.want_handshake = true; + self.timers.want_keepalive = false; + } + _ => {} + } + + let time = self.timers[TimeCurrent]; + self.timers[timer_name] = time; + } + + pub(super) fn timer_tick_session_established( + &mut self, + is_initiator: bool, + session_idx: usize, + ) { + self.timer_tick(TimeSessionEstablished); + self.timers.session_timers[session_idx % super::N_SESSIONS] = self.timers[TimeCurrent]; + self.timers.is_initiator = is_initiator; + } + + // We don't really clear the timers, but we set them to the current time to + // so the reference time frame is the same + fn clear_all(&mut self) { + for session in &mut self.sessions { + *session = None; + } + + self.packet_queue.clear(); + + self.timers.clear(); + } + + fn update_session_timers(&mut self, time_now: Duration) { + let timers = &mut self.timers; + + for (i, t) in timers.session_timers.iter_mut().enumerate() { + if time_now - *t > REJECT_AFTER_TIME { + if let Some(session) = self.sessions[i].take() { + tracing::debug!( + message = "SESSION_EXPIRED(REJECT_AFTER_TIME)", + session = session.receiving_index + ); + } + *t = time_now; + } + } + } + + pub fn update_timers<'a>(&mut self, dst: &'a mut [u8]) -> TunnResult<'a> { + let mut handshake_initiation_required = false; + let mut keepalive_required = false; + + let time = Instant::now(); + + if self.timers.should_reset_rr { + self.rate_limiter.reset_count(); + } + + // All the times are counted from tunnel initiation, for efficiency our timers + // are rounded to a second, as there is no real benefit to having highly + // accurate timers. + let now = time.duration_since(self.timers.time_started); + self.timers[TimeCurrent] = now; + + self.update_session_timers(now); + + // Load timers only once: + let session_established = self.timers[TimeSessionEstablished]; + let handshake_started = self.timers[TimeLastHandshakeStarted]; + let aut_packet_received = self.timers[TimeLastPacketReceived]; + let aut_packet_sent = self.timers[TimeLastPacketSent]; + let data_packet_received = self.timers[TimeLastDataPacketReceived]; + let data_packet_sent = self.timers[TimeLastDataPacketSent]; + let persistent_keepalive = self.timers.persistent_keepalive; + + { + if self.handshake.is_expired() { + return TunnResult::Err(WireGuardError::ConnectionExpired) + } + + // Clear cookie after COOKIE_EXPIRATION_TIME + if self.handshake.has_cookie() + && now - self.timers[TimeCookieReceived] >= COOKIE_EXPIRATION_TIME + { + self.handshake.clear_cookie(); + } + + // All ephemeral private keys and symmetric session keys are zeroed out after + // (REJECT_AFTER_TIME * 3) ms if no new keys have been exchanged. + if now - session_established >= REJECT_AFTER_TIME * 3 { + tracing::error!("CONNECTION_EXPIRED(REJECT_AFTER_TIME * 3)"); + self.handshake.set_expired(); + self.clear_all(); + return TunnResult::Err(WireGuardError::ConnectionExpired) + } + + if let Some(time_init_sent) = self.handshake.timer() { + // Handshake Initiation Retransmission + if now - handshake_started >= REKEY_ATTEMPT_TIME { + // After REKEY_ATTEMPT_TIME ms of trying to initiate a new handshake, + // the retries give up and cease, and clear all existing packets queued + // up to be sent. If a packet is explicitly queued up to be sent, then + // this timer is reset. + tracing::error!("CONNECTION_EXPIRED(REKEY_ATTEMPT_TIME)"); + self.handshake.set_expired(); + self.clear_all(); + return TunnResult::Err(WireGuardError::ConnectionExpired) + } + + if time_init_sent.elapsed() >= REKEY_TIMEOUT { + // We avoid using `time` here, because it can be earlier than `time_init_sent`. + // Once `checked_duration_since` is stable we can use that. + // A handshake initiation is retried after REKEY_TIMEOUT + jitter ms, + // if a response has not been received, where jitter is some random + // value between 0 and 333 ms. + tracing::warn!("HANDSHAKE(REKEY_TIMEOUT)"); + handshake_initiation_required = true; + } + } else { + if self.timers.is_initiator() { + // After sending a packet, if the sender was the original initiator + // of the handshake and if the current session key is REKEY_AFTER_TIME + // ms old, we initiate a new handshake. If the sender was the original + // responder of the handshake, it does not re-initiate a new handshake + // after REKEY_AFTER_TIME ms like the original initiator does. + if session_established < data_packet_sent + && now - session_established >= REKEY_AFTER_TIME + { + tracing::debug!("HANDSHAKE(REKEY_AFTER_TIME (on send))"); + handshake_initiation_required = true; + } + + // After receiving a packet, if the receiver was the original initiator + // of the handshake and if the current session key is REJECT_AFTER_TIME + // - KEEPALIVE_TIMEOUT - REKEY_TIMEOUT ms old, we initiate a new + // handshake. + if session_established < data_packet_received + && now - session_established + >= REJECT_AFTER_TIME - KEEPALIVE_TIMEOUT - REKEY_TIMEOUT + { + tracing::warn!( + "HANDSHAKE(REJECT_AFTER_TIME - KEEPALIVE_TIMEOUT - \ + REKEY_TIMEOUT \ + (on receive))" + ); + handshake_initiation_required = true; + } + } + + // If we have sent a packet to a given peer but have not received a + // packet after from that peer for (KEEPALIVE + REKEY_TIMEOUT) ms, + // we initiate a new handshake. + if data_packet_sent > aut_packet_received + && now - aut_packet_received >= KEEPALIVE_TIMEOUT + REKEY_TIMEOUT + && mem::replace(&mut self.timers.want_handshake, false) + { + tracing::warn!("HANDSHAKE(KEEPALIVE + REKEY_TIMEOUT)"); + handshake_initiation_required = true; + } + + if !handshake_initiation_required { + // If a packet has been received from a given peer, but we have not sent one + // back to the given peer in KEEPALIVE ms, we send an empty + // packet. + if data_packet_received > aut_packet_sent + && now - aut_packet_sent >= KEEPALIVE_TIMEOUT + && mem::replace(&mut self.timers.want_keepalive, false) + { + tracing::debug!("KEEPALIVE(KEEPALIVE_TIMEOUT)"); + keepalive_required = true; + } + + // Persistent KEEPALIVE + if persistent_keepalive > 0 + && (now - self.timers[TimePersistentKeepalive] + >= Duration::from_secs(persistent_keepalive as _)) + { + tracing::debug!("KEEPALIVE(PERSISTENT_KEEPALIVE)"); + self.timer_tick(TimePersistentKeepalive); + keepalive_required = true; + } + } + } + } + + if handshake_initiation_required { + return self.format_handshake_initiation(dst, true) + } + + if keepalive_required { + return self.encapsulate(&[], dst) + } + + TunnResult::Done + } + + pub fn time_since_last_handshake(&self) -> Option { + let current_session = self.current; + if self.sessions[current_session % super::N_SESSIONS].is_some() { + let duration_since_tun_start = Instant::now().duration_since(self.timers.time_started); + let duration_since_session_established = self.timers[TimeSessionEstablished]; + + Some(duration_since_tun_start - duration_since_session_established) + } else { + None + } + } + + pub fn persistent_keepalive(&self) -> Option { + let keepalive = self.timers.persistent_keepalive; + + if keepalive > 0 { + Some(keepalive as u16) + } else { + None + } + } +} diff --git a/burrow/src/wireguard/pcb.rs b/burrow/src/wireguard/pcb.rs new file mode 100755 index 0000000..a781870 --- /dev/null +++ b/burrow/src/wireguard/pcb.rs @@ -0,0 +1,135 @@ +use std::{net::SocketAddr, sync::Arc}; + +use anyhow::Error; +use fehler::throws; +use ip_network::IpNetwork; +use rand::random; +use tokio::{net::UdpSocket, sync::RwLock, task::JoinHandle}; +use tun::tokio::TunInterface; + +use super::{ + noise::{TunnResult, Tunnel}, + Peer, +}; + +#[derive(Debug)] +pub struct PeerPcb { + pub endpoint: SocketAddr, + pub allowed_ips: Vec, + pub handle: RwLock>>, + socket: RwLock>, + tunnel: RwLock, +} + +impl PeerPcb { + #[throws] + pub fn new(peer: Peer) -> Self { + let tunnel = RwLock::new( + Tunnel::new( + peer.private_key, + peer.public_key, + peer.preshared_key, + None, + 1, + None, + ) + .map_err(|s| anyhow::anyhow!("{}", s))?, + ); + Self { + endpoint: peer.endpoint, + allowed_ips: peer.allowed_ips, + handle: RwLock::new(None), + socket: RwLock::new(None), + tunnel, + } + } + + pub async fn open_if_closed(&self) -> Result<(), Error> { + if self.socket.read().await.is_none() { + let socket = UdpSocket::bind("0.0.0.0:0").await?; + socket.connect(self.endpoint).await?; + self.socket.write().await.replace(socket); + } + Ok(()) + } + + pub async fn run(&self, tun_interface: Arc>) -> Result<(), Error> { + tracing::debug!("starting read loop for pcb... for {:?}", &self); + let rid: i32 = random(); + let mut buf: [u8; 3000] = [0u8; 3000]; + tracing::debug!("start read loop {}", rid); + loop { + tracing::debug!("{}: waiting for packet", rid); + let guard = self.socket.read().await; + let Some(socket) = guard.as_ref() else { + continue + }; + let mut res_buf = [0; 1500]; + // tracing::debug!("{} : waiting for readability on {:?}", rid, socket); + let len = match socket.recv(&mut res_buf).await { + Ok(l) => l, + Err(e) => { + log::error!("{}: error reading from socket: {:?}", rid, e); + continue + } + }; + let mut res_dat = &res_buf[..len]; + tracing::debug!("{}: Decapsulating {} bytes", rid, len); + tracing::debug!("{:?}", &res_dat); + loop { + match self + .tunnel + .write() + .await + .decapsulate(None, res_dat, &mut buf[..]) + { + TunnResult::Done => break, + TunnResult::Err(e) => { + tracing::error!(message = "Decapsulate error", error = ?e); + break + } + TunnResult::WriteToNetwork(packet) => { + tracing::debug!("WriteToNetwork: {:?}", packet); + self.open_if_closed().await?; + socket.send(packet).await?; + tracing::debug!("WriteToNetwork done"); + res_dat = &[]; + continue + } + TunnResult::WriteToTunnelV4(packet, addr) => { + tracing::debug!("WriteToTunnelV4: {:?}, {:?}", packet, addr); + tun_interface.read().await.send(packet).await?; + break + } + TunnResult::WriteToTunnelV6(packet, addr) => { + tracing::debug!("WriteToTunnelV6: {:?}, {:?}", packet, addr); + tun_interface.read().await.send(packet).await?; + break + } + } + } + } + } + + pub async fn send(&self, src: &[u8]) -> Result<(), Error> { + let mut dst_buf = [0u8; 3000]; + match self.tunnel.write().await.encapsulate(src, &mut dst_buf[..]) { + TunnResult::Done => {} + TunnResult::Err(e) => { + tracing::error!(message = "Encapsulate error", error = ?e) + } + TunnResult::WriteToNetwork(packet) => { + self.open_if_closed().await?; + let handle = self.socket.read().await; + let Some(socket) = handle.as_ref() else { + tracing::error!("No socket for peer"); + return Ok(()) + }; + tracing::debug!("Our Encapsulated packet: {:?}", packet); + socket.send(packet).await?; + } + _ => panic!("Unexpected result from encapsulate"), + }; + Ok(()) + } +} diff --git a/burrow/src/wireguard/peer.rs b/burrow/src/wireguard/peer.rs new file mode 100755 index 0000000..131b0d4 --- /dev/null +++ b/burrow/src/wireguard/peer.rs @@ -0,0 +1,22 @@ +use std::{fmt, net::SocketAddr}; + +use ip_network::IpNetwork; +use x25519_dalek::{PublicKey, StaticSecret}; + +pub struct Peer { + pub endpoint: SocketAddr, + pub private_key: StaticSecret, + pub public_key: PublicKey, + pub allowed_ips: Vec, + pub preshared_key: Option<[u8; 32]>, +} + +impl fmt::Debug for Peer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Peer") + .field("endpoint", &self.endpoint) + .field("public_key", &self.public_key) + .field("allowed_ips", &self.allowed_ips) + .finish() + } +} diff --git a/tun/Cargo.toml b/tun/Cargo.toml index b95c1bf..e67e45f 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -39,5 +39,5 @@ anyhow = "1.0" bindgen = "0.65" reqwest = { version = "0.11", features = ["native-tls"] } ssri = { version = "9.0", default-features = false } -tokio = { version = "1.28", features = ["rt"] } +tokio = { version = "1.28", features = ["rt", "macros"] } zip = { version = "0.6", features = ["deflate"] } diff --git a/tun/build.rs b/tun/build.rs index 5569cc4..8da8a40 100644 --- a/tun/build.rs +++ b/tun/build.rs @@ -26,7 +26,7 @@ async fn generate(out_dir: &std::path::Path) -> anyhow::Result<()> { println!("cargo:rerun-if-changed={}", binary_path.to_str().unwrap()); if let (Ok(..), Ok(..)) = (File::open(&bindings_path), File::open(&binary_path)) { - return Ok(()); + return Ok(()) }; let archive = download(out_dir) @@ -80,9 +80,10 @@ async fn download(directory: &std::path::Path) -> anyhow::Result #[cfg(windows)] fn parse(file: std::fs::File) -> anyhow::Result<(bindgen::Bindings, Vec)> { - use anyhow::Context; use std::io::Read; + use anyhow::Context; + let reader = std::io::BufReader::new(file); let mut archive = zip::ZipArchive::new(reader)?; diff --git a/tun/src/lib.rs b/tun/src/lib.rs index 151c10d..a1ca636 100644 --- a/tun/src/lib.rs +++ b/tun/src/lib.rs @@ -2,11 +2,11 @@ #[cfg(target_os = "windows")] #[path = "windows/mod.rs"] -mod imp; +mod os_imp; #[cfg(any(target_os = "linux", target_vendor = "apple"))] #[path = "unix/mod.rs"] -pub(crate) mod imp; +pub(crate) mod os_imp; mod options; @@ -14,5 +14,5 @@ mod options; #[cfg(feature = "tokio")] pub mod tokio; -pub use imp::{TunInterface, TunQueue}; pub use options::TunOptions; +pub use os_imp::{TunInterface, TunQueue}; diff --git a/tun/src/options.rs b/tun/src/options.rs index e74afe3..339f71a 100644 --- a/tun/src/options.rs +++ b/tun/src/options.rs @@ -1,17 +1,27 @@ -use fehler::throws; use std::io::Error; -use super::TunInterface; +use fehler::throws; + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +#[cfg(feature = "tokio")] +use super::tokio::TunInterface; #[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) +)] pub struct TunOptions { /// (Windows + Linux) Name the tun interface. - pub(crate) name: Option, + pub name: Option, /// (Linux) Don't include packet information. - pub(crate) no_pi: Option<()>, + pub no_pi: bool, /// (Linux) Avoid opening an existing persistant device. - pub(crate) tun_excl: Option<()>, + pub tun_excl: bool, + /// (Apple) Retrieve the tun interface + pub tun_retrieve: bool, + /// (Linux) The IP address of the tun interface. + pub address: Option, } impl TunOptions { @@ -24,16 +34,26 @@ impl TunOptions { self } - pub fn no_pi(mut self, enable: bool) { - self.no_pi = enable.then_some(()); + pub fn no_pi(mut self, enable: bool) -> Self { + self.no_pi = enable; + self } - pub fn tun_excl(mut self, enable: bool) { - self.tun_excl = enable.then_some(()); + pub fn tun_excl(mut self, enable: bool) -> Self { + self.tun_excl = enable; + self } + pub fn address(mut self, address: impl ToString) -> Self { + self.address = Some(address.to_string()); + self + } + + #[cfg(any(target_os = "linux", target_vendor = "apple"))] + #[cfg(feature = "tokio")] #[throws] pub fn open(self) -> TunInterface { - TunInterface::new_with_options(self)? + let ti = super::TunInterface::new_with_options(self)?; + TunInterface::new(ti)? } } diff --git a/tun/src/tokio/mod.rs b/tun/src/tokio/mod.rs index 7828279..947fb74 100644 --- a/tun/src/tokio/mod.rs +++ b/tun/src/tokio/mod.rs @@ -1,22 +1,22 @@ use std::io; + use tokio::io::unix::AsyncFd; use tracing::instrument; #[derive(Debug)] pub struct TunInterface { - inner: AsyncFd, + pub inner: AsyncFd, } impl TunInterface { #[instrument] - pub fn new(tun: crate::TunInterface) -> io::Result { - Ok(Self { - inner: AsyncFd::new(tun)?, - }) + pub fn new(mut tun: crate::TunInterface) -> io::Result { + tun.set_nonblocking(true)?; + Ok(Self { inner: AsyncFd::new(tun)? }) } #[instrument] - pub async fn write(&self, buf: &[u8]) -> io::Result { + pub async fn send(&self, buf: &[u8]) -> io::Result { loop { let mut guard = self.inner.writable().await?; match guard.try_io(|inner| inner.get_ref().send(buf)) { @@ -27,12 +27,15 @@ impl TunInterface { } #[instrument] - pub async fn read(&mut self, buf: &mut [u8]) -> io::Result { + pub async fn recv(&self, buf: &mut [u8]) -> io::Result { loop { - let mut guard = self.inner.readable_mut().await?; - match guard.try_io(|inner| (*inner).get_mut().recv(buf)) { + let mut guard = self.inner.readable().await?; + match guard.try_io(|inner| inner.get_ref().recv(buf)) { Ok(result) => return result, - Err(_would_block) => continue, + Err(_would_block) => { + tracing::debug!("WouldBlock"); + continue + } } } } diff --git a/tun/src/unix/apple/kern_control.rs b/tun/src/unix/apple/kern_control.rs index abc1e04..76e576f 100644 --- a/tun/src/unix/apple/kern_control.rs +++ b/tun/src/unix/apple/kern_control.rs @@ -1,7 +1,6 @@ +use std::{io::Error, mem::size_of, os::unix::io::AsRawFd}; + use fehler::throws; -use std::io::Error; -use std::mem::size_of; -use std::os::unix::io::AsRawFd; use super::sys; @@ -16,10 +15,7 @@ pub trait SysControlSocket { impl SysControlSocket for socket2::Socket { #[throws] fn resolve(&self, name: &str, index: u32) -> socket2::SockAddr { - let mut info = sys::ctl_info { - ctl_id: 0, - ctl_name: [0; 96], - }; + let mut info = sys::ctl_info { ctl_id: 0, ctl_name: [0; 96] }; info.ctl_name[..name.len()].copy_from_slice(name.as_bytes()); unsafe { sys::resolve_ctl_info(self.as_raw_fd(), &mut info as *mut sys::ctl_info)? }; @@ -28,7 +24,7 @@ impl SysControlSocket for socket2::Socket { socket2::SockAddr::init(|addr_storage, len| { *len = size_of::() as u32; - let mut addr: &mut sys::sockaddr_ctl = &mut *addr_storage.cast(); + let addr: &mut sys::sockaddr_ctl = &mut *addr_storage.cast(); addr.sc_len = *len as u8; addr.sc_family = sys::AF_SYSTEM as u8; addr.ss_sysaddr = sys::AF_SYS_CONTROL as u16; diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index f4fd1e2..e72fb06 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,21 +1,24 @@ +use std::{ + io::{Error, IoSlice}, + mem, + net::{Ipv4Addr, SocketAddrV4}, + os::fd::{AsRawFd, FromRawFd, RawFd}, +}; + use byteorder::{ByteOrder, NetworkEndian}; use fehler::throws; use libc::{c_char, iovec, writev, AF_INET, AF_INET6}; -use tracing::info; use socket2::{Domain, SockAddr, Socket, Type}; -use std::io::IoSlice; -use std::net::{Ipv4Addr, SocketAddrV4}; -use std::os::fd::{AsRawFd, RawFd}; -use std::{io::Error, mem}; -use tracing::instrument; +use tracing::{self, instrument}; -mod kern_control; -mod sys; +pub mod kern_control; +pub mod sys; + +use kern_control::SysControlSocket; pub use super::queue::TunQueue; - -use super::{ifname_to_string, string_to_ifname, TunOptions}; -use kern_control::SysControlSocket; +use super::{ifname_to_string, string_to_ifname}; +use crate::TunOptions; #[derive(Debug)] pub struct TunInterface { @@ -31,8 +34,49 @@ impl TunInterface { #[throws] #[instrument] - pub fn new_with_options(_: TunOptions) -> TunInterface { - TunInterface::connect(0)? + pub fn new_with_options(options: TunOptions) -> TunInterface { + let ti = if options.tun_retrieve { + TunInterface::retrieve().ok_or(Error::new( + std::io::ErrorKind::NotFound, + "No tun interface found", + ))? + } else { + TunInterface::connect(0)? + }; + ti.configure(options)?; + ti + } + + pub fn retrieve() -> Option { + (3..100) + .filter_map(|fd| unsafe { + let peer_addr = socket2::SockAddr::init(|storage, len| { + *len = mem::size_of::() as u32; + libc::getpeername(fd, storage as *mut _, len); + Ok(()) + }) + .map(|(_, addr)| (fd, addr)); + peer_addr.ok() + }) + .filter(|(_fd, addr)| { + let ctl_addr = unsafe { &*(addr.as_ptr() as *const libc::sockaddr_ctl) }; + addr.family() == libc::AF_SYSTEM as u8 + && ctl_addr.ss_sysaddr == libc::AF_SYS_CONTROL as u16 + }) + .map(|(fd, _)| { + let socket = unsafe { socket2::Socket::from_raw_fd(fd) }; + TunInterface { socket } + }) + .next() + } + + #[throws] + fn configure(&self, options: TunOptions) { + if let Some(addr) = options.address { + if let Ok(addr) = addr.parse() { + self.set_ipv4_addr(addr)?; + } + } } #[throws] @@ -81,7 +125,7 @@ impl TunInterface { let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_addr = unsafe { *addr.as_ptr() }; self.perform(|fd| unsafe { sys::if_set_addr(fd, &iff) })?; - info!("ipv4_addr_set: {:?} (fd: {:?})", addr, self.as_raw_fd()) + tracing::info!("ipv4_addr_set: {:?} (fd: {:?})", addr, self.as_raw_fd()) } #[throws] @@ -118,7 +162,7 @@ impl TunInterface { let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_mtu = mtu; self.perform(|fd| unsafe { sys::if_set_mtu(fd, &iff) })?; - info!("mtu_set: {:?} (fd: {:?})", mtu, self.as_raw_fd()) + tracing::info!("mtu_set: {:?} (fd: {:?})", mtu, self.as_raw_fd()) } #[throws] @@ -140,7 +184,7 @@ impl TunInterface { let mut iff = self.ifreq()?; iff.ifr_ifru.ifru_netmask = unsafe { *addr.as_ptr() }; self.perform(|fd| unsafe { sys::if_set_netmask(fd, &iff) })?; - info!( + tracing::info!( "netmask_set: {:?} (fd: {:?})", unsafe { iff.ifr_ifru.ifru_netmask }, self.as_raw_fd() diff --git a/tun/src/unix/apple/sys.rs b/tun/src/unix/apple/sys.rs index c0ea613..b4d4a6a 100644 --- a/tun/src/unix/apple/sys.rs +++ b/tun/src/unix/apple/sys.rs @@ -2,11 +2,20 @@ use std::mem; use libc::{c_char, c_int, c_short, c_uint, c_ulong, sockaddr}; pub use libc::{ - c_void, sockaddr_ctl, sockaddr_in, socklen_t, AF_SYSTEM, AF_SYS_CONTROL, IFNAMSIZ, + c_void, + sockaddr_ctl, + sockaddr_in, + socklen_t, + AF_SYSTEM, + AF_SYS_CONTROL, + IFNAMSIZ, SYSPROTO_CONTROL, }; use nix::{ - ioctl_read_bad, ioctl_readwrite, ioctl_write_ptr_bad, request_code_readwrite, + ioctl_read_bad, + ioctl_readwrite, + ioctl_write_ptr_bad, + request_code_readwrite, request_code_write, }; diff --git a/tun/src/unix/linux/mod.rs b/tun/src/unix/linux/mod.rs index 75bb9d2..60d6341 100644 --- a/tun/src/unix/linux/mod.rs +++ b/tun/src/unix/linux/mod.rs @@ -1,18 +1,21 @@ +use std::{ + fs::OpenOptions, + io::{Error, Write}, + mem, + net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}, + os::{ + fd::RawFd, + unix::io::{AsRawFd, FromRawFd, IntoRawFd}, + }, +}; + use fehler::throws; - +use libc::in6_ifreq; use socket2::{Domain, SockAddr, Socket, Type}; -use std::fs::OpenOptions; -use std::io::{Error, Write}; -use std::mem; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}; -use std::os::fd::RawFd; -use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; - use tracing::{info, instrument}; -use libc::in6_ifreq; - -use super::{ifname_to_string, string_to_ifname, TunOptions}; +use super::{ifname_to_string, string_to_ifname}; +use crate::TunOptions; mod sys; @@ -38,10 +41,10 @@ impl TunInterface { let mut flags = libc::IFF_TUN as i16; - if options.no_pi.is_some() { + if options.no_pi { flags |= libc::IFF_NO_PI as i16; } - if options.tun_excl.is_some() { + if options.tun_excl { flags |= libc::IFF_TUN_EXCL as i16; } diff --git a/tun/src/unix/linux/sys.rs b/tun/src/unix/linux/sys.rs index 8d8725b..e12c8ec 100644 --- a/tun/src/unix/linux/sys.rs +++ b/tun/src/unix/linux/sys.rs @@ -1,10 +1,7 @@ -use nix::{ioctl_read_bad, ioctl_write_ptr_bad, request_code_read, request_code_write}; use std::mem::size_of; -pub use libc::ifreq; -pub use libc::sockaddr; -pub use libc::sockaddr_in; -pub use libc::sockaddr_in6; +pub use libc::{ifreq, sockaddr, sockaddr_in, sockaddr_in6}; +use nix::{ioctl_read_bad, ioctl_write_ptr_bad, request_code_read, request_code_write}; ioctl_write_ptr_bad!( tun_set_iff, diff --git a/tun/src/unix/mod.rs b/tun/src/unix/mod.rs index 9da4204..ae0b77a 100644 --- a/tun/src/unix/mod.rs +++ b/tun/src/unix/mod.rs @@ -1,10 +1,10 @@ use std::{ - io::{Error, Read}, + io::Error, + mem::MaybeUninit, os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}, }; -use tracing::instrument; -use super::TunOptions; +use tracing::instrument; mod queue; @@ -28,9 +28,8 @@ impl AsRawFd for TunInterface { impl FromRawFd for TunInterface { unsafe fn from_raw_fd(fd: RawFd) -> TunInterface { - TunInterface { - socket: socket2::Socket::from_raw_fd(fd), - } + let socket = socket2::Socket::from_raw_fd(fd); + TunInterface { socket } } } @@ -40,11 +39,26 @@ impl IntoRawFd for TunInterface { } } +unsafe fn assume_init(buf: &[MaybeUninit]) -> &[u8] { + &*(buf as *const [MaybeUninit] as *const [u8]) +} + impl TunInterface { #[throws] #[instrument] - pub fn recv(&mut self, buf: &mut [u8]) -> usize { - self.socket.read(buf)? + pub fn recv(&self, buf: &mut [u8]) -> usize { + // Use IoVec to read directly into target buffer + let mut tmp_buf = [MaybeUninit::uninit(); 1500]; + let len = self.socket.recv(&mut tmp_buf)?; + let result_buf = unsafe { assume_init(&tmp_buf[4..len]) }; + buf[..len - 4].copy_from_slice(result_buf); + len - 4 + } + + #[throws] + #[instrument] + pub fn set_nonblocking(&mut self, nb: bool) { + self.socket.set_nonblocking(nb)?; } } @@ -65,4 +79,4 @@ pub fn string_to_ifname(name: &str) -> [libc::c_char; libc::IFNAMSIZ] { let len = name.len().min(buf.len()); buf[..len].copy_from_slice(unsafe { &*(name.as_bytes() as *const _ as *const [libc::c_char]) }); buf -} \ No newline at end of file +} diff --git a/tun/src/unix/queue.rs b/tun/src/unix/queue.rs index 923f926..879dcd5 100644 --- a/tun/src/unix/queue.rs +++ b/tun/src/unix/queue.rs @@ -1,10 +1,10 @@ -use fehler::throws; - use std::{ io::{Error, Read, Write}, mem::MaybeUninit, os::unix::io::{AsRawFd, IntoRawFd, RawFd}, }; + +use fehler::throws; use tracing::instrument; use crate::TunInterface; @@ -15,10 +15,9 @@ pub struct TunQueue { } impl TunQueue { - #[throws] #[instrument] - pub fn recv(&self, buf: &mut [MaybeUninit]) -> usize { - self.socket.recv(buf)? + pub fn recv(&self, buf: &mut [MaybeUninit]) -> Result { + self.socket.recv(buf) } } @@ -43,9 +42,7 @@ impl Write for TunQueue { impl From for TunQueue { fn from(interface: TunInterface) -> TunQueue { - TunQueue { - socket: interface.socket, - } + TunQueue { socket: interface.socket } } } diff --git a/tun/src/windows/mod.rs b/tun/src/windows/mod.rs index bae75c0..dadd53f 100644 --- a/tun/src/windows/mod.rs +++ b/tun/src/windows/mod.rs @@ -1,15 +1,14 @@ -use std::fmt::Debug; +use std::{fmt::Debug, io::Error, ptr}; + use fehler::throws; -use std::io::Error; -use std::ptr; use widestring::U16CString; use windows::Win32::Foundation::GetLastError; mod queue; -use super::TunOptions; - pub use queue::TunQueue; +use super::TunOptions; + pub struct TunInterface { handle: sys::WINTUN_ADAPTER_HANDLE, name: String, @@ -40,10 +39,7 @@ impl TunInterface { if handle.is_null() { unsafe { GetLastError() }.ok()? } - TunInterface { - handle, - name: name_owned, - } + TunInterface { handle, name: name_owned } } pub fn name(&self) -> String { diff --git a/tun/tests/configure.rs b/tun/tests/configure.rs index 0f1199d..e7e2c6d 100644 --- a/tun/tests/configure.rs +++ b/tun/tests/configure.rs @@ -1,6 +1,6 @@ +use std::{io::Error, net::Ipv4Addr}; + use fehler::throws; -use std::io::Error; -use std::net::Ipv4Addr; use tun::TunInterface; #[test] diff --git a/tun/tests/packets.rs b/tun/tests/packets.rs index b160893..28090a2 100644 --- a/tun/tests/packets.rs +++ b/tun/tests/packets.rs @@ -1,7 +1,6 @@ -use fehler::throws; -use std::io::Error; +use std::{io::Error, net::Ipv4Addr}; -use std::net::Ipv4Addr; +use fehler::throws; use tun::TunInterface; #[throws] @@ -9,10 +8,10 @@ use tun::TunInterface; #[ignore = "requires interactivity"] #[cfg(not(target_os = "windows"))] fn tst_read() { - // This test is interactive, you need to send a packet to any server through 192.168.1.10 - // EG. `sudo route add 8.8.8.8 192.168.1.10`, + // This test is interactive, you need to send a packet to any server through + // 192.168.1.10 EG. `sudo route add 8.8.8.8 192.168.1.10`, //`dig @8.8.8.8 hackclub.com` - let mut tun = TunInterface::new()?; + let tun = TunInterface::new()?; println!("tun name: {:?}", tun.name()?); tun.set_ipv4_addr(Ipv4Addr::from([192, 168, 1, 10]))?; println!("tun ip: {:?}", tun.ipv4_addr()?); diff --git a/tun/tests/tokio.rs b/tun/tests/tokio.rs index e745c27..f7cb273 100644 --- a/tun/tests/tokio.rs +++ b/tun/tests/tokio.rs @@ -4,7 +4,7 @@ use std::net::Ipv4Addr; #[cfg(all(feature = "tokio", not(target_os = "windows")))] async fn test_create() { let tun = tun::TunInterface::new().unwrap(); - let async_tun = tun::tokio::TunInterface::new(tun).unwrap(); + let _ = tun::tokio::TunInterface::new(tun).unwrap(); } #[tokio::test] @@ -17,6 +17,6 @@ async fn test_write() { let async_tun = tun::tokio::TunInterface::new(tun).unwrap(); let mut buf = [0u8; 1500]; buf[0] = 6 << 4; - let bytes_written = async_tun.write(&buf).await.unwrap(); + let bytes_written = async_tun.send(&buf).await.unwrap(); assert!(bytes_written > 0); } From 2b9ecb7b6a60dcc1ed3b8ca56d08b7afb3af34a4 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 20 Jan 2024 09:39:30 -0800 Subject: [PATCH 092/128] Update Tunnel on the main thread Also updated it to use the new Swift Observable macro --- Apple/App/BurrowApp.swift | 2 +- Apple/App/Menu/MenuView.swift | 3 +- Apple/App/NetworkExtension+Async.swift | 24 +++--- Apple/App/Tunnel.swift | 32 ++++--- Apple/App/TunnelView.swift | 54 ++++++------ Apple/Burrow.xcodeproj/project.pbxproj | 27 +++--- .../xcshareddata/swiftpm/Package.resolved | 86 +++++++++++++++++++ .../xcshareddata/xcschemes/App.xcscheme | 2 +- .../xcschemes/NetworkExtension.xcscheme | 2 +- Apple/Configuration/Compiler.xcconfig | 4 +- 10 files changed, 167 insertions(+), 69 deletions(-) create mode 100644 Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Apple/App/BurrowApp.swift b/Apple/App/BurrowApp.swift index 6d798fb..e8aed86 100644 --- a/Apple/App/BurrowApp.swift +++ b/Apple/App/BurrowApp.swift @@ -15,7 +15,7 @@ struct BurrowApp: App { var body: some Scene { WindowGroup { - TunnelView() + TunnelView(tunnel: Self.tunnel) } } } diff --git a/Apple/App/Menu/MenuView.swift b/Apple/App/Menu/MenuView.swift index 9d8fb31..56a7494 100644 --- a/Apple/App/Menu/MenuView.swift +++ b/Apple/App/Menu/MenuView.swift @@ -8,7 +8,7 @@ import SwiftUI struct MenuItemToggleView: View { - @ObservedObject var tunnel: Tunnel + var tunnel: Tunnel var body: some View { HStack { @@ -23,7 +23,6 @@ struct MenuItemToggleView: View { .padding(.horizontal, 4) .padding(10) .frame(minWidth: 300, minHeight: 32, maxHeight: 32) - .task { await tunnel.update() } } } diff --git a/Apple/App/NetworkExtension+Async.swift b/Apple/App/NetworkExtension+Async.swift index ba478f3..4833efb 100644 --- a/Apple/App/NetworkExtension+Async.swift +++ b/Apple/App/NetworkExtension+Async.swift @@ -2,13 +2,13 @@ import NetworkExtension extension NEVPNManager { func remove() async throws { - let _: Void = try await withUnsafeThrowingContinuation { continuation in + _ = try await withUnsafeThrowingContinuation { continuation in removeFromPreferences(completionHandler: completion(continuation)) } } func save() async throws { - let _: Void = try await withUnsafeThrowingContinuation { continuation in + _ = try await withUnsafeThrowingContinuation { continuation in saveToPreferences(completionHandler: completion(continuation)) } } @@ -18,13 +18,7 @@ extension NETunnelProviderManager { class var managers: [NETunnelProviderManager] { get async throws { try await withUnsafeThrowingContinuation { continuation in - loadAllFromPreferences { managers, error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: managers ?? []) - } - } + loadAllFromPreferences(completionHandler: completion(continuation)) } } } @@ -32,10 +26,20 @@ extension NETunnelProviderManager { private func completion(_ continuation: UnsafeContinuation) -> (Error?) -> Void { return { error in - if let error = error { + if let error { continuation.resume(throwing: error) } else { continuation.resume(returning: ()) } } } + +private func completion(_ continuation: UnsafeContinuation) -> (T?, Error?) -> Void { + return { value, error in + if let error { + continuation.resume(throwing: error) + } else if let value { + continuation.resume(returning: value) + } + } +} diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift index e8bff22..0421a0c 100644 --- a/Apple/App/Tunnel.swift +++ b/Apple/App/Tunnel.swift @@ -2,15 +2,16 @@ import Combine import NetworkExtension import SwiftUI -@MainActor -class Tunnel: ObservableObject { - @Published private(set) var status: Status = .unknown - @Published private var error: NEVPNError? +@Observable class Tunnel { + private(set) var status: Status = .unknown + private var error: NEVPNError? private let bundleIdentifier: String private let configure: (NETunnelProviderManager, NETunnelProviderProtocol) -> Void private var tasks: [Task] = [] + // Each manager corresponds to one entry in the Settings app. + // Our goal is to maintain a single manager, so we create one if none exist and delete extra if there are any. private var managers: [NEVPNManager]? { didSet { status = currentStatus } } @@ -48,24 +49,31 @@ class Tunnel: ObservableObject { self.bundleIdentifier = bundleIdentifier self.configure = configure + listenForUpdates() + Task { await update() } + } + + private func listenForUpdates() { + let center = NotificationCenter.default let statusTask = Task { - for try await _ in NotificationCenter.default.notifications(named: .NEVPNStatusDidChange) { + for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { status = currentStatus } } let configurationTask = Task { - for try await _ in NotificationCenter.default.notifications(named: .NEVPNConfigurationChange) { + for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) { await update() } } tasks = [statusTask, configurationTask] } - func update() async { + private func update() async { do { - managers = try await NETunnelProviderManager.managers - } catch let error as NEVPNError { - self.error = error + let updated = try await NETunnelProviderManager.managers + await MainActor.run { managers = updated } + } catch let vpnError as NEVPNError { + error = vpnError } catch { print(error) } @@ -109,7 +117,9 @@ class Tunnel: ObservableObject { } deinit { - tasks.forEach { $0.cancel() } + for task in tasks { + task.cancel() + } } } diff --git a/Apple/App/TunnelView.swift b/Apple/App/TunnelView.swift index e3b9e28..dd91603 100644 --- a/Apple/App/TunnelView.swift +++ b/Apple/App/TunnelView.swift @@ -1,36 +1,34 @@ import SwiftUI struct TunnelView: View { -// @ObservedObject var tunnel: Tunnel + var tunnel: Tunnel var body: some View { - EmptyView() -// VStack { -// Text(verbatim: tunnel.status.description) -// switch tunnel.status { -// case .connected: -// Button("Disconnect", action: stop) -// case .permissionRequired: -// Button("Allow", action: configure) -// case .disconnected: -// Button("Start", action: start) -// default: -// EmptyView() -// } -// } -// .task { await tunnel.update() } -// .padding() + VStack { + Text(verbatim: tunnel.status.description) + switch tunnel.status { + case .connected: + Button("Disconnect", action: stop) + case .permissionRequired: + Button("Allow", action: configure) + case .disconnected: + Button("Start", action: start) + default: + EmptyView() + } + } + .padding() } -// private func start() { -// try? tunnel.start() -// } -// -// private func stop() { -// tunnel.stop() -// } -// -// private func configure() { -// Task { try await tunnel.configure() } -// } + private func start() { + try? tunnel.start() + } + + private func stop() { + tunnel.stop() + } + + private func configure() { + Task { try await tunnel.configure() } + } } diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index a8ff620..c0e4f09 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -196,7 +196,7 @@ buildRules = ( ); dependencies = ( - D0BCC6122A0B328800AD070D /* PBXTargetDependency */, + D08252712B5C3E2E005DA378 /* PBXTargetDependency */, ); name = NetworkExtension; productName = BurrowNetworkExtension; @@ -215,7 +215,7 @@ buildRules = ( ); dependencies = ( - D0BCC6142A0B329200AD070D /* PBXTargetDependency */, + D08252732B5C3E33005DA378 /* PBXTargetDependency */, D020F65C29E4A697002790F6 /* PBXTargetDependency */, ); name = App; @@ -231,7 +231,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; TargetAttributes = { D020F65229E4A697002790F6 = { CreatedOnToolsVersion = 14.3; @@ -251,6 +251,7 @@ ); mainGroup = D05B9F6929E39EEC008CB1F9; packageReferences = ( + D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */, ); productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */; projectDirPath = ""; @@ -337,13 +338,13 @@ target = D020F65229E4A697002790F6 /* NetworkExtension */; targetProxy = D020F65B29E4A697002790F6 /* PBXContainerItemProxy */; }; - D0BCC6122A0B328800AD070D /* PBXTargetDependency */ = { + D08252712B5C3E2E005DA378 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = D0BCC6112A0B328800AD070D /* SwiftLintPlugin */; + productRef = D08252702B5C3E2E005DA378 /* SwiftLintPlugin */; }; - D0BCC6142A0B329200AD070D /* PBXTargetDependency */ = { + D08252732B5C3E33005DA378 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = D0BCC6132A0B329200AD070D /* SwiftLintPlugin */; + productRef = D08252722B5C3E33005DA378 /* SwiftLintPlugin */; }; /* End PBXTargetDependency section */ @@ -423,25 +424,25 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */ = { + D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/realm/SwiftLint.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.51.0; + minimumVersion = 0.54.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - D0BCC6112A0B328800AD070D /* SwiftLintPlugin */ = { + D08252702B5C3E2E005DA378 /* SwiftLintPlugin */ = { isa = XCSwiftPackageProductDependency; - package = D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */; + package = D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; productName = "plugin:SwiftLintPlugin"; }; - D0BCC6132A0B329200AD070D /* SwiftLintPlugin */ = { + D08252722B5C3E33005DA378 /* SwiftLintPlugin */ = { isa = XCSwiftPackageProductDependency; - package = D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */; + package = D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; productName = "plugin:SwiftLintPlugin"; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..7522840 --- /dev/null +++ b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,86 @@ +{ + "pins" : [ + { + "identity" : "collectionconcurrencykit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", + "state" : { + "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", + "version" : "0.2.0" + } + }, + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "state" : { + "revision" : "7892a123f7e8d0fe62f9f03728b17bbd4f94df5c", + "version" : "1.8.1" + } + }, + { + "identity" : "sourcekitten", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/SourceKitten.git", + "state" : { + "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", + "version" : "0.34.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", + "version" : "1.2.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version" : "509.0.2" + } + }, + { + "identity" : "swiftlint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/realm/SwiftLint.git", + "state" : { + "revision" : "f17a4f9dfb6a6afb0408426354e4180daaf49cee", + "version" : "0.54.0" + } + }, + { + "identity" : "swiftytexttable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", + "state" : { + "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", + "version" : "0.9.0" + } + }, + { + "identity" : "swxmlhash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/drmohundro/SWXMLHash.git", + "state" : { + "revision" : "4d0f62f561458cbe1f732171e625f03195151b60", + "version" : "7.0.1" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", + "version" : "5.0.6" + } + } + ], + "version" : 2 +} diff --git a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme index 7bb7808..c63f8e6 100644 --- a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme +++ b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -1,6 +1,6 @@ Date: Sat, 20 Jan 2024 09:52:30 -0800 Subject: [PATCH 093/128] Updated to Xcode 15.2 --- .github/workflows/build-apple.yml | 6 +++--- .github/workflows/release-apple.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 1aadcc2..0ed6c83 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -9,7 +9,7 @@ on: jobs: build: name: Build App (${{ matrix.platform }}) - runs-on: macos-12 + runs-on: macos-14 strategy: fail-fast: false matrix: @@ -21,7 +21,7 @@ jobs: rust-targets: - aarch64-apple-ios - scheme: App - destination: platform=iOS Simulator,OS=16.2,name=iPhone 14 Pro + destination: platform=iOS Simulator,OS=17.2,name=iPhone 15 Pro platform: iOS Simulator sdk-name: iphonesimulator rust-targets: @@ -35,7 +35,7 @@ jobs: - x86_64-apple-darwin - aarch64-apple-darwin env: - DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index 8b8a76c..24fbeb5 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -6,7 +6,7 @@ on: jobs: build: name: Build ${{ matrix.configuration['platform'] }} Release - runs-on: macos-12 + runs-on: macos-14 strategy: fail-fast: false matrix: @@ -22,7 +22,7 @@ jobs: method: mac-application artifact-file: Burrow.app.txz env: - DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer steps: - name: Checkout uses: actions/checkout@v3 From fd9b3413acc823ebf8be7981083c0b88f1e1af14 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 20 Jan 2024 10:10:02 -0800 Subject: [PATCH 094/128] Update CODEOWNERS to reflect current owners --- .github/CODEOWNERS | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fe0babf..9a9b5b2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,4 @@ * @conradev @malted @JettChenT @jdogcoder -burrow/ @Muirrum -tun/ @Muirrum -Apple/ @jdogcoder -burrow-gtk/ @davnotdev \ No newline at end of file +burrow/ @conradev @malted @JettChenT @jdogcoder @Muirrum +tun/ @conradev @malted @JettChenT @jdogcoder @Muirrum +burrow-gtk/ @conradev @malted @JettChenT @jdogcoder @davnotdev From dfd4dbc81eaadeefb6958df94a6d2f4fb5db8e09 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sun, 21 Jan 2024 03:38:15 +0800 Subject: [PATCH 095/128] Wireguard Timer Support (#167) --- .github/actions/build-for-testing/action.yml | 1 + .github/workflows/build-apple.yml | 2 +- .github/workflows/release-apple.yml | 2 +- Makefile | 10 ++++++++ burrow/src/apple.rs | 2 +- burrow/src/wireguard/config.rs | 2 +- burrow/src/wireguard/iface.rs | 25 +++++++++++++++++-- burrow/src/wireguard/noise/mod.rs | 4 +++ burrow/src/wireguard/pcb.rs | 26 +++++++++++++++++++- tun/src/tokio/mod.rs | 1 - 10 files changed, 67 insertions(+), 8 deletions(-) diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index fb5dd8d..ce91b43 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -43,6 +43,7 @@ runs: -clonedSourcePackagesDirPath SourcePackages \ -packageCachePath $PWD/PackageCache \ -skipPackagePluginValidation \ + -skipMacroValidation \ -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ -resultBundlePath BuildResults.xcresult diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 0ed6c83..57a4977 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -9,7 +9,7 @@ on: jobs: build: name: Build App (${{ matrix.platform }}) - runs-on: macos-14 + runs-on: macos-13 strategy: fail-fast: false matrix: diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index 24fbeb5..3ea185d 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -6,7 +6,7 @@ on: jobs: build: name: Build ${{ matrix.configuration['platform'] }} Release - runs-on: macos-14 + runs-on: macos-13 strategy: fail-fast: false matrix: diff --git a/Makefile b/Makefile index 2988e5c..e8e5687 100644 --- a/Makefile +++ b/Makefile @@ -16,3 +16,13 @@ test-dns: @sudo route delete 8.8.8.8 @sudo route add 8.8.8.8 -interface utun$(tun_num) @dig @8.8.8.8 hackclub.com + +test-https: + @sudo route delete 193.183.0.162 + @sudo route add 193.183.0.162 -interface utun$(tun_num) + @curl -vv https://search.marginalia.nu + +test-http: + @sudo route delete 146.190.62.39 + @sudo route add 146.190.62.39 -interface utun$(tun_num) + @curl -vv 146.190.62.39:80 diff --git a/burrow/src/apple.rs b/burrow/src/apple.rs index 571b413..9fc0140 100644 --- a/burrow/src/apple.rs +++ b/burrow/src/apple.rs @@ -10,4 +10,4 @@ pub extern "C" fn initialize_oslog() { tracing_subscriber::registry().with(OsLogger::new("com.hackclub.burrow", "backend")); tracing::subscriber::set_global_default(collector).unwrap(); debug!("Initialized oslog tracing in libburrow rust FFI"); -} +} \ No newline at end of file diff --git a/burrow/src/wireguard/config.rs b/burrow/src/wireguard/config.rs index d86486e..afe7499 100644 --- a/burrow/src/wireguard/config.rs +++ b/burrow/src/wireguard/config.rs @@ -101,7 +101,7 @@ impl Default for Config { }, peers: vec![Peer { endpoint: "wg.burrow.rs:51820".into(), - allowed_ips: vec!["8.8.8.8/32".into()], + allowed_ips: vec!["8.8.8.8/32".into(), "0.0.0.0/0".into()], public_key: "uy75leriJay0+oHLhRMpV+A5xAQ0hCJ+q7Ww81AOvT4=".into(), preshared_key: Some("s7lx/mg+reVEMnGnqeyYOQkzD86n2+gYnx1M9ygi08k=".into()), persistent_keepalive: Default::default(), diff --git a/burrow/src/wireguard/iface.rs b/burrow/src/wireguard/iface.rs index 281cc4a..ba175de 100755 --- a/burrow/src/wireguard/iface.rs +++ b/burrow/src/wireguard/iface.rs @@ -135,7 +135,7 @@ impl Interface { debug!("spawning read task for peer {}", i); let pcb = pcbs.pcbs[i].clone(); let tun = tun.clone(); - let tsk = async move { + let main_tsk = async move { if let Err(e) = pcb.open_if_closed().await { log::error!("failed to open pcb: {}", e); return @@ -147,8 +147,29 @@ impl Interface { debug!("pcb ran successfully"); } }; + + let pcb = pcbs.pcbs[i].clone(); + let update_timers_tsk = async move { + let mut buf = [0u8; 65535]; + loop { + tokio::time::sleep(tokio::time::Duration::from_millis(250)).await; + pcb.update_timers(&mut buf).await; + } + }; + + let pcb = pcbs.pcbs[i].clone(); + let reset_rate_limiter_tsk = async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + pcb.reset_rate_limiter().await; + } + }; + tsks.extend(vec![ + tokio::spawn(main_tsk), + tokio::spawn(update_timers_tsk), + tokio::spawn(reset_rate_limiter_tsk) + ]); debug!("task made.."); - tsks.push(tokio::spawn(tsk)); } debug!("spawned read tasks"); } diff --git a/burrow/src/wireguard/noise/mod.rs b/burrow/src/wireguard/noise/mod.rs index 6ece759..24f4fbb 100755 --- a/burrow/src/wireguard/noise/mod.rs +++ b/burrow/src/wireguard/noise/mod.rs @@ -346,6 +346,10 @@ impl Tunnel { self.handle_verified_packet(packet, dst) } + pub fn reset_rate_limiter(&self) { + self.rate_limiter.reset_count(); + } + pub(crate) fn handle_verified_packet<'a>( &mut self, packet: Packet, diff --git a/burrow/src/wireguard/pcb.rs b/burrow/src/wireguard/pcb.rs index a781870..c6ebaa6 100755 --- a/burrow/src/wireguard/pcb.rs +++ b/burrow/src/wireguard/pcb.rs @@ -1,6 +1,6 @@ use std::{net::SocketAddr, sync::Arc}; -use anyhow::Error; +use anyhow::{Error, Result}; use fehler::throws; use ip_network::IpNetwork; use rand::random; @@ -132,4 +132,28 @@ impl PeerPcb { }; Ok(()) } + + pub async fn update_timers(&self, dst: &mut [u8]) -> Result<(), Error> { + match self.tunnel.write().await.update_timers(dst) { + TunnResult::Done => {} + TunnResult::Err(e) => { + tracing::error!(message = "Update timers error", error = ?e) + } + TunnResult::WriteToNetwork(packet) => { + self.open_if_closed().await?; + let handle = self.socket.read().await; + let Some(socket) = handle.as_ref() else { + tracing::error!("No socket for peer"); + return Ok(()) + }; + socket.send(packet).await?; + } + _ => panic!("Unexpected result from update_timers"), + }; + Ok(()) + } + + pub async fn reset_rate_limiter(&self) { + self.tunnel.read().await.reset_rate_limiter(); + } } diff --git a/tun/src/tokio/mod.rs b/tun/src/tokio/mod.rs index 947fb74..bd27109 100644 --- a/tun/src/tokio/mod.rs +++ b/tun/src/tokio/mod.rs @@ -26,7 +26,6 @@ impl TunInterface { } } - #[instrument] pub async fn recv(&self, buf: &mut [u8]) -> io::Result { loop { let mut guard = self.inner.readable().await?; From baa81eb939c59cc6fbb4ad0f1fa1a2597520a526 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 20 Jan 2024 11:36:44 -0800 Subject: [PATCH 096/128] Cancel in-progress runs when pushing new code --- .github/workflows/build-apple.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 57a4977..da0f56a 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -6,6 +6,9 @@ on: pull_request: branches: - "*" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: build: name: Build App (${{ matrix.platform }}) From 6990f90c2eb58fccaee9292668c1a8a448a13c44 Mon Sep 17 00:00:00 2001 From: David Zhong <91637806+davnotdev@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:10:24 -0800 Subject: [PATCH 097/128] Implement Gtk Network Status (#165) Implemented - Switch reacts to burrow socket and network changes - meson as build system - Basic diagnostics to ensure burrow is installed properly - Flatpak / Meson Building --- .github/workflows/build-flatpak.yml | 2 +- burrow-gtk/.cargo/config.toml | 2 + burrow-gtk/Cargo.lock | 1259 ++++++++++------- burrow-gtk/Cargo.toml | 11 +- .../com.hackclub.burrow.devel.json | 9 +- .../{ => build-aux}/com.hackclub.burrow.json | 9 +- burrow-gtk/build.rs | 16 + ...ub.burrow.desktop.in => app.desktop.in.in} | 6 +- burrow-gtk/data/app.gschema.xml.in | 5 + burrow-gtk/data/app.metainfo.xml.in | 16 + .../data/com.hackclub.burrow.appdata.xml.in | 8 - .../data/com.hackclub.burrow.gschema.xml | 5 - ...com.hackclub.burrow.svg => burrow-gtk.svg} | 0 ...w-symbolic.svg => burrow-gtk-symbolic.svg} | 2 +- burrow-gtk/data/icons/meson.build | 13 - burrow-gtk/data/meson.build | 105 +- burrow-gtk/data/resources.gresource.xml | 5 + burrow-gtk/meson.build | 56 + burrow-gtk/po/POTFILES | 5 +- burrow-gtk/po/meson.build | 2 +- burrow-gtk/src/.gitignore | 1 + burrow-gtk/src/components/app.rs | 136 ++ burrow-gtk/src/components/mod.rs | 20 + .../src/components/settings/diag_group.rs | 126 ++ burrow-gtk/src/components/settings/mod.rs | 5 + burrow-gtk/src/components/settings_screen.rs | 44 + burrow-gtk/src/components/switch_screen.rs | 158 +++ burrow-gtk/src/config.rs.in | 8 + burrow-gtk/src/diag.rs | 80 ++ burrow-gtk/src/main.rs | 88 +- burrow-gtk/src/meson.build | 34 + 31 files changed, 1571 insertions(+), 665 deletions(-) create mode 100644 burrow-gtk/.cargo/config.toml rename burrow-gtk/{ => build-aux}/com.hackclub.burrow.devel.json (84%) rename burrow-gtk/{ => build-aux}/com.hackclub.burrow.json (83%) create mode 100644 burrow-gtk/build.rs rename burrow-gtk/data/{com.hackclub.burrow.desktop.in => app.desktop.in.in} (60%) create mode 100644 burrow-gtk/data/app.gschema.xml.in create mode 100644 burrow-gtk/data/app.metainfo.xml.in delete mode 100644 burrow-gtk/data/com.hackclub.burrow.appdata.xml.in delete mode 100644 burrow-gtk/data/com.hackclub.burrow.gschema.xml rename burrow-gtk/data/icons/hicolor/scalable/apps/{com.hackclub.burrow.svg => burrow-gtk.svg} (100%) rename burrow-gtk/data/icons/hicolor/symbolic/apps/{com.hackclub.burrow-symbolic.svg => burrow-gtk-symbolic.svg} (97%) delete mode 100644 burrow-gtk/data/icons/meson.build create mode 100644 burrow-gtk/data/resources.gresource.xml create mode 100644 burrow-gtk/meson.build create mode 100644 burrow-gtk/src/.gitignore create mode 100644 burrow-gtk/src/components/app.rs create mode 100644 burrow-gtk/src/components/mod.rs create mode 100644 burrow-gtk/src/components/settings/diag_group.rs create mode 100644 burrow-gtk/src/components/settings/mod.rs create mode 100644 burrow-gtk/src/components/settings_screen.rs create mode 100644 burrow-gtk/src/components/switch_screen.rs create mode 100644 burrow-gtk/src/config.rs.in create mode 100644 burrow-gtk/src/diag.rs create mode 100644 burrow-gtk/src/meson.build diff --git a/.github/workflows/build-flatpak.yml b/.github/workflows/build-flatpak.yml index 8a70613..e0e804e 100644 --- a/.github/workflows/build-flatpak.yml +++ b/.github/workflows/build-flatpak.yml @@ -15,5 +15,5 @@ jobs: - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: bundle: Burrow.flatpak - manifest-path: burrow-gtk/com.hackclub.burrow.devel.json + manifest-path: burrow-gtk/build-aux/com.hackclub.burrow.devel.json cache-key: flatpak-builder-${{ github.sha }} diff --git a/burrow-gtk/.cargo/config.toml b/burrow-gtk/.cargo/config.toml new file mode 100644 index 0000000..87e5dd7 --- /dev/null +++ b/burrow-gtk/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(unix)'] +runner = "sh -c" diff --git a/burrow-gtk/Cargo.lock b/burrow-gtk/Cargo.lock index 3d8f154..d0b7009 100644 --- a/burrow-gtk/Cargo.lock +++ b/burrow-gtk/Cargo.lock @@ -17,6 +17,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.3" @@ -39,9 +49,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", @@ -59,58 +69,60 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "async-channel" -version = "1.9.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", "event-listener", + "event-listener-strategy", "futures-core", + "pin-project-lite", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -136,9 +148,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -187,7 +199,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.39", + "syn 2.0.48", "which", ] @@ -199,9 +211,24 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" @@ -222,34 +249,52 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" name = "burrow" version = "0.1.0" dependencies = [ + "aead", "anyhow", "async-channel", + "base64", + "blake2", "caps", + "chacha20poly1305", "clap", "env_logger", + "fehler", + "futures", + "hmac", + "ip_network", + "ip_network_table", + "ipnet", "libsystemd", "log", "nix", + "parking_lot", + "rand", + "rand_core", + "ring", "schemars", "serde", "serde_json", "tokio", "tracing", "tracing-journald", - "tracing-log", + "tracing-log 0.1.4", "tracing-oslog", "tracing-subscriber", "tun", + "uuid", + "x25519-dalek", ] [[package]] name = "burrow-gtk" version = "0.1.0" dependencies = [ + "anyhow", "burrow", + "gettext-rs", + "glib-build-tools", "relm4", - "relm4-components", - "relm4-icons", + "tokio", ] [[package]] @@ -287,11 +332,11 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.17.10" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3603c4028a5e368d09b51c8b624b9a46edcd7c3778284077a6125af73c9f0a" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "cairo-sys-rs", "glib", "libc", @@ -301,9 +346,9 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.17.10" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "691d0c66b1fb4881be80a760cb8fe76ea97218312f9dfe2c9cc0f496ca279cb1" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" dependencies = [ "glib-sys", "libc", @@ -341,9 +386,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.5" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a" dependencies = [ "smallvec", "target-lexicon", @@ -355,6 +400,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "cipher" version = "0.4.4" @@ -363,24 +432,25 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] name = "clang-sys" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.1", ] [[package]] name = "clap" -version = "4.4.8" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", @@ -388,9 +458,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.8" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -407,7 +477,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -424,9 +494,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] @@ -439,9 +509,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -449,15 +519,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -473,12 +543,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -487,14 +554,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] [[package]] -name = "deranged" -version = "0.3.9" +name = "curve25519-dalek" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] @@ -533,9 +628,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "is-terminal", @@ -552,19 +647,34 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "event-listener" -version = "2.5.3" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener", + "pin-project-lite", +] [[package]] name = "fastrand" @@ -592,6 +702,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "fiat-crypto" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + [[package]] name = "field-offset" version = "0.3.6" @@ -614,14 +730,13 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", "nanorand", - "pin-project", "spin", ] @@ -648,9 +763,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -663,9 +778,9 @@ checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -678,9 +793,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -688,15 +803,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -705,38 +820,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -752,11 +867,10 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.17.10" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "695d6bc846438c5708b07007537b9274d883373dd30858ca881d7d71b5540717" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" dependencies = [ - "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -766,9 +880,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.17.10" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9285ec3c113c66d7d0ab5676599176f1f42f4944ca1b581852215bf5694870cb" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" dependencies = [ "gio-sys", "glib-sys", @@ -779,11 +893,10 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.6.3" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff" +checksum = "7edb019ad581f8ecf8ea8e4baa6df7c483a95b5a59be3140be6a9c3b0c632af6" dependencies = [ - "bitflags 1.3.2", "cairo-rs", "gdk-pixbuf", "gdk4-sys", @@ -795,9 +908,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.6.3" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc92aa1608c089c49393d014c38ac0390d01e4841e1fedaa75dbcef77aaed64" +checksum = "dbab43f332a3cf1df9974da690b5bb0e26720ed09a228178ce52175372dcfef0" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -822,9 +935,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", @@ -834,18 +947,37 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.28.0" +name = "gettext-rs" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" +dependencies = [ + "cc", + "temp-dir", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gio" -version = "0.17.10" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" dependencies = [ - "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", @@ -861,9 +993,9 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.17.10" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" dependencies = [ "glib-sys", "gobject-sys", @@ -874,11 +1006,11 @@ dependencies = [ [[package]] name = "glib" -version = "0.17.10" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "futures-channel", "futures-core", "futures-executor", @@ -896,25 +1028,30 @@ dependencies = [ ] [[package]] -name = "glib-macros" -version = "0.17.10" +name = "glib-build-tools" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" +checksum = "3431c56f463443cba9bc3600248bc6d680cb614c2ee1cdd39dab5415bd12ac5c" + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ - "anyhow", "heck", - "proc-macro-crate", + "proc-macro-crate 2.0.1", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] name = "glib-sys" -version = "0.17.10" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" dependencies = [ "libc", "system-deps", @@ -928,9 +1065,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.17.10" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ "glib-sys", "libc", @@ -939,9 +1076,9 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.17.10" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def4bb01265b59ed548b05455040d272d989b3012c42d4c1bbd39083cb9b40d9" +checksum = "3b2228cda1505613a7a956cca69076892cfbda84fc2b7a62b94a41a272c0c401" dependencies = [ "glib", "graphene-sys", @@ -950,9 +1087,9 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.17.10" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1856fc817e6a6675e36cea0bd9a3afe296f5d9709d1e2d3182803ac77f0ab21d" +checksum = "cc4144cee8fc8788f2a9b73dc5f1d4e1189d1f95305c4cb7bd9c1af1cfa31f59" dependencies = [ "glib-sys", "libc", @@ -962,11 +1099,10 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.6.3" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c" +checksum = "0d958e351d2f210309b32d081c832d7de0aca0b077aa10d88336c6379bd01f7e" dependencies = [ - "bitflags 1.3.2", "cairo-rs", "gdk4", "glib", @@ -978,9 +1114,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.6.3" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07a84fb4dcf1323d29435aa85e2f5f58bef564342bef06775ec7bd0da1f01b0" +checksum = "12bd9e3effea989f020e8f1ff3fa3b8c63ba93d43b899c11a118868853a56d55" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -994,11 +1130,10 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.6.6" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b28a32a04cd75cef14a0983f8b0c669e0fe152a0a7725accdeb594e2c764c88b" +checksum = "5aeb51aa3e9728575a053e1f43543cd9992ac2477e1b186ad824fd4adfb70842" dependencies = [ - "bitflags 1.3.2", "cairo-rs", "field-offset", "futures-channel", @@ -1011,18 +1146,17 @@ dependencies = [ "gtk4-macros", "gtk4-sys", "libc", - "once_cell", "pango", ] [[package]] name = "gtk4-macros" -version = "0.6.6" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a4d6b61570f76d3ee542d984da443b1cd69b6105264c61afec3abed08c2500f" +checksum = "d57ec49cf9b657f69a05bca8027cff0a8dfd0c49e812be026fc7311f2163832f" dependencies = [ "anyhow", - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro-error", "proc-macro2", "quote", @@ -1031,9 +1165,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.6.3" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f8283f707b07e019e76c7f2934bdd4180c277e08aa93f4c0d8dd07b7a34e22f" +checksum = "54d8c4aa23638ce9faa2caf7e2a27d4a1295af2155c8e8d28c4d4eeca7a65eb8" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1048,29 +1182,11 @@ dependencies = [ "system-deps", ] -[[package]] -name = "gvdb" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7139233c0ecb66f285c47a3c1c02b35c8d52a42ca4c7448d0163e5637bb4bd3" -dependencies = [ - "byteorder", - "flate2", - "lazy_static", - "memmap2", - "quick-xml", - "safe-transmute", - "serde", - "serde_json", - "walkdir", - "zvariant", -] - [[package]] name = "h2" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -1078,7 +1194,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap", "slab", "tokio", "tokio-util", @@ -1087,15 +1203,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" @@ -1105,9 +1215,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "hex" @@ -1126,18 +1236,18 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "http" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f95b9abcae896730d42b78e09c155ed4ddf82c07b4de772c64aee5b2d8b7c150" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1146,9 +1256,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1175,9 +1285,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1190,7 +1300,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -1212,24 +1322,14 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.1.0" @@ -1237,7 +1337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown", ] [[package]] @@ -1249,28 +1349,53 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "ip_network_table" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" +dependencies = [ + "ip_network", + "ip_network_table-deps-treebitmap", +] + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +dependencies = [ + "serde", +] [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" @@ -1283,9 +1408,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -1304,11 +1429,10 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libadwaita" -version = "0.4.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab9c0843f9f23ff25634df2743690c3a1faffe0a190e60c490878517eb81abf" +checksum = "2fe7e70c06507ed10a16cda707f358fbe60fe0dc237498f78c686ade92fd979c" dependencies = [ - "bitflags 1.3.2", "gdk-pixbuf", "gdk4", "gio", @@ -1321,9 +1445,9 @@ dependencies = [ [[package]] name = "libadwaita-sys" -version = "0.4.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4231cb2499a9f0c4cdfa4885414b33e39901ddcac61150bc0bb4ff8a57ede404" +checksum = "5e10aaa38de1d53374f90deeb4535209adc40cc5dba37f9704724169bceec69a" dependencies = [ "gdk4-sys", "gio-sys", @@ -1337,9 +1461,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -1351,6 +1475,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "libsystemd" version = "0.6.0" @@ -1371,9 +1505,22 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] [[package]] name = "lock_api" @@ -1391,6 +1538,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1402,18 +1558,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "memmap2" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" -dependencies = [ - "libc", -] +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -1453,7 +1600,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1479,13 +1626,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1559,27 +1706,62 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.32.1" +name = "objc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.59" +version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", @@ -1596,7 +1778,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1607,9 +1789,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.95" +version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ "cc", "libc", @@ -1625,11 +1807,10 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pango" -version = "0.17.10" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35be456fc620e61f62dff7ff70fbd54dcbaf0a4b920c0f16de1107c47d921d48" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" dependencies = [ - "bitflags 1.3.2", "gio", "glib", "libc", @@ -1639,9 +1820,9 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.17.10" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da69f9f3850b0d8990d462f8c709561975e95f689c1cdf0fecdebde78b35195" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" dependencies = [ "glib-sys", "gobject-sys", @@ -1649,6 +1830,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1669,7 +1856,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1703,29 +1890,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "pin-project" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" @@ -1741,9 +1908,26 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" + +[[package]] +name = "platforms" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] [[package]] name = "powerfmt" @@ -1752,13 +1936,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "prettyplease" -version = "0.2.15" +name = "ppv-lite86" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1771,6 +1961,16 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "proc-macro-crate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1797,30 +1997,41 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] -name = "quick-xml" -version = "0.29.0" +name = "quote" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ - "memchr", - "serde", + "proc-macro2", ] [[package]] -name = "quote" -version = "1.0.33" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "proc-macro2", + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", ] [[package]] @@ -1828,6 +2039,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "redox_syscall" @@ -1840,13 +2054,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", + "regex-automata 0.4.4", "regex-syntax 0.8.2", ] @@ -1861,9 +2075,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" dependencies = [ "aho-corasick", "memchr", @@ -1884,9 +2098,8 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "relm4" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c16f3fad883034773b7f5af4d7e865532b8f3641e5a8bab2a34561a8d960d81" +version = "0.7.0-beta.2" +source = "git+https://github.com/Relm4/Relm4#e189eee06b887470e0fd65cbaf6d7c0161bed5ea" dependencies = [ "async-trait", "flume", @@ -1900,43 +2113,21 @@ dependencies = [ "tracing", ] -[[package]] -name = "relm4-components" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5485d72dc94c12a59c571d80cf9a545e5b9a2f0ebc90ea5fd234929a9376f66d" -dependencies = [ - "once_cell", - "relm4", - "tracker", -] - -[[package]] -name = "relm4-icons" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e28bcc718a587bcfa31b034e0b8f4efe5b70e945b7de9d7d154b45357a0dadc" -dependencies = [ - "gtk4", - "gvdb", -] - [[package]] name = "relm4-macros" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9340e2553c0a184a80a0bfa1dcf73c47f3d48933aa6be90724b202f9fbd24735" +version = "0.7.0-beta.2" +source = "git+https://github.com/Relm4/Relm4#e189eee06b887470e0fd65cbaf6d7c0161bed5ea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64", "bytes", @@ -1970,6 +2161,20 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1993,52 +2198,37 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "safe-transmute" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a01dab6acf992653be49205bdd549f32f17cb2803e8eacf1560bf97259aae8" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "schemars" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "dyn-clone", "schemars_derive", @@ -2048,9 +2238,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ "proc-macro2", "quote", @@ -2089,28 +2279,28 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2126,9 +2316,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -2137,9 +2327,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] @@ -2200,9 +2390,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" @@ -2215,9 +2405,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" @@ -2236,7 +2426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2264,12 +2454,6 @@ dependencies = [ "xxhash-rust", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.10.0" @@ -2295,9 +2479,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2340,50 +2524,56 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.12" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" + +[[package]] +name = "temp-dir" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd16aa9ffe15fe021c6ee3766772132c6e98dfa395a167e16864f61a9cfb71d6" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2398,9 +2588,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", "powerfmt", @@ -2431,9 +2621,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -2443,7 +2633,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2454,7 +2644,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2483,21 +2673,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.8" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.21.0", + "toml_edit 0.20.2", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] @@ -2508,18 +2698,18 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.1.0", + "indexmap", "toml_datetime", "winnow", ] [[package]] name = "toml_edit" -version = "0.21.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.1.0", + "indexmap", "serde", "serde_spanned", "toml_datetime", @@ -2551,7 +2741,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2586,6 +2776,17 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-oslog" version = "0.1.2" @@ -2603,9 +2804,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -2616,34 +2817,14 @@ dependencies = [ "thread_local", "tracing", "tracing-core", - "tracing-log", -] - -[[package]] -name = "tracker" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9636d15e370187f6bf55b79ce62ebf4221998bc0ba1774d7fa208b007f6bf8" -dependencies = [ - "tracker-macros", -] - -[[package]] -name = "tracker-macros" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca029746fbe0efda3298205de77bf759d7fef23ac97902641e0b49a623b0455f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", + "tracing-log 0.2.0", ] [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tun" @@ -2656,7 +2837,7 @@ dependencies = [ "futures", "lazy_static", "libc", - "libloading", + "libloading 0.7.4", "log", "nix", "reqwest", @@ -2680,9 +2861,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -2706,10 +2887,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] -name = "url" -version = "2.4.1" +name = "universal-hash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -2724,10 +2921,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ + "getrandom", "serde", ] @@ -2755,16 +2953,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "walkdir" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -2782,9 +2970,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2792,24 +2980,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -2819,9 +3007,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2829,28 +3017,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -2911,7 +3099,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -2920,7 +3108,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -2929,13 +3126,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -2944,36 +3156,72 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2981,10 +3229,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "winnow" -version = "0.5.19" +name = "windows_x86_64_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" dependencies = [ "memchr", ] @@ -2996,14 +3250,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", ] [[package]] name = "xxhash-rust" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" +checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] [[package]] name = "zip" @@ -3053,40 +3339,3 @@ dependencies = [ "cc", "pkg-config", ] - -[[package]] -name = "zvariant" -version = "3.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" -dependencies = [ - "byteorder", - "libc", - "serde", - "static_assertions", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "3.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 1.0.109", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] diff --git a/burrow-gtk/Cargo.toml b/burrow-gtk/Cargo.toml index 4763320..244c161 100644 --- a/burrow-gtk/Cargo.toml +++ b/burrow-gtk/Cargo.toml @@ -6,7 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -relm4 = { version = "0.6.2", features = ["libadwaita"] } -relm4-components = "0.6.2" -relm4-icons = { version = "0.6.0", features = ["plus"] } +anyhow = "1.0" +relm4 = { features = ["libadwaita", "gnome_45"], git = "https://github.com/Relm4/Relm4" } burrow = { version = "*", path = "../burrow/" } +tokio = { version = "1.35.0", features = ["time", "sync"] } +gettext-rs = { version = "0.7.0", features = ["gettext-system"] } + +[build-dependencies] +anyhow = "1.0" +glib-build-tools = "0.18.0" diff --git a/burrow-gtk/com.hackclub.burrow.devel.json b/burrow-gtk/build-aux/com.hackclub.burrow.devel.json similarity index 84% rename from burrow-gtk/com.hackclub.burrow.devel.json rename to burrow-gtk/build-aux/com.hackclub.burrow.devel.json index 8b32b02..4a2e5fc 100644 --- a/burrow-gtk/com.hackclub.burrow.devel.json +++ b/burrow-gtk/build-aux/com.hackclub.burrow.devel.json @@ -40,15 +40,12 @@ "name" : "burrow-gtk", "builddir" : true, "subdir" : "burrow-gtk", - "buildsystem" : "simple", - "build-commands": [ - "cargo build", - "install -Dm755 -t /app/bin target/debug/burrow-gtk" - ], + "buildsystem" : "meson", + "config-opts": ["--buildtype=debug"], "sources" : [ { "type": "dir", - "path": "../" + "path": "../../" } ] } diff --git a/burrow-gtk/com.hackclub.burrow.json b/burrow-gtk/build-aux/com.hackclub.burrow.json similarity index 83% rename from burrow-gtk/com.hackclub.burrow.json rename to burrow-gtk/build-aux/com.hackclub.burrow.json index 831a236..c8b68e5 100644 --- a/burrow-gtk/com.hackclub.burrow.json +++ b/burrow-gtk/build-aux/com.hackclub.burrow.json @@ -40,15 +40,12 @@ "name" : "burrow-gtk", "builddir" : true, "subdir" : "burrow-gtk", - "buildsystem" : "simple", - "build-commands": [ - "cargo build --release", - "install -Dm755 -t /app/bin target/release/burrow-gtk" - ], + "buildsystem" : "meson", + "config-opts": ["--buildtype=release"], "sources" : [ { "type": "dir", - "path": "../" + "path": "../../" } ] } diff --git a/burrow-gtk/build.rs b/burrow-gtk/build.rs new file mode 100644 index 0000000..4db0175 --- /dev/null +++ b/burrow-gtk/build.rs @@ -0,0 +1,16 @@ +use anyhow::Result; + +fn main() -> Result<()> { + compile_gresources()?; + + Ok(()) +} + +fn compile_gresources() -> Result<()> { + glib_build_tools::compile_resources( + &["data"], + "data/resources.gresource.xml", + "compiled.gresource", + ); + Ok(()) +} diff --git a/burrow-gtk/data/com.hackclub.burrow.desktop.in b/burrow-gtk/data/app.desktop.in.in similarity index 60% rename from burrow-gtk/data/com.hackclub.burrow.desktop.in rename to burrow-gtk/data/app.desktop.in.in index 91c463d..33b9c5b 100644 --- a/burrow-gtk/data/com.hackclub.burrow.desktop.in +++ b/burrow-gtk/data/app.desktop.in.in @@ -1,7 +1,7 @@ [Desktop Entry] -Name=Burrow -Exec=burrow-gtk -Icon=com.hackclub.burrow +Name=@APP_NAME_CAPITALIZED@ +Exec=@APP_NAME@ +Icon=@APP_ID@ Terminal=false Type=Application Categories=GTK;Network diff --git a/burrow-gtk/data/app.gschema.xml.in b/burrow-gtk/data/app.gschema.xml.in new file mode 100644 index 0000000..0541c6f --- /dev/null +++ b/burrow-gtk/data/app.gschema.xml.in @@ -0,0 +1,5 @@ + + + + + diff --git a/burrow-gtk/data/app.metainfo.xml.in b/burrow-gtk/data/app.metainfo.xml.in new file mode 100644 index 0000000..8cc2e59 --- /dev/null +++ b/burrow-gtk/data/app.metainfo.xml.in @@ -0,0 +1,16 @@ + + + @APP_ID@ + CC0 + GPL-3.0-or-later + @APP_NAME_CAPITALIZED@ + @APP_ID@.desktop + + +

No description

+
+ + +

No Summary

+
+
diff --git a/burrow-gtk/data/com.hackclub.burrow.appdata.xml.in b/burrow-gtk/data/com.hackclub.burrow.appdata.xml.in deleted file mode 100644 index 7f8e86b..0000000 --- a/burrow-gtk/data/com.hackclub.burrow.appdata.xml.in +++ /dev/null @@ -1,8 +0,0 @@ - - - com.hackclub.burrow.desktop - GPL-3.0-or-later - -

No description

-
-
diff --git a/burrow-gtk/data/com.hackclub.burrow.gschema.xml b/burrow-gtk/data/com.hackclub.burrow.gschema.xml deleted file mode 100644 index d1bceef..0000000 --- a/burrow-gtk/data/com.hackclub.burrow.gschema.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.burrow.svg b/burrow-gtk/data/icons/hicolor/scalable/apps/burrow-gtk.svg similarity index 100% rename from burrow-gtk/data/icons/hicolor/scalable/apps/com.hackclub.burrow.svg rename to burrow-gtk/data/icons/hicolor/scalable/apps/burrow-gtk.svg diff --git a/burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.burrow-symbolic.svg b/burrow-gtk/data/icons/hicolor/symbolic/apps/burrow-gtk-symbolic.svg similarity index 97% rename from burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.burrow-symbolic.svg rename to burrow-gtk/data/icons/hicolor/symbolic/apps/burrow-gtk-symbolic.svg index 0444828..5352e0a 100644 --- a/burrow-gtk/data/icons/hicolor/symbolic/apps/com.hackclub.burrow-symbolic.svg +++ b/burrow-gtk/data/icons/hicolor/symbolic/apps/burrow-gtk-symbolic.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/burrow-gtk/data/icons/meson.build b/burrow-gtk/data/icons/meson.build deleted file mode 100644 index 86f6480..0000000 --- a/burrow-gtk/data/icons/meson.build +++ /dev/null @@ -1,13 +0,0 @@ -application_id = 'com.hackclub.burrow' - -scalable_dir = join_paths('hicolor', 'scalable', 'apps') -install_data( - join_paths(scalable_dir, ('@0@.svg').format(application_id)), - install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) -) - -symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') -install_data( - join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)), - install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir) -) diff --git a/burrow-gtk/data/meson.build b/burrow-gtk/data/meson.build index fadf18f..2c3ffd8 100644 --- a/burrow-gtk/data/meson.build +++ b/burrow-gtk/data/meson.build @@ -1,39 +1,90 @@ +# app.desktop.in.in +desktop_conf = configuration_data() +desktop_conf.set('APP_ID', app_id) +desktop_conf.set('APP_NAME', app_name) +desktop_conf.set('APP_NAME_CAPITALIZED', app_name_capitalized) + +desktop_file_in = configure_file( + input: 'app.desktop.in.in', + output: '@BASENAME@', + configuration: desktop_conf, +) + desktop_file = i18n.merge_file( - input: 'com.hackclub.burrow.desktop.in', - output: 'com.hackclub.burrow.desktop', - type: 'desktop', - po_dir: '../po', - install: true, - install_dir: join_paths(get_option('datadir'), 'applications') + input: desktop_file_in, + output: app_id + '.desktop', + type: 'desktop', + po_dir: '../po', + install: true, + install_dir: datadir / 'applications', ) -desktop_utils = find_program('desktop-file-validate', required: false) -if desktop_utils.found() - test('Validate desktop file', desktop_utils, args: [desktop_file]) +if desktop_file_validate.found() + test( + 'validate-desktop', + desktop_file_validate, + args: [desktop_file], + ) endif -appstream_file = i18n.merge_file( - input: 'com.hackclub.burrow.appdata.xml.in', - output: 'com.hackclub.burrow.appdata.xml', - po_dir: '../po', - install: true, - install_dir: join_paths(get_option('datadir'), 'appdata') +# app.gschema.xml.in +gschema_conf = configuration_data() +gschema_conf.set('APP_ID', app_id) +gschema_conf.set('APP_NAME', app_name) +gschema_conf.set('APP_IDPATH', app_idpath) +gschema_file = configure_file( + input: 'app.gschema.xml.in', + output: app_id + '.gschema.xml', + configuration: gschema_conf, + install: true, + install_dir: datadir / 'glib-2.0' / 'schemas', +) + +if glib_compile_schemas.found() + test( + 'validate-gschema', + glib_compile_schemas, + args: [ + '--dry-run', + datadir / 'glib-2.0' / 'schemas', + ], + ) +endif + +# app.metainfo.xml.in +appdata_conf = configuration_data() +appdata_conf.set('APP_ID', app_id) +appdata_conf.set('APP_NAME', app_name) +appdata_conf.set('APP_NAME_CAPITALIZED', app_name_capitalized) +appdata_file_in = configure_file( + input: 'app.metainfo.xml.in', + output: '@BASENAME@', + configuration: appdata_conf, +) +appdata_file = i18n.merge_file( + input: appdata_file_in, + output: app_id + '.metainfo.xml', + po_dir: '../po', + install: true, + install_dir: datadir / 'metainfo', ) -appstream_util = find_program('appstream-util', required: false) if appstream_util.found() - test('Validate appstream file', appstream_util, args: ['validate', appstream_file]) + test( + 'validate-appdata', + appstream_util, + args: ['validate', '--nonet', appdata_file], + ) endif -install_data('com.hackclub.burrow.gschema.xml', - install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') +install_data( + 'icons/hicolor/scalable/apps/' + app_name + '.svg', + install_dir: datadir / 'icons' / 'hicolor' / 'scalable' / 'apps', + rename: app_id + '.svg', ) -compile_schemas = find_program('glib-compile-schemas', required: false) -if compile_schemas.found() - test('Validate schema file', - compile_schemas, - args: ['--strict', '--dry-run', meson.current_source_dir()]) -endif - -subdir('icons') +install_data( + 'icons/hicolor/symbolic/apps/' + app_name + '-symbolic.svg', + install_dir: datadir / 'icons' / 'hicolor' / 'symbolic' / 'apps', + rename: app_id + '-symbolic.svg', +) diff --git a/burrow-gtk/data/resources.gresource.xml b/burrow-gtk/data/resources.gresource.xml new file mode 100644 index 0000000..969e77c --- /dev/null +++ b/burrow-gtk/data/resources.gresource.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/burrow-gtk/meson.build b/burrow-gtk/meson.build new file mode 100644 index 0000000..70d8403 --- /dev/null +++ b/burrow-gtk/meson.build @@ -0,0 +1,56 @@ +project( + 'burrow-gtk', + ['rust'], + version: '0.0.1', + meson_version: '>= 1.0', +) + +# Find Cargo +cargo_bin = find_program('cargo') +cargo_env = ['CARGO_HOME=' + meson.project_build_root()] +cargo_opt = ['--manifest-path', meson.project_source_root() / 'Cargo.toml'] +cargo_opt += ['--target-dir', meson.project_build_root() / 'target'] + +# Config +prefix = get_option('prefix') +datadir = prefix / get_option('datadir') +localedir = prefix / get_option('localedir') + +app_name = 'burrow-gtk' +app_name_capitalized = 'Burrow' +base_id = 'com.hackclub.burrow' +app_idpath = '/com/hackclub/' + app_name + '/' +if get_option('buildtype') == 'release' + cargo_opt += ['--release'] + rust_target = 'release' + app_id = base_id +else + rust_target = 'debug' + app_id = base_id + '-' + 'devel' +endif + +# Imports +i18n = import('i18n') +gnome = import('gnome') + +# External Dependencies +dependency('gtk4', version: '>= 4.12') +dependency('libadwaita-1', version: '>= 1.4') + +glib_compile_resources = find_program('glib-compile-resources', required: true) +glib_compile_schemas = find_program('glib-compile-schemas', required: true) +desktop_file_validate = find_program('desktop-file-validate', required: false) +appstream_util = find_program('appstream-util', required: false) +fc_cache = find_program('fc-cache', required: false) + +# Our Sources +subdir('po') +subdir('data') +subdir('src') + +# Gnome Post Install +gnome.post_install( + glib_compile_schemas: true, + gtk_update_icon_cache: true, + update_desktop_database: true, +) diff --git a/burrow-gtk/po/POTFILES b/burrow-gtk/po/POTFILES index d1acb5a..08b570f 100644 --- a/burrow-gtk/po/POTFILES +++ b/burrow-gtk/po/POTFILES @@ -1,4 +1 @@ -data/com.hackclub.Burrow.desktop.in -data/com.hackclub.Burrow.appdata.xml.in -data/com.hackclub.Burrow.gschema.xml -src/window.ui +data/app.desktop.in.in diff --git a/burrow-gtk/po/meson.build b/burrow-gtk/po/meson.build index 4b239a8..597577b 100644 --- a/burrow-gtk/po/meson.build +++ b/burrow-gtk/po/meson.build @@ -1 +1 @@ -i18n.gettext('burrow-gtk', preset: 'glib') +i18n.gettext(app_name, preset: 'glib') diff --git a/burrow-gtk/src/.gitignore b/burrow-gtk/src/.gitignore new file mode 100644 index 0000000..c6bb786 --- /dev/null +++ b/burrow-gtk/src/.gitignore @@ -0,0 +1 @@ +config.rs diff --git a/burrow-gtk/src/components/app.rs b/burrow-gtk/src/components/app.rs new file mode 100644 index 0000000..b42b718 --- /dev/null +++ b/burrow-gtk/src/components/app.rs @@ -0,0 +1,136 @@ +use super::*; +use anyhow::Context; +use std::time::Duration; + +const RECONNECT_POLL_TIME: Duration = Duration::from_secs(5); + +pub struct App { + daemon_client: Arc>>, + _settings_screen: Controller, + switch_screen: AsyncController, +} + +#[derive(Debug)] +pub enum AppMsg { + None, + PostInit, +} + +impl App { + pub fn run() { + let app = RelmApp::new(config::ID); + Self::setup_gresources().unwrap(); + Self::setup_i18n().unwrap(); + + app.run_async::(()); + } + + fn setup_i18n() -> Result<()> { + gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, ""); + gettextrs::bindtextdomain(config::GETTEXT_PACKAGE, config::LOCALEDIR)?; + gettextrs::bind_textdomain_codeset(config::GETTEXT_PACKAGE, "UTF-8")?; + gettextrs::textdomain(config::GETTEXT_PACKAGE)?; + Ok(()) + } + + fn setup_gresources() -> Result<()> { + gtk::gio::resources_register_include!("compiled.gresource") + .context("Failed to register and include compiled gresource.") + } +} + +#[relm4::component(pub, async)] +impl AsyncComponent for App { + type Init = (); + type Input = AppMsg; + type Output = (); + type CommandOutput = (); + + view! { + adw::Window { + set_title: Some("Burrow"), + set_default_size: (640, 480), + } + } + + async fn init( + _: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let daemon_client = Arc::new(Mutex::new(DaemonClient::new().await.ok())); + + let switch_screen = switch_screen::SwitchScreen::builder() + .launch(switch_screen::SwitchScreenInit { + daemon_client: Arc::clone(&daemon_client), + }) + .forward(sender.input_sender(), |_| AppMsg::None); + + let settings_screen = settings_screen::SettingsScreen::builder() + .launch(settings_screen::SettingsScreenInit { + daemon_client: Arc::clone(&daemon_client), + }) + .forward(sender.input_sender(), |_| AppMsg::None); + + let widgets = view_output!(); + + let view_stack = adw::ViewStack::new(); + view_stack.add_titled(switch_screen.widget(), None, "Switch"); + view_stack.add_titled(settings_screen.widget(), None, "Settings"); + + let view_switcher_bar = adw::ViewSwitcherBar::builder().stack(&view_stack).build(); + view_switcher_bar.set_reveal(true); + + let toolbar = adw::ToolbarView::new(); + toolbar.add_top_bar( + &adw::HeaderBar::builder() + .title_widget(>k::Label::new(Some("Burrow"))) + .build(), + ); + toolbar.add_bottom_bar(&view_switcher_bar); + toolbar.set_content(Some(&view_stack)); + + root.set_content(Some(&toolbar)); + + sender.input(AppMsg::PostInit); + + let model = App { + daemon_client, + switch_screen, + _settings_screen: settings_screen, + }; + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + _msg: Self::Input, + _sender: AsyncComponentSender, + _root: &Self::Root, + ) { + loop { + tokio::time::sleep(RECONNECT_POLL_TIME).await; + { + let mut daemon_client = self.daemon_client.lock().await; + let mut disconnected_daemon_client = false; + + if let Some(daemon_client) = daemon_client.as_mut() { + if let Err(_e) = daemon_client.send_command(DaemonCommand::ServerInfo).await { + disconnected_daemon_client = true; + self.switch_screen + .emit(switch_screen::SwitchScreenMsg::DaemonDisconnect); + } + } + + if disconnected_daemon_client || daemon_client.is_none() { + *daemon_client = DaemonClient::new().await.ok(); + if daemon_client.is_some() { + self.switch_screen + .emit(switch_screen::SwitchScreenMsg::DaemonReconnect); + } + } + } + } + } +} diff --git a/burrow-gtk/src/components/mod.rs b/burrow-gtk/src/components/mod.rs new file mode 100644 index 0000000..b1cc938 --- /dev/null +++ b/burrow-gtk/src/components/mod.rs @@ -0,0 +1,20 @@ +use super::*; +use adw::prelude::*; +use burrow::{DaemonClient, DaemonCommand, DaemonResponseData}; +use gtk::Align; +use relm4::{ + component::{ + AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncComponentSender, + AsyncController, + }, + prelude::*, +}; +use std::sync::Arc; +use tokio::sync::Mutex; + +mod app; +mod settings; +mod settings_screen; +mod switch_screen; + +pub use app::*; diff --git a/burrow-gtk/src/components/settings/diag_group.rs b/burrow-gtk/src/components/settings/diag_group.rs new file mode 100644 index 0000000..be542cd --- /dev/null +++ b/burrow-gtk/src/components/settings/diag_group.rs @@ -0,0 +1,126 @@ +use super::*; +use diag::{StatusTernary, SystemSetup}; + +#[derive(Debug)] +pub struct DiagGroup { + daemon_client: Arc>>, + + init_system: SystemSetup, + service_installed: StatusTernary, + socket_installed: StatusTernary, + socket_enabled: StatusTernary, + daemon_running: bool, +} + +pub struct DiagGroupInit { + pub daemon_client: Arc>>, +} + +impl DiagGroup { + async fn new(daemon_client: Arc>>) -> Result { + let setup = SystemSetup::new(); + let daemon_running = daemon_client.lock().await.is_some(); + + Ok(Self { + service_installed: setup.is_service_installed()?, + socket_installed: setup.is_socket_installed()?, + socket_enabled: setup.is_socket_enabled()?, + daemon_running, + init_system: setup, + daemon_client, + }) + } +} + +#[derive(Debug)] +pub enum DiagGroupMsg { + Refresh, +} + +#[relm4::component(pub, async)] +impl AsyncComponent for DiagGroup { + type Init = DiagGroupInit; + type Input = DiagGroupMsg; + type Output = (); + type CommandOutput = (); + + view! { + #[name(group)] + adw::PreferencesGroup { + set_title: "Diagnose", + set_description: Some("Diagnose Burrow"), + + adw::ActionRow { + #[watch] + set_title: &format!("Init System: {}", model.init_system) + }, + adw::ActionRow { + #[watch] + set_title: &format!( + "Service installed: {}", + status_ternary_to_str(model.service_installed) + ) + }, + adw::ActionRow { + #[watch] + set_title: &format!( + "Socket installed: {}", + status_ternary_to_str(model.socket_installed) + ) + }, + adw::ActionRow { + #[watch] + set_title: &format!( + "Socket enabled: {}", + status_ternary_to_str(model.socket_enabled) + ) + }, + adw::ActionRow { + #[watch] + set_title: &format!( + "Daemon running: {}", + if model.daemon_running { "Yes" } else { "No" } + ) + }, + gtk::Button { + set_label: "Refresh", + connect_clicked => DiagGroupMsg::Refresh + } + } + } + + async fn init( + init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + // Should be impossible to panic here + let model = DiagGroup::new(init.daemon_client).await.unwrap(); + + let widgets = view_output!(); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + msg: Self::Input, + _sender: AsyncComponentSender, + _root: &Self::Root, + ) { + match msg { + DiagGroupMsg::Refresh => { + // Should be impossible to panic here + *self = Self::new(Arc::clone(&self.daemon_client)).await.unwrap(); + } + } + } +} + +fn status_ternary_to_str(status: StatusTernary) -> &'static str { + match status { + StatusTernary::True => "Yes", + StatusTernary::False => "No", + StatusTernary::NA => "N/A", + } +} diff --git a/burrow-gtk/src/components/settings/mod.rs b/burrow-gtk/src/components/settings/mod.rs new file mode 100644 index 0000000..53f46d4 --- /dev/null +++ b/burrow-gtk/src/components/settings/mod.rs @@ -0,0 +1,5 @@ +use super::*; + +mod diag_group; + +pub use diag_group::{DiagGroup, DiagGroupInit}; diff --git a/burrow-gtk/src/components/settings_screen.rs b/burrow-gtk/src/components/settings_screen.rs new file mode 100644 index 0000000..778eb84 --- /dev/null +++ b/burrow-gtk/src/components/settings_screen.rs @@ -0,0 +1,44 @@ +use super::*; + +pub struct SettingsScreen { + _diag_group: AsyncController, +} + +pub struct SettingsScreenInit { + pub daemon_client: Arc>>, +} + +#[relm4::component(pub)] +impl SimpleComponent for SettingsScreen { + type Init = SettingsScreenInit; + type Input = (); + type Output = (); + + view! { + #[name(preferences)] + adw::PreferencesPage {} + } + + fn init( + init: Self::Init, + root: Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let diag_group = settings::DiagGroup::builder() + .launch(settings::DiagGroupInit { + daemon_client: Arc::clone(&init.daemon_client), + }) + .forward(sender.input_sender(), |_| ()); + + let widgets = view_output!(); + widgets.preferences.add(diag_group.widget()); + + let model = SettingsScreen { + _diag_group: diag_group, + }; + + ComponentParts { model, widgets } + } + + fn update(&mut self, _: Self::Input, _sender: ComponentSender) {} +} diff --git a/burrow-gtk/src/components/switch_screen.rs b/burrow-gtk/src/components/switch_screen.rs new file mode 100644 index 0000000..a296c09 --- /dev/null +++ b/burrow-gtk/src/components/switch_screen.rs @@ -0,0 +1,158 @@ +use super::*; + +pub struct SwitchScreen { + daemon_client: Arc>>, + switch: gtk::Switch, + switch_screen: gtk::Box, + disconnected_banner: adw::Banner, +} + +pub struct SwitchScreenInit { + pub daemon_client: Arc>>, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum SwitchScreenMsg { + DaemonReconnect, + DaemonDisconnect, + Start, + Stop, +} + +#[relm4::component(pub, async)] +impl AsyncComponent for SwitchScreen { + type Init = SwitchScreenInit; + type Input = SwitchScreenMsg; + type Output = (); + type CommandOutput = (); + + view! { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: Align::BaselineFill, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + set_margin_all: 5, + set_valign: Align::Start, + + #[name(setup_banner)] + adw::Banner { + set_title: "Burrow is not running!", + }, + }, + + #[name(switch_screen)] + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + set_margin_all: 5, + set_valign: Align::Center, + set_vexpand: true, + + gtk::Label { + set_label: "Burrow Switch", + }, + + #[name(switch)] + gtk::Switch { + set_halign: Align::Center, + set_hexpand: false, + set_vexpand: false, + connect_active_notify => move |switch| + sender.input(if switch.is_active() { SwitchScreenMsg::Start } else { SwitchScreenMsg::Stop }) + }, + } + } + } + + async fn init( + init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let mut initial_switch_status = false; + let mut initial_daemon_server_down = false; + + if let Some(daemon_client) = init.daemon_client.lock().await.as_mut() { + if let Ok(res) = daemon_client + .send_command(DaemonCommand::ServerInfo) + .await + .as_ref() + { + initial_switch_status = match res.result.as_ref() { + Ok(DaemonResponseData::None) => false, + Ok(DaemonResponseData::ServerInfo(_)) => true, + _ => false, + }; + } else { + initial_daemon_server_down = true; + } + } else { + initial_daemon_server_down = true; + } + + let widgets = view_output!(); + + widgets.switch.set_active(initial_switch_status); + + if initial_daemon_server_down { + *init.daemon_client.lock().await = None; + widgets.switch.set_active(false); + widgets.switch_screen.set_sensitive(false); + widgets.setup_banner.set_revealed(true); + } + + let model = SwitchScreen { + daemon_client: init.daemon_client, + switch: widgets.switch.clone(), + switch_screen: widgets.switch_screen.clone(), + disconnected_banner: widgets.setup_banner.clone(), + }; + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + msg: Self::Input, + _: AsyncComponentSender, + _root: &Self::Root, + ) { + let mut disconnected_daemon_client = false; + + if let Some(daemon_client) = self.daemon_client.lock().await.as_mut() { + match msg { + Self::Input::Start => { + if let Err(_e) = daemon_client + .send_command(DaemonCommand::Start(Default::default())) + .await + { + disconnected_daemon_client = true; + } + } + Self::Input::Stop => { + if let Err(_e) = daemon_client.send_command(DaemonCommand::Stop).await { + disconnected_daemon_client = true; + } + } + _ => {} + } + } else { + disconnected_daemon_client = true; + } + + if msg == Self::Input::DaemonReconnect { + self.disconnected_banner.set_revealed(false); + self.switch_screen.set_sensitive(true); + } + + if disconnected_daemon_client || msg == Self::Input::DaemonDisconnect { + *self.daemon_client.lock().await = None; + self.switch.set_active(false); + self.switch_screen.set_sensitive(false); + self.disconnected_banner.set_revealed(true); + } + } +} diff --git a/burrow-gtk/src/config.rs.in b/burrow-gtk/src/config.rs.in new file mode 100644 index 0000000..7da2f3f --- /dev/null +++ b/burrow-gtk/src/config.rs.in @@ -0,0 +1,8 @@ +#[allow(unused)] +pub const ID: &str = @ID@; +#[allow(unused)] +pub const VERSION: &str = @VERSION@; +#[allow(unused)] +pub const LOCALEDIR: &str = @LOCALEDIR@; +#[allow(unused)] +pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; diff --git a/burrow-gtk/src/diag.rs b/burrow-gtk/src/diag.rs new file mode 100644 index 0000000..348293e --- /dev/null +++ b/burrow-gtk/src/diag.rs @@ -0,0 +1,80 @@ +use super::*; +use std::{fmt::Display, fs, process::Command}; + +const SYSTEMD_SOCKET_LOC: &str = "/etc/systemd/system/burrow.socket"; +const SYSTEMD_SERVICE_LOC: &str = "/etc/systemd/system/burrow.service"; + +// I don't like this type very much. +#[derive(Debug, Clone, Copy)] +pub enum StatusTernary { + True, + False, + NA, +} + +// Realistically, we may not explicitly "support" non-systemd platforms which would simply this +// code greatly. +// Along with replacing [`StatusTernary`] with good old [`bool`]. +#[derive(Debug, Clone, Copy)] +pub enum SystemSetup { + Systemd, + Other, +} + +impl SystemSetup { + pub fn new() -> Self { + if Command::new("systemctl").arg("--version").output().is_ok() { + SystemSetup::Systemd + } else { + SystemSetup::Other + } + } + + pub fn is_service_installed(&self) -> Result { + match self { + SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into()), + SystemSetup::Other => Ok(StatusTernary::NA), + } + } + + pub fn is_socket_installed(&self) -> Result { + match self { + SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into()), + SystemSetup::Other => Ok(StatusTernary::NA), + } + } + + pub fn is_socket_enabled(&self) -> Result { + match self { + SystemSetup::Systemd => { + let output = Command::new("systemctl") + .arg("is-enabled") + .arg("burrow.socket") + .output()? + .stdout; + let output = String::from_utf8(output)?; + Ok((output == "enabled\n").into()) + } + SystemSetup::Other => Ok(StatusTernary::NA), + } + } +} + +impl From for StatusTernary { + fn from(value: bool) -> Self { + if value { + StatusTernary::True + } else { + StatusTernary::False + } + } +} + +impl Display for SystemSetup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + SystemSetup::Systemd => "Systemd", + SystemSetup::Other => "Other", + }) + } +} diff --git a/burrow-gtk/src/main.rs b/burrow-gtk/src/main.rs index d91b6c2..6f91e2a 100644 --- a/burrow-gtk/src/main.rs +++ b/burrow-gtk/src/main.rs @@ -1,87 +1,11 @@ -use adw::prelude::*; -use burrow::{DaemonClient, DaemonCommand, DaemonStartOptions}; -use gtk::Align; -use relm4::{ - component::{AsyncComponent, AsyncComponentParts, AsyncComponentSender}, - prelude::*, -}; +use anyhow::Result; -struct App {} +pub mod components; +mod diag; -#[derive(Debug)] -enum Msg { - Start, - Stop, -} - -#[relm4::component(async)] -impl AsyncComponent for App { - type Init = (); - type Input = Msg; - type Output = (); - type CommandOutput = (); - - view! { - adw::Window { - set_title: Some("Simple app"), - set_default_size: (640, 480), - - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 5, - set_margin_all: 5, - set_valign: Align::Center, - - gtk::Label { - set_label: "Burrow GTK Switch", - }, - - gtk::Switch { - set_halign: Align::Center, - set_hexpand: false, - set_vexpand: false, - connect_active_notify => move |switch| - sender.input(if switch.is_active() { Msg::Start } else { Msg::Stop }) - }, - } - } - } - - async fn init( - _: Self::Init, - root: Self::Root, - sender: AsyncComponentSender, - ) -> AsyncComponentParts { - let model = App {}; - - let widgets = view_output!(); - - AsyncComponentParts { model, widgets } - } - - async fn update( - &mut self, - msg: Self::Input, - _sender: AsyncComponentSender, - _root: &Self::Root, - ) { - match msg { - Msg::Start => { - let mut client = DaemonClient::new().await.unwrap(); - client - .send_command(DaemonCommand::Start(DaemonStartOptions::default())) - .await - .unwrap(); - } - Msg::Stop => { - let mut client = DaemonClient::new().await.unwrap(); - client.send_command(DaemonCommand::Stop).await.unwrap(); - } - } - } -} +// Generated using meson +mod config; fn main() { - let app = RelmApp::new("com.hackclub.burrow"); - app.run_async::(()); + components::App::run(); } diff --git a/burrow-gtk/src/meson.build b/burrow-gtk/src/meson.build new file mode 100644 index 0000000..ed77771 --- /dev/null +++ b/burrow-gtk/src/meson.build @@ -0,0 +1,34 @@ +# config.rs.in +global_conf = configuration_data() +global_conf.set_quoted('ID', app_id) +global_conf.set_quoted('VERSION', meson.project_version()) +global_conf.set_quoted('LOCALEDIR', localedir) +global_conf.set_quoted('GETTEXT_PACKAGE', app_name) +config = configure_file( + input: 'config.rs.in', + output: 'config.rs', + configuration: global_conf, +) + +run_command( + 'cp', + meson.project_build_root() / 'src' / 'config.rs', + meson.project_source_root() / 'src', + check: true, +) + +# Cargo Build +cargo_build = custom_target( + 'cargo-build', + build_by_default: true, + build_always_stale: true, + output: meson.project_name(), + console: true, + install: true, + install_dir: get_option('bindir'), + command: [ + 'env', cargo_env, + cargo_bin, 'build', + cargo_opt, '&&', 'cp', 'target' / rust_target / meson.project_name(), '@OUTPUT@', + ] + ) From 7d8958e0e75d84b9a3711d2756ddeaff3b90ebd5 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sun, 21 Jan 2024 03:44:58 +0800 Subject: [PATCH 098/128] Add handling for connection expiration --- burrow/src/wireguard/pcb.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/burrow/src/wireguard/pcb.rs b/burrow/src/wireguard/pcb.rs index c6ebaa6..dfa6d2a 100755 --- a/burrow/src/wireguard/pcb.rs +++ b/burrow/src/wireguard/pcb.rs @@ -5,7 +5,9 @@ use fehler::throws; use ip_network::IpNetwork; use rand::random; use tokio::{net::UdpSocket, sync::RwLock, task::JoinHandle}; +use tokio::io::AsyncWrite; use tun::tokio::TunInterface; +use crate::wireguard::noise::errors::WireGuardError; use super::{ noise::{TunnResult, Tunnel}, @@ -62,6 +64,7 @@ impl PeerPcb { tracing::debug!("{}: waiting for packet", rid); let guard = self.socket.read().await; let Some(socket) = guard.as_ref() else { + self.open_if_closed().await?; continue }; let mut res_buf = [0; 1500]; @@ -136,6 +139,10 @@ impl PeerPcb { pub async fn update_timers(&self, dst: &mut [u8]) -> Result<(), Error> { match self.tunnel.write().await.update_timers(dst) { TunnResult::Done => {} + TunnResult::Err(WireGuardError::ConnectionExpired) => { + tracing::debug!("Connection expired, closing socket"); + self.socket.write().await.take(); + } TunnResult::Err(e) => { tracing::error!(message = "Update timers error", error = ?e) } From 0b46ac57b7ee49a022546e54f564b0bbdd5d28ee Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Tue, 23 Jan 2024 16:54:11 +0800 Subject: [PATCH 099/128] Add tokio console and more debugging feats --- Cargo.lock | 318 +++++++++++++++++++++++++++++++++++- Makefile | 9 +- burrow/Cargo.toml | 6 +- burrow/src/main.rs | 28 ++-- burrow/src/wireguard/pcb.rs | 9 +- 5 files changed, 352 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc8d3b3..7399bd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,12 +121,90 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "async-trait" +version = "0.1.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.68" @@ -247,6 +325,7 @@ dependencies = [ "caps", "chacha20poly1305", "clap", + "console-subscriber", "env_logger", "etherparse", "fehler", @@ -459,6 +538,43 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "console-api" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -500,14 +616,20 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.16" +name = "crossbeam-channel" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" dependencies = [ - "cfg-if", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -870,6 +992,19 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64", + "byteorder", + "flate2", + "nom", + "num-traits", +] + [[package]] name = "heck" version = "0.4.1" @@ -961,6 +1096,18 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1079,6 +1226,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1192,6 +1348,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.5.0" @@ -1314,6 +1476,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1459,6 +1630,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1519,6 +1710,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.33" @@ -1698,6 +1921,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.13" @@ -1967,6 +2196,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "tempfile" version = "3.6.0" @@ -2066,9 +2301,20 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "2.1.0" @@ -2090,6 +2336,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -2104,6 +2361,59 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" diff --git a/Makefile b/Makefile index e8e5687..18b4b27 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ tun_num := $(shell ifconfig | awk -F 'utun|[: ]' '/utun[0-9]/ {print $$2}' | tail -n 1) +cargo_console := RUST_BACKTRACE=1 RUST_LOG=debug RUSTFLAGS='--cfg tokio_unstable' cargo run --all-features +cargo_norm := RUST_BACKTRACE=1 RUST_LOG=debug cargo run check: @cargo check @@ -6,11 +8,14 @@ check: build: @cargo run build +daemon-console: + @$(cargo_console) daemon + daemon: - @RUST_BACKTRACE=1 RUST_LOG=debug cargo run daemon + @$(cargo_norm) daemon start: - @RUST_BACKTRACE=1 RUST_LOG=debug cargo run start + @$(cargo_norm) start test-dns: @sudo route delete 8.8.8.8 diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index f263cc6..0fb2443 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["lib", "staticlib"] [dependencies] anyhow = "1.0" -tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util", "rt-multi-thread", "time"] } +tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util", "rt-multi-thread", "time", "tracing"] } tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } clap = { version = "4.3.2", features = ["derive"] } tracing = "0.1" @@ -40,6 +40,7 @@ async-channel = "2.1.1" schemars = "0.8" futures = "0.3.28" uuid = { version = "1.6.1", features = ["v4"] } +console-subscriber = { version = "0.2.0" , optional = true} [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5.5" @@ -60,3 +61,6 @@ assets = [ ] post_install_script = "../package/rpm/post_install" pre_uninstall_script = "../package/rpm/pre_uninstall" + +[features] +tokio-console = ["dep:console-subscriber"] \ No newline at end of file diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 9a692d6..18eaf77 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -98,19 +98,27 @@ async fn initialize_tracing() -> Result<()> { #[cfg(any(target_os = "linux", target_vendor = "apple"))] { - let maybe_layer = system_log()?; - if let Some(layer) = maybe_layer { - let logger = layer.with_subscriber( - FmtSubscriber::builder() - .with_line_number(true) - .with_env_filter(EnvFilter::from_default_env()) - .finish(), + let maybe_layer = system_log()?; + if let Some(layer) = maybe_layer { + let registry = tracing_subscriber::registry() + .with(layer) + .with(tracing_subscriber::fmt::layer() + .with_line_number(true) + .with_filter(EnvFilter::from_default_env()) ); - tracing::subscriber::set_global_default(logger) - .context("Failed to set the global tracing subscriber")?; + + #[cfg(feature = "tokio-console")] + let registry = registry.with( + console_subscriber::spawn() + .with_filter(EnvFilter::from_default_env() + .add_directive("tokio=trace".parse()?) + .add_directive("runtime=trace".parse()?) + ) + ); + + tracing::subscriber::set_global_default(registry).context("Failed to set the global tracing subscriber")?; } } - Ok(()) } diff --git a/burrow/src/wireguard/pcb.rs b/burrow/src/wireguard/pcb.rs index dfa6d2a..69b290f 100755 --- a/burrow/src/wireguard/pcb.rs +++ b/burrow/src/wireguard/pcb.rs @@ -115,9 +115,12 @@ impl PeerPcb { } pub async fn send(&self, src: &[u8]) -> Result<(), Error> { + tracing::debug!("Sending packet: {:?}", src); let mut dst_buf = [0u8; 3000]; match self.tunnel.write().await.encapsulate(src, &mut dst_buf[..]) { - TunnResult::Done => {} + TunnResult::Done => { + tracing::debug!("Encapsulate done"); + } TunnResult::Err(e) => { tracing::error!(message = "Encapsulate error", error = ?e) } @@ -137,6 +140,7 @@ impl PeerPcb { } pub async fn update_timers(&self, dst: &mut [u8]) -> Result<(), Error> { + tracing::debug!("update timers called..."); match self.tunnel.write().await.update_timers(dst) { TunnResult::Done => {} TunnResult::Err(WireGuardError::ConnectionExpired) => { @@ -147,6 +151,7 @@ impl PeerPcb { tracing::error!(message = "Update timers error", error = ?e) } TunnResult::WriteToNetwork(packet) => { + tracing::debug!("Sending Packet for timer update: {:?}", packet); self.open_if_closed().await?; let handle = self.socket.read().await; let Some(socket) = handle.as_ref() else { @@ -154,9 +159,11 @@ impl PeerPcb { return Ok(()) }; socket.send(packet).await?; + tracing::debug!("Sent Packet for timer update"); } _ => panic!("Unexpected result from update_timers"), }; + tracing::debug!("update timers exit..."); Ok(()) } From 079c4f676db3c0a375b80560247cee6af5b87247 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Thu, 25 Jan 2024 01:07:40 +0800 Subject: [PATCH 100/128] Update locking --- burrow/src/wireguard/pcb.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/burrow/src/wireguard/pcb.rs b/burrow/src/wireguard/pcb.rs index 69b290f..13a2df1 100755 --- a/burrow/src/wireguard/pcb.rs +++ b/burrow/src/wireguard/pcb.rs @@ -1,10 +1,11 @@ use std::{net::SocketAddr, sync::Arc}; +use std::time::Duration; use anyhow::{Error, Result}; use fehler::throws; use ip_network::IpNetwork; use rand::random; -use tokio::{net::UdpSocket, sync::RwLock, task::JoinHandle}; +use tokio::{net::UdpSocket, sync::RwLock, task::JoinHandle, time::timeout}; use tokio::io::AsyncWrite; use tun::tokio::TunInterface; use crate::wireguard::noise::errors::WireGuardError; @@ -94,7 +95,7 @@ impl PeerPcb { TunnResult::WriteToNetwork(packet) => { tracing::debug!("WriteToNetwork: {:?}", packet); self.open_if_closed().await?; - socket.send(packet).await?; + self.socket.read().await.as_ref().unwrap().send(packet).await?; tracing::debug!("WriteToNetwork done"); res_dat = &[]; continue @@ -140,12 +141,9 @@ impl PeerPcb { } pub async fn update_timers(&self, dst: &mut [u8]) -> Result<(), Error> { - tracing::debug!("update timers called..."); match self.tunnel.write().await.update_timers(dst) { TunnResult::Done => {} TunnResult::Err(WireGuardError::ConnectionExpired) => { - tracing::debug!("Connection expired, closing socket"); - self.socket.write().await.take(); } TunnResult::Err(e) => { tracing::error!(message = "Update timers error", error = ?e) @@ -163,7 +161,6 @@ impl PeerPcb { } _ => panic!("Unexpected result from update_timers"), }; - tracing::debug!("update timers exit..."); Ok(()) } From 9e03c9680cfa9818b46d85f01c4506837d4de452 Mon Sep 17 00:00:00 2001 From: Jasper Mayone Date: Fri, 26 Jan 2024 01:33:26 -0500 Subject: [PATCH 101/128] Added Xcode Path Switching to Getting Started --- docs/GETTING_STARTED.md | 102 ++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index e43680d..764c219 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -1,6 +1,5 @@ # Getting Started - ## Dependencies Before you can start working on Burrow, you'll need to install some dependencies. They are different for each platform: @@ -8,60 +7,79 @@ Before you can start working on Burrow, you'll need to install some dependencies
Linux - 1. Install **rustup** using the instructions on the [website](https://rustup.rs/): - ```bash - $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - ``` +1. Install **rustup** using the instructions on the [website](https://rustup.rs/): - 2. Install **Visual Studio Code** from the [website](https://code.visualstudio.com/#alt-downloads), [Snap Store](https://snapcraft.io/code), or your package manager of choice. +```bash +$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +2. Install **Visual Studio Code** from the [website](https://code.visualstudio.com/#alt-downloads), [Snap Store](https://snapcraft.io/code), or your package manager of choice.
macOS - 1. Install **rustup** using the instructions on the [website](https://rustup.rs/): - ```bash - $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - ``` +1. Install **rustup** using the instructions on the [website](https://rustup.rs/): - 2. Download and install **Visual Studio Code** from the [website](https://code.visualstudio.com/#alt-downloads), or by using brew: - ``` - brew install --cask visual-studio-code - ``` +```bash +$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` - 3. Download and Install **Xcode** from the [App Store](https://apps.apple.com/us/app/xcode/id497799835) or the [Apple Developer](https://developer.apple.com/downloads) website. +2. Download and install **Visual Studio Code** from the [website](https://code.visualstudio.com/#alt-downloads), or by using brew: + +``` +brew install --cask visual-studio-code +``` + +3. Download and Install **Xcode** from the [App Store](https://apps.apple.com/us/app/xcode/id497799835) or the [Apple Developer](https://developer.apple.com/downloads) website. + +4. Make sure the _"current"_ version of Xcode matches the one you are using: + +``` +$ xcode-select -p +``` + +If the output is not the version of Xcode you just installed, run the following command to switch to the new version: + +``` +$ sudo xcode-select -s {PATH_TO_XCODE} +```
-
Windows - 1. Download **Visual Studio** community edition from the [website](https://visualstudio.microsoft.com/vs/). Install the components for "Desktop Development with C++" +1. Download **Visual Studio** community edition from the [website](https://visualstudio.microsoft.com/vs/). Install the components for "Desktop Development with C++" - 2. Install [**Visual Studio Code**](https://apps.microsoft.com/store/detail/visual-studio-code/XP9KHM4BK9FZ7Q), [**PowerShell**](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D) and [**Windows Terminal**](https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701) from the Microsoft Store +2. Install [**Visual Studio Code**](https://apps.microsoft.com/store/detail/visual-studio-code/XP9KHM4BK9FZ7Q), [**PowerShell**](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D) and [**Windows Terminal**](https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701) from the Microsoft Store - 3. Open Windows Terminal and use [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) to install **git**, **LLVM** and **rustup**: - ```posh - winget install Git.Git - winget install LLVM.LLVM - winget install Rustlang.Rustup - ``` +3. Open Windows Terminal and use [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) to install **git**, **LLVM** and **rustup**: + +```posh +winget install Git.Git +winget install LLVM.LLVM +winget install Rustlang.Rustup +``` + +4. Install Rust using rustup: + +```posh +rustup toolchain install stable-msvc +``` - 4. Install Rust using rustup: - ```posh - rustup toolchain install stable-msvc - ```
## Building 1. Clone the repository: + ``` git clone git@github.com:hackclub/burrow.git ``` 2. Open the `burrow` folder in Visual Studio Code: + ``` code burrow ``` @@ -74,37 +92,39 @@ code burrow ## Running -
Command Line - You can run burrow on the command line with cargo: +You can run burrow on the command line with cargo: - ``` - cargo run - ``` +``` +cargo run +``` + +Cargo will ask for your password because burrow needs permission in order to create a tunnel. - Cargo will ask for your password because burrow needs permission in order to create a tunnel.
Visual Studio Code - You can debug the Rust program inside of Visual Studio using the **Run and Debug** tab. +You can debug the Rust program inside of Visual Studio using the **Run and Debug** tab. + +**_This does not work fully yet_**. Visual Studio Code does not have a way to debug programs with administrative privileges. - **_This does not work fully yet_**. Visual Studio Code does not have a way to debug programs with administrative privileges.
iOS or macOS - You can run the Burrow app on iOS or macOS using **Xcode**. +You can run the Burrow app on iOS or macOS using **Xcode**. - You will need to be logged in with your Apple ID, and it should be a part of **The Hack Foundation** team: +You will need to be logged in with your Apple ID, and it should be a part of **The Hack Foundation** team: - + - If your Apple ID is not a part of The Hack Foundation team, ask the Slack channel for assistance. +If your Apple ID is not a part of The Hack Foundation team, ask the Slack channel for assistance. + +You should now be able to run the app by opening `Apple/Burrow.xcodeproj` in Xcode, selecting the **App** scheme and clicking **Run**. - You should now be able to run the app by opening `Apple/Burrow.xcodeproj` in Xcode, selecting the **App** scheme and clicking **Run**.
From 7cc1f3119e8d774bee8df4ff8e4ddc0d62a8d8f5 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sun, 21 Jan 2024 16:18:13 -0800 Subject: [PATCH 102/128] Simplified process startup on macOS and Linux --- .vscode/settings.json | 3 +- Apple/App/Menu/MenuView.swift | 6 +- Apple/App/Tunnel.swift | 53 +- Apple/Burrow.xcodeproj/project.pbxproj | 183 ++- Apple/Configuration/App.xcconfig | 2 +- Apple/Configuration/Compiler.xcconfig | 2 +- Apple/Configuration/Extension.xcconfig | 2 + Apple/NetworkExtension/BurrowIpc.swift | 133 --- Apple/NetworkExtension/Client.swift | 60 + Apple/NetworkExtension/DataTypes.swift | 14 +- .../NetworkExtension/NWConnection+Async.swift | 32 + .../NetworkExtension-macOS.entitlements | 10 +- .../NetworkExtension.xcconfig | 2 +- .../NewlineProtocolFramer.swift | 54 + .../PacketTunnelProvider.swift | 61 +- Apple/NetworkExtension/libburrow/libburrow.h | 4 +- Apple/Shared/Constants.swift | 22 + Apple/Shared/Constants/Constants.h | 11 + Apple/Shared/Constants/module.modulemap | 4 + Apple/Shared/Logging.swift | 19 + Apple/Shared/Shared.xcconfig | 5 + Cargo.lock | 1040 ++++++++--------- burrow/Cargo.toml | 54 +- burrow/src/apple.rs | 13 - burrow/src/daemon/apple.rs | 55 + burrow/src/daemon/mod.rs | 53 +- burrow/src/daemon/net/apple.rs | 32 - burrow/src/daemon/net/mod.rs | 18 +- burrow/src/daemon/net/systemd.rs | 33 - burrow/src/daemon/net/unix.rs | 273 +++-- burrow/src/daemon/net/windows.rs | 27 +- burrow/src/daemon/response.rs | 4 +- burrow/src/lib.rs | 10 +- burrow/src/main.rs | 106 +- burrow/src/tracing.rs | 63 + burrow/src/wireguard/iface.rs | 10 +- burrow/src/wireguard/mod.rs | 1 - burrow/src/wireguard/noise/mod.rs | 8 +- burrow/src/wireguard/pcb.rs | 17 +- tun/src/unix/apple/mod.rs | 1 - 40 files changed, 1343 insertions(+), 1157 deletions(-) delete mode 100644 Apple/NetworkExtension/BurrowIpc.swift create mode 100644 Apple/NetworkExtension/Client.swift create mode 100644 Apple/NetworkExtension/NWConnection+Async.swift create mode 100644 Apple/NetworkExtension/NewlineProtocolFramer.swift create mode 100644 Apple/Shared/Constants.swift create mode 100644 Apple/Shared/Constants/Constants.h create mode 100644 Apple/Shared/Constants/module.modulemap create mode 100644 Apple/Shared/Logging.swift create mode 100644 Apple/Shared/Shared.xcconfig delete mode 100644 burrow/src/apple.rs create mode 100644 burrow/src/daemon/apple.rs delete mode 100644 burrow/src/daemon/net/apple.rs delete mode 100644 burrow/src/daemon/net/systemd.rs create mode 100644 burrow/src/tracing.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 5fbfc5c..3c714be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,6 @@ ], "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer", - } + }, + "rust-analyzer.inlayHints.typeHints.enable": false } \ No newline at end of file diff --git a/Apple/App/Menu/MenuView.swift b/Apple/App/Menu/MenuView.swift index 56a7494..eab8da2 100644 --- a/Apple/App/Menu/MenuView.swift +++ b/Apple/App/Menu/MenuView.swift @@ -39,10 +39,10 @@ extension Tunnel { var isOn: Binding { Binding { switch self.status { - case .unknown, .disabled, .disconnecting, .disconnected, .invalid, .permissionRequired, .configurationReadWriteFailed: - return false case .connecting, .reasserting, .connected: - return true + true + default: + false } } set: { newValue in switch (self.status, newValue) { diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift index 0421a0c..5542170 100644 --- a/Apple/App/Tunnel.swift +++ b/Apple/App/Tunnel.swift @@ -1,11 +1,13 @@ -import Combine +import BurrowShared import NetworkExtension import SwiftUI -@Observable class Tunnel { +@Observable +class Tunnel { private(set) var status: Status = .unknown private var error: NEVPNError? + private let logger = Logger.logger(for: Tunnel.self) private let bundleIdentifier: String private let configure: (NETunnelProviderManager, NETunnelProviderProtocol) -> Void private var tasks: [Task] = [] @@ -49,33 +51,34 @@ import SwiftUI self.bundleIdentifier = bundleIdentifier self.configure = configure - listenForUpdates() - Task { await update() } - } - - private func listenForUpdates() { let center = NotificationCenter.default - let statusTask = Task { - for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { - status = currentStatus - } - } - let configurationTask = Task { + let configurationChanged = Task { for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) { await update() } } - tasks = [statusTask, configurationTask] + let statusChanged = Task { + for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { + await MainActor.run { + status = currentStatus + } + } + } + tasks = [configurationChanged, statusChanged] + + Task { await update() } } private func update() async { do { let updated = try await NETunnelProviderManager.managers - await MainActor.run { managers = updated } + await MainActor.run { + managers = updated + } } catch let vpnError as NEVPNError { error = vpnError } catch { - print(error) + logger.error("Failed to update VPN configurations: \(error)") } } @@ -117,9 +120,7 @@ import SwiftUI } deinit { - for task in tasks { - task.cancel() - } + tasks.forEach { $0.cancel() } } } @@ -127,19 +128,19 @@ extension NEVPNConnection { var tunnelStatus: Tunnel.Status { switch status { case .connected: - return .connected(connectedDate!) + .connected(connectedDate!) case .connecting: - return .connecting + .connecting case .disconnecting: - return .disconnecting + .disconnecting case .disconnected: - return .disconnected + .disconnected case .reasserting: - return .reasserting + .reasserting case .invalid: - return .invalid + .invalid @unknown default: - return .unknown + .unknown } } } diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index c0e4f09..428d9ab 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -8,14 +8,20 @@ /* Begin PBXBuildFile section */ 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; - 0B46E8E02AC918CA00BA2A3C /* BurrowIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* BurrowIpc.swift */; }; + 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuView.swift */; }; + D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; + D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; + D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; + D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; + D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; }; D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; D05B9F7829E39EEC008CB1F9 /* TunnelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */; }; D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D05B9F7929E39EED008CB1F9 /* Assets.xcassets */; }; + D08252762B5C9FC4005DA378 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08252752B5C9FC4005DA378 /* Constants.swift */; }; D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */; }; D0BCC5FF2A086E1C00AD070D /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FE2A086E1C00AD070D /* Status.swift */; }; D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B98FC629FDC5B5004E7149 /* Tunnel.swift */; }; @@ -24,6 +30,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + D00117462B30373100D87C25 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D00117372B30341C00D87C25; + remoteInfo = Shared; + }; + D00117482B30373500D87C25 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D00117372B30341C00D87C25; + remoteInfo = Shared; + }; D020F65B29E4A697002790F6 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; @@ -49,8 +69,14 @@ /* Begin PBXFileReference section */ 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; - 0B46E8DF2AC918CA00BA2A3C /* BurrowIpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowIpc.swift; sourceTree = ""; }; + 0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; 43AA26D72A10004900F14CE6 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; + D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = ""; }; + D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = ""; }; + D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; }; + D001173A2B30341C00D87C25 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; + D00117412B30347800D87C25 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + D00117422B30348D00D87C25 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = ""; }; @@ -70,6 +96,8 @@ D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowApp.swift; sourceTree = ""; }; D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelView.swift; sourceTree = ""; }; D05B9F7929E39EED008CB1F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D08252742B5C9DEB005DA378 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; + D08252752B5C9FC4005DA378 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; D0B98FBF29FD8072004E7149 /* build-rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-rust.sh"; sourceTree = ""; }; D0B98FC629FDC5B5004E7149 /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; D0B98FD829FDDB6F004E7149 /* libburrow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libburrow.h; sourceTree = ""; }; @@ -80,10 +108,18 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + D00117352B30341C00D87C25 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D020F65029E4A697002790F6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */, D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -92,6 +128,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -106,6 +143,33 @@ path = Menu; sourceTree = ""; }; + D00117392B30341C00D87C25 /* Shared */ = { + isa = PBXGroup; + children = ( + D001173A2B30341C00D87C25 /* Logging.swift */, + D08252752B5C9FC4005DA378 /* Constants.swift */, + D00117422B30348D00D87C25 /* Shared.xcconfig */, + D001173F2B30347800D87C25 /* Constants */, + ); + path = Shared; + sourceTree = ""; + }; + D001173F2B30347800D87C25 /* Constants */ = { + isa = PBXGroup; + children = ( + D08252742B5C9DEB005DA378 /* Constants.h */, + D00117412B30347800D87C25 /* module.modulemap */, + ); + path = Constants; + sourceTree = ""; + }; + D00117432B30372900D87C25 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; D020F63C29E4A1FF002790F6 /* Configuration */ = { isa = PBXGroup; children = ( @@ -122,12 +186,14 @@ isa = PBXGroup; children = ( D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */, + 0B46E8DF2AC918CA00BA2A3C /* Client.swift */, + 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, + D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */, + D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */, D020F65929E4A697002790F6 /* Info.plist */, D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */, D020F66629E4A95D002790F6 /* NetworkExtension-macOS.entitlements */, D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */, - 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, - 0B46E8DF2AC918CA00BA2A3C /* BurrowIpc.swift */, D0B98FD729FDDB57004E7149 /* libburrow */, ); path = NetworkExtension; @@ -138,8 +204,10 @@ children = ( D05B9F7429E39EEC008CB1F9 /* App */, D020F65629E4A697002790F6 /* NetworkExtension */, + D00117392B30341C00D87C25 /* Shared */, D020F63C29E4A1FF002790F6 /* Configuration */, D05B9F7329E39EEC008CB1F9 /* Products */, + D00117432B30372900D87C25 /* Frameworks */, ); sourceTree = ""; }; @@ -148,6 +216,7 @@ children = ( D05B9F7229E39EEC008CB1F9 /* Burrow.app */, D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */, + D00117382B30341C00D87C25 /* libBurrowShared.a */, ); name = Products; sourceTree = ""; @@ -184,6 +253,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + D00117372B30341C00D87C25 /* Shared */ = { + isa = PBXNativeTarget; + buildConfigurationList = D001173C2B30341C00D87C25 /* Build configuration list for PBXNativeTarget "Shared" */; + buildPhases = ( + D00117342B30341C00D87C25 /* Sources */, + D00117352B30341C00D87C25 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D082527D2B5DEB80005DA378 /* PBXTargetDependency */, + ); + name = Shared; + productName = Shared; + productReference = D00117382B30341C00D87C25 /* libBurrowShared.a */; + productType = "com.apple.product-type.library.static"; + }; D020F65229E4A697002790F6 /* NetworkExtension */ = { isa = PBXNativeTarget; buildConfigurationList = D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */; @@ -196,7 +282,8 @@ buildRules = ( ); dependencies = ( - D08252712B5C3E2E005DA378 /* PBXTargetDependency */, + D08252792B5DEB78005DA378 /* PBXTargetDependency */, + D00117492B30373500D87C25 /* PBXTargetDependency */, ); name = NetworkExtension; productName = BurrowNetworkExtension; @@ -215,7 +302,8 @@ buildRules = ( ); dependencies = ( - D08252732B5C3E33005DA378 /* PBXTargetDependency */, + D082527B2B5DEB7D005DA378 /* PBXTargetDependency */, + D00117472B30373100D87C25 /* PBXTargetDependency */, D020F65C29E4A697002790F6 /* PBXTargetDependency */, ); name = App; @@ -230,9 +318,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1510; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1430; TargetAttributes = { + D00117372B30341C00D87C25 = { + CreatedOnToolsVersion = 15.1; + }; D020F65229E4A697002790F6 = { CreatedOnToolsVersion = 14.3; }; @@ -251,7 +342,7 @@ ); mainGroup = D05B9F6929E39EEC008CB1F9; packageReferences = ( - D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */, + D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */, ); productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */; projectDirPath = ""; @@ -259,6 +350,7 @@ targets = ( D05B9F7129E39EEC008CB1F9 /* App */, D020F65229E4A697002790F6 /* NetworkExtension */, + D00117372B30341C00D87C25 /* Shared */, ); }; /* End PBXProject section */ @@ -306,12 +398,23 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + D00117342B30341C00D87C25 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D001173B2B30341C00D87C25 /* Logging.swift in Sources */, + D08252762B5C9FC4005DA378 /* Constants.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D020F64F29E4A697002790F6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */, 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */, - 0B46E8E02AC918CA00BA2A3C /* BurrowIpc.swift in Sources */, + D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */, + 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */, D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -333,22 +436,50 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + D00117472B30373100D87C25 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D00117372B30341C00D87C25 /* Shared */; + targetProxy = D00117462B30373100D87C25 /* PBXContainerItemProxy */; + }; + D00117492B30373500D87C25 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D00117372B30341C00D87C25 /* Shared */; + targetProxy = D00117482B30373500D87C25 /* PBXContainerItemProxy */; + }; D020F65C29E4A697002790F6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D020F65229E4A697002790F6 /* NetworkExtension */; targetProxy = D020F65B29E4A697002790F6 /* PBXContainerItemProxy */; }; - D08252712B5C3E2E005DA378 /* PBXTargetDependency */ = { + D08252792B5DEB78005DA378 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = D08252702B5C3E2E005DA378 /* SwiftLintPlugin */; + productRef = D08252782B5DEB78005DA378 /* SwiftLintPlugin */; }; - D08252732B5C3E33005DA378 /* PBXTargetDependency */ = { + D082527B2B5DEB7D005DA378 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = D08252722B5C3E33005DA378 /* SwiftLintPlugin */; + productRef = D082527A2B5DEB7D005DA378 /* SwiftLintPlugin */; + }; + D082527D2B5DEB80005DA378 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D082527C2B5DEB80005DA378 /* SwiftLintPlugin */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + D001173D2B30341C00D87C25 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D00117422B30348D00D87C25 /* Shared.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + D001173E2B30341C00D87C25 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D00117422B30348D00D87C25 /* Shared.xcconfig */; + buildSettings = { + }; + name = Release; + }; D020F65F29E4A697002790F6 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */; @@ -394,6 +525,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + D001173C2B30341C00D87C25 /* Build configuration list for PBXNativeTarget "Shared" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D001173D2B30341C00D87C25 /* Debug */, + D001173E2B30341C00D87C25 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -424,7 +564,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { + D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/realm/SwiftLint.git"; requirement = { @@ -435,14 +575,19 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - D08252702B5C3E2E005DA378 /* SwiftLintPlugin */ = { + D08252782B5DEB78005DA378 /* SwiftLintPlugin */ = { isa = XCSwiftPackageProductDependency; - package = D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; + package = D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; productName = "plugin:SwiftLintPlugin"; }; - D08252722B5C3E33005DA378 /* SwiftLintPlugin */ = { + D082527A2B5DEB7D005DA378 /* SwiftLintPlugin */ = { isa = XCSwiftPackageProductDependency; - package = D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; + package = D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; + productName = "plugin:SwiftLintPlugin"; + }; + D082527C2B5DEB80005DA378 /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; productName = "plugin:SwiftLintPlugin"; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Apple/Configuration/App.xcconfig b/Apple/Configuration/App.xcconfig index f536e9d..8448773 100644 --- a/Apple/Configuration/App.xcconfig +++ b/Apple/Configuration/App.xcconfig @@ -1,5 +1,5 @@ - SKIP_INSTALL = NO +MERGED_BINARY_TYPE = manual LD_RUNPATH_SEARCH_PATHS[sdk=iphone*] = $(inherited) @executable_path/Frameworks LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks diff --git a/Apple/Configuration/Compiler.xcconfig b/Apple/Configuration/Compiler.xcconfig index 36a451d..aa486a7 100644 --- a/Apple/Configuration/Compiler.xcconfig +++ b/Apple/Configuration/Compiler.xcconfig @@ -21,7 +21,7 @@ CODE_SIGN_IDENTITY = Apple Development INFOPLIST_FILE = Configuration/Info.plist GENERATE_INFOPLIST_FILE = YES -INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 Hack Club +INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023-2024 Hack Club INFOPLIST_KEY_CFBundleDisplayName = Burrow ENABLE_BITCODE = NO diff --git a/Apple/Configuration/Extension.xcconfig b/Apple/Configuration/Extension.xcconfig index dfe9f5c..f8d90a3 100644 --- a/Apple/Configuration/Extension.xcconfig +++ b/Apple/Configuration/Extension.xcconfig @@ -1,2 +1,4 @@ +MERGED_BINARY_TYPE = manual + LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks LD_RUNPATH_SEARCH_PATHS[sdk=macos*] = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks diff --git a/Apple/NetworkExtension/BurrowIpc.swift b/Apple/NetworkExtension/BurrowIpc.swift deleted file mode 100644 index 279cdf1..0000000 --- a/Apple/NetworkExtension/BurrowIpc.swift +++ /dev/null @@ -1,133 +0,0 @@ -import Foundation -import Network -import os - -final class LineProtocol: NWProtocolFramerImplementation { - static let definition = NWProtocolFramer.Definition(implementation: LineProtocol.self) - static let label = "Lines" - init(framer: NWProtocolFramer.Instance) { } - func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { .ready } - func stop(framer: NWProtocolFramer.Instance) -> Bool { true } - func wakeup(framer: NWProtocolFramer.Instance) { } - func cleanup(framer: NWProtocolFramer.Instance) { } - func lines(from buffer: UnsafeMutableRawBufferPointer?) -> (lines: [Data], size: Int)? { - guard let buffer = buffer else { return nil } - let lines = buffer - .split(separator: 10) - guard !lines.isEmpty else { return nil } - let size = lines - .lazy - .map(\.count) - .reduce(0, +) + lines.count - let strings = lines - .lazy - .map { Data($0) } - return (lines: Array(strings), size: size) - } - func handleInput(framer: NWProtocolFramer.Instance) -> Int { - var result: [Data] = [] - _ = framer.parseInput(minimumIncompleteLength: 1, maximumLength: 16_000) { buffer, _ in - guard let (lines, size) = lines(from: buffer) else { - return 0 - } - result = lines - return size - } - for line in result { - framer.deliverInput(data: line, message: .init(instance: framer), isComplete: true) - } - return 0 - } - func handleOutput( - framer: NWProtocolFramer.Instance, - message: NWProtocolFramer.Message, - messageLength: Int, - isComplete: Bool - ) { - do { - try framer.writeOutputNoCopy(length: messageLength) - } catch { - } - } -} - -extension NWConnection { - func receiveMessage() async throws -> (Data?, NWConnection.ContentContext?, Bool) { - try await withUnsafeThrowingContinuation { continuation in - receiveMessage { completeContent, contentContext, isComplete, error in - if let error = error { - continuation.resume(throwing: error) - } - continuation.resume(returning: (completeContent, contentContext, isComplete)) - } - } - } - func send_raw(_ request: Data) async throws -> Data { - try await withCheckedThrowingContinuation { continuation in - let comp: NWConnection.SendCompletion = .contentProcessed {error in - if let error = error { - continuation.resume(with: .failure(error)) - } else { - continuation.resume(with: .success(request)) - } - } - self.send(content: request, completion: comp) - } - } -} - -final class BurrowIpc { - let connection: NWConnection - private var generator = SystemRandomNumberGenerator() - private var logger: Logger - init(logger: Logger) { - let params = NWParameters.tcp - params.defaultProtocolStack - .applicationProtocols - .insert(NWProtocolFramer.Options(definition: LineProtocol.definition), at: 0) - let connection = NWConnection(to: .unix(path: "burrow.sock"), using: params) - connection.start(queue: .global()) - self.connection = connection - self.logger = logger - } - func send(_ request: T) async throws -> U { - do { - let id: UInt = generator.next(upperBound: UInt.max) - var copy = request - copy.id = id - var data = try JSONEncoder().encode(request) - data.append(contentsOf: [10]) - _ = try await self.connection.send_raw(data) - return try JSONDecoder().decode(Response.self, from: data).result - } catch { - throw error - } - } - - func receive_raw() async throws -> Data { - let (completeContent, _, _) = try await connection.receiveMessage() - self.logger.info("Received raw message response") - guard let data = completeContent else { - throw BurrowError.resultIsNone - } - return data - } - - func request(_ request: any Request, type: U.Type) async throws -> U { - do { - var data: Data = try JSONEncoder().encode(request) - data.append(contentsOf: [10]) - _ = try await self.connection.send_raw(data) - self.logger.debug("message sent") - let receivedData = try await receive_raw() - self.logger.info("Received result: \(String(decoding: receivedData, as: UTF8.self))") - return try self.parse_response(receivedData) - } catch { - throw error - } - } - - func parse_response(_ response: Data) throws -> U { - try JSONDecoder().decode(U.self, from: response) - } -} diff --git a/Apple/NetworkExtension/Client.swift b/Apple/NetworkExtension/Client.swift new file mode 100644 index 0000000..a924c29 --- /dev/null +++ b/Apple/NetworkExtension/Client.swift @@ -0,0 +1,60 @@ +import BurrowShared +import Foundation +import Network + +final class Client { + let connection: NWConnection + + private let logger: Logger = Logger.logger(for: Client.self) + private var generator = SystemRandomNumberGenerator() + + convenience init() throws { + self.init(url: try Constants.socketURL) + } + + init(url: URL) { + let endpoint: NWEndpoint + if url.isFileURL { + endpoint = .unix(path: url.path(percentEncoded: false)) + } else { + endpoint = .url(url) + } + + let parameters = NWParameters.tcp + parameters.defaultProtocolStack + .applicationProtocols + .insert(NWProtocolFramer.Options(definition: NewlineProtocolFramer.definition), at: 0) + connection = NWConnection(to: endpoint, using: parameters) + connection.start(queue: .global()) + } + + func request(_ request: any Request, type: U.Type = U.self) async throws -> U { + do { + var copy = request + copy.id = generator.next(upperBound: UInt.max) + let content = try JSONEncoder().encode(copy) + logger.debug("> \(String(decoding: content, as: UTF8.self))") + + try await self.connection.send(content: content) + let (response, _, _) = try await connection.receiveMessage() + + logger.debug("< \(String(decoding: response, as: UTF8.self))") + return try JSONDecoder().decode(U.self, from: response) + } catch { + logger.error("\(error, privacy: .public)") + throw error + } + } + + deinit { + connection.cancel() + } +} + +extension Constants { + static var socketURL: URL { + get throws { + try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory) + } + } +} diff --git a/Apple/NetworkExtension/DataTypes.swift b/Apple/NetworkExtension/DataTypes.swift index 5d73805..391bfed 100644 --- a/Apple/NetworkExtension/DataTypes.swift +++ b/Apple/NetworkExtension/DataTypes.swift @@ -8,10 +8,11 @@ enum BurrowError: Error { case resultIsNone } -protocol Request: Codable where CommandT: Codable { - associatedtype CommandT +protocol Request: Codable where Command: Codable { + associatedtype Command + var id: UInt { get set } - var command: CommandT { get set } + var command: Command { get set } } struct BurrowSingleCommand: Request { @@ -38,13 +39,6 @@ struct BurrowStartRequest: Codable { let Start: StartOptions } -func start_req_fd(id: UInt) -> BurrowRequest { - let command = BurrowStartRequest(Start: BurrowStartRequest.StartOptions( - tun: BurrowStartRequest.TunOptions(name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: nil) - )) - return BurrowRequest(id: id, command: command) -} - struct Response: Decodable where T: Decodable { var id: UInt var result: T diff --git a/Apple/NetworkExtension/NWConnection+Async.swift b/Apple/NetworkExtension/NWConnection+Async.swift new file mode 100644 index 0000000..c21fdc0 --- /dev/null +++ b/Apple/NetworkExtension/NWConnection+Async.swift @@ -0,0 +1,32 @@ +import Foundation +import Network + +extension NWConnection { + // swiftlint:disable:next large_tuple + func receiveMessage() async throws -> (Data, NWConnection.ContentContext?, Bool) { + try await withUnsafeThrowingContinuation { continuation in + receiveMessage { completeContent, contentContext, isComplete, error in + if let error { + continuation.resume(throwing: error) + } else { + guard let completeContent = completeContent else { + fatalError("Both error and completeContent were nil") + } + continuation.resume(returning: (completeContent, contentContext, isComplete)) + } + } + } + } + + func send(content: Data) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + send(content: content, completion: .contentProcessed { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } + }) + } + } +} diff --git a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements index edb3f26..63efcfc 100644 --- a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements +++ b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements @@ -2,17 +2,19 @@ - com.apple.security.network.client - - com.apple.security.network.server - com.apple.developer.networking.networkextension packet-tunnel-provider + com.apple.security.app-sandbox + com.apple.security.application-groups $(APP_GROUP_IDENTIFIER) + com.apple.security.network.client + + com.apple.security.network.server + diff --git a/Apple/NetworkExtension/NetworkExtension.xcconfig b/Apple/NetworkExtension/NetworkExtension.xcconfig index 3b94990..35c7198 100644 --- a/Apple/NetworkExtension/NetworkExtension.xcconfig +++ b/Apple/NetworkExtension/NetworkExtension.xcconfig @@ -6,6 +6,6 @@ PRODUCT_BUNDLE_IDENTIFIER = $(NETWORK_EXTENSION_BUNDLE_IDENTIFIER) INFOPLIST_FILE = NetworkExtension/Info.plist CODE_SIGN_ENTITLEMENTS = NetworkExtension/NetworkExtension-iOS.entitlements -CODE_SIGN_ENTITLEMENTS[sdk=macos*] = NetworkExtension/NetworkExtension-macOS.entitlements +CODE_SIGN_ENTITLEMENTS[sdk=macosx*] = NetworkExtension/NetworkExtension-macOS.entitlements SWIFT_INCLUDE_PATHS = $(inherited) $(PROJECT_DIR)/NetworkExtension diff --git a/Apple/NetworkExtension/NewlineProtocolFramer.swift b/Apple/NetworkExtension/NewlineProtocolFramer.swift new file mode 100644 index 0000000..d2f71e5 --- /dev/null +++ b/Apple/NetworkExtension/NewlineProtocolFramer.swift @@ -0,0 +1,54 @@ +import Foundation +import Network + +final class NewlineProtocolFramer: NWProtocolFramerImplementation { + private static let delimeter: UInt8 = 10 // `\n` + + static let definition = NWProtocolFramer.Definition(implementation: NewlineProtocolFramer.self) + static let label = "Lines" + + init(framer: NWProtocolFramer.Instance) { } + + func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { .ready } + func stop(framer: NWProtocolFramer.Instance) -> Bool { true } + + func wakeup(framer: NWProtocolFramer.Instance) { } + func cleanup(framer: NWProtocolFramer.Instance) { } + + func handleInput(framer: NWProtocolFramer.Instance) -> Int { + while true { + var result: [Data] = [] + let parsed = framer.parseInput(minimumIncompleteLength: 1, maximumLength: 16_000) { buffer, _ in + guard let buffer else { return 0 } + var lines = buffer + .split(separator: Self.delimeter, omittingEmptySubsequences: false) + .map { Data($0) } + guard lines.count > 1 else { return 0 } + _ = lines.popLast() + + result = lines + return lines.reduce(lines.count) { $0 + $1.count } + } + + guard parsed && !result.isEmpty else { break } + + for line in result { + framer.deliverInput(data: line, message: .init(instance: framer), isComplete: true) + } + } + return 0 + } + + func handleOutput( + framer: NWProtocolFramer.Instance, + message: NWProtocolFramer.Message, + messageLength: Int, + isComplete: Bool + ) { + do { + try framer.writeOutputNoCopy(length: messageLength) + framer.writeOutput(data: [Self.delimeter]) + } catch { + } + } +} diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 19fa760..9231676 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -1,51 +1,49 @@ +import BurrowShared import libburrow import NetworkExtension import os class PacketTunnelProvider: NEPacketTunnelProvider { - let logger = Logger(subsystem: "com.hackclub.burrow", category: "frontend") - var client: BurrowIpc? - var osInitialized = false + private let logger = Logger.logger(for: PacketTunnelProvider.self) + override func startTunnel(options: [String: NSObject]? = nil) async throws { - logger.log("Starting tunnel") - if !osInitialized { - libburrow.initialize_oslog() - osInitialized = true - } - libburrow.start_srv() - client = BurrowIpc(logger: logger) - logger.info("Started server") do { - let command = BurrowSingleCommand(id: 0, command: "ServerConfig") - guard let data = try await client?.request(command, type: Response>.self) - else { - throw BurrowError.cantParseResult - } + libburrow.spawnInProcess(socketPath: try Constants.socketURL.path) + + let client = try Client() + + let command = BurrowRequest(id: 0, command: "ServerConfig") + let data = try await client.request(command, type: Response>.self) + let encoded = try JSONEncoder().encode(data.result) self.logger.log("Received final data: \(String(decoding: encoded, as: UTF8.self))") guard let serverconfig = data.result.Ok else { throw BurrowError.resultIsError } - guard let tunNs = self.generateTunSettings(from: serverconfig) else { + guard let tunNs = generateTunSettings(from: serverconfig) else { throw BurrowError.addrDoesntExist } try await self.setTunnelNetworkSettings(tunNs) self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") - // let tunFd = self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int; - // self.logger.info("Found File Descriptor: \(tunFd)") - let startCommand = start_req_fd(id: 1) - guard let data = try await client?.request(startCommand, type: Response>.self) - else { - throw BurrowError.cantParseResult - } - let encodedStartRes = try JSONEncoder().encode(data.result) - self.logger.log("Received start server response: \(String(decoding: encodedStartRes, as: UTF8.self))") + let startRequest = BurrowRequest( + id: .random(in: (.min)..<(.max)), + command: BurrowStartRequest( + Start: BurrowStartRequest.StartOptions( + tun: BurrowStartRequest.TunOptions( + name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: nil + ) + ) + ) + ) + let response = try await client.request(startRequest, type: Response>.self) + self.logger.log("Received start server response: \(String(describing: response.result))") } catch { - self.logger.error("An error occurred: \(error)") + self.logger.error("Failed to start tunnel: \(error)") throw error } } + private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { let cfig = from.ServerConfig guard let addr = cfig.address else { @@ -57,13 +55,4 @@ class PacketTunnelProvider: NEPacketTunnelProvider { logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") return nst } - override func stopTunnel(with reason: NEProviderStopReason) async { - } - override func handleAppMessage(_ messageData: Data) async -> Data? { - messageData - } - override func sleep() async { - } - override func wake() { - } } diff --git a/Apple/NetworkExtension/libburrow/libburrow.h b/Apple/NetworkExtension/libburrow/libburrow.h index 32d1d3b..e500de4 100644 --- a/Apple/NetworkExtension/libburrow/libburrow.h +++ b/Apple/NetworkExtension/libburrow/libburrow.h @@ -1,2 +1,2 @@ -void start_srv(); -void initialize_oslog(); +__attribute__((__swift_name__("spawnInProcess(socketPath:)"))) +extern void spawn_in_process(const char * __nullable path); diff --git a/Apple/Shared/Constants.swift b/Apple/Shared/Constants.swift new file mode 100644 index 0000000..cb56cb3 --- /dev/null +++ b/Apple/Shared/Constants.swift @@ -0,0 +1,22 @@ +@_implementationOnly import Constants + +public enum Constants { + enum Error: Swift.Error { + case invalidAppGroupIdentifier + } + + public static let bundleIdentifier = AppBundleIdentifier + public static let appGroupIdentifier = AppGroupIdentifier + + public static var groupContainerURL: URL { + get throws { try _groupContainerURL.get() } + } + + private static let _groupContainerURL: Result = { + guard let groupContainerURL = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else { + return .failure(.invalidAppGroupIdentifier) + } + return .success(groupContainerURL) + }() +} diff --git a/Apple/Shared/Constants/Constants.h b/Apple/Shared/Constants/Constants.h new file mode 100644 index 0000000..09806c5 --- /dev/null +++ b/Apple/Shared/Constants/Constants.h @@ -0,0 +1,11 @@ +#import + +#define MACRO_STRING_(m) #m +#define MACRO_STRING(m) @MACRO_STRING_(m) + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const AppBundleIdentifier = MACRO_STRING(APP_BUNDLE_IDENTIFIER); +static NSString * const AppGroupIdentifier = MACRO_STRING(APP_GROUP_IDENTIFIER); + +NS_ASSUME_NONNULL_END diff --git a/Apple/Shared/Constants/module.modulemap b/Apple/Shared/Constants/module.modulemap new file mode 100644 index 0000000..7ee21fc --- /dev/null +++ b/Apple/Shared/Constants/module.modulemap @@ -0,0 +1,4 @@ +module Constants { + header "Constants.h" + export * +} diff --git a/Apple/Shared/Logging.swift b/Apple/Shared/Logging.swift new file mode 100644 index 0000000..36f024c --- /dev/null +++ b/Apple/Shared/Logging.swift @@ -0,0 +1,19 @@ +import os +@_exported import OSLog + +extension Logger { + private static let loggers: OSAllocatedUnfairLock<[String: Logger]> = OSAllocatedUnfairLock(initialState: [:]) + + public static let subsystem = Constants.bundleIdentifier + + public static func logger(for type: Any.Type) -> Logger { + let category = String(describing: type) + let logger = loggers.withLock { loggers in + if let logger = loggers[category] { return logger } + let logger = Logger(subsystem: subsystem, category: category) + loggers[category] = logger + return logger + } + return logger + } +} diff --git a/Apple/Shared/Shared.xcconfig b/Apple/Shared/Shared.xcconfig new file mode 100644 index 0000000..50718bd --- /dev/null +++ b/Apple/Shared/Shared.xcconfig @@ -0,0 +1,5 @@ +PRODUCT_NAME = BurrowShared +MERGEABLE_LIBRARY = YES + +SWIFT_INCLUDE_PATHS = $(PROJECT_DIR)/Shared/Constants +GCC_PREPROCESSOR_DEFINITIONS = APP_BUNDLE_IDENTIFIER=$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER=$(APP_GROUP_IDENTIFIER) diff --git a/Cargo.lock b/Cargo.lock index 7399bd5..85f11e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -40,73 +40,66 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "async-channel" @@ -140,18 +133,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] name = "async-trait" -version = "0.1.76" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -207,9 +200,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -222,9 +215,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -273,7 +266,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.39", + "syn 2.0.48", "which", ] @@ -285,9 +278,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "blake2" @@ -309,9 +302,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "burrow" @@ -325,19 +318,18 @@ dependencies = [ "caps", "chacha20poly1305", "clap", + "console", "console-subscriber", - "env_logger", - "etherparse", "fehler", "futures", "hmac", "insta", "ip_network", "ip_network_table", - "ipnet", "libsystemd", "log", - "nix", + "nix 0.27.1", + "once_cell", "parking_lot", "rand", "rand_core", @@ -348,25 +340,24 @@ dependencies = [ "tokio", "tracing", "tracing-journald", - "tracing-log", + "tracing-log 0.1.4", "tracing-oslog", "tracing-subscriber", "tun", - "uuid", "x25519-dalek", ] [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bzip2" @@ -461,31 +452,30 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.1", ] [[package]] name = "clap" -version = "4.3.10" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.10" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -495,21 +485,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.2" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "colorchoice" @@ -519,23 +509,24 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", - "windows-sys 0.45.0", + "unicode-width", + "windows-sys 0.52.0", ] [[package]] @@ -583,9 +574,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -593,15 +584,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -665,7 +656,16 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", ] [[package]] @@ -687,9 +687,9 @@ checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encode_unicode" @@ -699,61 +699,34 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] [[package]] -name = "env_logger" -version = "0.10.0" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "errno-dragonfly", "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "etherparse" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcb08c4aab4e2985045305551e67126b43f1b6b136bc4e1cd87fb0327877a611" -dependencies = [ - "arrayvec", + "windows-sys 0.52.0", ] [[package]] name = "event-listener" -version = "4.0.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", @@ -772,12 +745,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fehler" @@ -801,15 +771,15 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -838,18 +808,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -862,9 +832,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -872,15 +842,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -889,38 +859,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -946,9 +916,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -957,9 +927,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -969,9 +939,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -979,7 +949,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -992,6 +962,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -1013,9 +989,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "hex" @@ -1033,10 +1009,19 @@ dependencies = [ ] [[package]] -name = "http" -version = "0.2.9" +name = "home" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1045,9 +1030,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1062,9 +1047,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -1074,9 +1059,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1089,7 +1074,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -1123,9 +1108,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1138,7 +1123,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", ] [[package]] @@ -1164,26 +1159,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ip_network" version = "0.4.1" @@ -1208,23 +1183,9 @@ checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -dependencies = [ - "serde", -] - -[[package]] -name = "is-terminal" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" -dependencies = [ - "hermit-abi", - "rustix 0.38.1", - "windows-sys 0.48.0", -] +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itertools" @@ -1237,24 +1198,24 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -1273,9 +1234,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -1288,15 +1249,25 @@ dependencies = [ ] [[package]] -name = "libsystemd" -version = "0.6.0" +name = "libloading" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b9597a67aa1c81a6624603e6bd0bcefb9e0f94c9c54970ec53771082104b4e" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "libsystemd" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c592dc396b464005f78a5853555b9f240bc5378bf5221acc4e129910b2678869" dependencies = [ "hmac", "libc", "log", - "nix", + "nix 0.27.1", "nom", "once_cell", "serde", @@ -1313,21 +1284,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1335,9 +1300,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "matchers" @@ -1345,7 +1310,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -1356,9 +1321,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -1370,10 +1335,19 @@ dependencies = [ ] [[package]] -name = "miette" -version = "5.9.0" +name = "memoffset" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a236ff270093b0b67451bc50a509bd1bad302cb1d3c7d37d5efe931238581fa9" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" dependencies = [ "miette-derive", "once_cell", @@ -1383,13 +1357,13 @@ dependencies = [ [[package]] name = "miette-derive" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1415,9 +1389,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", @@ -1444,16 +1418,27 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "memoffset 0.7.1", "pin-utils", - "static_assertions", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", + "memoffset 0.9.0", ] [[package]] @@ -1497,18 +1482,18 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -1518,11 +1503,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", @@ -1539,7 +1524,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1550,9 +1535,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ "cc", "libc", @@ -1584,15 +1569,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -1626,28 +1611,28 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1664,15 +1649,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" [[package]] name = "platforms" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" [[package]] name = "poly1305" @@ -1685,6 +1670,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1693,19 +1684,19 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.9" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -1730,7 +1721,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1744,9 +1735,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1783,22 +1774,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.8.4" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.2", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", ] [[package]] @@ -1810,6 +1802,17 @@ dependencies = [ "regex-syntax 0.6.29", ] +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + [[package]] name = "regex-syntax" version = "0.6.29" @@ -1818,15 +1821,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64", "bytes", @@ -1849,6 +1852,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", "tower-service", @@ -1896,29 +1900,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.21" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25693a73057a1b4cb56179dd3c7ea21a7c6c5ee7d85781f5749b46f34b79c" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc6396159432b5c8490d4e301d8c705f61860b8b6c863bf79942ce5401968f3" -dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.2", "errno", "libc", - "linux-raw-sys 0.4.3", - "windows-sys 0.48.0", + "linux-raw-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1929,24 +1919,24 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] name = "schemars" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "dyn-clone", "schemars_derive", @@ -1956,9 +1946,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ "proc-macro2", "quote", @@ -1968,15 +1958,15 @@ dependencies = [ [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -1987,9 +1977,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -1997,28 +1987,28 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.164" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.164" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2034,9 +2024,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.99" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" +checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" dependencies = [ "itoa", "ryu", @@ -2068,9 +2058,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -2079,9 +2069,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2090,50 +2080,60 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "similar" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.11.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -2142,9 +2142,9 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "ssri" -version = "9.0.0" +version = "9.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5327a6eb28e137e180380169adeae3ac6128438ca1e8a8dc80118f3d1812cbd" +checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082" dependencies = [ "base64", "digest", @@ -2156,12 +2156,6 @@ dependencies = [ "xxhash-rust", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.10.0" @@ -2187,9 +2181,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2203,46 +2197,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "tempfile" -version = "3.6.0" +name = "system-configuration" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "autocfg", - "cfg-if", - "fastrand", - "redox_syscall", - "rustix 0.37.21", - "windows-sys 0.48.0", + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", ] [[package]] -name = "termcolor" -version = "1.2.0" +name = "system-configuration-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ - "winapi-util", + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2257,19 +2262,21 @@ dependencies = [ [[package]] name = "time" -version = "0.3.22" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ + "deranged", + "powerfmt", "serde", "time-core", ] [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tinyvec" @@ -2288,18 +2295,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", - "socket2", + "socket2 0.5.5", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -2317,13 +2323,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2349,9 +2355,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -2396,7 +2402,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 1.9.3", "pin-project", "pin-project-lite", "rand", @@ -2422,11 +2428,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2434,20 +2439,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -2466,12 +2471,23 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" dependencies = [ - "lazy_static", "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", "tracing-core", ] @@ -2492,9 +2508,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -2505,14 +2521,14 @@ dependencies = [ "thread_local", "tracing", "tracing-core", - "tracing-log", + "tracing-log 0.2.0", ] [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tun" @@ -2525,13 +2541,13 @@ dependencies = [ "futures", "lazy_static", "libc", - "libloading", + "libloading 0.7.4", "log", - "nix", + "nix 0.26.4", "reqwest", "schemars", "serde", - "socket2", + "socket2 0.4.10", "ssri", "tempfile", "tokio", @@ -2543,21 +2559,21 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -2570,9 +2586,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "universal-hash" @@ -2592,9 +2608,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -2609,11 +2625,10 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ - "getrandom", "serde", ] @@ -2652,9 +2667,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2662,24 +2677,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -2689,9 +2704,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2699,28 +2714,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -2728,13 +2743,14 @@ dependencies = [ [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix", ] [[package]] @@ -2759,15 +2775,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2780,31 +2787,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.48.5", ] [[package]] @@ -2813,130 +2796,140 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] @@ -2953,9 +2946,9 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" +checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" [[package]] name = "yaml-rust" @@ -2968,9 +2961,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -2983,7 +2976,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3027,11 +3020,10 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.9+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 0fb2443..4e7688b 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -12,46 +12,44 @@ crate-type = ["lib", "staticlib"] anyhow = "1.0" tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util", "rt-multi-thread", "time", "tracing"] } tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } -clap = { version = "4.3.2", features = ["derive"] } +clap = { version = "4.4", features = ["derive"] } tracing = "0.1" tracing-log = "0.1" -tracing-journald = "0.3" -tracing-oslog = {git = "https://github.com/Stormshield-robinc/tracing-oslog"} -tracing-subscriber = { version = "0.3" , features = ["std", "env-filter"]} -env_logger = "0.10" +tracing-oslog = { git = "https://github.com/Stormshield-robinc/tracing-oslog" } +tracing-subscriber = { version = "0.3" , features = ["std", "env-filter"] } log = "0.4" serde = { version = "1", features = ["derive"] } -serde_json = "1" -blake2 = "0.10.6" -chacha20poly1305 = "0.10.1" -rand = "0.8.5" -rand_core = "0.6.4" -aead = "0.5.2" -x25519-dalek = { version = "2.0.0", features = ["reusable_secrets", "static_secrets"] } -ring = "0.17.7" -parking_lot = "0.12.1" +serde_json = "1.0" +blake2 = "0.10" +chacha20poly1305 = "0.10" +rand = "0.8" +rand_core = "0.6" +aead = "0.5" +x25519-dalek = { version = "2.0", features = ["reusable_secrets", "static_secrets"] } +ring = "0.17" +parking_lot = "0.12" hmac = "0.12" -ipnet = { version = "2.8.0", features = ["serde"] } -base64 = "0.21.4" -fehler = "1.0.0" -ip_network_table = "0.2.0" -ip_network = "0.4.0" -async-channel = "2.1.1" +base64 = "0.21" +fehler = "1.0" +ip_network_table = "0.2" +ip_network = "0.4" +async-channel = "2.1" schemars = "0.8" futures = "0.3.28" -uuid = { version = "1.6.1", features = ["v4"] } -console-subscriber = { version = "0.2.0" , optional = true} +once_cell = "1.19" +console-subscriber = { version = "0.2.0" , optional = true } +console = "0.15.8" [target.'cfg(target_os = "linux")'.dependencies] -caps = "0.5.5" -libsystemd = "0.6" +caps = "0.5" +libsystemd = "0.7" +tracing-journald = "0.3" [target.'cfg(target_vendor = "apple")'.dependencies] -nix = { version = "0.26.2" } +nix = { version = "0.27" } [dev-dependencies] -insta = { version = "1.32.0", features = ["yaml"] } -etherparse = "0.12" +insta = { version = "1.32", features = ["yaml"] } [package.metadata.generate-rpm] assets = [ @@ -63,4 +61,4 @@ post_install_script = "../package/rpm/post_install" pre_uninstall_script = "../package/rpm/pre_uninstall" [features] -tokio-console = ["dep:console-subscriber"] \ No newline at end of file +tokio-console = ["dep:console-subscriber"] diff --git a/burrow/src/apple.rs b/burrow/src/apple.rs deleted file mode 100644 index 9fc0140..0000000 --- a/burrow/src/apple.rs +++ /dev/null @@ -1,13 +0,0 @@ -use tracing::debug; -use tracing_oslog::OsLogger; -use tracing_subscriber::layer::SubscriberExt; - -pub use crate::daemon::start_srv; - -#[no_mangle] -pub extern "C" fn initialize_oslog() { - let collector = - tracing_subscriber::registry().with(OsLogger::new("com.hackclub.burrow", "backend")); - tracing::subscriber::set_global_default(collector).unwrap(); - debug!("Initialized oslog tracing in libburrow rust FFI"); -} \ No newline at end of file diff --git a/burrow/src/daemon/apple.rs b/burrow/src/daemon/apple.rs new file mode 100644 index 0000000..9460613 --- /dev/null +++ b/burrow/src/daemon/apple.rs @@ -0,0 +1,55 @@ +use std::{ + ffi::{c_char, CStr}, + path::PathBuf, + sync::Arc, + thread, +}; + +use once_cell::sync::OnceCell; +use tokio::{ + runtime::{Builder, Handle}, + sync::Notify, +}; +use tracing::error; + +use crate::daemon::daemon_main; + +static BURROW_NOTIFY: OnceCell> = OnceCell::new(); +static BURROW_HANDLE: OnceCell = OnceCell::new(); + +#[no_mangle] +pub unsafe extern "C" fn spawn_in_process(path: *const c_char) { + crate::tracing::initialize(); + + let notify = BURROW_NOTIFY.get_or_init(|| Arc::new(Notify::new())); + let handle = BURROW_HANDLE.get_or_init(|| { + let path_buf = if path.is_null() { + None + } else { + Some(PathBuf::from(CStr::from_ptr(path).to_str().unwrap())) + }; + let sender = notify.clone(); + + let (handle_tx, handle_rx) = tokio::sync::oneshot::channel(); + thread::spawn(move || { + let runtime = Builder::new_multi_thread() + .worker_threads(4) + .enable_all() + .thread_name("burrow-worker") + .build() + .unwrap(); + handle_tx.send(runtime.handle().clone()).unwrap(); + runtime.block_on(async { + let result = daemon_main(path_buf.as_deref(), Some(sender.clone())).await; + if let Err(error) = result.as_ref() { + error!("Burrow thread exited: {}", error); + } + result + }) + }); + handle_rx.blocking_recv().unwrap() + }); + + let receiver = notify.clone(); + handle.block_on(async move { receiver.notified().await }); +} diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs index 5a35b28..2a971dd 100644 --- a/burrow/src/daemon/mod.rs +++ b/burrow/src/daemon/mod.rs @@ -1,5 +1,6 @@ -use std::sync::Arc; +use std::{path::Path, sync::Arc}; +pub mod apple; mod command; mod instance; mod net; @@ -8,44 +9,52 @@ mod response; use anyhow::Result; pub use command::{DaemonCommand, DaemonStartOptions}; use instance::DaemonInstance; -#[cfg(target_vendor = "apple")] -pub use net::start_srv; -pub use net::DaemonClient; +pub use net::{DaemonClient, Listener}; pub use response::{DaemonResponse, DaemonResponseData, ServerInfo}; use tokio::sync::{Notify, RwLock}; +use tracing::{error, info}; -use crate::{ - daemon::net::listen, - wireguard::{Config, Interface}, -}; +use crate::wireguard::{Config, Interface}; -pub async fn daemon_main(notify_ready: Option>) -> Result<()> { +pub async fn daemon_main(path: Option<&Path>, notify_ready: Option>) -> Result<()> { let (commands_tx, commands_rx) = async_channel::unbounded(); let (response_tx, response_rx) = async_channel::unbounded(); + let listener = if let Some(path) = path { + info!("Creating listener... {:?}", path); + Listener::new_with_path(commands_tx, response_rx, path) + } else { + info!("Creating listener..."); + Listener::new(commands_tx, response_rx) + }; + if let Some(n) = notify_ready { + n.notify_one() + } + let listener = listener?; + let config = Config::default(); let iface: Interface = config.try_into()?; + let mut instance = DaemonInstance::new(commands_rx, response_tx, Arc::new(RwLock::new(iface))); - let mut inst: DaemonInstance = - DaemonInstance::new(commands_rx, response_tx, Arc::new(RwLock::new(iface))); + info!("Starting daemon..."); - tracing::info!("Starting daemon jobs..."); - - let inst_job = tokio::spawn(async move { - let res = inst.run().await; - if let Err(e) = res { - tracing::error!("Error when running instance: {}", e); + let main_job = tokio::spawn(async move { + let result = instance.run().await; + if let Err(e) = result.as_ref() { + error!("Instance exited: {}", e); } + result }); - let listen_job = tokio::spawn(async move { - let res = listen(commands_tx, response_rx, notify_ready).await; - if let Err(e) = res { - tracing::error!("Error when listening: {}", e); + let listener_job = tokio::spawn(async move { + let result = listener.run().await; + if let Err(e) = result.as_ref() { + error!("Listener exited: {}", e); } + result }); - tokio::try_join!(inst_job, listen_job) + tokio::try_join!(main_job, listener_job) .map(|_| ()) .map_err(|e| e.into()) } diff --git a/burrow/src/daemon/net/apple.rs b/burrow/src/daemon/net/apple.rs deleted file mode 100644 index 143e913..0000000 --- a/burrow/src/daemon/net/apple.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::sync::Arc; -use std::thread; - -use tokio::runtime::Runtime; -use tokio::sync::Notify; -use tracing::{error, info}; - -use crate::daemon::{daemon_main, DaemonClient}; - -#[no_mangle] -pub extern "C" fn start_srv() { - info!("Starting server"); - let start_notify = Arc::new(Notify::new()); - let start_recv = start_notify.clone(); - let _handle = thread::spawn(move || { - let rt = Runtime::new().unwrap(); - rt.block_on(async { - if let Err(e) = daemon_main(Some(start_notify.clone())).await { - error!("Error when starting rpc server: {}", e); - } - }); - start_notify.notify_one(); - }); - let rt = Runtime::new().unwrap(); - rt.block_on(async { - start_recv.notified().await; - match DaemonClient::new().await { - Ok(..) => info!("Server successfully started"), - Err(e) => error!("Could not connect to server: {}", e) - } - }); -} diff --git a/burrow/src/daemon/net/mod.rs b/burrow/src/daemon/net/mod.rs index d369f40..fe35bae 100644 --- a/burrow/src/daemon/net/mod.rs +++ b/burrow/src/daemon/net/mod.rs @@ -4,28 +4,18 @@ use super::DaemonCommand; #[cfg(target_family = "unix")] mod unix; -#[cfg(all(target_family = "unix", not(target_os = "linux")))] -pub use unix::{listen, DaemonClient}; -#[cfg(target_os = "linux")] -mod systemd; -#[cfg(target_os = "linux")] -pub use systemd::{listen, DaemonClient}; +#[cfg(target_family = "unix")] +pub use unix::{DaemonClient, Listener}; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] -pub use windows::{listen, DaemonClient}; - -#[cfg(target_vendor = "apple")] -mod apple; - -#[cfg(target_vendor = "apple")] -pub use apple::start_srv; +pub use windows::{DaemonClient, Listener}; #[derive(Clone, Serialize, Deserialize)] pub struct DaemonRequest { - pub id: u32, + pub id: u64, pub command: DaemonCommand, } diff --git a/burrow/src/daemon/net/systemd.rs b/burrow/src/daemon/net/systemd.rs deleted file mode 100644 index 4534742..0000000 --- a/burrow/src/daemon/net/systemd.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::os::fd::IntoRawFd; -use std::sync::Arc; - -use anyhow::Result; -use tokio::sync::Notify; - -use super::*; -use crate::daemon::DaemonResponse; - -pub async fn listen( - cmd_tx: async_channel::Sender, - rsp_rx: async_channel::Receiver, - notify: Option> -) -> Result<()> { - if !libsystemd::daemon::booted() - || listen_with_systemd(cmd_tx.clone(), rsp_rx.clone()) - .await - .is_err() - { - unix::listen(cmd_tx, rsp_rx, notify).await?; - } - Ok(()) -} - -async fn listen_with_systemd( - cmd_tx: async_channel::Sender, - rsp_rx: async_channel::Receiver, -) -> Result<()> { - let fds = libsystemd::activation::receive_descriptors(false)?; - super::unix::listen_with_optional_fd(cmd_tx, rsp_rx, Some(fds[0].clone().into_raw_fd()), None).await -} - -pub type DaemonClient = unix::DaemonClient; diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs index 948bdff..26e901d 100644 --- a/burrow/src/daemon/net/unix.rs +++ b/burrow/src/daemon/net/unix.rs @@ -1,21 +1,15 @@ -use std::{ - io, - os::{ - fd::{FromRawFd, RawFd}, - unix::net::UnixListener as StdUnixListener, - }, - path::{Path, PathBuf}, -}; -use std::sync::Arc; +#[cfg(target_os = "linux")] +use std::os::fd::{IntoRawFd, RawFd}; +use std::{ffi::OsStr, io, path::Path}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Error, Result}; +use fehler::throws; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{UnixListener, UnixStream}, }; -use tracing::{debug, info}; +use tracing::{debug, error, info}; -use tokio::sync::Notify; use super::*; use crate::daemon::{DaemonCommand, DaemonResponse, DaemonResponseData}; @@ -25,141 +19,178 @@ const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; #[cfg(target_vendor = "apple")] const UNIX_SOCKET_PATH: &str = "burrow.sock"; -#[cfg(target_os = "macos")] -fn fetch_socket_path() -> Option { - let tries = vec![ - "burrow.sock".to_string(), - format!( - "{}/Library/Containers/com.hackclub.burrow.network/Data/burrow.sock", - std::env::var("HOME").unwrap_or_default() - ) - .to_string(), - ]; - for path in tries { - let path = PathBuf::from(path); - if path.exists() { - return Some(path) - } - } - None -} - -#[cfg(not(target_os = "macos"))] -fn fetch_socket_path() -> Option { - Some(Path::new(UNIX_SOCKET_PATH).to_path_buf()) -} - -pub async fn listen( +#[derive(Debug)] +pub struct Listener { cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, - notify: Option> -) -> Result<()> { - listen_with_optional_fd(cmd_tx, rsp_rx, None, notify).await + inner: UnixListener, } -pub(crate) async fn listen_with_optional_fd( - cmd_tx: async_channel::Sender, - rsp_rx: async_channel::Receiver, - raw_fd: Option, - notify: Option> -) -> Result<()> { - let path = Path::new(UNIX_SOCKET_PATH); - - let listener = if let Some(raw_fd) = raw_fd { - let listener = unsafe { StdUnixListener::from_raw_fd(raw_fd) }; - listener.set_nonblocking(true)?; - UnixListener::from_std(listener) - } else { - UnixListener::bind(path) - }; - let listener = if let Ok(listener) = listener { - listener - } else { - // Won't help all that much, if we use the async version of fs. - if let Some(par) = path.parent() { - std::fs::create_dir_all(par)?; - } - match std::fs::remove_file(path) { - Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), - stuff => stuff, - }?; - info!("Relative path: {}", path.to_string_lossy()); - UnixListener::bind(path)? - }; - if let Some(notify) = notify { - notify.notify_one(); +impl Listener { + #[throws] + pub fn new( + cmd_tx: async_channel::Sender, + rsp_rx: async_channel::Receiver, + ) -> Self { + let path = Path::new(OsStr::new(UNIX_SOCKET_PATH)); + Self::new_with_path(cmd_tx, rsp_rx, path)? } - loop { - let (stream, _) = listener.accept().await?; - let cmd_tx = cmd_tx.clone(); - // I'm pretty sure we won't need to manually join / shut this down, - // `lines` will return Err during dropping, and this task should exit - // gracefully. - let rsp_rxc = rsp_rx.clone(); - tokio::task::spawn(async move { - let cmd_tx = cmd_tx; - let mut stream = stream; - let (mut read_stream, mut write_stream) = stream.split(); - let buf_reader = BufReader::new(&mut read_stream); - let mut lines = buf_reader.lines(); - while let Ok(Some(line)) = lines.next_line().await { - info!("Got line: {}", line); - debug!("Line raw data: {:?}", line.as_bytes()); - let mut res: DaemonResponse = DaemonResponseData::None.into(); - let req = match serde_json::from_str::(&line) { - Ok(req) => Some(req), - Err(e) => { - res.result = Err(e.to_string()); - tracing::error!("Failed to parse request: {}", e); - None - } - }; - let mut res = serde_json::to_string(&res).unwrap(); - res.push('\n'); + #[throws] + #[cfg(target_os = "linux")] + pub fn new_with_path( + cmd_tx: async_channel::Sender, + rsp_rx: async_channel::Receiver, + path: &Path, + ) -> Self { + let inner = listener_from_path_or_fd(&path, raw_fd())?; + Self { cmd_tx, rsp_rx, inner } + } - if let Some(req) = req { - cmd_tx.send(req.command).await.unwrap(); - let res = rsp_rxc.recv().await.unwrap().with_id(req.id); - let mut retres = serde_json::to_string(&res).unwrap(); - retres.push('\n'); - info!("Sending response: {}", retres); - write_stream.write_all(retres.as_bytes()).await.unwrap(); - } else { - write_stream.write_all(res.as_bytes()).await.unwrap(); + #[throws] + #[cfg(not(target_os = "linux"))] + pub fn new_with_path( + cmd_tx: async_channel::Sender, + rsp_rx: async_channel::Receiver, + path: &Path, + ) -> Self { + let inner = listener_from_path(path)?; + Self { cmd_tx, rsp_rx, inner } + } + + pub async fn run(&self) -> Result<()> { + info!("Waiting for connections..."); + loop { + let (stream, _) = self.inner.accept().await?; + let cmd_tx = self.cmd_tx.clone(); + let rsp_rxc = self.rsp_rx.clone(); + tokio::task::spawn(async move { + info!("Got connection: {:?}", stream); + Self::stream(stream, cmd_tx, rsp_rxc).await; + }); + } + } + + async fn stream( + stream: UnixStream, + cmd_tx: async_channel::Sender, + rsp_rxc: async_channel::Receiver, + ) { + let mut stream = stream; + let (mut read_stream, mut write_stream) = stream.split(); + let buf_reader = BufReader::new(&mut read_stream); + let mut lines = buf_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + info!("Line: {}", line); + let mut res: DaemonResponse = DaemonResponseData::None.into(); + let req = match serde_json::from_str::(&line) { + Ok(req) => Some(req), + Err(e) => { + res.result = Err(e.to_string()); + error!("Failed to parse request: {}", e); + None } + }; + let mut res = serde_json::to_string(&res).unwrap(); + res.push('\n'); + + if let Some(req) = req { + cmd_tx.send(req.command).await.unwrap(); + let res = rsp_rxc.recv().await.unwrap().with_id(req.id); + let mut retres = serde_json::to_string(&res).unwrap(); + retres.push('\n'); + info!("Sending response: {}", retres); + write_stream.write_all(retres.as_bytes()).await.unwrap(); + } else { + write_stream.write_all(res.as_bytes()).await.unwrap(); } - }); + } } } +#[cfg(target_os = "linux")] +fn raw_fd() -> Option { + if !libsystemd::daemon::booted() { + return None; + } + + match libsystemd::activation::receive_descriptors(false) { + Ok(descriptors) => descriptors.into_iter().map(|d| d.into_raw_fd()).next(), + Err(e) => { + tracing::error!("Failed to receive descriptors: {}", e); + None + } + } +} + +#[throws] +#[cfg(target_os = "linux")] +fn listener_from_path_or_fd(path: &Path, raw_fd: Option) -> UnixListener { + match raw_fd.map(listener_from_fd) { + Some(Ok(listener)) => listener, + _ => listener_from_path(path)?, + } +} + +#[throws] +#[cfg(target_os = "linux")] +fn listener_from_fd(fd: RawFd) -> UnixListener { + use std::os::fd::FromRawFd; + + let listener = unsafe { std::os::unix::net::UnixListener::from_raw_fd(fd) }; + listener.set_nonblocking(true)?; + UnixListener::from_std(listener)? +} + +#[throws] +fn listener_from_path(path: &Path) -> UnixListener { + let error = match UnixListener::bind(path) { + Ok(listener) => return listener, + Err(e) => e, + }; + + match error.kind() { + io::ErrorKind::NotFound => { + if let Some(parent) = path.parent() { + info!("Creating parent directory {:?}", parent); + std::fs::create_dir_all(parent)?; + } + } + io::ErrorKind::AddrInUse => { + info!("Removing existing file"); + match std::fs::remove_file(path) { + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + stuff => stuff, + }?; + } + _ => error!("Failed to bind to {:?}: {}", path, error), + } + + UnixListener::bind(path)? +} + #[derive(Debug)] pub struct DaemonClient { - connection: UnixStream, + stream: UnixStream, } impl DaemonClient { pub async fn new() -> Result { - let path = fetch_socket_path().ok_or(anyhow!("Failed to find socket path"))?; - // debug!("found path: {:?}", path); - let connection = UnixStream::connect(path).await?; - debug!("connected to socket"); - Ok(Self { connection }) + let path = Path::new(OsStr::new(UNIX_SOCKET_PATH)); + Self::new_with_path(path).await } - pub async fn new_with_path(path: &str) -> Result { - let path = Path::new(path); - let connection = UnixStream::connect(path).await?; - - Ok(Self { connection }) + pub async fn new_with_path(path: &Path) -> Result { + let stream = UnixStream::connect(path).await?; + Ok(Self { stream }) } pub async fn send_command(&mut self, command: DaemonCommand) -> Result { let mut command = serde_json::to_string(&DaemonRequest { id: 0, command })?; command.push('\n'); - self.connection.write_all(command.as_bytes()).await?; - let buf_reader = BufReader::new(&mut self.connection); + self.stream.write_all(command.as_bytes()).await?; + let buf_reader = BufReader::new(&mut self.stream); let mut lines = buf_reader.lines(); let response = lines .next_line() diff --git a/burrow/src/daemon/net/windows.rs b/burrow/src/daemon/net/windows.rs index c4a1d71..5918260 100644 --- a/burrow/src/daemon/net/windows.rs +++ b/burrow/src/daemon/net/windows.rs @@ -1,23 +1,34 @@ use anyhow::Result; +use fehler::throws; -use super::*; +use super::DaemonCommand; use crate::daemon::DaemonResponse; -pub async fn listen( - _cmd_tx: async_channel::Sender, - _rsp_rx: async_channel::Receiver, -) -> Result<()> { - unimplemented!("This platform does not currently support daemon mode.") +pub struct Listener; + +impl Listener { + pub fn new_with_path( + cmd_tx: async_channel::Sender, + rsp_rx: async_channel::Receiver, + path: &Path, + ) -> Self { + Self + } + + pub async fn run(&self) -> Result<()> { + Ok(()) + } } +#[derive(Debug)] pub struct DaemonClient; impl DaemonClient { pub async fn new() -> Result { - unimplemented!("This platform does not currently support daemon mode.") + Ok(Self) } - pub async fn send_command(&mut self, _: DaemonCommand) -> Result<()> { + pub async fn send_command(&mut self, command: DaemonCommand) -> Result { unimplemented!("This platform does not currently support daemon mode.") } } diff --git a/burrow/src/daemon/response.rs b/burrow/src/daemon/response.rs index 386da46..172d4c7 100644 --- a/burrow/src/daemon/response.rs +++ b/burrow/src/daemon/response.rs @@ -6,7 +6,7 @@ use tun::TunInterface; pub struct DaemonResponse { // Error types can't be serialized, so this is the second best option. pub result: Result, - pub id: u32, + pub id: u64, } impl DaemonResponse { @@ -25,7 +25,7 @@ impl From for DaemonResponse { } impl DaemonResponse { - pub fn with_id(self, id: u32) -> Self { + pub fn with_id(self, id: u64) -> Self { Self { id, ..self } } } diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index 3dfc4ac..c5406b2 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -3,6 +3,10 @@ pub mod wireguard; #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod daemon; +pub(crate) mod tracing; + +#[cfg(target_vendor = "apple")] +pub use daemon::apple::spawn_in_process; #[cfg(any(target_os = "linux", target_vendor = "apple"))] pub use daemon::{ DaemonClient, @@ -12,9 +16,3 @@ pub use daemon::{ DaemonStartOptions, ServerInfo, }; - -#[cfg(target_vendor = "apple")] -mod apple; - -#[cfg(target_vendor = "apple")] -pub use apple::*; diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 18eaf77..79bb70b 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -1,14 +1,9 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use clap::{Args, Parser, Subcommand}; -use tracing::instrument; -use tracing_log::LogTracer; -use tracing_oslog::OsLogger; -use tracing_subscriber::{prelude::*, EnvFilter, FmtSubscriber}; -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -use tun::TunInterface; #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod daemon; +pub(crate) mod tracing; #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod wireguard; @@ -39,8 +34,6 @@ struct Cli { enum Commands { /// Start Burrow Start(StartArgs), - /// Retrieve the file descriptor of the tun interface - Retrieve(RetrieveArgs), /// Stop Burrow daemon Stop, /// Start Burrow daemon @@ -54,9 +47,6 @@ enum Commands { #[derive(Args)] struct StartArgs {} -#[derive(Args)] -struct RetrieveArgs {} - #[derive(Args)] struct DaemonArgs {} @@ -71,57 +61,6 @@ async fn try_start() -> Result<()> { .map(|_| ()) } -#[cfg(target_vendor = "apple")] -#[instrument] -async fn try_retrieve() -> Result<()> { - LogTracer::init() - .context("Failed to initialize LogTracer") - .unwrap(); - - if cfg!(target_os = "linux") || cfg!(target_vendor = "apple") { - let maybe_layer = system_log().unwrap(); - if let Some(layer) = maybe_layer { - let logger = layer.with_subscriber(FmtSubscriber::new()); - tracing::subscriber::set_global_default(logger) - .context("Failed to set the global tracing subscriber") - .unwrap(); - } - } - - let iface2 = TunInterface::retrieve().ok_or(anyhow::anyhow!("No interface found"))?; - tracing::info!("{:?}", iface2); - Ok(()) -} - -async fn initialize_tracing() -> Result<()> { - LogTracer::init().context("Failed to initialize LogTracer")?; - - #[cfg(any(target_os = "linux", target_vendor = "apple"))] - { - let maybe_layer = system_log()?; - if let Some(layer) = maybe_layer { - let registry = tracing_subscriber::registry() - .with(layer) - .with(tracing_subscriber::fmt::layer() - .with_line_number(true) - .with_filter(EnvFilter::from_default_env()) - ); - - #[cfg(feature = "tokio-console")] - let registry = registry.with( - console_subscriber::spawn() - .with_filter(EnvFilter::from_default_env() - .add_directive("tokio=trace".parse()?) - .add_directive("runtime=trace".parse()?) - ) - ); - - tracing::subscriber::set_global_default(registry).context("Failed to set the global tracing subscriber")?; - } - } - Ok(()) -} - #[cfg(any(target_os = "linux", target_vendor = "apple"))] async fn try_stop() -> Result<()> { let mut client = DaemonClient::new().await?; @@ -176,11 +115,6 @@ async fn try_start() -> Result<()> { Ok(()) } -#[cfg(not(target_vendor = "apple"))] -async fn try_retrieve() -> Result<()> { - Ok(()) -} - #[cfg(not(any(target_os = "linux", target_vendor = "apple")))] async fn try_stop() -> Result<()> { Ok(()) @@ -195,26 +129,17 @@ async fn try_serverinfo() -> Result<()> { async fn try_serverconfig() -> Result<()> { Ok(()) } + #[cfg(any(target_os = "linux", target_vendor = "apple"))] #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { - initialize_tracing().await?; - tracing::info!("Platform: {}", std::env::consts::OS); + tracing::initialize(); let cli = Cli::parse(); match &cli.command { - Commands::Start(..) => { - try_start().await?; - tracing::info!("FINISHED"); - } - Commands::Retrieve(..) => { - try_retrieve().await?; - tracing::info!("FINISHED"); - } - Commands::Stop => { - try_stop().await?; - } - Commands::Daemon(_) => daemon::daemon_main(None).await?, + Commands::Start(..) => try_start().await?, + Commands::Stop => try_stop().await?, + Commands::Daemon(_) => daemon::daemon_main(None, None).await?, Commands::ServerInfo => try_serverinfo().await?, Commands::ServerConfig => try_serverconfig().await?, } @@ -222,23 +147,6 @@ async fn main() -> Result<()> { Ok(()) } -#[cfg(target_os = "linux")] -fn system_log() -> Result> { - let maybe_journald = tracing_journald::layer(); - match maybe_journald { - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - tracing::trace!("journald not found"); - Ok(None) - } - _ => Ok(Some(maybe_journald?)), - } -} - -#[cfg(target_vendor = "apple")] -fn system_log() -> Result> { - Ok(Some(OsLogger::new("com.hackclub.burrow", "burrow-cli"))) -} - #[cfg(not(any(target_os = "linux", target_vendor = "apple")))] pub fn main() { eprintln!("This platform is not supported currently.") diff --git a/burrow/src/tracing.rs b/burrow/src/tracing.rs new file mode 100644 index 0000000..279f45d --- /dev/null +++ b/burrow/src/tracing.rs @@ -0,0 +1,63 @@ +use std::sync::Once; + +use tracing::{error, info}; +use tracing_subscriber::{ + layer::{Layer, SubscriberExt}, + EnvFilter, + Registry, +}; + +static TRACING: Once = Once::new(); + +pub fn initialize() { + TRACING.call_once(|| { + if let Err(e) = tracing_log::LogTracer::init() { + error!("Failed to initialize LogTracer: {}", e); + } + + #[cfg(target_os = "windows")] + let system_log = Some(tracing_subscriber::fmt::layer()); + + #[cfg(target_os = "linux")] + let system_log = match tracing_journald::layer() { + Ok(layer) => Some(layer), + Err(e) => { + if e.kind() != std::io::ErrorKind::NotFound { + error!("Failed to initialize journald: {}", e); + } + None + } + }; + + #[cfg(target_vendor = "apple")] + let system_log = Some(tracing_oslog::OsLogger::new( + "com.hackclub.burrow", + "tracing", + )); + + let stderr = (console::user_attended_stderr() || system_log.is_none()).then(|| { + tracing_subscriber::fmt::layer() + .with_level(true) + .with_writer(std::io::stderr) + .compact() + .with_filter(EnvFilter::from_default_env()) + }); + + let subscriber = Registry::default().with(stderr).with(system_log); + + #[cfg(feature = "tokio-console")] + let subscriber = subscriber.with( + console_subscriber::spawn().with_filter( + EnvFilter::from_default_env() + .add_directive("tokio=trace".parse().unwrap()) + .add_directive("runtime=trace".parse().unwrap()), + ), + ); + + if let Err(e) = tracing::subscriber::set_global_default(subscriber) { + error!("Failed to initialize logging: {}", e); + } + + info!("Initialized logging") + }); +} diff --git a/burrow/src/wireguard/iface.rs b/burrow/src/wireguard/iface.rs index ba175de..620c96c 100755 --- a/burrow/src/wireguard/iface.rs +++ b/burrow/src/wireguard/iface.rs @@ -153,7 +153,13 @@ impl Interface { let mut buf = [0u8; 65535]; loop { tokio::time::sleep(tokio::time::Duration::from_millis(250)).await; - pcb.update_timers(&mut buf).await; + match pcb.update_timers(&mut buf).await { + Ok(..) => (), + Err(e) => { + error!("Failed to update timers: {}", e); + return + } + } } }; @@ -167,7 +173,7 @@ impl Interface { tsks.extend(vec![ tokio::spawn(main_tsk), tokio::spawn(update_timers_tsk), - tokio::spawn(reset_rate_limiter_tsk) + tokio::spawn(reset_rate_limiter_tsk), ]); debug!("task made.."); } diff --git a/burrow/src/wireguard/mod.rs b/burrow/src/wireguard/mod.rs index b2e7b54..15563fb 100755 --- a/burrow/src/wireguard/mod.rs +++ b/burrow/src/wireguard/mod.rs @@ -8,4 +8,3 @@ pub use config::Config; pub use iface::Interface; pub use pcb::PeerPcb; pub use peer::Peer; -pub use x25519_dalek::{PublicKey, StaticSecret}; diff --git a/burrow/src/wireguard/noise/mod.rs b/burrow/src/wireguard/noise/mod.rs index 24f4fbb..aa06652 100755 --- a/burrow/src/wireguard/noise/mod.rs +++ b/burrow/src/wireguard/noise/mod.rs @@ -44,13 +44,7 @@ const MAX_QUEUE_DEPTH: usize = 256; const N_SESSIONS: usize = 8; pub mod x25519 { - pub use x25519_dalek::{ - EphemeralSecret, - PublicKey, - ReusableSecret, - SharedSecret, - StaticSecret, - }; + pub use x25519_dalek::{PublicKey, ReusableSecret, SharedSecret, StaticSecret}; } #[derive(Debug)] diff --git a/burrow/src/wireguard/pcb.rs b/burrow/src/wireguard/pcb.rs index 13a2df1..db57968 100755 --- a/burrow/src/wireguard/pcb.rs +++ b/burrow/src/wireguard/pcb.rs @@ -1,19 +1,17 @@ use std::{net::SocketAddr, sync::Arc}; -use std::time::Duration; use anyhow::{Error, Result}; use fehler::throws; use ip_network::IpNetwork; use rand::random; -use tokio::{net::UdpSocket, sync::RwLock, task::JoinHandle, time::timeout}; -use tokio::io::AsyncWrite; +use tokio::{net::UdpSocket, sync::RwLock, task::JoinHandle}; use tun::tokio::TunInterface; -use crate::wireguard::noise::errors::WireGuardError; use super::{ noise::{TunnResult, Tunnel}, Peer, }; +use crate::wireguard::noise::errors::WireGuardError; #[derive(Debug)] pub struct PeerPcb { @@ -95,7 +93,13 @@ impl PeerPcb { TunnResult::WriteToNetwork(packet) => { tracing::debug!("WriteToNetwork: {:?}", packet); self.open_if_closed().await?; - self.socket.read().await.as_ref().unwrap().send(packet).await?; + self.socket + .read() + .await + .as_ref() + .unwrap() + .send(packet) + .await?; tracing::debug!("WriteToNetwork done"); res_dat = &[]; continue @@ -143,8 +147,7 @@ impl PeerPcb { pub async fn update_timers(&self, dst: &mut [u8]) -> Result<(), Error> { match self.tunnel.write().await.update_timers(dst) { TunnResult::Done => {} - TunnResult::Err(WireGuardError::ConnectionExpired) => { - } + TunnResult::Err(WireGuardError::ConnectionExpired) => {} TunnResult::Err(e) => { tracing::error!(message = "Update timers error", error = ?e) } diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index e72fb06..2787cde 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -16,7 +16,6 @@ pub mod sys; use kern_control::SysControlSocket; -pub use super::queue::TunQueue; use super::{ifname_to_string, string_to_ifname}; use crate::TunOptions; From 44ecf042a373291ec9ea73cfbb1b4eadc9e67242 Mon Sep 17 00:00:00 2001 From: dav Date: Sun, 28 Jan 2024 14:09:41 -0800 Subject: [PATCH 103/128] GTK App Getting Started Instructions --- docs/GTK_APP.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/GTK_APP.md diff --git a/docs/GTK_APP.md b/docs/GTK_APP.md new file mode 100644 index 0000000..9b103a3 --- /dev/null +++ b/docs/GTK_APP.md @@ -0,0 +1,85 @@ +# Linux GTK App Getting Started + +## Dependencies + +### Install Build Dependencies + +
+ Fedora + + 1. Install build dependencies + + ``` + sudo dnf install clang ninja cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib + ``` + + 2. Install flatpak builder (Optional) + + ``` + sudo dnf install flatpak-builder + ``` + +
+ +### Flatpak Build Dependencies (Optional) + +``` +flatpak install --user \ + org.gnome.Platform/x86_64/45 \ + org.freedesktop.Sdk.Extension.rust-stable/x86_64/23.08 +``` + +## Building + +
+ General + + 1. Enter the `burrow-gtk` + + ```bash + cd burrow-gtk + ``` + + 2. Perform the meson build + ``` + meson setup build + meson compile -C build + ``` + +
+ +
+ Flatpak + + 1. Compile and install the flatpak + + ``` + flatpak-builder + --user --install --force-clean --disable-rofiles-fuse \ + flatpak_debug/ \ + burrow-gtk/build-aux/com.hackclub.burrow.devel.json + ``` + +
+ + +## Running + +
+ General + + The compiled binary can be found in `build/src/burrow-gtk`. + + ``` + ./build/src/burrow-gtk + ``` +
+ +
+ Flatpak + + ``` + flatpak run com.hackclub.burrow-devel + ``` + +
From ab73183b2bdeaaa28584404074c46f3584b4a097 Mon Sep 17 00:00:00 2001 From: David Zhong <91637806+davnotdev@users.noreply.github.com> Date: Thu, 15 Feb 2024 19:27:14 -0800 Subject: [PATCH 104/128] Add ability to build GTK app AppImage (#240) #238 Add AppImage build support Implements - Downgrade to libadwaita 1.3 for wider distro support - Add build script, workflow, and docs for AppImage - Add build docs for Debian (apt) and Void Linux - Building AppImage in CI --- .github/workflows/build-appimage.yml | 23 ++ .github/workflows/build-flatpak.yml | 5 +- burrow-gtk/Cargo.lock | 255 ++++++++++--------- burrow-gtk/Cargo.toml | 2 +- burrow-gtk/build-aux/Dockerfile | 18 ++ burrow-gtk/build-aux/build_appimage.sh | 28 ++ burrow-gtk/meson.build | 4 +- burrow-gtk/src/components/app.rs | 22 +- burrow-gtk/src/components/settings_screen.rs | 2 +- burrow-gtk/src/components/switch_screen.rs | 2 +- docs/GTK_APP.md | 97 ++++++- 11 files changed, 311 insertions(+), 147 deletions(-) create mode 100644 .github/workflows/build-appimage.yml create mode 100644 burrow-gtk/build-aux/Dockerfile create mode 100755 burrow-gtk/build-aux/build_appimage.sh diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml new file mode 100644 index 0000000..ef5c525 --- /dev/null +++ b/.github/workflows/build-appimage.yml @@ -0,0 +1,23 @@ +name: Build AppImage +on: + push: + branches: [main] + pull_request: +jobs: + appimage: + name: Build AppImage + runs-on: ubuntu-latest + container: docker + steps: + - uses: actions/checkout@v4 + - name: Build AppImage + run: | + docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile + docker create --name temp appimage-builder + docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . + docker rm temp + - uses: actions/upload-artifact@v4 + with: + name: AppImage + path: Burrow-x86_64.AppImage + diff --git a/.github/workflows/build-flatpak.yml b/.github/workflows/build-flatpak.yml index e0e804e..d74eec3 100644 --- a/.github/workflows/build-flatpak.yml +++ b/.github/workflows/build-flatpak.yml @@ -1,7 +1,4 @@ -on: - push: - branches: [main] - pull_request: +on: workflow_dispatch name: Build Flatpak jobs: flatpak: diff --git a/burrow-gtk/Cargo.lock b/burrow-gtk/Cargo.lock index d0b7009..6721318 100644 --- a/burrow-gtk/Cargo.lock +++ b/burrow-gtk/Cargo.lock @@ -257,16 +257,16 @@ dependencies = [ "caps", "chacha20poly1305", "clap", - "env_logger", + "console", "fehler", "futures", "hmac", "ip_network", "ip_network_table", - "ipnet", "libsystemd", "log", - "nix", + "nix 0.27.1", + "once_cell", "parking_lot", "rand", "rand_core", @@ -281,7 +281,6 @@ dependencies = [ "tracing-oslog", "tracing-subscriber", "tun", - "uuid", "x25519-dalek", ] @@ -332,11 +331,11 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.18.5" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +checksum = "ab3603c4028a5e368d09b51c8b624b9a46edcd7c3778284077a6125af73c9f0a" dependencies = [ - "bitflags 2.4.2", + "bitflags 1.3.2", "cairo-sys-rs", "glib", "libc", @@ -346,9 +345,9 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.18.2" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +checksum = "691d0c66b1fb4881be80a760cb8fe76ea97218312f9dfe2c9cc0f496ca279cb1" dependencies = [ "glib-sys", "libc", @@ -501,6 +500,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -617,6 +629,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -626,19 +644,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -730,13 +735,14 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.10.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" dependencies = [ "futures-core", "futures-sink", "nanorand", + "pin-project", "spin", ] @@ -867,10 +873,11 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.18.5" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +checksum = "695d6bc846438c5708b07007537b9274d883373dd30858ca881d7d71b5540717" dependencies = [ + "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -880,9 +887,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.18.0" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +checksum = "9285ec3c113c66d7d0ab5676599176f1f42f4944ca1b581852215bf5694870cb" dependencies = [ "gio-sys", "glib-sys", @@ -893,10 +900,11 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.7.3" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edb019ad581f8ecf8ea8e4baa6df7c483a95b5a59be3140be6a9c3b0c632af6" +checksum = "c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff" dependencies = [ + "bitflags 1.3.2", "cairo-rs", "gdk-pixbuf", "gdk4-sys", @@ -908,9 +916,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.7.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbab43f332a3cf1df9974da690b5bb0e26720ed09a228178ce52175372dcfef0" +checksum = "1bc92aa1608c089c49393d014c38ac0390d01e4841e1fedaa75dbcef77aaed64" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -974,10 +982,11 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gio" -version = "0.18.4" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +checksum = "a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a" dependencies = [ + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", @@ -993,9 +1002,9 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.18.1" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" dependencies = [ "glib-sys", "gobject-sys", @@ -1006,11 +1015,11 @@ dependencies = [ [[package]] name = "glib" -version = "0.18.5" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" dependencies = [ - "bitflags 2.4.2", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-executor", @@ -1035,23 +1044,24 @@ checksum = "3431c56f463443cba9bc3600248bc6d680cb614c2ee1cdd39dab5415bd12ac5c" [[package]] name = "glib-macros" -version = "0.18.5" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" dependencies = [ + "anyhow", "heck", - "proc-macro-crate 2.0.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.48", + "syn 1.0.109", ] [[package]] name = "glib-sys" -version = "0.18.1" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0" dependencies = [ "libc", "system-deps", @@ -1065,9 +1075,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.18.0" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" dependencies = [ "glib-sys", "libc", @@ -1076,9 +1086,9 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.18.1" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2228cda1505613a7a956cca69076892cfbda84fc2b7a62b94a41a272c0c401" +checksum = "def4bb01265b59ed548b05455040d272d989b3012c42d4c1bbd39083cb9b40d9" dependencies = [ "glib", "graphene-sys", @@ -1087,9 +1097,9 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.18.1" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4144cee8fc8788f2a9b73dc5f1d4e1189d1f95305c4cb7bd9c1af1cfa31f59" +checksum = "1856fc817e6a6675e36cea0bd9a3afe296f5d9709d1e2d3182803ac77f0ab21d" dependencies = [ "glib-sys", "libc", @@ -1099,10 +1109,11 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.7.3" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d958e351d2f210309b32d081c832d7de0aca0b077aa10d88336c6379bd01f7e" +checksum = "6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c" dependencies = [ + "bitflags 1.3.2", "cairo-rs", "gdk4", "glib", @@ -1114,9 +1125,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.7.3" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bd9e3effea989f020e8f1ff3fa3b8c63ba93d43b899c11a118868853a56d55" +checksum = "c07a84fb4dcf1323d29435aa85e2f5f58bef564342bef06775ec7bd0da1f01b0" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -1130,10 +1141,11 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.7.3" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb51aa3e9728575a053e1f43543cd9992ac2477e1b186ad824fd4adfb70842" +checksum = "b28a32a04cd75cef14a0983f8b0c669e0fe152a0a7725accdeb594e2c764c88b" dependencies = [ + "bitflags 1.3.2", "cairo-rs", "field-offset", "futures-channel", @@ -1146,17 +1158,18 @@ dependencies = [ "gtk4-macros", "gtk4-sys", "libc", + "once_cell", "pango", ] [[package]] name = "gtk4-macros" -version = "0.7.2" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d57ec49cf9b657f69a05bca8027cff0a8dfd0c49e812be026fc7311f2163832f" +checksum = "6a4d6b61570f76d3ee542d984da443b1cd69b6105264c61afec3abed08c2500f" dependencies = [ "anyhow", - "proc-macro-crate 1.3.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -1165,9 +1178,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.7.3" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54d8c4aa23638ce9faa2caf7e2a27d4a1295af2155c8e8d28c4d4eeca7a65eb8" +checksum = "5f8283f707b07e019e76c7f2934bdd4180c277e08aa93f4c0d8dd07b7a34e22f" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1277,12 +1290,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.28" @@ -1376,20 +1383,6 @@ name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -dependencies = [ - "serde", -] - -[[package]] -name = "is-terminal" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys 0.52.0", -] [[package]] name = "itoa" @@ -1429,10 +1422,11 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libadwaita" -version = "0.5.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe7e70c06507ed10a16cda707f358fbe60fe0dc237498f78c686ade92fd979c" +checksum = "1ab9c0843f9f23ff25634df2743690c3a1faffe0a190e60c490878517eb81abf" dependencies = [ + "bitflags 1.3.2", "gdk-pixbuf", "gdk4", "gio", @@ -1445,9 +1439,9 @@ dependencies = [ [[package]] name = "libadwaita-sys" -version = "0.5.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e10aaa38de1d53374f90deeb4535209adc40cc5dba37f9704724169bceec69a" +checksum = "4231cb2499a9f0c4cdfa4885414b33e39901ddcac61150bc0bb4ff8a57ede404" dependencies = [ "gdk4-sys", "gio-sys", @@ -1487,14 +1481,14 @@ dependencies = [ [[package]] name = "libsystemd" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b9597a67aa1c81a6624603e6bd0bcefb9e0f94c9c54970ec53771082104b4e" +checksum = "c592dc396b464005f78a5853555b9f240bc5378bf5221acc4e129910b2678869" dependencies = [ "hmac", "libc", "log", - "nix", + "nix 0.27.1", "nom", "once_cell", "serde", @@ -1675,6 +1669,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", + "memoffset 0.9.0", +] + [[package]] name = "nom" version = "7.1.3" @@ -1807,10 +1813,11 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pango" -version = "0.18.3" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +checksum = "35be456fc620e61f62dff7ff70fbd54dcbaf0a4b920c0f16de1107c47d921d48" dependencies = [ + "bitflags 1.3.2", "gio", "glib", "libc", @@ -1820,9 +1827,9 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.18.0" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +checksum = "3da69f9f3850b0d8990d462f8c709561975e95f689c1cdf0fecdebde78b35195" dependencies = [ "glib-sys", "gobject-sys", @@ -1894,6 +1901,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1961,16 +1988,6 @@ dependencies = [ "toml_edit 0.19.15", ] -[[package]] -name = "proc-macro-crate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" -dependencies = [ - "toml_datetime", - "toml_edit 0.20.2", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2098,8 +2115,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "relm4" -version = "0.7.0-beta.2" -source = "git+https://github.com/Relm4/Relm4#e189eee06b887470e0fd65cbaf6d7c0161bed5ea" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c16f3fad883034773b7f5af4d7e865532b8f3641e5a8bab2a34561a8d960d81" dependencies = [ "async-trait", "flume", @@ -2115,8 +2133,9 @@ dependencies = [ [[package]] name = "relm4-macros" -version = "0.7.0-beta.2" -source = "git+https://github.com/Relm4/Relm4#e189eee06b887470e0fd65cbaf6d7c0161bed5ea" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9340e2553c0a184a80a0bfa1dcf73c47f3d48933aa6be90724b202f9fbd24735" dependencies = [ "proc-macro2", "quote", @@ -2547,15 +2566,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.56" @@ -2633,6 +2643,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.5", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] @@ -2839,7 +2850,7 @@ dependencies = [ "libc", "libloading 0.7.4", "log", - "nix", + "nix 0.26.4", "reqwest", "schemars", "serde", @@ -2925,7 +2936,6 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ - "getrandom", "serde", ] @@ -3078,15 +3088,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/burrow-gtk/Cargo.toml b/burrow-gtk/Cargo.toml index 244c161..21cb52e 100644 --- a/burrow-gtk/Cargo.toml +++ b/burrow-gtk/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] anyhow = "1.0" -relm4 = { features = ["libadwaita", "gnome_45"], git = "https://github.com/Relm4/Relm4" } +relm4 = { version = "0.6", features = ["libadwaita", "gnome_44"]} burrow = { version = "*", path = "../burrow/" } tokio = { version = "1.35.0", features = ["time", "sync"] } gettext-rs = { version = "0.7.0", features = ["gettext-system"] } diff --git a/burrow-gtk/build-aux/Dockerfile b/burrow-gtk/build-aux/Dockerfile new file mode 100644 index 0000000..df07c4a --- /dev/null +++ b/burrow-gtk/build-aux/Dockerfile @@ -0,0 +1,18 @@ +FROM fedora:39 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN set -eux && \ + dnf update -y && \ + dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal +ENV PATH="/root/.cargo/bin:${PATH}" + +WORKDIR /app +COPY . /app + +RUN cd /app/burrow-gtk/ && \ + ./build-aux/build_appimage.sh + + diff --git a/burrow-gtk/build-aux/build_appimage.sh b/burrow-gtk/build-aux/build_appimage.sh new file mode 100755 index 0000000..248cca7 --- /dev/null +++ b/burrow-gtk/build-aux/build_appimage.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -ex + +BURROW_GTK_ROOT="$(readlink -f $(dirname -- "$(readlink -f -- "$BASH_SOURCE")")/..)" +BURROW_GTK_BUILD="$BURROW_GTK_ROOT/build-appimage" +LINUXDEPLOY_VERSION="${LINUXDEPLOY_VERSION:-"1-alpha-20240109-1"}" + +if [ "$BURROW_GTK_ROOT" != $(pwd) ]; then + echo "Make sure to cd into burrow-gtk" + exit 1 +fi + +ARCHITECTURE=$(lscpu | grep Architecture | awk '{print $2}') + +if [ "$ARCHITECTURE" == "x86_64" ]; then + wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/$LINUXDEPLOY_VERSION/linuxdeploy-x86_64.AppImage" -o /dev/null -O /tmp/linuxdeploy + chmod a+x /tmp/linuxdeploy +elif [ "$ARCHITECTURE" == "aarch64" ]; then + wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/$LINUXDEPLOY_VERSION/linuxdeploy-aarch64.AppImage" -o /dev/null -O /tmp/linuxdeploy + chmod a+x /tmp/linuxdeploy +fi + +meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr +meson compile -C $BURROW_GTK_BUILD +DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD +/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir --output appimage +mv *.AppImage $BURROW_GTK_BUILD diff --git a/burrow-gtk/meson.build b/burrow-gtk/meson.build index 70d8403..8c2d5c1 100644 --- a/burrow-gtk/meson.build +++ b/burrow-gtk/meson.build @@ -34,8 +34,8 @@ i18n = import('i18n') gnome = import('gnome') # External Dependencies -dependency('gtk4', version: '>= 4.12') -dependency('libadwaita-1', version: '>= 1.4') +dependency('gtk4', version: '>= 4.0') +dependency('libadwaita-1', version: '>= 1.2') glib_compile_resources = find_program('glib-compile-resources', required: true) glib_compile_schemas = find_program('glib-compile-schemas', required: true) diff --git a/burrow-gtk/src/components/app.rs b/burrow-gtk/src/components/app.rs index b42b718..57348ef 100644 --- a/burrow-gtk/src/components/app.rs +++ b/burrow-gtk/src/components/app.rs @@ -81,16 +81,28 @@ impl AsyncComponent for App { let view_switcher_bar = adw::ViewSwitcherBar::builder().stack(&view_stack).build(); view_switcher_bar.set_reveal(true); - let toolbar = adw::ToolbarView::new(); - toolbar.add_top_bar( + // When libadwaita 1.4 support becomes more avaliable, this approach is more appropriate + // + // let toolbar = adw::ToolbarView::new(); + // toolbar.add_top_bar( + // &adw::HeaderBar::builder() + // .title_widget(>k::Label::new(Some("Burrow"))) + // .build(), + // ); + // toolbar.add_bottom_bar(&view_switcher_bar); + // toolbar.set_content(Some(&view_stack)); + // root.set_content(Some(&toolbar)); + + let content = gtk::Box::new(gtk::Orientation::Vertical, 0); + content.append( &adw::HeaderBar::builder() .title_widget(>k::Label::new(Some("Burrow"))) .build(), ); - toolbar.add_bottom_bar(&view_switcher_bar); - toolbar.set_content(Some(&view_stack)); + content.append(&view_stack); + content.append(&view_switcher_bar); - root.set_content(Some(&toolbar)); + root.set_content(Some(&content)); sender.input(AppMsg::PostInit); diff --git a/burrow-gtk/src/components/settings_screen.rs b/burrow-gtk/src/components/settings_screen.rs index 778eb84..0a29e43 100644 --- a/burrow-gtk/src/components/settings_screen.rs +++ b/burrow-gtk/src/components/settings_screen.rs @@ -21,7 +21,7 @@ impl SimpleComponent for SettingsScreen { fn init( init: Self::Init, - root: Self::Root, + root: &Self::Root, sender: ComponentSender, ) -> ComponentParts { let diag_group = settings::DiagGroup::builder() diff --git a/burrow-gtk/src/components/switch_screen.rs b/burrow-gtk/src/components/switch_screen.rs index a296c09..f660536 100644 --- a/burrow-gtk/src/components/switch_screen.rs +++ b/burrow-gtk/src/components/switch_screen.rs @@ -29,7 +29,7 @@ impl AsyncComponent for SwitchScreen { view! { gtk::Box { set_orientation: gtk::Orientation::Vertical, - set_valign: Align::BaselineFill, + set_valign: Align::Fill, gtk::Box { set_orientation: gtk::Orientation::Vertical, diff --git a/docs/GTK_APP.md b/docs/GTK_APP.md index 9b103a3..ef73d2b 100644 --- a/docs/GTK_APP.md +++ b/docs/GTK_APP.md @@ -1,22 +1,79 @@ # Linux GTK App Getting Started +Currently, the GTK App can be built as a binary or as an AppImage. +Note that the flatpak version can compile but will not run properly! + ## Dependencies ### Install Build Dependencies
- Fedora + Debian - 1. Install build dependencies + > Note: Burrow currently cannot compile on Debian Stable (Bookworm) due to its outdated dependencies + + 1. Install build dependencies ``` - sudo dnf install clang ninja cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib + sudo apt install -y clang meson cmake pkg-config libgtk-4-dev libadwaita-1-dev gettext desktop-file-utils ``` 2. Install flatpak builder (Optional) ``` - sudo dnf install flatpak-builder + sudo apt install -y flatpak-builder + ``` + + 3. Install AppImage build tools (Optional) + + ``` + sudo apt install -y wget fuse file + ``` + +
+ +
+ Fedora + + 1. Install build dependencies + + ``` + sudo dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib + ``` + + 2. Install flatpak builder (Optional) + + ``` + sudo dnf install -y flatpak-builder + ``` + + 3. Install AppImage build tools (Optional) + + ``` + sudo dnf install -y util-linux wget fuse fuse-libs file + ``` + +
+ +
+ Void Linux (glibc) + + 1. Install build dependencies + + ``` + sudo xbps-install -Sy gcc clang meson cmake pkg-config gtk4-devel gettext desktop-file-utils gtk4-update-icon-cache appstream-glib + ``` + + 2. Install flatpak builder (Optional) + + ``` + sudo xbps-install -Sy flatpak-builder + ``` + + 3. Install AppImage build tools (Optional) + + ``` + sudo xbps-install -Sy wget fuse file ```
@@ -51,10 +108,10 @@ flatpak install --user \
Flatpak - 1. Compile and install the flatpak + 1. Compile and install the flatpak ``` - flatpak-builder + flatpak-builder --user --install --force-clean --disable-rofiles-fuse \ flatpak_debug/ \ burrow-gtk/build-aux/com.hackclub.burrow.devel.json @@ -62,6 +119,23 @@ flatpak install --user \
+
+ AppImage + + 1. Enter the `burrow-gtk` + + ```bash + cd burrow-gtk + ``` + + 2. Compile the AppImage + + ``` + ./build-aux/build_appimage.sh + ``` + +
+ ## Running @@ -83,3 +157,14 @@ flatpak install --user \ ``` + +
+ AppImage + + The compiled binary can be found in `build-appimage/Burrow-*.AppImage`. + + ``` + ./build-appimage/Burrow-*.AppImage + ``` + +
From cca59992147479925a0dbd08dc9bccc80d10d5a3 Mon Sep 17 00:00:00 2001 From: David Zhong <91637806+davnotdev@users.noreply.github.com> Date: Thu, 15 Feb 2024 19:34:15 -0800 Subject: [PATCH 105/128] Bump Rust version in Dockerfile clap-rs recently bumped their MSRV to 1.74 breaking the docker build. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b1500bb..3c12d45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.70.0-slim-bookworm AS builder +FROM docker.io/library/rust:1.74.0-slim-bookworm AS builder ARG TARGETPLATFORM ARG LLVM_VERSION=16 From 2088ae6ede685880ae3f18401812a34e5c0253b9 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Fri, 2 Feb 2024 14:48:13 +0800 Subject: [PATCH 106/128] Add Support for IPV6 and Arbitrary Server Address Add IPV6 support for Apple Devices Note: Works in GUI not CLI Adds Support for Arbitrary Server Address --- Apple/NetworkExtension/DataTypes.swift | 4 +- .../PacketTunnelProvider.swift | 21 +++- Cargo.lock | 16 +-- Dockerfile | 2 +- Makefile | 25 ++++- burrow-server-compose.yml | 38 +++++++ burrow/src/daemon/response.rs | 4 +- ...ommand__daemoncommand_serialization-2.snap | 2 +- ..._command__daemoncommand_serialization.snap | 2 +- ...n__response__response_serialization-4.snap | 2 +- burrow/src/main.rs | 2 +- burrow/src/tracing.rs | 1 + burrow/src/wireguard/config.rs | 10 +- server_patch.txt | 21 ++++ tun/Cargo.toml | 2 +- tun/src/options.rs | 6 +- tun/src/unix/apple/kern_control.rs | 2 +- tun/src/unix/apple/mod.rs | 55 +++++++-- tun/src/unix/apple/sys.rs | 104 +++++++++++++++++- tun/tests/packets.rs | 13 +++ 20 files changed, 276 insertions(+), 56 deletions(-) create mode 100644 burrow-server-compose.yml create mode 100644 server_patch.txt diff --git a/Apple/NetworkExtension/DataTypes.swift b/Apple/NetworkExtension/DataTypes.swift index 391bfed..1409fde 100644 --- a/Apple/NetworkExtension/DataTypes.swift +++ b/Apple/NetworkExtension/DataTypes.swift @@ -31,7 +31,7 @@ struct BurrowStartRequest: Codable { let no_pi: Bool let tun_excl: Bool let tun_retrieve: Bool - let address: String? + let address: [String] } struct StartOptions: Codable { let tun: TunOptions @@ -51,7 +51,7 @@ struct BurrowResult: Codable where T: Codable { struct ServerConfigData: Codable { struct InternalConfig: Codable { - let address: String? + let address: [String] let name: String? let mtu: Int32? } diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 9231676..bfdb34a 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -31,7 +31,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { command: BurrowStartRequest( Start: BurrowStartRequest.StartOptions( tun: BurrowStartRequest.TunOptions( - name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: nil + name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: [] ) ) ) @@ -46,12 +46,21 @@ class PacketTunnelProvider: NEPacketTunnelProvider { private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { let cfig = from.ServerConfig - guard let addr = cfig.address else { - return nil - } - // Using a makeshift remote tunnel address let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") - nst.ipv4Settings = NEIPv4Settings(addresses: [addr], subnetMasks: ["255.255.255.0"]) + var v4Addresses = [String]() + var v6Addresses = [String]() + for addr in cfig.address { + if IPv4Address(addr) != nil { + v6Addresses.append(addr) + } + if IPv6Address(addr) != nil { + v4Addresses.append(addr) + } + } + nst.ipv4Settings = NEIPv4Settings(addresses: v4Addresses, subnetMasks: v4Addresses.map { _ in + "255.255.255.0" + }) + nst.ipv6Settings = NEIPv6Settings(addresses: v6Addresses, networkPrefixLengths: v6Addresses.map { _ in 64 }) logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") return nst } diff --git a/Cargo.lock b/Cargo.lock index 85f11e7..a75bd28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1074,7 +1074,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio", "tower-service", "tracing", @@ -2114,16 +2114,6 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.5" @@ -2305,7 +2295,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -2547,7 +2537,7 @@ dependencies = [ "reqwest", "schemars", "serde", - "socket2 0.4.10", + "socket2", "ssri", "tempfile", "tokio", diff --git a/Dockerfile b/Dockerfile index 3c12d45..9f54478 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.74.0-slim-bookworm AS builder +FROM docker.io/library/rust:1.76.0-slim-bookworm AS builder ARG TARGETPLATFORM ARG LLVM_VERSION=16 diff --git a/Makefile b/Makefile index 18b4b27..97d2d5a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -tun_num := $(shell ifconfig | awk -F 'utun|[: ]' '/utun[0-9]/ {print $$2}' | tail -n 1) +tun := $(shell ifconfig -l | sed 's/ /\n/g' | grep utun | tail -n 1) cargo_console := RUST_BACKTRACE=1 RUST_LOG=debug RUSTFLAGS='--cfg tokio_unstable' cargo run --all-features cargo_norm := RUST_BACKTRACE=1 RUST_LOG=debug cargo run @@ -19,15 +19,28 @@ start: test-dns: @sudo route delete 8.8.8.8 - @sudo route add 8.8.8.8 -interface utun$(tun_num) + @sudo route add 8.8.8.8 -interface $(tun) @dig @8.8.8.8 hackclub.com test-https: @sudo route delete 193.183.0.162 - @sudo route add 193.183.0.162 -interface utun$(tun_num) + @sudo route add 193.183.0.162 -interface $(tun) @curl -vv https://search.marginalia.nu +v4_target := 146.190.62.39 test-http: - @sudo route delete 146.190.62.39 - @sudo route add 146.190.62.39 -interface utun$(tun_num) - @curl -vv 146.190.62.39:80 + @sudo route delete ${v4_target} + @sudo route add ${v4_target} -interface $(tun) + @curl -vv ${v4_target}:80 + +test-ipv4: + @sudo route delete ${v4_target} + @sudo route add ${v4_target} -interface $(tun) + @ping ${v4_target} + +v6_target := 2001:4860:4860::8888 +test-ipv6: + @sudo route delete ${v6_target} + @sudo route -n add -inet6 ${v6_target} -interface $(tun) + @echo preparing + @sudo ping6 -v ${v6_target} diff --git a/burrow-server-compose.yml b/burrow-server-compose.yml new file mode 100644 index 0000000..4ba31ee --- /dev/null +++ b/burrow-server-compose.yml @@ -0,0 +1,38 @@ +version: "2.1" +networks: + wg6: + enable_ipv6: true + ipam: + driver: default + config: + - subnet: "aa:bb:cc:de::/64" +services: + burrow: + image: lscr.io/linuxserver/wireguard:latest + privileged: true + container_name: burrow_server + cap_add: + - NET_ADMIN + - SYS_MODULE + environment: + - PUID=1000 + - PGID=1000 + - TZ=Asia/Calcutta + - SERVERURL=wg.burrow.rs + - SERVERPORT=51820 + - PEERS=10 + - PEERDNS=1.1.1.1 + - INTERNAL_SUBNET=10.13.13.0 + - ALLOWEDIPS=0.0.0.0/0, ::/0 + - PERSISTENTKEEPALIVE_PEERS=all + - LOG_CONFS=true #optional + volumes: + - ./config:/config + - /lib/modules:/lib/modules + ports: + - 51820:51820/udp + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv6.conf.all.disable_ipv6=0 + - net.ipv6.conf.eth0.proxy_ndp=1 + restart: unless-stopped \ No newline at end of file diff --git a/burrow/src/daemon/response.rs b/burrow/src/daemon/response.rs index 172d4c7..37ee5d9 100644 --- a/burrow/src/daemon/response.rs +++ b/burrow/src/daemon/response.rs @@ -57,7 +57,7 @@ impl TryFrom<&TunInterface> for ServerInfo { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct ServerConfig { - pub address: Option, + pub address: Vec, pub name: Option, pub mtu: Option, } @@ -65,7 +65,7 @@ pub struct ServerConfig { impl Default for ServerConfig { fn default() -> Self { Self { - address: Some("10.13.13.2".to_string()), // Dummy remote address + address: vec!["10.13.13.2".to_string()], // Dummy remote address name: None, mtu: None, } diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap index 0eb9096..f78eeaa 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/command.rs expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions {\n tun: TunOptions { ..TunOptions::default() },\n })).unwrap()" --- -{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":null}}} +{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":[]}}} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap index bfd5117..eee563d 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/command.rs expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap()" --- -{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":null}}} +{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":[]}}} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap index 9752ebc..0b9385c 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/response.rs expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig(ServerConfig::default()))))?" --- -{"result":{"Ok":{"ServerConfig":{"address":"10.13.13.2","name":null,"mtu":null}}},"id":0} +{"result":{"Ok":{"ServerConfig":{"address":["10.13.13.2"],"name":null,"mtu":null}}},"id":0} diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 79bb70b..71d1c02 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -55,7 +55,7 @@ async fn try_start() -> Result<()> { let mut client = DaemonClient::new().await?; client .send_command(DaemonCommand::Start(DaemonStartOptions { - tun: TunOptions::new().address("10.13.13.2"), + tun: TunOptions::new().address(vec!["10.13.13.2", "::2"]), })) .await .map(|_| ()) diff --git a/burrow/src/tracing.rs b/burrow/src/tracing.rs index 279f45d..861b41f 100644 --- a/burrow/src/tracing.rs +++ b/burrow/src/tracing.rs @@ -39,6 +39,7 @@ pub fn initialize() { tracing_subscriber::fmt::layer() .with_level(true) .with_writer(std::io::stderr) + .with_line_number(true) .compact() .with_filter(EnvFilter::from_default_env()) }); diff --git a/burrow/src/wireguard/config.rs b/burrow/src/wireguard/config.rs index afe7499..ed7b3cd 100644 --- a/burrow/src/wireguard/config.rs +++ b/burrow/src/wireguard/config.rs @@ -42,7 +42,7 @@ pub struct Peer { pub struct Interface { pub private_key: String, - pub address: String, + pub address: Vec, pub listen_port: u32, pub dns: Vec, pub mtu: Option, @@ -93,8 +93,8 @@ impl Default for Config { fn default() -> Self { Self { interface: Interface { - private_key: "GNqIAOCRxjl/cicZyvkvpTklgQuUmGUIEkH7IXF/sEE=".into(), - address: "10.13.13.2/24".into(), + private_key: "OEPVdomeLTxTIBvv3TYsJRge0Hp9NMiY0sIrhT8OWG8=".into(), + address: vec!["10.13.13.2/24".into()], listen_port: 51820, dns: Default::default(), mtu: Default::default(), @@ -102,8 +102,8 @@ impl Default for Config { peers: vec![Peer { endpoint: "wg.burrow.rs:51820".into(), allowed_ips: vec!["8.8.8.8/32".into(), "0.0.0.0/0".into()], - public_key: "uy75leriJay0+oHLhRMpV+A5xAQ0hCJ+q7Ww81AOvT4=".into(), - preshared_key: Some("s7lx/mg+reVEMnGnqeyYOQkzD86n2+gYnx1M9ygi08k=".into()), + public_key: "8GaFjVO6c4luCHG4ONO+1bFG8tO+Zz5/Gy+Geht1USM=".into(), + preshared_key: Some("ha7j4BjD49sIzyF9SNlbueK0AMHghlj6+u0G3bzC698=".into()), persistent_keepalive: Default::default(), name: Default::default(), }], diff --git a/server_patch.txt b/server_patch.txt new file mode 100644 index 0000000..de8e14c --- /dev/null +++ b/server_patch.txt @@ -0,0 +1,21 @@ +# Add this to ~/server/wg0.conf upon regeneration + +PostUp = iptables -A FORWARD -i %i -j ACCEPT + +PostUp = iptables -A FORWARD -o %i -j ACCEPT + +PostUp = iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE + +PostUp = ip6tables -A FORWARD -i %i -j ACCEPT + +PostUp = ip6tables -A FORWARD -o %i -j ACCEPT + +PostDown = iptables -D FORWARD -i %i -j ACCEPT + +PostDown = iptables -D FORWARD -o %i -j ACCEPT + +PostDown = iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE + +PostDown = ip6tables -D FORWARD -i %i -j ACCEPT + +PostDown = ip6tables -D FORWARD -o %i -j ACCEPT \ No newline at end of file diff --git a/tun/Cargo.toml b/tun/Cargo.toml index e67e45f..7413f65 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" libc = "0.2" fehler = "1.0" nix = { version = "0.26", features = ["ioctl"] } -socket2 = "0.4" +socket2 = "0.5" tokio = { version = "1.28", features = [] } byteorder = "1.4" tracing = "0.1" diff --git a/tun/src/options.rs b/tun/src/options.rs index 339f71a..e21bf5f 100644 --- a/tun/src/options.rs +++ b/tun/src/options.rs @@ -21,7 +21,7 @@ pub struct TunOptions { /// (Apple) Retrieve the tun interface pub tun_retrieve: bool, /// (Linux) The IP address of the tun interface. - pub address: Option, + pub address: Vec, } impl TunOptions { @@ -44,8 +44,8 @@ impl TunOptions { self } - pub fn address(mut self, address: impl ToString) -> Self { - self.address = Some(address.to_string()); + pub fn address(mut self, address: Vec) -> Self { + self.address = address.iter().map(|x| x.to_string()).collect(); self } diff --git a/tun/src/unix/apple/kern_control.rs b/tun/src/unix/apple/kern_control.rs index 76e576f..6075233 100644 --- a/tun/src/unix/apple/kern_control.rs +++ b/tun/src/unix/apple/kern_control.rs @@ -21,7 +21,7 @@ impl SysControlSocket for socket2::Socket { unsafe { sys::resolve_ctl_info(self.as_raw_fd(), &mut info as *mut sys::ctl_info)? }; let (_, addr) = unsafe { - socket2::SockAddr::init(|addr_storage, len| { + socket2::SockAddr::try_init(|addr_storage, len| { *len = size_of::() as u32; let addr: &mut sys::sockaddr_ctl = &mut *addr_storage.cast(); diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 2787cde..6e859ca 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,13 +1,11 @@ -use std::{ - io::{Error, IoSlice}, - mem, - net::{Ipv4Addr, SocketAddrV4}, - os::fd::{AsRawFd, FromRawFd, RawFd}, -}; +use std::{io::{Error, IoSlice}, mem, net::{Ipv4Addr, SocketAddrV4}, os::fd::{AsRawFd, FromRawFd, RawFd}, ptr}; +use std::net::{IpAddr, Ipv6Addr, SocketAddrV6}; +use std::ptr::addr_of; use byteorder::{ByteOrder, NetworkEndian}; use fehler::throws; -use libc::{c_char, iovec, writev, AF_INET, AF_INET6}; +use libc::{c_char, iovec, writev, AF_INET, AF_INET6, sockaddr_in6}; +use nix::sys::socket::SockaddrIn6; use socket2::{Domain, SockAddr, Socket, Type}; use tracing::{self, instrument}; @@ -49,7 +47,7 @@ impl TunInterface { pub fn retrieve() -> Option { (3..100) .filter_map(|fd| unsafe { - let peer_addr = socket2::SockAddr::init(|storage, len| { + let peer_addr = socket2::SockAddr::try_init(|storage, len| { *len = mem::size_of::() as u32; libc::getpeername(fd, storage as *mut _, len); Ok(()) @@ -71,9 +69,12 @@ impl TunInterface { #[throws] fn configure(&self, options: TunOptions) { - if let Some(addr) = options.address { - if let Ok(addr) = addr.parse() { - self.set_ipv4_addr(addr)?; + for addr in options.address{ + if let Ok(addr) = addr.parse::() { + match addr { + IpAddr::V4(addr) => {self.set_ipv4_addr(addr)?} + IpAddr::V6(addr) => {self.set_ipv6_addr(addr)?} + } } } } @@ -117,6 +118,14 @@ impl TunInterface { iff } + #[throws] + #[instrument] + fn in6_ifreq(&self) -> sys::in6_ifreq { + let mut iff: sys::in6_ifreq = unsafe { mem::zeroed() }; + iff.ifr_name = string_to_ifname(&self.name()?); + iff + } + #[throws] #[instrument] pub fn set_ipv4_addr(&self, addr: Ipv4Addr) { @@ -136,6 +145,21 @@ impl TunInterface { Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)) } + #[throws] + pub fn set_ipv6_addr(&self, addr: Ipv6Addr) { + // let addr = SockAddr::from(SocketAddrV6::new(addr, 0, 0, 0)); + // println!("addr: {:?}", addr); + // let mut iff = self.in6_ifreq()?; + // let sto = addr.as_storage(); + // let ifadddr_ptr: *const sockaddr_in6 = addr_of!(sto).cast(); + // iff.ifr_ifru.ifru_addr = unsafe { *ifadddr_ptr }; + // println!("ifru addr set"); + // println!("{:?}", sys::SIOCSIFADDR_IN6); + // self.perform6(|fd| unsafe { sys::if_set_addr6(fd, &iff) })?; + // tracing::info!("ipv6_addr_set"); + tracing::warn!("Setting IPV6 address on MacOS CLI mode is not supported yet."); + } + #[throws] fn perform(&self, perform: impl FnOnce(RawFd) -> Result) -> R { let span = tracing::info_span!("perform", fd = self.as_raw_fd()); @@ -145,6 +169,15 @@ impl TunInterface { perform(socket.as_raw_fd())? } + #[throws] + fn perform6(&self, perform: impl FnOnce(RawFd) -> Result) -> R { + let span = tracing::info_span!("perform6", fd = self.as_raw_fd()); + let _enter = span.enter(); + + let socket = Socket::new(Domain::IPV6, Type::DGRAM, None)?; + perform(socket.as_raw_fd())? + } + #[throws] #[instrument] pub fn mtu(&self) -> i32 { diff --git a/tun/src/unix/apple/sys.rs b/tun/src/unix/apple/sys.rs index b4d4a6a..d48d6ee 100644 --- a/tun/src/unix/apple/sys.rs +++ b/tun/src/unix/apple/sys.rs @@ -1,6 +1,6 @@ use std::mem; -use libc::{c_char, c_int, c_short, c_uint, c_ulong, sockaddr}; +use libc::{c_char, c_int, c_short, c_uint, c_ulong, sockaddr, sockaddr_in6, time_t}; pub use libc::{ c_void, sockaddr_ctl, @@ -23,6 +23,7 @@ pub const UTUN_CONTROL_NAME: &str = "com.apple.net.utun_control"; pub const UTUN_OPT_IFNAME: libc::c_int = 2; pub const MAX_KCTL_NAME: usize = 96; +pub const SCOPE6_ID_MAX: usize = 16; #[repr(C)] #[derive(Copy, Clone, Debug)] @@ -74,7 +75,107 @@ pub struct ifreq { pub ifr_ifru: ifr_ifru, } +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct in6_addrlifetime{ + pub ia6t_expire: time_t, + pub ia6t_preferred: time_t, + pub ia6t_vltime: u32, + pub ia6t_pltime: u32, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct in6_ifstat { + pub ifs6_in_receive: u64, + pub ifs6_in_hdrerr: u64, + pub ifs6_in_toobig: u64, + pub ifs6_in_noroute: u64, + pub ifs6_in_addrerr: u64, + pub ifs6_in_protounknown: u64, + pub ifs6_in_truncated: u64, + pub ifs6_in_discard: u64, + pub ifs6_in_deliver: u64, + pub ifs6_out_forward: u64, + pub ifs6_out_request: u64, + pub ifs6_out_discard: u64, + pub ifs6_out_fragok: u64, + pub ifs6_out_fragfail: u64, + pub ifs6_out_fragcreat: u64, + pub ifs6_reass_reqd: u64, + pub ifs6_reass_ok: u64, + pub ifs6_atmfrag_rcvd: u64, + pub ifs6_reass_fail: u64, + pub ifs6_in_mcast: u64, + pub ifs6_out_mcast: u64, + pub ifs6_cantfoward_icmp6: u64, + pub ifs6_addr_expiry_cnt: u64, + pub ifs6_pfx_expiry_cnt: u64, + pub ifs6_defrtr_expiry_cnt: u64, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct icmp6_ifstat { + pub ifs6_in_msg: u64, + pub ifs6_in_error: u64, + pub ifs6_in_dstunreach: u64, + pub ifs6_in_adminprohib: u64, + pub ifs6_in_timeexceed: u64, + pub ifs6_in_paramprob: u64, + pub ifs6_in_pkttoobig: u64, + pub ifs6_in_echo: u64, + pub ifs6_in_echoreply: u64, + pub ifs6_in_routersolicit: u64, + pub ifs6_in_routeradvert: u64, + pub ifs6_in_neighborsolicit: u64, + pub ifs6_in_neighboradvert: u64, + pub ifs6_in_redirect: u64, + pub ifs6_in_mldquery: u64, + pub ifs6_in_mldreport: u64, + pub ifs6_in_mlddone: u64, + pub ifs6_out_msg: u64, + pub ifs6_out_error: u64, + pub ifs6_out_dstunreach: u64, + pub ifs6_out_adminprohib: u64, + pub ifs6_out_timeexceed: u64, + pub ifs6_out_paramprob: u64, + pub ifs6_out_pkttoobig: u64, + pub ifs6_out_echo: u64, + pub ifs6_out_echoreply: u64, + pub ifs6_out_routersolicit: u64, + pub ifs6_out_routeradvert: u64, + pub ifs6_out_neighborsolicit: u64, + pub ifs6_out_neighboradvert: u64, + pub ifs6_out_redirect: u64, + pub ifs6_out_mldquery: u64, + pub ifs6_out_mldreport: u64, + pub ifs6_out_mlddone: u64, +} + +#[repr(C)] +pub union ifr_ifru6 { + pub ifru_addr: sockaddr_in6, + pub ifru_dstaddr: sockaddr_in6, + pub ifru_flags: c_int, + pub ifru_flags6: c_int, + pub ifru_metric: c_int, + pub ifru_intval: c_int, + pub ifru_data: *mut c_char, + pub ifru_lifetime: in6_addrlifetime, // ifru_lifetime + pub ifru_stat: in6_ifstat, + pub ifru_icmp6stat: icmp6_ifstat, + pub ifru_scope_id: [u32; SCOPE6_ID_MAX] +} + +#[repr(C)] +pub struct in6_ifreq { + pub ifr_name: [c_char; IFNAMSIZ], + pub ifr_ifru: ifr_ifru6, +} + pub const SIOCSIFADDR: c_ulong = request_code_write!(b'i', 12, mem::size_of::()); +pub const SIOCSIFADDR_IN6: c_ulong = request_code_write!(b'i', 12, mem::size_of::()); pub const SIOCGIFMTU: c_ulong = request_code_readwrite!(b'i', 51, mem::size_of::()); pub const SIOCSIFMTU: c_ulong = request_code_write!(b'i', 52, mem::size_of::()); pub const SIOCGIFNETMASK: c_ulong = request_code_readwrite!(b'i', 37, mem::size_of::()); @@ -97,5 +198,6 @@ ioctl_read_bad!(if_get_addr, libc::SIOCGIFADDR, ifreq); ioctl_read_bad!(if_get_mtu, SIOCGIFMTU, ifreq); ioctl_read_bad!(if_get_netmask, SIOCGIFNETMASK, ifreq); ioctl_write_ptr_bad!(if_set_addr, SIOCSIFADDR, ifreq); +ioctl_write_ptr_bad!(if_set_addr6, SIOCSIFADDR_IN6, in6_ifreq); ioctl_write_ptr_bad!(if_set_mtu, SIOCSIFMTU, ifreq); ioctl_write_ptr_bad!(if_set_netmask, SIOCSIFNETMASK, ifreq); diff --git a/tun/tests/packets.rs b/tun/tests/packets.rs index 28090a2..80c078b 100644 --- a/tun/tests/packets.rs +++ b/tun/tests/packets.rs @@ -1,4 +1,5 @@ use std::{io::Error, net::Ipv4Addr}; +use std::net::Ipv6Addr; use fehler::throws; use tun::TunInterface; @@ -33,3 +34,15 @@ fn write_packets() { let bytes_written = tun.send(&buf)?; assert_eq!(bytes_written, 1504); } + +#[test] +#[throws] +#[ignore = "requires interactivity"] +#[cfg(not(target_os = "windows"))] +fn set_ipv6() { + let tun = TunInterface::new()?; + println!("tun name: {:?}", tun.name()?); + let targ_addr: Ipv6Addr = "::1".parse().unwrap(); + println!("v6 addr: {:?}", targ_addr); + tun.set_ipv6_addr(targ_addr)?; +} \ No newline at end of file From 29d2bfae3faefe70af393896bd1cdfc9a4174611 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 19 Feb 2024 11:28:00 +0000 Subject: [PATCH 107/128] Remove redundant type annotation --- Apple/NetworkExtension/Client.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apple/NetworkExtension/Client.swift b/Apple/NetworkExtension/Client.swift index a924c29..e7c1bc8 100644 --- a/Apple/NetworkExtension/Client.swift +++ b/Apple/NetworkExtension/Client.swift @@ -5,7 +5,7 @@ import Network final class Client { let connection: NWConnection - private let logger: Logger = Logger.logger(for: Client.self) + private let logger = Logger.logger(for: Client.self) private var generator = SystemRandomNumberGenerator() convenience init() throws { From c4c342dc8b79872989a02b99fc21235dacf30eea Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sun, 11 Feb 2024 03:17:14 +0800 Subject: [PATCH 108/128] Add implementation for stop command This adds implementation for stopping the tunnel via the `Stop` command. --- .../PacketTunnelProvider.swift | 11 +++ Makefile | 3 + burrow/src/daemon/instance.rs | 26 +++---- burrow/src/wireguard/iface.rs | 73 ++++++++++++++----- burrow/src/wireguard/pcb.rs | 6 +- 5 files changed, 81 insertions(+), 38 deletions(-) diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index bfdb34a..7073401 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -44,6 +44,17 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } } + override func stopTunnel(with reason: NEProviderStopReason) async { + do { + let client = try Client() + let command = BurrowRequest(id: 0, command: "Stop") + let data = try await client.request(command, type: Response>.self) + self.logger.log("Stopped client.") + } catch { + self.logger.error("Failed to stop tunnel: \(error)") + } + } + private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { let cfig = from.ServerConfig let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") diff --git a/Makefile b/Makefile index 97d2d5a..d0c9bd9 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,9 @@ daemon: start: @$(cargo_norm) start +stop: + @$(cargo_norm) stop + test-dns: @sudo route delete 8.8.8.8 @sudo route add 8.8.8.8 -interface $(tun) diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index 34e9878..0d3e726 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -21,7 +21,7 @@ enum RunState { pub struct DaemonInstance { rx: async_channel::Receiver, sx: async_channel::Sender, - tun_interface: Option>>, + tun_interface: Arc>>, wg_interface: Arc>, wg_state: RunState, } @@ -36,7 +36,7 @@ impl DaemonInstance { rx, sx, wg_interface, - tun_interface: None, + tun_interface: Arc::new(RwLock::new(None)), wg_state: RunState::Idle, } } @@ -50,15 +50,15 @@ impl DaemonInstance { warn!("Got start, but tun interface already up."); } RunState::Idle => { - let tun_if = Arc::new(RwLock::new(st.tun.open()?)); + let tun_if = st.tun.open()?; + debug!("Setting tun on wg_interface"); + self.wg_interface.read().await.set_tun(tun_if).await; + debug!("tun set on wg_interface"); debug!("Setting tun_interface"); - self.tun_interface = Some(tun_if.clone()); + self.tun_interface = self.wg_interface.read().await.get_tun(); debug!("tun_interface set: {:?}", self.tun_interface); - debug!("Setting tun on wg_interface"); - self.wg_interface.write().await.set_tun(tun_if); - debug!("tun set on wg_interface"); debug!("Cloning wg_interface"); let tmp_wg = self.wg_interface.clone(); @@ -82,22 +82,18 @@ impl DaemonInstance { } Ok(DaemonResponseData::None) } - DaemonCommand::ServerInfo => match &self.tun_interface { + DaemonCommand::ServerInfo => match &self.tun_interface.read().await.as_ref() { None => Ok(DaemonResponseData::None), Some(ti) => { info!("{:?}", ti); Ok(DaemonResponseData::ServerInfo(ServerInfo::try_from( - ti.read().await.inner.get_ref(), + ti.inner.get_ref(), )?)) } }, DaemonCommand::Stop => { - if self.tun_interface.is_some() { - self.tun_interface = None; - info!("Daemon stopping tun interface."); - } else { - warn!("Got stop, but tun interface is not up.") - } + self.wg_interface.read().await.remove_tun().await; + self.wg_state = RunState::Idle; Ok(DaemonResponseData::None) } DaemonCommand::ServerConfig => { diff --git a/burrow/src/wireguard/iface.rs b/burrow/src/wireguard/iface.rs index 620c96c..6097082 100755 --- a/burrow/src/wireguard/iface.rs +++ b/burrow/src/wireguard/iface.rs @@ -1,10 +1,11 @@ use std::{net::IpAddr, sync::Arc}; +use std::ops::Deref; use anyhow::Error; use fehler::throws; use futures::future::join_all; use ip_network_table::IpNetworkTable; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, Notify}; use tracing::{debug, error}; use tun::tokio::TunInterface; @@ -46,9 +47,21 @@ impl FromIterator for IndexedPcbs { } } +enum IfaceStatus { + Running, + Idle +} + pub struct Interface { - tun: Option>>, + tun: Arc>>, pcbs: Arc, + status: Arc>, + stop_notifier: Arc, +} + +async fn is_running(status: Arc>) -> bool { + let st = status.read().await; + matches!(st.deref(), IfaceStatus::Running) } impl Interface { @@ -60,35 +73,54 @@ impl Interface { .collect::>()?; let pcbs = Arc::new(pcbs); - Self { pcbs, tun: None } + Self { pcbs, tun: Arc::new(RwLock::new(None)), status: Arc::new(RwLock::new(IfaceStatus::Idle)), stop_notifier: Arc::new(Notify::new()) } } - pub fn set_tun(&mut self, tun: Arc>) { - self.tun = Some(tun); + pub async fn set_tun(&self, tun: TunInterface) { + debug!("Setting tun interface"); + self.tun.write().await.replace(tun); + let mut st = self.status.write().await; + *st = IfaceStatus::Running; + } + + pub fn get_tun(&self) -> Arc>> { + self.tun.clone() + } + + pub async fn remove_tun(&self){ + let mut st = self.status.write().await; + self.stop_notifier.notify_waiters(); + *st = IfaceStatus::Idle; } pub async fn run(&self) -> anyhow::Result<()> { let pcbs = self.pcbs.clone(); let tun = self .tun - .clone() - .ok_or(anyhow::anyhow!("tun interface does not exist"))?; + .clone(); + let status = self.status.clone(); + let stop_notifier = self.stop_notifier.clone(); log::info!("Starting interface"); let outgoing = async move { - loop { + while is_running(status.clone()).await { let mut buf = [0u8; 3000]; let src = { - let src = match tun.read().await.recv(&mut buf[..]).await { - Ok(len) => &buf[..len], - Err(e) => { - error!("Failed to read from interface: {}", e); - continue - } + let t = tun.read().await; + let Some(_tun) = t.as_ref() else { + continue; }; - debug!("Read {} bytes from interface", src.len()); - src + tokio::select! { + _ = stop_notifier.notified() => continue, + pkg = _tun.recv(&mut buf[..]) => match pkg { + Ok(len) => &buf[..len], + Err(e) => { + error!("Failed to read from interface: {}", e); + continue + } + }, + } }; let dst_addr = match Tunnel::dst_address(src) { @@ -123,8 +155,7 @@ impl Interface { let mut tsks = vec![]; let tun = self .tun - .clone() - .ok_or(anyhow::anyhow!("tun interface does not exist"))?; + .clone(); let outgoing = tokio::task::spawn(outgoing); tsks.push(outgoing); debug!("preparing to spawn read tasks"); @@ -149,9 +180,10 @@ impl Interface { }; let pcb = pcbs.pcbs[i].clone(); + let status = self.status.clone(); let update_timers_tsk = async move { let mut buf = [0u8; 65535]; - loop { + while is_running(status.clone()).await { tokio::time::sleep(tokio::time::Duration::from_millis(250)).await; match pcb.update_timers(&mut buf).await { Ok(..) => (), @@ -164,8 +196,9 @@ impl Interface { }; let pcb = pcbs.pcbs[i].clone(); + let status = self.status.clone(); let reset_rate_limiter_tsk = async move { - loop { + while is_running(status.clone()).await { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; pcb.reset_rate_limiter().await; } diff --git a/burrow/src/wireguard/pcb.rs b/burrow/src/wireguard/pcb.rs index db57968..974d84e 100755 --- a/burrow/src/wireguard/pcb.rs +++ b/burrow/src/wireguard/pcb.rs @@ -54,7 +54,7 @@ impl PeerPcb { Ok(()) } - pub async fn run(&self, tun_interface: Arc>) -> Result<(), Error> { + pub async fn run(&self, tun_interface: Arc>>) -> Result<(), Error> { tracing::debug!("starting read loop for pcb... for {:?}", &self); let rid: i32 = random(); let mut buf: [u8; 3000] = [0u8; 3000]; @@ -106,12 +106,12 @@ impl PeerPcb { } TunnResult::WriteToTunnelV4(packet, addr) => { tracing::debug!("WriteToTunnelV4: {:?}, {:?}", packet, addr); - tun_interface.read().await.send(packet).await?; + tun_interface.read().await.as_ref().ok_or(anyhow::anyhow!("tun interface does not exist"))?.send(packet).await?; break } TunnResult::WriteToTunnelV6(packet, addr) => { tracing::debug!("WriteToTunnelV6: {:?}, {:?}", packet, addr); - tun_interface.read().await.send(packet).await?; + tun_interface.read().await.as_ref().ok_or(anyhow::anyhow!("tun interface does not exist"))?.send(packet).await?; break } } From c755f752a0a621c742011af183627c11eeca6ce7 Mon Sep 17 00:00:00 2001 From: David Zhong <91637806+davnotdev@users.noreply.github.com> Date: Sat, 9 Mar 2024 17:52:59 -0800 Subject: [PATCH 109/128] Implement launching a local daemon (#261) Allow AppImage and non-systemd systems to launch a local burrow daemon. --- README.md | 3 +- burrow-gtk/build-aux/build_appimage.sh | 6 +- burrow-gtk/src/components/app.rs | 21 +++- burrow-gtk/src/components/mod.rs | 1 + .../src/components/settings/daemon_group.rs | 111 ++++++++++++++++++ .../src/components/settings/diag_group.rs | 16 +-- burrow-gtk/src/components/settings/mod.rs | 5 +- burrow-gtk/src/components/settings_screen.rs | 41 +++++-- burrow-gtk/src/diag.rs | 15 ++- 9 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 burrow-gtk/src/components/settings/daemon_group.rs diff --git a/README.md b/README.md index 7492039..89914d0 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@ Burrow is an open source tool for burrowing through firewalls, built by teenager ## Contributing -Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! For more information on how to contribute, please see [CONTRIBUTING.md] +Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! Checkout [GETTING_STARTED.md](./docs/GETTING_STARTED.md) for build instructions and [GTK_APP.md](./docs/GTK_APP.md) for the Linux app. The project structure is divided in the following folders: ``` Apple/ # Xcode project for burrow on macOS and iOS burrow/ # Higher-level API library for tun and tun-async +burrow-gtk/ # GTK project for burrow on Linux tun/ # Low-level interface to OS networking src/ tokio/ # Async/Tokio code diff --git a/burrow-gtk/build-aux/build_appimage.sh b/burrow-gtk/build-aux/build_appimage.sh index 248cca7..cd58c17 100755 --- a/burrow-gtk/build-aux/build_appimage.sh +++ b/burrow-gtk/build-aux/build_appimage.sh @@ -5,6 +5,7 @@ set -ex BURROW_GTK_ROOT="$(readlink -f $(dirname -- "$(readlink -f -- "$BASH_SOURCE")")/..)" BURROW_GTK_BUILD="$BURROW_GTK_ROOT/build-appimage" LINUXDEPLOY_VERSION="${LINUXDEPLOY_VERSION:-"1-alpha-20240109-1"}" +BURROW_BUILD_TYPE="${BURROW_BUILD_TYPE:-"release"}" if [ "$BURROW_GTK_ROOT" != $(pwd) ]; then echo "Make sure to cd into burrow-gtk" @@ -21,8 +22,9 @@ elif [ "$ARCHITECTURE" == "aarch64" ]; then chmod a+x /tmp/linuxdeploy fi -meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr +meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr --buildtype $BURROW_BUILD_TYPE meson compile -C $BURROW_GTK_BUILD DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD -/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir --output appimage +cargo b --$BURROW_BUILD_TYPE --manifest-path=../Cargo.toml +/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir -e $BURROW_GTK_BUILD/../../target/$BURROW_BUILD_TYPE/burrow --output appimage mv *.AppImage $BURROW_GTK_BUILD diff --git a/burrow-gtk/src/components/app.rs b/burrow-gtk/src/components/app.rs index 57348ef..62c98c0 100644 --- a/burrow-gtk/src/components/app.rs +++ b/burrow-gtk/src/components/app.rs @@ -6,7 +6,7 @@ const RECONNECT_POLL_TIME: Duration = Duration::from_secs(5); pub struct App { daemon_client: Arc>>, - _settings_screen: Controller, + settings_screen: Controller, switch_screen: AsyncController, } @@ -109,7 +109,7 @@ impl AsyncComponent for App { let model = App { daemon_client, switch_screen, - _settings_screen: settings_screen, + settings_screen, }; AsyncComponentParts { model, widgets } @@ -132,14 +132,23 @@ impl AsyncComponent for App { disconnected_daemon_client = true; self.switch_screen .emit(switch_screen::SwitchScreenMsg::DaemonDisconnect); + self.settings_screen + .emit(settings_screen::SettingsScreenMsg::DaemonStateChange) } } if disconnected_daemon_client || daemon_client.is_none() { - *daemon_client = DaemonClient::new().await.ok(); - if daemon_client.is_some() { - self.switch_screen - .emit(switch_screen::SwitchScreenMsg::DaemonReconnect); + match DaemonClient::new().await { + Ok(new_daemon_client) => { + *daemon_client = Some(new_daemon_client); + self.switch_screen + .emit(switch_screen::SwitchScreenMsg::DaemonReconnect); + self.settings_screen + .emit(settings_screen::SettingsScreenMsg::DaemonStateChange) + } + Err(_e) => { + // TODO: Handle Error + } } } } diff --git a/burrow-gtk/src/components/mod.rs b/burrow-gtk/src/components/mod.rs index b1cc938..b134809 100644 --- a/burrow-gtk/src/components/mod.rs +++ b/burrow-gtk/src/components/mod.rs @@ -18,3 +18,4 @@ mod settings_screen; mod switch_screen; pub use app::*; +pub use settings::{DaemonGroupMsg, DiagGroupMsg}; diff --git a/burrow-gtk/src/components/settings/daemon_group.rs b/burrow-gtk/src/components/settings/daemon_group.rs new file mode 100644 index 0000000..3817ca6 --- /dev/null +++ b/burrow-gtk/src/components/settings/daemon_group.rs @@ -0,0 +1,111 @@ +use super::*; +use std::process::Command; + +#[derive(Debug)] +pub struct DaemonGroup { + system_setup: SystemSetup, + daemon_client: Arc>>, + already_running: bool, +} + +pub struct DaemonGroupInit { + pub daemon_client: Arc>>, + pub system_setup: SystemSetup, +} + +#[derive(Debug)] +pub enum DaemonGroupMsg { + LaunchLocal, + DaemonStateChange, +} + +#[relm4::component(pub, async)] +impl AsyncComponent for DaemonGroup { + type Init = DaemonGroupInit; + type Input = DaemonGroupMsg; + type Output = (); + type CommandOutput = (); + + view! { + #[name(group)] + adw::PreferencesGroup { + #[watch] + set_sensitive: + (model.system_setup == SystemSetup::AppImage || model.system_setup == SystemSetup::Other) && + !model.already_running, + set_title: "Local Daemon", + set_description: Some("Run Local Daemon"), + + gtk::Button { + set_label: "Launch", + connect_clicked => DaemonGroupMsg::LaunchLocal + } + } + } + + async fn init( + init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + // Should be impossible to panic here + let model = DaemonGroup { + system_setup: init.system_setup, + daemon_client: init.daemon_client.clone(), + already_running: init.daemon_client.lock().await.is_some(), + }; + + let widgets = view_output!(); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + msg: Self::Input, + _sender: AsyncComponentSender, + _root: &Self::Root, + ) { + match msg { + DaemonGroupMsg::LaunchLocal => { + let burrow_original_bin = std::env::vars() + .find(|(k, _)| k == "APPDIR") + .map(|(_, v)| v + "/usr/bin/burrow") + .unwrap_or("/usr/bin/burrow".to_owned()); + + let mut burrow_bin = + String::from_utf8(Command::new("mktemp").output().unwrap().stdout).unwrap(); + burrow_bin.pop(); + + let privileged_spawn_script = format!( + r#"TEMP=$(mktemp -p /root) +cp {} $TEMP +chmod +x $TEMP +setcap CAP_NET_BIND_SERVICE,CAP_NET_ADMIN+eip $TEMP +mv $TEMP /tmp/burrow-detached-daemon"#, + burrow_original_bin + ) + .replace('\n', "&&"); + + // TODO: Handle error condition + + Command::new("pkexec") + .arg("sh") + .arg("-c") + .arg(privileged_spawn_script) + .arg(&burrow_bin) + .output() + .unwrap(); + + Command::new("/tmp/burrow-detached-daemon") + .env("RUST_LOG", "debug") + .arg("daemon") + .spawn() + .unwrap(); + } + DaemonGroupMsg::DaemonStateChange => { + self.already_running = self.daemon_client.lock().await.is_some(); + } + } + } +} diff --git a/burrow-gtk/src/components/settings/diag_group.rs b/burrow-gtk/src/components/settings/diag_group.rs index be542cd..a15e0ea 100644 --- a/burrow-gtk/src/components/settings/diag_group.rs +++ b/burrow-gtk/src/components/settings/diag_group.rs @@ -1,11 +1,10 @@ use super::*; -use diag::{StatusTernary, SystemSetup}; #[derive(Debug)] pub struct DiagGroup { daemon_client: Arc>>, - init_system: SystemSetup, + system_setup: SystemSetup, service_installed: StatusTernary, socket_installed: StatusTernary, socket_enabled: StatusTernary, @@ -14,19 +13,20 @@ pub struct DiagGroup { pub struct DiagGroupInit { pub daemon_client: Arc>>, + pub system_setup: SystemSetup, } impl DiagGroup { async fn new(daemon_client: Arc>>) -> Result { - let setup = SystemSetup::new(); + let system_setup = SystemSetup::new(); let daemon_running = daemon_client.lock().await.is_some(); Ok(Self { - service_installed: setup.is_service_installed()?, - socket_installed: setup.is_socket_installed()?, - socket_enabled: setup.is_socket_enabled()?, + service_installed: system_setup.is_service_installed()?, + socket_installed: system_setup.is_socket_installed()?, + socket_enabled: system_setup.is_socket_enabled()?, daemon_running, - init_system: setup, + system_setup, daemon_client, }) } @@ -52,7 +52,7 @@ impl AsyncComponent for DiagGroup { adw::ActionRow { #[watch] - set_title: &format!("Init System: {}", model.init_system) + set_title: &format!("System Type: {}", model.system_setup) }, adw::ActionRow { #[watch] diff --git a/burrow-gtk/src/components/settings/mod.rs b/burrow-gtk/src/components/settings/mod.rs index 53f46d4..aa87db2 100644 --- a/burrow-gtk/src/components/settings/mod.rs +++ b/burrow-gtk/src/components/settings/mod.rs @@ -1,5 +1,8 @@ use super::*; +use diag::{StatusTernary, SystemSetup}; +mod daemon_group; mod diag_group; -pub use diag_group::{DiagGroup, DiagGroupInit}; +pub use daemon_group::{DaemonGroup, DaemonGroupInit, DaemonGroupMsg}; +pub use diag_group::{DiagGroup, DiagGroupInit, DiagGroupMsg}; diff --git a/burrow-gtk/src/components/settings_screen.rs b/burrow-gtk/src/components/settings_screen.rs index 0a29e43..971f262 100644 --- a/burrow-gtk/src/components/settings_screen.rs +++ b/burrow-gtk/src/components/settings_screen.rs @@ -1,17 +1,24 @@ use super::*; +use diag::SystemSetup; pub struct SettingsScreen { - _diag_group: AsyncController, + diag_group: AsyncController, + daemon_group: AsyncController, } pub struct SettingsScreenInit { pub daemon_client: Arc>>, } +#[derive(Debug, PartialEq, Eq)] +pub enum SettingsScreenMsg { + DaemonStateChange, +} + #[relm4::component(pub)] impl SimpleComponent for SettingsScreen { type Init = SettingsScreenInit; - type Input = (); + type Input = SettingsScreenMsg; type Output = (); view! { @@ -24,21 +31,41 @@ impl SimpleComponent for SettingsScreen { root: &Self::Root, sender: ComponentSender, ) -> ComponentParts { + let system_setup = SystemSetup::new(); + let diag_group = settings::DiagGroup::builder() .launch(settings::DiagGroupInit { + system_setup, daemon_client: Arc::clone(&init.daemon_client), }) - .forward(sender.input_sender(), |_| ()); + .forward(sender.input_sender(), |_| { + SettingsScreenMsg::DaemonStateChange + }); + + let daemon_group = settings::DaemonGroup::builder() + .launch(settings::DaemonGroupInit { + system_setup, + daemon_client: Arc::clone(&init.daemon_client), + }) + .forward(sender.input_sender(), |_| { + SettingsScreenMsg::DaemonStateChange + }); let widgets = view_output!(); widgets.preferences.add(diag_group.widget()); + widgets.preferences.add(daemon_group.widget()); - let model = SettingsScreen { - _diag_group: diag_group, - }; + let model = SettingsScreen { diag_group, daemon_group }; ComponentParts { model, widgets } } - fn update(&mut self, _: Self::Input, _sender: ComponentSender) {} + fn update(&mut self, _: Self::Input, _sender: ComponentSender) { + // Currently, `SettingsScreenMsg` only has one variant, so the if is ambiguous. + // + // if let SettingsScreenMsg::DaemonStateChange = msg { + self.diag_group.emit(DiagGroupMsg::Refresh); + self.daemon_group.emit(DaemonGroupMsg::DaemonStateChange); + // } + } } diff --git a/burrow-gtk/src/diag.rs b/burrow-gtk/src/diag.rs index 348293e..ab4757e 100644 --- a/burrow-gtk/src/diag.rs +++ b/burrow-gtk/src/diag.rs @@ -15,15 +15,18 @@ pub enum StatusTernary { // Realistically, we may not explicitly "support" non-systemd platforms which would simply this // code greatly. // Along with replacing [`StatusTernary`] with good old [`bool`]. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SystemSetup { Systemd, + AppImage, Other, } impl SystemSetup { pub fn new() -> Self { - if Command::new("systemctl").arg("--version").output().is_ok() { + if is_appimage() { + SystemSetup::AppImage + } else if Command::new("systemctl").arg("--version").output().is_ok() { SystemSetup::Systemd } else { SystemSetup::Other @@ -33,6 +36,7 @@ impl SystemSetup { pub fn is_service_installed(&self) -> Result { match self { SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into()), + SystemSetup::AppImage => Ok(StatusTernary::NA), SystemSetup::Other => Ok(StatusTernary::NA), } } @@ -40,6 +44,7 @@ impl SystemSetup { pub fn is_socket_installed(&self) -> Result { match self { SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into()), + SystemSetup::AppImage => Ok(StatusTernary::NA), SystemSetup::Other => Ok(StatusTernary::NA), } } @@ -55,6 +60,7 @@ impl SystemSetup { let output = String::from_utf8(output)?; Ok((output == "enabled\n").into()) } + SystemSetup::AppImage => Ok(StatusTernary::NA), SystemSetup::Other => Ok(StatusTernary::NA), } } @@ -74,7 +80,12 @@ impl Display for SystemSetup { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { SystemSetup::Systemd => "Systemd", + SystemSetup::AppImage => "AppImage", SystemSetup::Other => "Other", }) } } + +pub fn is_appimage() -> bool { + std::env::vars().any(|(k, _)| k == "APPDIR") +} From 51fd638b7250f81b3899113033e2668d6975847c Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 24 Feb 2024 09:51:38 -0800 Subject: [PATCH 110/128] Update for Xcode 15.2 --- Apple/Burrow.xcodeproj/project.pbxproj | 2 +- Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme | 2 +- .../xcshareddata/xcschemes/NetworkExtension.xcscheme | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 428d9ab..6127e1a 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -319,7 +319,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1510; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1520; TargetAttributes = { D00117372B30341C00D87C25 = { CreatedOnToolsVersion = 15.1; diff --git a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme index c63f8e6..670823d 100644 --- a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme +++ b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -1,6 +1,6 @@ Date: Sat, 24 Feb 2024 09:49:07 -0800 Subject: [PATCH 111/128] Introduce initial UI for connecting to networks --- .swiftlint.yml | 5 +- Apple/App/App.xcconfig | 5 + Apple/App/AppDelegate.swift | 3 +- .../HackClub.colorset/Contents.json | 20 + .../HackClub.imageset/Contents.json | 12 + .../flag-standalone-wtransparent.pdf | Bin 0 -> 3501 bytes .../WireGuard.colorset/Contents.json | 20 + .../WireGuard.imageset/Contents.json | 15 + .../WireGuard.imageset/WireGuard.svg | 6 + .../WireGuardTitle.imageset/Contents.json | 21 + .../WireGuardTitle.svg | 3 + Apple/App/BurrowApp.swift | 16 +- Apple/App/BurrowView.swift | 26 + Apple/App/FloatingButtonStyle.swift | 50 ++ Apple/App/MainMenu.xib | 679 ++++++++++++++++++ Apple/App/Menu/MenuView.swift | 60 -- Apple/App/MenuItemToggleView.swift | 64 ++ Apple/App/NetworkExtensionTunnel.swift | 167 +++++ Apple/App/NetworkView.swift | 88 +++ Apple/App/Networks/HackClub.swift | 23 + Apple/App/Networks/Network.swift | 10 + Apple/App/Networks/WireGuard.swift | 30 + Apple/App/Status.swift | 42 -- Apple/App/Tunnel.swift | 178 ++--- Apple/App/TunnelButton.swift | 61 ++ Apple/App/TunnelStatusView.swift | 37 + Apple/App/TunnelView.swift | 34 - Apple/Burrow.xcodeproj/project.pbxproj | 76 +- .../xcshareddata/swiftpm/Package.resolved | 16 +- .../PacketTunnelProvider.swift | 8 +- Apple/Shared/Constants.swift | 1 + Apple/Shared/Constants/Constants.h | 1 + Apple/Shared/Shared.xcconfig | 2 +- 33 files changed, 1458 insertions(+), 321 deletions(-) create mode 100644 Apple/App/Assets.xcassets/HackClub.colorset/Contents.json create mode 100644 Apple/App/Assets.xcassets/HackClub.imageset/Contents.json create mode 100644 Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf create mode 100644 Apple/App/Assets.xcassets/WireGuard.colorset/Contents.json create mode 100644 Apple/App/Assets.xcassets/WireGuard.imageset/Contents.json create mode 100644 Apple/App/Assets.xcassets/WireGuard.imageset/WireGuard.svg create mode 100644 Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json create mode 100644 Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg create mode 100644 Apple/App/BurrowView.swift create mode 100644 Apple/App/FloatingButtonStyle.swift create mode 100644 Apple/App/MainMenu.xib delete mode 100644 Apple/App/Menu/MenuView.swift create mode 100644 Apple/App/MenuItemToggleView.swift create mode 100644 Apple/App/NetworkExtensionTunnel.swift create mode 100644 Apple/App/NetworkView.swift create mode 100644 Apple/App/Networks/HackClub.swift create mode 100644 Apple/App/Networks/Network.swift create mode 100644 Apple/App/Networks/WireGuard.swift delete mode 100644 Apple/App/Status.swift create mode 100644 Apple/App/TunnelButton.swift create mode 100644 Apple/App/TunnelStatusView.swift delete mode 100644 Apple/App/TunnelView.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index d609718..22ef035 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -46,7 +46,6 @@ opt_in_rules: - multiline_parameters - multiline_parameters_brackets - no_extension_access_modifier -- no_grouping_extension - nslocalizedstring_key - nslocalizedstring_require_bundle - number_separator @@ -76,9 +75,7 @@ opt_in_rules: - sorted_first_last - sorted_imports - static_operator -- strict_fileprivate - strong_iboutlet -- switch_case_on_newline - test_case_accessibility - toggle_bool - trailing_closure @@ -97,3 +94,5 @@ disabled_rules: - force_try - nesting - todo +- trailing_comma +- switch_case_on_newline diff --git a/Apple/App/App.xcconfig b/Apple/App/App.xcconfig index 1d63205..4e42ddc 100644 --- a/Apple/App/App.xcconfig +++ b/Apple/App/App.xcconfig @@ -11,7 +11,12 @@ INFOPLIST_KEY_UIStatusBarStyle[sdk=iphone*] = UIStatusBarStyleDefault INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad[sdk=iphone*] = UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone[sdk=iphone*] = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2 +EXCLUDED_SOURCE_FILE_NAMES = MainMenu.xib +EXCLUDED_SOURCE_FILE_NAMES[sdk=macosx*] = +INFOPLIST_KEY_LSUIElement[sdk=macosx*] = YES +INFOPLIST_KEY_NSMainNibFile[sdk=macosx*] = MainMenu +INFOPLIST_KEY_NSPrincipalClass[sdk=macosx*] = NSApplication INFOPLIST_KEY_LSApplicationCategoryType[sdk=macosx*] = public.app-category.utilities CODE_SIGN_ENTITLEMENTS = App/App-iOS.entitlements diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index f42b52f..6085d85 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -3,6 +3,7 @@ import AppKit import SwiftUI @MainActor +@NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { private let quitItem: NSMenuItem = { let quitItem = NSMenuItem( @@ -16,7 +17,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { }() private let toggleItem: NSMenuItem = { - let toggleView = NSHostingView(rootView: MenuItemToggleView(tunnel: BurrowApp.tunnel)) + let toggleView = NSHostingView(rootView: MenuItemToggleView()) toggleView.frame.size = CGSize(width: 300, height: 32) toggleView.autoresizingMask = [.width] diff --git a/Apple/App/Assets.xcassets/HackClub.colorset/Contents.json b/Apple/App/Assets.xcassets/HackClub.colorset/Contents.json new file mode 100644 index 0000000..911b4b1 --- /dev/null +++ b/Apple/App/Assets.xcassets/HackClub.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x50", + "green" : "0x37", + "red" : "0xEC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apple/App/Assets.xcassets/HackClub.imageset/Contents.json b/Apple/App/Assets.xcassets/HackClub.imageset/Contents.json new file mode 100644 index 0000000..ddd0664 --- /dev/null +++ b/Apple/App/Assets.xcassets/HackClub.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "flag-standalone-wtransparent.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf b/Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1506fe9390e19160cb5f8732ffe6ba2c488975ce GIT binary patch literal 3501 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 + + + + + \ No newline at end of file diff --git a/Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json b/Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json new file mode 100644 index 0000000..782dd12 --- /dev/null +++ b/Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "WireGuardTitle.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg b/Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg new file mode 100644 index 0000000..64946da --- /dev/null +++ b/Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg @@ -0,0 +1,3 @@ + + + diff --git a/Apple/App/BurrowApp.swift b/Apple/App/BurrowApp.swift index e8aed86..21ebf84 100644 --- a/Apple/App/BurrowApp.swift +++ b/Apple/App/BurrowApp.swift @@ -1,21 +1,13 @@ import SwiftUI -@main +#if !os(macOS) @MainActor +@main struct BurrowApp: App { - static let tunnel = Tunnel { manager, proto in - proto.serverAddress = "hackclub.com" - manager.localizedDescription = "Burrow" - } - - #if os(macOS) - @NSApplicationDelegateAdaptor(AppDelegate.self) - var delegate - #endif - var body: some Scene { WindowGroup { - TunnelView(tunnel: Self.tunnel) + BurrowView() } } } +#endif diff --git a/Apple/App/BurrowView.swift b/Apple/App/BurrowView.swift new file mode 100644 index 0000000..b78b1e1 --- /dev/null +++ b/Apple/App/BurrowView.swift @@ -0,0 +1,26 @@ +import SwiftUI + +struct BurrowView: View { + var body: some View { + NavigationStack { + VStack { + NetworkCarouselView() + Spacer() + TunnelStatusView() + TunnelButton() + .padding(.bottom) + } + .padding() + .navigationTitle("Networks") + } + } +} + +#if DEBUG +struct NetworkView_Previews: PreviewProvider { + static var previews: some View { + BurrowView() + .environment(\.tunnel, PreviewTunnel()) + } +} +#endif diff --git a/Apple/App/FloatingButtonStyle.swift b/Apple/App/FloatingButtonStyle.swift new file mode 100644 index 0000000..53ab5ed --- /dev/null +++ b/Apple/App/FloatingButtonStyle.swift @@ -0,0 +1,50 @@ +import SwiftUI + +struct FloatingButtonStyle: ButtonStyle { + static let duration = 0.08 + + var color: Color + var cornerRadius: CGFloat + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.headline) + .foregroundColor(.white) + .frame(minHeight: 48) + .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill( + LinearGradient( + colors: [ + configuration.isPressed ? color.opacity(0.9) : color.opacity(0.9), + configuration.isPressed ? color.opacity(0.9) : color + ], + startPoint: .init(x: 0.2, y: 0), + endPoint: .init(x: 0.8, y: 1) + ) + ) + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill(configuration.isPressed ? .black : .white) + ) + ) + .shadow(color: .black.opacity(configuration.isPressed ? 0.0 : 0.1), radius: 2.5, x: 0, y: 2) + .scaleEffect(configuration.isPressed ? 0.975 : 1.0) + .padding(.bottom, 2) + .animation( + configuration.isPressed ? .easeOut(duration: Self.duration) : .easeIn(duration: Self.duration), + value: configuration.isPressed + ) + } +} + +extension ButtonStyle where Self == FloatingButtonStyle { + static var floating: FloatingButtonStyle { + floating() + } + + static func floating(color: Color = .accentColor, cornerRadius: CGFloat = 10) -> FloatingButtonStyle { + FloatingButtonStyle(color: color, cornerRadius: cornerRadius) + } +} diff --git a/Apple/App/MainMenu.xib b/Apple/App/MainMenu.xib new file mode 100644 index 0000000..8933f30 --- /dev/null +++ b/Apple/App/MainMenu.xib @@ -0,0 +1,679 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Apple/App/Menu/MenuView.swift b/Apple/App/Menu/MenuView.swift deleted file mode 100644 index eab8da2..0000000 --- a/Apple/App/Menu/MenuView.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// MenuView.swift -// App -// -// Created by Thomas Stubblefield on 5/13/23. -// - -import SwiftUI - -struct MenuItemToggleView: View { - var tunnel: Tunnel - - var body: some View { - HStack { - Text("Burrow") - .font(.headline) - Spacer() - Toggle("Burrow", isOn: tunnel.isOn) - .labelsHidden() - .disabled(tunnel.isDisabled) - .toggleStyle(.switch) - } - .padding(.horizontal, 4) - .padding(10) - .frame(minWidth: 300, minHeight: 32, maxHeight: 32) - } -} - -extension Tunnel { - var isDisabled: Bool { - switch self.status { - case .disconnected, .permissionRequired, .connected: - return false - case .unknown, .disabled, .connecting, .reasserting, .disconnecting, .invalid, .configurationReadWriteFailed: - return true - } - } - - var isOn: Binding { - Binding { - switch self.status { - case .connecting, .reasserting, .connected: - true - default: - false - } - } set: { newValue in - switch (self.status, newValue) { - case (.permissionRequired, true): - Task { try await self.configure() } - case (.disconnected, true): - try? self.start() - case (.connected, false): - self.stop() - default: - return - } - } - } -} diff --git a/Apple/App/MenuItemToggleView.swift b/Apple/App/MenuItemToggleView.swift new file mode 100644 index 0000000..07db51d --- /dev/null +++ b/Apple/App/MenuItemToggleView.swift @@ -0,0 +1,64 @@ +// +// MenuItemToggleView.swift +// App +// +// Created by Thomas Stubblefield on 5/13/23. +// + +import SwiftUI + +struct MenuItemToggleView: View { + @Environment(\.tunnel) + var tunnel: Tunnel + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text("Burrow") + .font(.headline) + Text(tunnel.status.description) + .font(.subheadline) + } + Spacer() + Toggle(isOn: tunnel.toggleIsOn) { + } + .disabled(tunnel.toggleDisabled) + .toggleStyle(.switch) + } + .accessibilityElement(children: .combine) + .padding(.horizontal, 4) + .padding(10) + .frame(minWidth: 300, minHeight: 32, maxHeight: 32) + } +} + +extension Tunnel { + fileprivate var toggleDisabled: Bool { + switch status { + case .disconnected, .permissionRequired, .connected, .disconnecting: + false + case .unknown, .disabled, .connecting, .reasserting, .invalid, .configurationReadWriteFailed: + true + } + } + + var toggleIsOn: Binding { + Binding { + switch status { + case .connecting, .reasserting, .connected: + true + default: + false + } + } set: { newValue in + switch (status, newValue) { + case (.permissionRequired, true): + enable() + case (_, true): + start() + case (_, false): + stop() + } + } + } +} diff --git a/Apple/App/NetworkExtensionTunnel.swift b/Apple/App/NetworkExtensionTunnel.swift new file mode 100644 index 0000000..08002de --- /dev/null +++ b/Apple/App/NetworkExtensionTunnel.swift @@ -0,0 +1,167 @@ +import BurrowShared +import NetworkExtension + +@Observable +class NetworkExtensionTunnel: Tunnel { + @MainActor private(set) var status: TunnelStatus = .unknown + private var error: NEVPNError? + + private let logger = Logger.logger(for: Tunnel.self) + private let bundleIdentifier: String + private var tasks: [Task] = [] + + // Each manager corresponds to one entry in the Settings app. + // Our goal is to maintain a single manager, so we create one if none exist and delete any extra. + private var managers: [NEVPNManager]? { + didSet { Task { await updateStatus() } } + } + + private var currentStatus: TunnelStatus { + guard let managers = managers else { + guard let error = error else { + return .unknown + } + + switch error.code { + case .configurationReadWriteFailed: + return .configurationReadWriteFailed + default: + return .unknown + } + } + + guard let manager = managers.first else { + return .permissionRequired + } + + guard manager.isEnabled else { + return .disabled + } + + return manager.connection.tunnelStatus + } + + convenience init() { + self.init(Constants.networkExtensionBundleIdentifier) + } + + init(_ bundleIdentifier: String) { + self.bundleIdentifier = bundleIdentifier + + let center = NotificationCenter.default + let configurationChanged = Task { [weak self] in + for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) { + await self?.update() + } + } + let statusChanged = Task { [weak self] in + for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { + await self?.updateStatus() + } + } + tasks = [configurationChanged, statusChanged] + + Task { await update() } + } + + private func update() async { + do { + managers = try await NETunnelProviderManager.managers + await self.updateStatus() + } catch let vpnError as NEVPNError { + error = vpnError + } catch { + logger.error("Failed to update VPN configurations: \(error)") + } + } + + private func updateStatus() async { + await MainActor.run { + status = currentStatus + } + } + + func configure() async throws { + if managers == nil { + await update() + } + + guard let managers = managers else { return } + + if managers.count > 1 { + try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in + for manager in managers.suffix(from: 1) { + group.addTask { try await manager.remove() } + } + try await group.waitForAll() + } + } + + guard managers.isEmpty else { return } + + let manager = NETunnelProviderManager() + manager.localizedDescription = "Burrow" + + let proto = NETunnelProviderProtocol() + proto.providerBundleIdentifier = bundleIdentifier + proto.serverAddress = "hackclub.com" + + manager.protocolConfiguration = proto + try await manager.save() + } + + func start() { + guard let manager = managers?.first else { return } + Task { + do { + if !manager.isEnabled { + manager.isEnabled = true + try await manager.save() + } + try manager.connection.startVPNTunnel() + } catch { + logger.error("Failed to start: \(error)") + } + } + } + + func stop() { + guard let manager = managers?.first else { return } + manager.connection.stopVPNTunnel() + } + + func enable() { + Task { + do { + try await configure() + } catch { + logger.error("Failed to enable: \(error)") + } + } + } + + deinit { + tasks.forEach { $0.cancel() } + } +} + +extension NEVPNConnection { + fileprivate var tunnelStatus: TunnelStatus { + switch status { + case .connected: + .connected(connectedDate!) + case .connecting: + .connecting + case .disconnecting: + .disconnecting + case .disconnected: + .disconnected + case .reasserting: + .reasserting + case .invalid: + .invalid + @unknown default: + .unknown + } + } +} diff --git a/Apple/App/NetworkView.swift b/Apple/App/NetworkView.swift new file mode 100644 index 0000000..290254c --- /dev/null +++ b/Apple/App/NetworkView.swift @@ -0,0 +1,88 @@ +import SwiftUI + +struct NetworkView: View { + var color: Color + var content: () -> Content + + private var gradient: LinearGradient { + LinearGradient( + colors: [ + color.opacity(0.8), + color + ], + startPoint: .init(x: 0.2, y: 0), + endPoint: .init(x: 0.8, y: 1) + ) + } + + var body: some View { + content() + .frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(gradient) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + ) + .shadow(color: .black.opacity(0.1), radius: 3.0, x: 0, y: 2) + } +} + +struct AddNetworkView: View { + var body: some View { + Text("Add Network") + .frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175) + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(style: .init(lineWidth: 2, dash: [6])) + ) + } +} + +extension NetworkView where Content == AnyView { + init(network: any Network) { + color = network.backgroundColor + content = { AnyView(network.label) } + } +} + +struct NetworkCarouselView: View { + var networks: [any Network] = [ + HackClub(id: "1"), + HackClub(id: "2"), + WireGuard(id: "4"), + HackClub(id: "5"), + ] + + var body: some View { + ScrollView(.horizontal) { + LazyHStack { + ForEach(networks, id: \.id) { network in + NetworkView(network: network) + .containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center) + .scrollTransition(.interactive, axis: .horizontal) { content, phase in + content + .scaleEffect(1.0 - abs(phase.value) * 0.1) + } + } + AddNetworkView() + } + .scrollTargetLayout() + } + .scrollClipDisabled() + .scrollIndicators(.hidden) + .defaultScrollAnchor(.center) + .scrollTargetBehavior(.viewAligned) + .containerRelativeFrame(.horizontal) + } +} + +#if DEBUG +struct NetworkCarouselView_Previews: PreviewProvider { + static var previews: some View { + NetworkCarouselView() + } +} +#endif diff --git a/Apple/App/Networks/HackClub.swift b/Apple/App/Networks/HackClub.swift new file mode 100644 index 0000000..f7df674 --- /dev/null +++ b/Apple/App/Networks/HackClub.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct HackClub: Network { + var id: String + var backgroundColor: Color { .init("HackClub") } + + var label: some View { + GeometryReader { reader in + VStack(alignment: .leading) { + Image("HackClub") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: reader.size.height / 4) + Spacer() + Text("@conradev") + .foregroundStyle(.white) + .font(.body.monospaced()) + } + .padding() + .frame(maxWidth: .infinity) + } + } +} diff --git a/Apple/App/Networks/Network.swift b/Apple/App/Networks/Network.swift new file mode 100644 index 0000000..d441d24 --- /dev/null +++ b/Apple/App/Networks/Network.swift @@ -0,0 +1,10 @@ +import SwiftUI + +protocol Network { + associatedtype Label: View + + var id: String { get } + var backgroundColor: Color { get } + + var label: Label { get } +} diff --git a/Apple/App/Networks/WireGuard.swift b/Apple/App/Networks/WireGuard.swift new file mode 100644 index 0000000..499288a --- /dev/null +++ b/Apple/App/Networks/WireGuard.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct WireGuard: Network { + var id: String + var backgroundColor: Color { .init("WireGuard") } + + var label: some View { + GeometryReader { reader in + VStack(alignment: .leading) { + HStack { + Image("WireGuard") + .resizable() + .aspectRatio(contentMode: .fit) + Image("WireGuardTitle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: reader.size.width / 2) + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: reader.size.height / 4) + Spacer() + Text("@conradev") + .foregroundStyle(.white) + .font(.body.monospaced()) + } + .padding() + .frame(maxWidth: .infinity) + } + } +} diff --git a/Apple/App/Status.swift b/Apple/App/Status.swift deleted file mode 100644 index c08cdd1..0000000 --- a/Apple/App/Status.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import NetworkExtension - -extension Tunnel { - enum Status: CustomStringConvertible, Equatable, Hashable { - case unknown - case permissionRequired - case disabled - case connecting - case connected(Date) - case disconnecting - case disconnected - case reasserting - case invalid - case configurationReadWriteFailed - - var description: String { - switch self { - case .unknown: - return "Unknown" - case .permissionRequired: - return "Permission Required" - case .disconnected: - return "Disconnected" - case .disabled: - return "Disabled" - case .connecting: - return "Connecting" - case .connected: - return "Connected" - case .disconnecting: - return "Disconnecting" - case .reasserting: - return "Reasserting" - case .invalid: - return "Invalid" - case .configurationReadWriteFailed: - return "System Error" - } - } - } -} diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift index 5542170..8db366f 100644 --- a/Apple/App/Tunnel.swift +++ b/Apple/App/Tunnel.swift @@ -1,146 +1,50 @@ -import BurrowShared -import NetworkExtension import SwiftUI +protocol Tunnel { + var status: TunnelStatus { get } + + func start() + func stop() + func enable() +} + +enum TunnelStatus: Equatable, Hashable { + case unknown + case permissionRequired + case disabled + case connecting + case connected(Date) + case disconnecting + case disconnected + case reasserting + case invalid + case configurationReadWriteFailed +} + +struct TunnelKey: EnvironmentKey { + static let defaultValue: any Tunnel = NetworkExtensionTunnel() +} + +extension EnvironmentValues { + var tunnel: any Tunnel { + get { self[TunnelKey.self] } + set { self[TunnelKey.self] = newValue } + } +} + +#if DEBUG @Observable -class Tunnel { - private(set) var status: Status = .unknown - private var error: NEVPNError? +class PreviewTunnel: Tunnel { + var status: TunnelStatus = .permissionRequired - private let logger = Logger.logger(for: Tunnel.self) - private let bundleIdentifier: String - private let configure: (NETunnelProviderManager, NETunnelProviderProtocol) -> Void - private var tasks: [Task] = [] - - // Each manager corresponds to one entry in the Settings app. - // Our goal is to maintain a single manager, so we create one if none exist and delete extra if there are any. - private var managers: [NEVPNManager]? { - didSet { status = currentStatus } + func start() { + status = .connected(.now) } - - private var currentStatus: Status { - guard let managers = managers else { - guard let error = error else { - return .unknown - } - - switch error.code { - case .configurationReadWriteFailed: - return .configurationReadWriteFailed - default: - return .unknown - } - } - - guard let manager = managers.first else { - return .permissionRequired - } - - guard manager.isEnabled else { - return .disabled - } - - return manager.connection.tunnelStatus - } - - convenience init(configure: @escaping (NETunnelProviderManager, NETunnelProviderProtocol) -> Void) { - self.init("com.hackclub.burrow.network", configure: configure) - } - - init(_ bundleIdentifier: String, configure: @escaping (NETunnelProviderManager, NETunnelProviderProtocol) -> Void) { - self.bundleIdentifier = bundleIdentifier - self.configure = configure - - let center = NotificationCenter.default - let configurationChanged = Task { - for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) { - await update() - } - } - let statusChanged = Task { - for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { - await MainActor.run { - status = currentStatus - } - } - } - tasks = [configurationChanged, statusChanged] - - Task { await update() } - } - - private func update() async { - do { - let updated = try await NETunnelProviderManager.managers - await MainActor.run { - managers = updated - } - } catch let vpnError as NEVPNError { - error = vpnError - } catch { - logger.error("Failed to update VPN configurations: \(error)") - } - } - - func configure() async throws { - if managers == nil { - await update() - } - - guard let managers = managers else { return } - - if managers.count > 1 { - try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in - for manager in managers.suffix(from: 1) { - group.addTask { try await manager.remove() } - } - try await group.waitForAll() - } - } - - if managers.isEmpty { - let manager = NETunnelProviderManager() - let proto = NETunnelProviderProtocol() - proto.providerBundleIdentifier = bundleIdentifier - configure(manager, proto) - - manager.protocolConfiguration = proto - try await manager.save() - } - } - - func start() throws { - guard let manager = managers?.first else { return } - try manager.connection.startVPNTunnel() - } - func stop() { - guard let manager = managers?.first else { return } - manager.connection.stopVPNTunnel() + status = .disconnected } - - deinit { - tasks.forEach { $0.cancel() } - } -} - -extension NEVPNConnection { - var tunnelStatus: Tunnel.Status { - switch status { - case .connected: - .connected(connectedDate!) - case .connecting: - .connecting - case .disconnecting: - .disconnecting - case .disconnected: - .disconnected - case .reasserting: - .reasserting - case .invalid: - .invalid - @unknown default: - .unknown - } + func enable() { + status = .disconnected } } +#endif diff --git a/Apple/App/TunnelButton.swift b/Apple/App/TunnelButton.swift new file mode 100644 index 0000000..df8d7e6 --- /dev/null +++ b/Apple/App/TunnelButton.swift @@ -0,0 +1,61 @@ +import SwiftUI + +struct TunnelButton: View { + @Environment(\.tunnel) + var tunnel: any Tunnel + + var body: some View { + if let action = tunnel.action { + Button { + tunnel.perform(action) + } label: { + Text(action.description) + } + .padding(.horizontal) + .buttonStyle(.floating) + } + } +} + +extension Tunnel { + fileprivate var action: TunnelButton.Action? { + switch status { + case .permissionRequired, .invalid: + .enable + case .disabled, .disconnecting, .disconnected: + .start + case .connecting, .connected, .reasserting: + .stop + case .unknown, .configurationReadWriteFailed: + nil + } + } +} + +extension TunnelButton { + fileprivate enum Action { + case enable + case start + case stop + } +} + +extension TunnelButton.Action { + var description: LocalizedStringKey { + switch self { + case .enable: "Enable" + case .start: "Start" + case .stop: "Stop" + } + } +} + +extension Tunnel { + fileprivate func perform(_ action: TunnelButton.Action) { + switch action { + case .enable: enable() + case .start: start() + case .stop: stop() + } + } +} diff --git a/Apple/App/TunnelStatusView.swift b/Apple/App/TunnelStatusView.swift new file mode 100644 index 0000000..3593516 --- /dev/null +++ b/Apple/App/TunnelStatusView.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct TunnelStatusView: View { + @Environment(\.tunnel) + var tunnel: any Tunnel + + var body: some View { + Text(tunnel.status.description) + } +} + +extension TunnelStatus: CustomStringConvertible { + var description: String { + switch self { + case .unknown: + "Unknown" + case .permissionRequired: + "Permission Required" + case .disconnected: + "Disconnected" + case .disabled: + "Disabled" + case .connecting: + "Connecting…" + case .connected: + "Connected" + case .disconnecting: + "Disconnecting…" + case .reasserting: + "Reasserting…" + case .invalid: + "Invalid" + case .configurationReadWriteFailed: + "System Error" + } + } +} diff --git a/Apple/App/TunnelView.swift b/Apple/App/TunnelView.swift deleted file mode 100644 index dd91603..0000000 --- a/Apple/App/TunnelView.swift +++ /dev/null @@ -1,34 +0,0 @@ -import SwiftUI - -struct TunnelView: View { - var tunnel: Tunnel - - var body: some View { - VStack { - Text(verbatim: tunnel.status.description) - switch tunnel.status { - case .connected: - Button("Disconnect", action: stop) - case .permissionRequired: - Button("Allow", action: configure) - case .disconnected: - Button("Start", action: start) - default: - EmptyView() - } - } - .padding() - } - - private func start() { - try? tunnel.start() - } - - private func stop() { - tunnel.stop() - } - - private func configure() { - Task { try await tunnel.configure() } - } -} diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 6127e1a..8717a30 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -9,24 +9,32 @@ /* Begin PBXBuildFile section */ 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; - 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuView.swift */; }; + 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; }; D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; }; + D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A79302B81630D0024EC91 /* NetworkView.swift */; }; D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D032E6522B8A79C20006B8AD /* HackClub.swift in Sources */ = {isa = PBXBuildFile; fileRef = D032E6512B8A79C20006B8AD /* HackClub.swift */; }; + D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D032E6532B8A79DA0006B8AD /* WireGuard.swift */; }; D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; - D05B9F7829E39EEC008CB1F9 /* TunnelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */; }; + D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */; }; D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D05B9F7929E39EED008CB1F9 /* Assets.xcassets */; }; + D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */; }; D08252762B5C9FC4005DA378 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08252752B5C9FC4005DA378 /* Constants.swift */; }; + D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D09150412B9D2AF700BE3CB0 /* MainMenu.xib */; }; D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */; }; - D0BCC5FF2A086E1C00AD070D /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FE2A086E1C00AD070D /* Status.swift */; }; D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B98FC629FDC5B5004E7149 /* Tunnel.swift */; }; D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BCC6032A09535900AD070D /* libburrow.a */; }; D0BCC60A2A09A0B800AD070D /* build-rust.sh in Resources */ = {isa = PBXBuildFile; fileRef = D0B98FBF29FD8072004E7149 /* build-rust.sh */; }; + D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */; }; + D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5952B818B2900F6A84B /* TunnelButton.swift */; }; + D0FAB5982B818B8200F6A84B /* TunnelStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */; }; + D0FAB59A2B818B9600F6A84B /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5992B818B9600F6A84B /* Network.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -70,7 +78,7 @@ /* Begin PBXFileReference section */ 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; 0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; - 43AA26D72A10004900F14CE6 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; + 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = ""; }; D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = ""; }; D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = ""; }; D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -78,6 +86,7 @@ D00117412B30347800D87C25 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D00117422B30348D00D87C25 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D01A79302B81630D0024EC91 /* NetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkView.swift; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = ""; }; D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -92,19 +101,26 @@ D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "NetworkExtension-iOS.entitlements"; sourceTree = ""; }; D020F66829E4AA74002790F6 /* App-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "App-iOS.entitlements"; sourceTree = ""; }; D020F66929E4AA74002790F6 /* App-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "App-macOS.entitlements"; sourceTree = ""; }; + D032E6512B8A79C20006B8AD /* HackClub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackClub.swift; sourceTree = ""; }; + D032E6532B8A79DA0006B8AD /* WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuard.swift; sourceTree = ""; }; D05B9F7229E39EEC008CB1F9 /* Burrow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Burrow.app; sourceTree = BUILT_PRODUCTS_DIR; }; D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowApp.swift; sourceTree = ""; }; - D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelView.swift; sourceTree = ""; }; + D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowView.swift; sourceTree = ""; }; D05B9F7929E39EED008CB1F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingButtonStyle.swift; sourceTree = ""; }; D08252742B5C9DEB005DA378 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; D08252752B5C9FC4005DA378 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + D09150412B9D2AF700BE3CB0 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; D0B98FBF29FD8072004E7149 /* build-rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-rust.sh"; sourceTree = ""; }; D0B98FC629FDC5B5004E7149 /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; D0B98FD829FDDB6F004E7149 /* libburrow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libburrow.h; sourceTree = ""; }; D0B98FDC29FDDDCF004E7149 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkExtension+Async.swift"; sourceTree = ""; }; - D0BCC5FE2A086E1C00AD070D /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; D0BCC6032A09535900AD070D /* libburrow.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libburrow.a; sourceTree = BUILT_PRODUCTS_DIR; }; + D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionTunnel.swift; sourceTree = ""; }; + D0FAB5952B818B2900F6A84B /* TunnelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelButton.swift; sourceTree = ""; }; + D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusView.swift; sourceTree = ""; }; + D0FAB5992B818B9600F6A84B /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -135,14 +151,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 43AA26D62A0FFFD000F14CE6 /* Menu */ = { - isa = PBXGroup; - children = ( - 43AA26D72A10004900F14CE6 /* MenuView.swift */, - ); - path = Menu; - sourceTree = ""; - }; D00117392B30341C00D87C25 /* Shared */ = { isa = PBXGroup; children = ( @@ -199,6 +207,16 @@ path = NetworkExtension; sourceTree = ""; }; + D032E64D2B8A69C90006B8AD /* Networks */ = { + isa = PBXGroup; + children = ( + D0FAB5992B818B9600F6A84B /* Network.swift */, + D032E6512B8A79C20006B8AD /* HackClub.swift */, + D032E6532B8A79DA0006B8AD /* WireGuard.swift */, + ); + path = Networks; + sourceTree = ""; + }; D05B9F6929E39EEC008CB1F9 = { isa = PBXGroup; children = ( @@ -224,14 +242,20 @@ D05B9F7429E39EEC008CB1F9 /* App */ = { isa = PBXGroup; children = ( - 43AA26D62A0FFFD000F14CE6 /* Menu */, D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */, D00AA8962A4669BC005C8102 /* AppDelegate.swift */, - D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */, + 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */, + D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */, + D01A79302B81630D0024EC91 /* NetworkView.swift */, + D032E64D2B8A69C90006B8AD /* Networks */, + D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */, + D0FAB5952B818B2900F6A84B /* TunnelButton.swift */, D0B98FC629FDC5B5004E7149 /* Tunnel.swift */, - D0BCC5FE2A086E1C00AD070D /* Status.swift */, + D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */, D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */, + D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */, D05B9F7929E39EED008CB1F9 /* Assets.xcassets */, + D09150412B9D2AF700BE3CB0 /* MainMenu.xib */, D020F66829E4AA74002790F6 /* App-iOS.entitlements */, D020F66929E4AA74002790F6 /* App-macOS.entitlements */, D020F64929E4A34B002790F6 /* App.xcconfig */, @@ -369,6 +393,7 @@ buildActionMask = 2147483647; files = ( D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */, + D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -423,12 +448,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D0FAB59A2B818B9600F6A84B /* Network.swift in Sources */, D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */, - 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */, - D05B9F7829E39EEC008CB1F9 /* TunnelView.swift in Sources */, - D0BCC5FF2A086E1C00AD070D /* Status.swift in Sources */, + D0FAB5982B818B8200F6A84B /* TunnelStatusView.swift in Sources */, + 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */, + D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */, + D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */, + D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */, D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */, + D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */, + D032E6522B8A79C20006B8AD /* HackClub.swift in Sources */, D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */, + D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */, + D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */, D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -568,8 +600,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/realm/SwiftLint.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.54.0; + branch = main; + kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7522840..9378372 100644 --- a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", - "version" : "1.2.2" + "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", + "version" : "1.2.3" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/SwiftLint.git", "state" : { - "revision" : "f17a4f9dfb6a6afb0408426354e4180daaf49cee", - "version" : "0.54.0" + "branch" : "main", + "revision" : "7595ad3fafc1a31086dc40ba01fd898bf6b42d5f" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/drmohundro/SWXMLHash.git", "state" : { - "revision" : "4d0f62f561458cbe1f732171e625f03195151b60", - "version" : "7.0.1" + "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", + "version" : "7.0.2" } }, { diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 7073401..a07daa3 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -6,10 +6,16 @@ import os class PacketTunnelProvider: NEPacketTunnelProvider { private let logger = Logger.logger(for: PacketTunnelProvider.self) - override func startTunnel(options: [String: NSObject]? = nil) async throws { + override init() { do { libburrow.spawnInProcess(socketPath: try Constants.socketURL.path) + } catch { + logger.error("Failed to spawn: \(error)") + } + } + override func startTunnel(options: [String: NSObject]? = nil) async throws { + do { let client = try Client() let command = BurrowRequest(id: 0, command: "ServerConfig") diff --git a/Apple/Shared/Constants.swift b/Apple/Shared/Constants.swift index cb56cb3..634c500 100644 --- a/Apple/Shared/Constants.swift +++ b/Apple/Shared/Constants.swift @@ -7,6 +7,7 @@ public enum Constants { public static let bundleIdentifier = AppBundleIdentifier public static let appGroupIdentifier = AppGroupIdentifier + public static let networkExtensionBundleIdentifier = NetworkExtensionBundleIdentifier public static var groupContainerURL: URL { get throws { try _groupContainerURL.get() } diff --git a/Apple/Shared/Constants/Constants.h b/Apple/Shared/Constants/Constants.h index 09806c5..5278b61 100644 --- a/Apple/Shared/Constants/Constants.h +++ b/Apple/Shared/Constants/Constants.h @@ -7,5 +7,6 @@ NS_ASSUME_NONNULL_BEGIN static NSString * const AppBundleIdentifier = MACRO_STRING(APP_BUNDLE_IDENTIFIER); static NSString * const AppGroupIdentifier = MACRO_STRING(APP_GROUP_IDENTIFIER); +static NSString * const NetworkExtensionBundleIdentifier = MACRO_STRING(NETWORK_EXTENSION_BUNDLE_IDENTIFIER); NS_ASSUME_NONNULL_END diff --git a/Apple/Shared/Shared.xcconfig b/Apple/Shared/Shared.xcconfig index 50718bd..f344e8b 100644 --- a/Apple/Shared/Shared.xcconfig +++ b/Apple/Shared/Shared.xcconfig @@ -2,4 +2,4 @@ PRODUCT_NAME = BurrowShared MERGEABLE_LIBRARY = YES SWIFT_INCLUDE_PATHS = $(PROJECT_DIR)/Shared/Constants -GCC_PREPROCESSOR_DEFINITIONS = APP_BUNDLE_IDENTIFIER=$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER=$(APP_GROUP_IDENTIFIER) +GCC_PREPROCESSOR_DEFINITIONS = APP_BUNDLE_IDENTIFIER=$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER=$(APP_GROUP_IDENTIFIER) NETWORK_EXTENSION_BUNDLE_IDENTIFIER=$(NETWORK_EXTENSION_BUNDLE_IDENTIFIER) From 4334f8c9c9e31f92ddfe94f5aaddc6e91432d16d Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 16 Mar 2024 10:34:59 -0700 Subject: [PATCH 112/128] Configure CARGO_TARGET_DIR to be inside of DerivedData --- .github/actions/build-for-testing/action.yml | 10 ++++++---- .github/actions/export/action.yml | 3 +++ Apple/NetworkExtension/libburrow/build-rust.sh | 8 ++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index ce91b43..2c66963 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -24,6 +24,7 @@ runs: path: | Apple/PackageCache Apple/SourcePackages + Apple/DerivedData key: ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }} restore-keys: | ${{ runner.os }}-${{ inputs.scheme }}- @@ -33,17 +34,18 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - xcodebuild clean build-for-testing \ + xcodebuild build-for-testing \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + -onlyUsePackageVersionsFromResolvedFile \ -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -onlyUsePackageVersionsFromResolvedFile \ -clonedSourcePackagesDirPath SourcePackages \ -packageCachePath $PWD/PackageCache \ - -skipPackagePluginValidation \ - -skipMacroValidation \ + -derivedDataPath $PWD/DerivedData \ -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ -resultBundlePath BuildResults.xcresult diff --git a/.github/actions/export/action.yml b/.github/actions/export/action.yml index bf007a7..635732c 100644 --- a/.github/actions/export/action.yml +++ b/.github/actions/export/action.yml @@ -37,6 +37,9 @@ runs: -exportArchive \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + -onlyUsePackageVersionsFromResolvedFile \ -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index 1ac73fb..fffa0d0 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -56,10 +56,10 @@ CARGO_ARGS+=("--lib") # Pass the configuration (Debug or Release) through to cargo if [[ $SWIFT_ACTIVE_COMPILATION_CONDITIONS == *DEBUG* ]]; then - CARGO_DIR="debug" + CARGO_TARGET_SUBDIR="debug" else CARGO_ARGS+=("--release") - CARGO_DIR="release" + CARGO_TARGET_SUBDIR="release" fi if [[ -x "$(command -v rustup)" ]]; then @@ -70,11 +70,11 @@ fi # Run cargo without the various environment variables set by Xcode. # Those variables can confuse cargo and the build scripts it runs. -env -i PATH="$CARGO_PATH" cargo build "${CARGO_ARGS[@]}" +env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" cargo build "${CARGO_ARGS[@]}" mkdir -p "${BUILT_PRODUCTS_DIR}" # Use `lipo` to merge the architectures together into BUILT_PRODUCTS_DIR /usr/bin/xcrun --sdk $PLATFORM_NAME lipo \ - -create $(printf "${PROJECT_DIR}/../target/%q/${CARGO_DIR}/libburrow.a " "${RUST_TARGETS[@]}") \ + -create $(printf "${CONFIGURATION_TEMP_DIR}/target/%q/${CARGO_TARGET_SUBDIR}/libburrow.a " "${RUST_TARGETS[@]}") \ -output "${BUILT_PRODUCTS_DIR}/libburrow.a" From cb1bc1c8aa01e25ed0c7ddc65812d6670288a16b Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 16 Mar 2024 10:40:09 -0700 Subject: [PATCH 113/128] Update release pipelines to upload release artifacts --- .github/actions/archive/action.yml | 7 +- .github/actions/build-for-testing/action.yml | 2 +- .github/actions/export/action.yml | 5 +- .github/actions/notarize/action.yml | 58 ++++ .../actions/test-without-building/action.yml | 7 +- .github/workflows/build-appimage.yml | 7 +- .github/workflows/build-apple.yml | 9 +- .github/workflows/build-docker.yml | 1 + .github/workflows/build-rpm.yml | 8 +- .github/workflows/build-rust.yml | 2 +- .github/workflows/lint-git.yml | 21 +- .github/workflows/lint-swift.yml | 7 +- .github/workflows/release-appimage.yml | 29 ++ .github/workflows/release-apple.yml | 102 +++++-- .github/workflows/release-if-needed.yaml | 21 ++ .github/workflows/release-now.yml | 17 ++ .../AppIcon.appiconset/100.png | Bin 0 -> 4300 bytes .../AppIcon.appiconset/1024.png | Bin 0 -> 97634 bytes .../AppIcon.appiconset/114.png | Bin 0 -> 4903 bytes .../AppIcon.appiconset/120.png | Bin 0 -> 5372 bytes .../AppIcon.appiconset/128.png | Bin 0 -> 5713 bytes .../AppIcon.appiconset/144.png | Bin 0 -> 6559 bytes .../AppIcon.appiconset/152.png | Bin 0 -> 7005 bytes .../Assets.xcassets/AppIcon.appiconset/16.png | Bin 0 -> 684 bytes .../AppIcon.appiconset/167.png | Bin 0 -> 7880 bytes .../AppIcon.appiconset/172.png | Bin 0 -> 7994 bytes .../AppIcon.appiconset/180.png | Bin 0 -> 8614 bytes .../AppIcon.appiconset/196.png | Bin 0 -> 9835 bytes .../Assets.xcassets/AppIcon.appiconset/20.png | Bin 0 -> 927 bytes .../AppIcon.appiconset/216.png | Bin 0 -> 10925 bytes .../AppIcon.appiconset/256.png | Bin 0 -> 12031 bytes .../Assets.xcassets/AppIcon.appiconset/29.png | Bin 0 -> 1308 bytes .../Assets.xcassets/AppIcon.appiconset/32.png | Bin 0 -> 1460 bytes .../Assets.xcassets/AppIcon.appiconset/40.png | Bin 0 -> 1732 bytes .../Assets.xcassets/AppIcon.appiconset/48.png | Bin 0 -> 2060 bytes .../Assets.xcassets/AppIcon.appiconset/50.png | Bin 0 -> 2130 bytes .../AppIcon.appiconset/512.png | Bin 0 -> 30526 bytes .../Assets.xcassets/AppIcon.appiconset/55.png | Bin 0 -> 2298 bytes .../Assets.xcassets/AppIcon.appiconset/57.png | Bin 0 -> 2470 bytes .../Assets.xcassets/AppIcon.appiconset/58.png | Bin 0 -> 2497 bytes .../Assets.xcassets/AppIcon.appiconset/60.png | Bin 0 -> 2536 bytes .../Assets.xcassets/AppIcon.appiconset/64.png | Bin 0 -> 2614 bytes .../Assets.xcassets/AppIcon.appiconset/72.png | Bin 0 -> 2949 bytes .../Assets.xcassets/AppIcon.appiconset/76.png | Bin 0 -> 3340 bytes .../Assets.xcassets/AppIcon.appiconset/80.png | Bin 0 -> 3427 bytes .../Assets.xcassets/AppIcon.appiconset/87.png | Bin 0 -> 3704 bytes .../Assets.xcassets/AppIcon.appiconset/88.png | Bin 0 -> 3738 bytes .../AppIcon.appiconset/Contents.json | 285 +++++++++++++++++- Apple/Burrow.xcodeproj/project.pbxproj | 26 +- Apple/Configuration/Compiler.xcconfig | 2 +- Apple/Configuration/Version.xcconfig | 0 Tools/version.sh | 51 ++++ 52 files changed, 593 insertions(+), 74 deletions(-) create mode 100644 .github/actions/notarize/action.yml create mode 100644 .github/workflows/release-appimage.yml create mode 100644 .github/workflows/release-if-needed.yaml create mode 100644 .github/workflows/release-now.yml create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/100.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/114.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/120.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/128.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/144.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/152.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/16.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/167.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/172.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/180.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/196.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/20.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/216.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/256.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/29.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/32.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/40.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/48.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/50.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/512.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/55.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/57.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/58.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/60.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/64.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/72.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/76.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/80.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/87.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/88.png create mode 100644 Apple/Configuration/Version.xcconfig create mode 100755 Tools/version.sh diff --git a/.github/actions/archive/action.yml b/.github/actions/archive/action.yml index c34bd3c..37282e1 100644 --- a/.github/actions/archive/action.yml +++ b/.github/actions/archive/action.yml @@ -26,9 +26,12 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - xcodebuild archive \ + xcodebuild clean archive \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + -onlyUsePackageVersionsFromResolvedFile \ -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ @@ -38,6 +41,4 @@ runs: -archivePath '${{ inputs.archive-path }}' \ -resultBundlePath BuildResults.xcresult - ./Tools/xcresulttool-github BuildResults.xcresult - rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index 2c66963..084ba81 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -18,7 +18,7 @@ inputs: runs: using: composite steps: - - name: Cache Swift Packages + - name: Xcode Cache uses: actions/cache@v3 with: path: | diff --git a/.github/actions/export/action.yml b/.github/actions/export/action.yml index 635732c..8f891be 100644 --- a/.github/actions/export/action.yml +++ b/.github/actions/export/action.yml @@ -1,4 +1,4 @@ -name: Notarize +name: Export inputs: app-store-key: description: App Store key in PEM PKCS#8 format @@ -24,8 +24,7 @@ inputs: runs: using: composite steps: - - id: notarize - shell: bash + - shell: bash working-directory: Apple run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 diff --git a/.github/actions/notarize/action.yml b/.github/actions/notarize/action.yml new file mode 100644 index 0000000..290ed86 --- /dev/null +++ b/.github/actions/notarize/action.yml @@ -0,0 +1,58 @@ +name: Notarize +inputs: + app-store-key: + description: App Store key in PEM PKCS#8 format + required: true + app-store-key-id: + description: App Store key ID + required: true + app-store-key-issuer-id: + description: App Store key issuer ID + required: true + archive-path: + description: Xcode archive path + required: true + export-path: + description: The path to export the archive to + required: true +outputs: + notarized-app: + description: The compressed and notarized app + value: ${{ steps.notarize.outputs.notarized-app }} +runs: + using: composite + steps: + - id: notarize + shell: bash + working-directory: Apple + run: | + echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 + + echo '{"destination":"upload","method":"developer-id"}' \ + | plutil -convert xml1 -o ExportOptions.plist - + + xcodebuild \ + -exportArchive \ + -allowProvisioningUpdates \ + -allowProvisioningDeviceRegistration \ + -authenticationKeyID ${{ inputs.app-store-key-id }} \ + -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ + -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ + -archivePath '${{ inputs.archive-path }}' \ + -exportOptionsPlist ExportOptions.plist + + until xcodebuild \ + -exportNotarizedApp \ + -allowProvisioningUpdates \ + -allowProvisioningDeviceRegistration \ + -authenticationKeyID ${{ inputs.app-store-key-id }} \ + -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ + -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ + -archivePath '${{ inputs.archive-path }}' \ + -exportPath ${{ inputs.export-path }} + do + echo "Failed to export app, trying again in 10s..." + sleep 10 + done + + rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist diff --git a/.github/actions/test-without-building/action.yml b/.github/actions/test-without-building/action.yml index 5903d07..a097d4a 100644 --- a/.github/actions/test-without-building/action.yml +++ b/.github/actions/test-without-building/action.yml @@ -18,9 +18,6 @@ inputs: runs: using: composite steps: - - shell: bash - id: vars - run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - shell: bash working-directory: Apple run: | @@ -28,10 +25,10 @@ runs: -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ ${{ inputs.test-plan && '-testPlan ' }}${{ inputs.test-plan }} \ - -resultBundlePath "${{ inputs.artifact-prefix }}-${{ steps.vars.outputs.sha_short }}.xcresult" + -resultBundlePath "${{ inputs.artifact-prefix }}.xcresult" - uses: kishikawakatsumi/xcresulttool@v1 if: always() with: - path: Apple/${{ inputs.artifact-prefix }}-${{ steps.vars.outputs.sha_short }}.xcresult + path: Apple/${{ inputs.artifact-prefix }}.xcresult title: ${{ inputs.check-name }} show-passed-tests: false diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index ef5c525..bb510fb 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -1,8 +1,11 @@ name: Build AppImage on: push: - branches: [main] + branches: + - main pull_request: + branches: + - "*" jobs: appimage: name: Build AppImage @@ -17,7 +20,7 @@ jobs: docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . docker rm temp - uses: actions/upload-artifact@v4 + name: Upload to GitHub with: name: AppImage path: Burrow-x86_64.AppImage - diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index da0f56a..00b6bec 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -1,4 +1,4 @@ -name: Apple Build +name: Build Apple Apps on: push: branches: @@ -12,7 +12,7 @@ concurrency: jobs: build: name: Build App (${{ matrix.platform }}) - runs-on: macos-13 + runs-on: macos-14 strategy: fail-fast: false matrix: @@ -53,7 +53,6 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: stable targets: ${{ join(matrix.rust-targets, ', ') }} - name: Build id: build @@ -64,7 +63,7 @@ jobs: app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} - - name: Xcode Unit Test + - name: Run Unit Tests if: ${{ matrix.xcode-unit-test != '' }} continue-on-error: true uses: ./.github/actions/test-without-building @@ -74,7 +73,7 @@ jobs: test-plan: ${{ matrix.xcode-unit-test }} artifact-prefix: unit-tests-${{ matrix.sdk-name }} check-name: Xcode Unit Tests (${{ matrix.platform }}) - - name: Xcode UI Test + - name: Run UI Tests if: ${{ matrix.xcode-ui-test != '' }} continue-on-error: true uses: ./.github/actions/test-without-building diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 1ce7a9a..307a93c 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -33,6 +33,7 @@ jobs: images: ghcr.io/${{ github.repository }} tags: | type=sha + type=match,pattern=builds/(.*),group=1 type=raw,value=latest,enable={{is_default_branch}} - name: Build and Push uses: docker/build-push-action@v4 diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml index fd5837c..e0ce8df 100644 --- a/.github/workflows/build-rpm.yml +++ b/.github/workflows/build-rpm.yml @@ -1,10 +1,5 @@ +on: workflow_dispatch name: Build RPM -on: - push: - branches: [ "main" ] - pull_request: - branches: - - "*" jobs: build: name: Build RPM @@ -20,4 +15,3 @@ jobs: strip -s target/release/burrow - name: Build RPM run: cargo generate-rpm -p burrow - diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 4c3782a..3255fc7 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -1,4 +1,4 @@ -name: Rust Build +name: Build Rust Crate on: push: branches: diff --git a/.github/workflows/lint-git.yml b/.github/workflows/lint-git.yml index aefe199..2f7c72e 100644 --- a/.github/workflows/lint-git.yml +++ b/.github/workflows/lint-git.yml @@ -8,13 +8,14 @@ jobs: name: Git Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - - name: Install Gitlint - shell: bash - run: python -m pip install gitlint - - name: Run Gitlint - shell: bash - run: gitlint --commits "${{ github.event.pull_request.base.sha }}..HEAD" + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: Install + shell: bash + run: python -m pip install gitlint + - name: Lint + shell: bash + run: gitlint --commits "${{ github.event.pull_request.base.sha }}..HEAD" diff --git a/.github/workflows/lint-swift.yml b/.github/workflows/lint-swift.yml index 7e62afd..a2cc96a 100644 --- a/.github/workflows/lint-swift.yml +++ b/.github/workflows/lint-swift.yml @@ -1,8 +1,5 @@ name: Swift Lint on: - push: - branches: - - main pull_request: branches: - "*" @@ -14,8 +11,6 @@ jobs: image: ghcr.io/realm/swiftlint:latest steps: - name: Checkout - uses: actions/checkout@v3 - with: - ssh-key: ${{ secrets.DEPLOY_KEY }} + uses: actions/checkout@v4 - name: Lint run: swiftlint lint --reporter github-actions-logging diff --git a/.github/workflows/release-appimage.yml b/.github/workflows/release-appimage.yml new file mode 100644 index 0000000..e566186 --- /dev/null +++ b/.github/workflows/release-appimage.yml @@ -0,0 +1,29 @@ +name: Release (AppImage) +on: + release: + types: + - created +jobs: + appimage: + name: Build AppImage + runs-on: ubuntu-latest + container: docker + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build + run: | + docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile + docker create --name temp appimage-builder + docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . + docker rm temp + - name: Upload to GitHub + uses: SierraSoftworks/gh-releases@v1.0.7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release_tag: ${{ github.ref_name }} + overwrite: 'true' + files: | + Burrow-x86_64.AppImage diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index 3ea185d..786fb54 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -1,65 +1,115 @@ -name: Build Apple Release +name: Release (Apple) on: release: types: - created jobs: build: - name: Build ${{ matrix.configuration['platform'] }} Release - runs-on: macos-13 + name: Build ${{ matrix.platform }} Release + runs-on: macos-14 + permissions: + contents: write strategy: fail-fast: false matrix: - configuration: - - scheme: App (iOS) - destination: generic/platform=iOS + include: + - destination: generic/platform=iOS platform: iOS - method: ad-hoc - artifact-file: Apple/Release/Burrow.ipa - - scheme: App (macOS) - destination: generic/platform=macOS + rust-targets: + - aarch64-apple-ios + - destination: generic/platform=macOS platform: macOS - method: mac-application - artifact-file: Burrow.app.txz + rust-targets: + - x86_64-apple-darwin + - aarch64-apple-darwin env: DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: - ssh-key: ${{ secrets.DEPLOY_KEY }} - submodules: recursive + fetch-depth: 0 - name: Import Certificate uses: ./.github/actions/import-cert with: certificate: ${{ secrets.DEVELOPER_CERT }} password: ${{ secrets.DEVELOPER_CERT_PASSWORD }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ join(matrix.rust-targets, ', ') }} + - name: Configure Version + shell: bash + run: Tools/version.sh - name: Archive uses: ./.github/actions/archive with: - scheme: ${{ matrix.configuration['scheme'] }} - destination: ${{ matrix.configuration['destination'] }} + scheme: App + destination: ${{ matrix.destination }} app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive - - name: Export Locally + - name: Notarize (macOS) + if: ${{ matrix.platform == 'macOS' }} + uses: ./.github/actions/notarize + with: + app-store-key: ${{ secrets.APPSTORE_KEY }} + app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} + app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} + archive-path: Burrow.xcarchive + - name: Export IPA (iOS) + if: ${{ matrix.platform == 'iOS' }} uses: ./.github/actions/export with: - method: ${{ matrix.configuration['method'] }} + method: ad-hoc destination: export app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive export-path: Release - - name: Compress - if: ${{ matrix.configuration['platform'] == 'macOS' }} + - name: Compress (iOS) + if: ${{ matrix.platform == 'iOS' }} shell: bash - run: tar --options xz:compression-level=9 -C Apple/Release -cJf Burrow.app.txz ./ - - name: Attach Artifact - uses: SierraSoftworks/gh-releases@v1.0.6 + run: | + cp Apple/Release/Burrow.ipa Burrow.ipa + aa archive -a lzma -b 8m -d Apple -subdir Burrow.xcarchive -o Burrow-${{ matrix.platform }}.xcarchive.aar + rm -rf Apple/Release + - name: Compress (macOS) + if: ${{ matrix.platform == 'macOS' }} + shell: bash + run: | + aa archive -a lzma -b 8m -d Apple/Release -subdir Burrow.app -o Burrow.app.aar + aa archive -a lzma -b 8m -d Apple -subdir Burrow.xcarchive -o Burrow-${{ matrix.platform }}.xcarchive.aar + rm -rf Apple/Release + - name: Upload to GitHub (iOS) + if: ${{ matrix.platform == 'iOS' }} + uses: SierraSoftworks/gh-releases@v1.0.7 with: token: ${{ secrets.GITHUB_TOKEN }} - overwrite: 'false' - files: ${{ matrix.configuration['artifact-file'] }} + release_tag: ${{ github.ref_name }} + overwrite: 'true' + files: | + Burrow.ipa + Burrow-${{ matrix.platform }}.xcarchive.aar + - name: Upload to GitHub (macOS) + if: ${{ matrix.platform == 'macOS' }} + uses: SierraSoftworks/gh-releases@v1.0.7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release_tag: ${{ github.ref_name }} + overwrite: 'true' + files: | + Burrow.aap.aar + Burrow-${{ matrix.platform }}.xcarchive.aar + - name: Upload to App Store Connect + uses: ./.github/actions/export + with: + method: app-store + destination: upload + app-store-key: ${{ secrets.APPSTORE_KEY }} + app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} + app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} + archive-path: Burrow.xcarchive + export-path: Release diff --git a/.github/workflows/release-if-needed.yaml b/.github/workflows/release-if-needed.yaml new file mode 100644 index 0000000..0d2eb97 --- /dev/null +++ b/.github/workflows/release-if-needed.yaml @@ -0,0 +1,21 @@ +name: Create Release If Needed +on: + workflow_dispatch: + schedule: + - cron: '0 10 * * *' +concurrency: + group: ${{ github.workflow }} +jobs: + create: + name: Create Release If Needed + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - shell: bash + run: | + if [[ $(Tools/version.sh status) == "dirty" ]]; then + gh workflow run release-now.yml + fi diff --git a/.github/workflows/release-now.yml b/.github/workflows/release-now.yml new file mode 100644 index 0000000..229f6c9 --- /dev/null +++ b/.github/workflows/release-now.yml @@ -0,0 +1,17 @@ +name: Create Release +on: workflow_dispatch +concurrency: + group: ${{ github.workflow }} +jobs: + create: + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - shell: bash + run: Tools/version.sh increment diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/100.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000000000000000000000000000000000000..f86c1399c3d698390d66fbb50d647227b18e27d3 GIT binary patch literal 4300 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000000000000000000000000000000000000..872c9ceebe2427014f0b0df5ac04e039eab233e7 GIT binary patch literal 97634 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 Avj6}9 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/114.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000000000000000000000000000000000000..3bb278dcb871e79ea334cbf2fb52709ab2f59358 GIT binary patch literal 4903 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/120.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000000000000000000000000000000000000..185615e46f63c82ed3c573b3a40cfe4241415f96 GIT binary patch literal 5372 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/152.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000000000000000000000000000000000000..c95ea8adb863f3ff13de0196b7c5eead477c92d1 GIT binary patch literal 7005 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 QC_OTGy85}Sb4q9e03{nEdjJ3c literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/167.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ad6a290de1c1239bf2083e1368baa8d02b3a12 GIT binary patch literal 7880 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 YUwSX8Wv;TuO3*B_r>mdKI;Vst0AhsQKmY&$ literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/172.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 0000000000000000000000000000000000000000..9f3bdb4fad9eb45a63f51fb30f82f441cce4e406 GIT binary patch literal 7994 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 O3kFYDKbLh*2~7Zq8JSD~ literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/196.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 0000000000000000000000000000000000000000..ea95961f24a3f43ae19a0b98994c3c3eb4e25c0f GIT binary patch literal 9835 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/20.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000000000000000000000000000000000000..aec8236e31d4c3ecfd37aa26067b2d233681dec8 GIT binary patch literal 927 zcmeAS@N?(olHy`uVBq!ia0y~yU=RUe4mJh`hTcwDUIqpR#^NA%Cx&(BWL`2bFu0^f zc&7RKGH5X{FmNz1wr4W1fRr#WFi0_g0P_My24=7bBLl+%Cb+D~0%imoq;S*S4}TdL zm^nRN978MwOGB)^vmFKIxddKtlGbH;ZNlZWB|yMQK!o$ibA65_TphhPZfLj!b9hP$ z=(rpdVRSs`6cVCyL}h~71UJ^B?rSGaKQ}l3(L39n73}9McYpqowr}R$$4P>}`fF-y z1A~Kuo4=aRy?pSi^n>r;%@z9@tt~7RUMyR_+*E9V-t_J}dE14(uk$uAG!_>ZZ*+u$Vu8e)Qj? zM_K)Sd_G*cB69!Uy{Ns$Vpk$wtoZo#>(s*!4fy%_Kc742=j^aGYU}U4du`<;B_9T8 z965jf{3>$^mXe%HFHOFG{aR==bIVn(+GEK_b{?$7 z-M_!}XVQgf4*Ch(Y$ImW9dvhi`S@|OP-n}l#9#-Dva+&E@80Q&a0`nwj$8Nch0U+A^LLuRYu7G@K>xHIi*FuH3cT5=eE$6T&F7y#Uc6ZO@QnWc{+sW= ze?D`jhmWsq_ik%hk!uH5&F`20;YA9CESY+MEjBxQwd}OcM&2aGi7WT+jn%yFo08ACirKVdXWu0w-34pcu6@Y0 z%eO6PrAyH-(*qaG4tP9U(l)Ju@z184Yvjy-C~<0CdKnWR|M|fK1$QOEy2{Ff7rr!j z8|3HYe0cS0)!oD2zI{8!&CUJhYn5cUkTBacl~w0m*Gb35#l4!z#lmD2d!Z!AWa{xG z!&!6YEa~gx6Ed)!yh0%2TR!tEwm&alc3!)7ZOXYzmxOF>Y$n`E{ZwJIW#2wOW@hFs zn>IP^-Mja|#kg4!e=QrBSW>O)nP&R5J%9FW$L`&;pPGGm^G4_XojX_NKIM%+VsSKV z#mc(4M++{S{Xe>TwRWz>Iu#aY#@go#>YV-unQ=WA&ux&f`u}e3zO=vW(FtqxLn;b4 PfHH@ttDnm{r-UW|ePo_4 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/216.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 0000000000000000000000000000000000000000..9f0e3ced8cc443ad41167f7c681b2771e2f280a5 GIT binary patch literal 10925 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 T9=^`Nz`)??>gTe~DWM4fJXd8Z literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/256.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000000000000000000000000000000000000..a82ce93b6e54b92c4a7f6b6b704e6a80cef29b29 GIT binary patch literal 12031 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 XX){xU+Ks9kpvC8&u6{1-oD!MU~ox| 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 Bnf(9& literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/40.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000000000000000000000000000000000000..1f7f5e9b82b370a76fee8759b984ac541f74ca05 GIT binary patch literal 1732 zcmeAS@N?(olHy`uVBq!ia0y~yV9)?z4mJh`hMs>rav2yH7>k44ofy`glX=O&z~GV^ z;hE;^%b>-;z`()4*q+J20#d@jz#zo{0?Z2-8JNK$j0_A5nBcM+3z!jXkix2GX=@l5 z*z`PI977^F&qjEAT!|O?H*?w|wuNdb5)m1bGFxx>s0KmPNu{(<@T(sSke zs^3*Jd31iYpQcb>;4D9V+BBV)*$xg3Thq=;algz`P1q{<=lA#Zq7o7>CLJ%z)#Hr` z`6c7%!oyV55fdFf`|FE~%6(5JD!Z={R`)BI{X%h(9jDTZUXAuGJ9ez_wX(L({QmCl zjN^RqMMXxXB_$cQHx68jc6@Iu)bRi3XZP29yu5*aeteU;I5`)t4qt!i`T6-TPfyn` zd3=nQi=Y4bwl{BbUcG;Rz3jk;wiWIyXFGg-d;%gPXZD48dUDqO`4K2Q;mngKDs%1Y z<6h6Tt)6!5`0=GT6GcjvcAweAayD`1{Q1jYUtb@bk)dI2TlPkRmxm|AqqE2M(&590 zOaA;Q+!nb#Pd0XEQET15KPSsBm|Cwt*wf?Pl=an8V%xTDm#(dizIk$8?Cuu_o7tt9 z_!i`gTVGtaPVd&{bpGTc$BtdPySsdAr!|j;g`mk?+v;yU-`?Lpzd+hNZ^^P{YQAh$ z%hr8MJALEpyE{9b4Gj%@W=@;tws-H|TZcsFF+0xOvv~31l2=zW*$%#1vu@q8_xJZ_ zFPPfqsx2jXgX3Sny#0!G>-w%-J8bvq)2A1AcbBixwMtsW*<>qq{MfOK%gcOoB4T2E zB1Dvo&M$eB%T=?vIO5}`35ymjdhz!5_A>{2mfW+QYAEmf{r&yzSIQ1nzj}Xv|JwNd zbw9W4-(?&$_mb~{4_%DN%&J0d<_Ue>ClG?6ks~f_o!#AwpFK;vn-;(HQbUz-R@SQkCnu)loZH)aH{pswN zq)o$DY}`0eNl8h9C)%aFeEW}tSQpXxHx_(6Ia$5r!2!l?k^Adx@9wFbyy2zu1;(2B zcD0{+6&&77446DqOIuqqz&GX8*|Vy1PM1~w{bedDB9gJUTU`H>rEZmi+UnA)ER0$G zJW4gczGV6?*s^7c_@(RX<2h3tph?g&4hWY#R$47{ZiRo-r zT3h}7U8MhfyRCe3HW`lYrCqr-l}r`B0Y4tM_1xYf5MVCmy*4QO^fcXRQek1ipLeU@<)nBgS>=IV&+GO9sx$Zs(*Mj}Q+gYx99oBrD zU+-6;{j*T1<7bO!@6;|EwO{qBGgg0KjQ{XbOTmw&{LPJnx0|YFerySU&!LyR`onYv z?)$g4X3w0mtEC`v?)P(gBH3c82du60v?C)U_uN!dRSm4J-o4_(&XrYbPfu#pWqB{U zC)Q<2+^G#m1=T-&cc40C;f9mh`A8e3S*c=h@%Dot|*zN=R3K z|8pC$>*wZL?=+rmp0Bp2z18Bwhe{w%Lv-k$o bKl{%ZwTOAY=_1dApc=%})z4*}Q$iB}#*82B literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/48.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 0000000000000000000000000000000000000000..4a67ebf1ff3082c1593c6123d95178cac73312a3 GIT binary patch literal 2060 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?FU~ox| z@J#ddWzb?^VBlb2Y|mt10V!c%V31+}0pfiP8`}O!N#904^tPFBp6}md@emjq((sccJv)V7My`MPR zEVO_A{k{Eb^`|GDXJ!~W-?AAPJo$MCZe{ywo_@?aZdYN~3Z55npRl3Ti z>dT2=zqhS1JnMCgrdOBe}7uR|A&X!Blp$p zys|Dfx-@WI#6~B(x<4y+ZJ5bBaSP{;%GnPR?(eI;w6C_h`_++7;iv!p{$`tVS$R%c z$Db*h!E54Yo8_i_e|J|*WJ1IGlBGUOiVeo;=REH3tK~h=z{vdM&(F`#`VXvKxI^fc zqn?8L4E?>;-=_sH_lq>IU$WNFQ&pAG-tqn2-RjB5`%K$qt;?n~v-8h7S)wf7_hg}S z`;zSI>(a`vtOzXn{VjKMbd6$Gtcz7-P-$j*F@B9*kG|Hdb^*4fK$%V*sUH42QrdMB`pdT zFqP*AO4?R!xwE72@UN++YnIJOW3Bu9vg-LcSxNi4Jt6DkVhz8%yzIWA%TZrtqC<1)S)6Wc zQ}d%>XVKFxyTum^@@^eg5qjwUq3u_Htn&mGg)i6;)m4@=d*JxgEt&vtZ8QB zO*uQuw8@o)X_0%s+}U{t{{8)Z^}_e}_qQIIKV3im)s;tFjkaeurJjCrcX#=+$IU5= z?|*xFSv_)V*3>KOW~%8dFBm2+72c8<|3v-QgM-aaUtL|@#3E~1w1n|E z-!#syx($rc3on$tQJ!!{P`TaZ{*J=MCV6)v7?m%6{``4sAIs632mJ$@cyb?E&!6hL z;7)py%!9(;4;D#0{O~Sp>WN(IMLUb1zj}CYV)vcQv$wLYtXR0~#o@#IPcK}v^bX4l z$%L2(O4C~=c9%U_Bztvjw0Tkbql~}5zD|@Dt!0~Q(Qr9Ghb2KEur%1MbH~a>sto(< z|Hr+qV4G{98pgyT=Ev`6Y}dc}3}>SSj%qSOdWUVNA50LyW?qq_4bU5N-zD47bbrcZ#vV$P;*KDSG)DKrQXw5mA?8>=Phu+ zSu1W=iRT7QXO<5y@9nKVi4F@8ZNin|o;eI&#`a0D&5!R)zR4k4K)#&6TM`|%@C^+>P z=uIz3?OR>kcVda>WUrVPLH3f2HlG{q>;6P6^Pit*u5jaMl6iNmE{jWWW4oUhQ$WKH zw|jdk7l*%KWo7kZdAi88nOOZog-|QC_d-{k^@}5(}Ld zU7N9Jk{RPIj!p$}HeRU|mt7?13mOhrFp|+~Q58g~S zWGp*U^KE%pg>T<|0RvY9CI;^ZJErjqPpNdiJ#$K=@B(2Abq1AR*Vaa}UhZs&d^F8^ z&$RE%4%IWKJymmPx06t_iff42THx9#|3_n=-tA4gpD$c=)f8*goUUA!!c+gE?7-K^ zIml*oFn3txF9@oM NJzf1=);T3K0RTZ8(J24` literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/50.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000000000000000000000000000000000000..88985d899a650f753e81263822484c009fc74f37 GIT binary patch literal 2130 zcmeAS@N?(olHy`uVBq!ia0y~yU@!t<4mJh`208nVjSLJ7jKx9jP7LeL$-HD>U~ox| z@J#ddWzb?^VBlb2Y|mt10V!c%V31+}0p~9smxS;sz+S=K@udlC{pKDR*v^%vY>Xy{xkRT=@ zwu0Z^a+_|ykjl8UgmZ1|?rCL_5(}2Su+I43*Vo~<_EZ|1=HI*Xqes?y+Pc`? z%bplzu4MA!I`aMQ_4VQ6n?^ESr1v8UO$NH9K?ttkljRwvJy0NqglJZ*9x<-d*-~ z(?)~sm!gs_wH`d(ePNz$wT8L*bp~EJn;Gx!?bQ}aI?2nx7-jSR-rmK0vQ`|Ee|>#@ z`B<-XuHxeR;t_mzlli`?zc@Wze@pIdv)i+yw&i$6EMK+!yuyUPMnUaU7H-*cC3b&Z z>?PhUg(gLBR!@p@nHI#c{YJgX#uNpm*#|rIzQ4a;e)Q6Ojt{>c(?HT$$RE|=V6V33RIld%kPlM9-2b8~t(U&lO+Rjd2FIas8p z@ZH*&%)T}A@-YTQUd!TVJ)bIpP!#IA7ou9C%vrg{!HWaG`^HOI|_}W zwLZMPy?sl~&mt#=hNB-8zTMwZsGRDM$ngE$U2c^rTeGh(bL|#8xri&wd{>HLhri`{r<+1J-yx_je!Tnw`cgpDix#Yl2OfD9m=YL>& zI>BRa)mNjVR;908Zd^F@^gtu?m95#~i=Ab2qY@Zd*W^aBE^;c__EEaDCD&fz%lrHB zOiW^*E-ZBBWiX1%*|edrkC%A@-@nh{4J!=pe-{*(rW-BB!SSJfN8sA1ttyvYkF7o` zzdHAyb*Mt)+_|l(AzzNQOf8Zrm2sCXe|bsOJolE#?w$uP)N-}M))dHmmC1;FxMRES zOD!Qz0nsjL5sl-UU)-p@v&?t4%=}mup7$sE%CndmZfRVe>(R&{Fr|5tOsUN7nMqTu zZYAv1U|i8~qERAwUt-8~XX#h7c(i<{ykEoE8FVtF;;(%}!oz>zUPiG(Q7+=SMrt=% zxNq)>jFRYA*H)WwYO1z(O69jVGw;r6zIk7-qo|{RL!@8cep=O@4snJ>fvx<1H_3nK zZF}6@W58tG!1y^w{6vG!3+D9?n&&;XPPy2*KFzg8(jjM;Ln7O!>T7Evr%5lZ?nrWN zVZS6QGyUeFFJE2;XI|ddaKYaG!g5W{Sq0%XMNd2~GpEa6SkY(4zko+%e!Ox>?}XsVIJmf%i# z+bWk0d}*Q$L6$BrZ-g}b@O(VmJU`>*rKM#zlCCe(3|=PCuuvoZ>c#rwtV~MrC!U_3 zp84g)#k4e;Mc39wi+|qd&8o)KbTd@k(Q1F{X|YfI(Up~zCGYN7UcS10hH2H*76%9R zr8N#e3NA1AmuGHq7TMRI!MOI)ai&M#lg}J%W@nx+v8(*OpR23uDZdKMgzTO`PcE0^ zKa4pFtjpgmDSLZMaseYV+l7;p)u+aYG^~D+bX?t%l|!k9L&~mZM{oYsRZ}Be)9+1B z_Yn>WVq0WWlUlHKSJ6|itQ#8^YPQT4|0kgJMBc7OK_`T7;g?TOy)zH5txil_7_%+$ z>_YkJ0t{=9PG6XN@?Iu$VB0h?vlD|~p!<{)L7mGX9@*e14)k}2OaPqVs6wlX_; zdUAe!clWl)^;(YmyvC6ywA3$JZ>pC_NY$-%%sS|#y7hoP?}tOBHGf~J`OZpN?tYF(9-TNHRb1jX e%TzQt`44y1boslVv_oct>T^$5KbLh*2~7YSk=^_N literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/512.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000000000000000000000000000000000000..e5cbf6a41af399c14ec320ed7e32f82d8cb1c8c8 GIT binary patch literal 30526 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/55.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 0000000000000000000000000000000000000000..dc079ea3ce00174757dc48b083b5852252fdc5f7 GIT binary patch literal 2298 zcmeAS@N?(olHy`uVBq!ia0y~yU@!+^4mJh`h84FjxiK&>Fct^7J29*~C-ahlfx#s; z!ZXd+mqCkxfq{d8u|1Q41*C+5fkBD^1eg~vGBATh7#SEAFu`Tb7ce8(AcdZ-XTlj6 zI8r=a978f#-$tc(hZZ+`b7i-^Sz~ZE$RTt^fFjrN_3`_i z*d?VbH1rqT+gF>d7q=%P;QFf2(10>x1q>UEG+Ei-s#M1kc0Jx=e3orqvjf+9k%vx>dn-SuWnEm<%CstW zw^`|%8xxC)i?^+@{P|&cc8=xbRbgvaCAaZNI*IGW1Q>m2pA+Kk_2ACbrb(OA z&UOV9eRy!NBr&J?1f5T3U-{o9AooPHEBH=zo~C z^835kSr&y#PoF%|IB{{Y`{l2%uTO2B-+F{a{XoKdZZVw&udc4v{=BdDcTi^LN~3lo zr}c9)&Gv{De|r-tWs))BRlrg&(OI_DW_v3?AM@%`x95`KDBN4|@lf8b4xJ52HyXC* z-(Q!-Jmvlx%|t2Q59Q%S@9*txv7LC>id*FT11O6>~v6$C1J+WZMnBE zU0&{=Yv0iQc!R9u4@VZ|Q^hr2GP9%hF1UYVWAa+IcPbrTfe%tTcbC6kcV1$~maMB? zW_fp3NSn8GIwXpfygk(r7ri}isafu=6_tk;eXx(~YZ&P(7+$c<}3si--A+$u(;%*|EdoP_`nMW%;`~pLh>N zGflA(OL)^8_h9Mq9!cj9i`@HUHvT#&engw|;M&;TVaE?;^U7ERqzimhj`=#*x_pXS z=!AD(wlzN{Ec2bc?7V2$iPuZZ-`tpJXR&9}fp7ck{;p!+ld*7^Yh4~Tp<-_2mJGqY zb$_EWV^X&oyB%Iy{r}%y-kkzfi~{Svy}d20q@;O)`PZ9UTeT$$5_&`%l^(e~FucA# z-oLW4auvs|7sB~mQQ?(=0;cac^{+&4%h~AX?eLJb{OhZ$Uw(d0xAvLmY1( zfE$a;-rdpEQ92WDZQEPVI_Cw$gdG77B0kJ(|8C3Jva9g%u`O9wwGMIXcUf@%RzK)p zd4P4EpSzi)II~GkRoby0$!DjgYPWrNKm2>qLyyD%(w8T$3SFJ{a>C{*GV0CzYgqXF zOqw_C;b{A=8oWI(_VDki4xOT9^ zLiz89brv=WA7x}(*40#YV}VPa+kwsvKG**@pZ?OLuFQW7>Ld9sv`+FYLED zTvzkBY4SlCm1y>c+SmI77O=|*Jr5OHCCu8lnALhk-QQnjW#8W3PXE6A)Ku-;yy5TE zpU4<6K1o`%ke#bNH_YDe&aM9&zjQIFt3^aaWSrl4evW1GukY{kEflr|vV@5^wVW&B z`^a9Tz+be{JZ@jj%@4QREDImCT;;D?bcBgrb#d2`whI5ph2NY?`fa#2oevUtZW#Pi zdh@gh0d>a*tv;q)+A@>ltPX}BTlD_k-s-@7VGsKzzb{ia1|4T-ig;^edi&I*lMH9 zODel36}aX7V$6MF)BHzGRkwiYQYqUBsmHs8zkgs!&zdy*&6SnGN3@c!ZkXTp(KJxu zK~iJO<%8n=MgD4Z9$b)tCLD;<|*@v<5g9G|S_dQaE8xFPYdh{yrfhdO^8PVT+1 z?^g1J85U==KDwK?32$z*KWtF5?U+Pw+P){eN6OfK2uoR96jaz-Dco>OR7_){td)h} zk$YE|m@XJxu2kYLJ$7KHlkwHQs`;mzopr0JaY? ARsaA1 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/57.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000000000000000000000000000000000000..de4fddce4ba58acb4f396557fc65181b9b381877 GIT binary patch literal 2470 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 E08H7PLjV8( literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/60.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000000000000000000000000000000000000..2a9e939762ab0475869f28a49192ff72b839a963 GIT binary patch literal 2536 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/64.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000000000000000000000000000000000000..c67e407c57ddde65c52019d65d36c4566c883fd2 GIT binary patch literal 2614 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hEk44ofy`glX=O&z~GV^ z;hE;^%b>-;z`()4*q+J20#d@jz#zo{0?Z2-8JNK$j0_A5nBcMw3z!jXkV3aWrwt4Y zoC`c%978G?-$ti*h+2!BTetW7x(#7(p0!>Xm zf>X6ZPX+DrnQ0WVEhlpIx`>TQCh6y7s=mIOI!$|~!w$8w3%?QOY>tx8`-)Xp-?_4@t&{q?HZ)0S!rYpC!ri2VQecemKXS65eG z*Yv?&o9r;Hs2+yedxf)lAk|5D(lAXGP!-_#ogWJvAaq__FAH4g)Vk%#obf3_#=tN!vr(07JG;{%zS ziO)_???2Wr@6Ru9cO~|$-7cdxrx=DApFf_SuD>Pgs@AHAjf=!Ym`?XB73ra3n*2(2r8eC*W0 zjXV+yE9OjJeWj6^o#Wi(wTzW-Z*9$deQoU-ub&GdDqUF{thLh5&s%zKu66XGkX0d_ zVe8|3#dF1X75kc2UYToMZo{7P?99wB zkB)M?&MoCQs-?nk=i`TmheNhzh4z|=w4ao|yHy(p&)AxKd)kJN4_ShfxgS~C7hS(}ZjR;TT-~O(-ww5M zUp0Aqd;9uH!rYq+G_@FJndh&|w5k0yWtrdHS#JXt$M3Hzd3s9p?z#2;i&pXpWPm ze4V_W`R_+>60WU@3|ZnK*sWgHDXt%PB_(0y1krQ+3_p_?85lTQ?g}2PVhC<=XSU1_ zySKl7J*)B5i&GW3o|!y4JMHy@gsC^=3m(~>QO{&BaK0}2(15w`BFmz%wNYDAPm8fi z{cCbEPgxtab&+#BU!nNc zx(3z4qP4G%bP8Wt=*;e2SXPjKf8W!*d9NoNU`)uIVOVEh`Ke`@&&)|@1C!$4&wKS@ z5%=G9vAdUj6MuH=@acmy_gpyd^GEMx@PU+5sXa?pPME&nh4GJqh7fn(1p0-5Qd)kW2D&@J2w(fh_7oIU!6+6IC@XpF~&b=MW_bt18 z#L?Y?{dmdYryH3jT***Ls_9_%QrPf7sF|H#Ok6KU;Pfo zxMa^!*|Dl=$qQkZ111w*7_d7poD!qMS=prWq;lSb;uxt0flm@KJo0uiQ!lc;-Pym& z=h1uzhJ>FBriZbRcdewTHY}IPk@|*L!zI}s|xWFE*XZf z3nABZKL1|uX$qsjl%|P4{;(!^?Mf zcek@1_#6@;6zcBBQCj};(ow6K2Mdjz(&j9>xwts`$)0HIq9+~8cGvzcyL!aN+u(P_ z)TKq&ZZa=ARJ1vjwMgK`!JR6%9)0vuWPHFk-@blX;p1ZyT_U)90wiC_*;FjZ+?2FS z$))kODigzH2JV=%Gwo`v6n*sDqwFoLo?Cma>7MSP$mo!~d2zVHqLP=FUj2(-QfxKZ zkXxRg;l*hqMYo+rPrWAH&RT22;IQD?jg85(RJUIj^;6Vg$lzwuF{x7)Y292V{8q{| zE8_b7Cv&bW-}WL|ZJ8)HgPyI=%&M=iB8@6yr*|%~dD6wfz@5*?mmeP=pT&RqnrNU%5*vfb-H9K&t}D9P zXI)v5Q_d23B7osT^@qly$!fkz8re~^<`K>9U~JCXyf|ia8gJ;jn43Xt(_{dW2;J}z2M-ESGxZ}?*?pIhh-GpIA<7mdKI;Vst0GtrOb^rhX literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/72.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000000000000000000000000000000000000..d09aebe2529fad76747e209af0feda3b13f6f319 GIT binary patch literal 2949 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 DBn3h} literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/76.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000000000000000000000000000000000000..3e649b670c64e7412d4090370e63a90a5892b991 GIT binary patch literal 3340 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/80.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000000000000000000000000000000000000..6dad29fe73c8977d94c44b69bc61aee1c65a2db5 GIT binary patch literal 3427 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 O*XQZ#=d#Wzp$Py3$KQMa literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/88.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 0000000000000000000000000000000000000000..b1a478afd6545419176d90a1f389de4e7084be65 GIT binary patch literal 3738 zcmeAS@N?(olHy`uVBq!ia0y~yV2A)=4mJh`hQg@^CJYP=jKx9jP7LeL$-HD>U~ox| z@J#ddWzb?^VBlb2Y|mt10V!c%V31+}0p=S zJ{C_G$B+ufw{uzFh#VI`F3u&ge224hVR`!!kA<3goO!AOCM?`iTT~k#1vyO+Dd*rj zc$fQN)9&bk^nah{{<;6;?xyYK`|sy}&s)FKv+w5pcjwMj&#$!3FYl>vVpEoiQ5Lwr zb=g# z@j!k{ZX*AtUeQZITV-aO=Zif(J^gxsu$qs?2jvT#LMk7A?#{TlDCz2|P%GO{pSWIZ zU}C%BKi@9X;Qznh>oaa_SjaY0BTz|k?u>uYX1q$t$9jI{GyMPe`~BDZTeGeTwe!hd z`l*;dMev+}*?~Jr@9*u^uKMyqkwN4_!onXH3y=57w&t`S{rvp=(Qa}5BO8;CyPThA z>pDrr^NQ^y!L06SeEuabE;z>QF7qwsRev#onJr?|D)an%9UGI6ckxIXX#{KtYvS2a z`ubX^fvM)@gFB0#yL^6jHZh4)e`rk57N(NAMcZ0ly!C0w92Qarh1>5Y3#oJq(^uB zil)~Mj4jP~cbBhU5w>=gYLAR%QOExO|Dv~_ZcLcrs2{Px;mPUg=Z*3$&gw>Q%UCy| zbDm1=rB>$a46_f$?WqXdl6BQfyMH^=mbn#kCae4FvGGc&%(JUq75ZfABL82O4>rW@ zty2B>=d=Grv)rhu+jri~ys~1VMeVOG=T5%;c2o3#%(=e3Kv;6O>QY(df@UUOZ6mo#?S7*9v|<2Rd_s^f5MNC zW!tL0zG6Dd+Pb4*sg1znla6sBee(8tudc3Mp88;YUBSA?$9jbewsCm8F8Te2BcZr> zL*3u1z*QldYem?jR|F_-EO{ApDSqpDKW*0yJ3{krY;c^S8+|QE%_4exN|$hgcJ|8P z<$Ym?a&+teew|+N``g;4>~$i(cOEkT3t{|mW~Q<0xjB|wd-i;KSTnb7RTA%v=;LQ+ zo4Zcei~X~$d~elPF3;-paPydZ>F4KlDmu4yOwkP1x{&v)Rf4%~!3Mh)2f5vzlhqWz z-Y9D*Jp1QJ?7o`FE!o%oqBs9MoA%+sK_wquT`Dzx@%Q)l?pdbUQLmEf%%AgU*zdN;I56eozS`d|Q?){MT7SM=KL5y_oyA*Q zKYu&#*HIALT(d+xC1PLAPK}ry8NF$-j=UBh|`latPqv3ZiEOf4&<-nktef0OomBGueXv>_N=(?LrVttCphqAzxL8`g}|F^m4i?+4I z?XO##cl_~*sf%r#L>K35vUv6O^L72eMJ}8htG;IG-r7_7`HL|3g>_7FwpCk%raV)2 z=&Jws)BVZG$;X9EW{K4N3~ZRRL}PBTv0LPxr0Z)UmA$6vWNK!Advo*XmzS51w)4wR z@#vT+_p9|x6Qcyzhi0aZ4+PK+M1ocA%knh z&1V0DE4p^RZq0Ix)zdt@E z&wr>Dpc!QVu2;SV3x>;v)#m7gE{#!pyVV$j5kk0n>Vdr1VLw9x-uYUdHPeoMm zzCZbQj%%~nCe2u#7CN&aA$YmpRX!f0z)Bncx;L2zR5Z5hMZA))|MSpz%{g7E&*Eme zw_H3$ZL~R_El)W=&z6aKCCfBx10U`KC(LH?aP0|tF(*_;u)(unU5J8v$Haslj^G8~ z1QR?nW321{?a8>fsFf}I>46_x7~*bD*&geYTDgjMoA3>u6Xyh(#CAEakKg~#cN)K( zjX>@1Z=#-))j|uBA0OCv^2#HZBaCXYXZm+cRbKM`-<6fY-Ga()947h|ACE7uTIMrz z%hgY6%OW-=B|SYgHKgahn%jMW2MS4yljH8pWVDh|l#$!IR7Or_$$rJTYa%ymvAe}j zw~5!%HYc~|l9!UMR^E(}_>_FWqWasK z#^fc3O}*nd z9<8z9lI2QK_Z|GlFWus`T<;rtY=&Vnk513Q$|sH0dxUJ5B^**_uYSNFlyLHUSn{zR z$zv0h-Fa@>Xslq+u71g5xhvP>eBOzb%u+f!(v=mO6F8x`+mnkJ1Hav$bY@qlgZ1mL4{AA89UGqX+x00k>S@_4F;w66 zdi$iCY1`t*2exV?uq)h36=cnEKi-%fc!1Ag%V{O98-0Du*B3R&He^h971Rj2 z`PsE}&4MX&t;<*WhsU_ze_e0NpyP08#}kJ&LBh|UG^}dWyPDwSaNtCM-#)V^OyZ#v z=GY|3*wx&apnTZw@8*NoG7Owrf6KjSdKc8tx^HvZS)(UjwO6y*Z$3Gt?Cd8Mk~B&1 z*W{aG211MmMiX~;yhs+&5&tAvq_;sTb-_EQS0BEGN2ttMK2h{V$@6ovi-VW@waHY3 zE?TTCZ}VgwBbUt=UKWXdhXV@Sq0UeD|NS;wO8c;ghGv7+u{q3^DasqpG&JeV;Cn2y zIETfjcv93V`D0J#@WrHkzQfBIc7CmP@Uos+ecU@$-ZUh!uu4ciY!7`|!!2&7(CO&D z@2J?j=T7WXj-2PsV|M!&!j`n=^JnuHT{CBzRH{ns2+-6xuw!DnMg6~;M%VU~KpB%| zxAdd8@py(Xqy#mbJF2p5i(jATi3Lm=#u8Xnso||A3q3qF^vS|O?M++F0g&mfi z+4x)d8E5iZ8Hdx`PNY9m3S0O7-rnfxyr~tcFS!}{R@f|OaAuS0a=$pkw&m>f6A=ba zxqO-a`AyTA`QrZm{dxZ;m@Tt9!eOxSfocBMOLDm;oIRPoH)Ok*rgV2s@MAP<-m90+ zop?>)r_hm@yqtPx@2v{f>qRc9&aen;n7A=AVNGPlg$0dlr*#=G&;Ir0rSiMGyO*av zn!3b4O2&e5#`M;`*V}ldmreU0Wn1a@{)kZafg?-{b}~g6=$gN~vY_&JPwejvn(X%v zwJu+3Qz;{Uz{C4xV&jdT$H(RCw@l&PmwdEKbXUd+*UYacBV>+98caPom9gYa=(F>J zNr7h$wQ_5&3_3F3sKib!*Wl8JuISxmYvZ3y$3Z?mK$wtD6}Zx zF*tcZv|0JctCIRYf$pSj`S<-iMMX4DFl$QlBrJKFtNLx@pRd>Bw;uguc{q)|&1mPF zn6@1<)_gNs9v-^4qtH3m^6~kFCb_q+yg#RLkio3^=%xGL-rjyK>T=M+p!nz{xx(I9 zW}bs*x{_=2|Ni>A)%9TZWo<#Z=p2EpI1ynEgY_Od2M-15L_O2Ce_0zRn|)=)!_6-R zFYRKTrgbdzKnTZ_2MQZp4&3+NTN%j1puBcx=Yh(mX3N77)erM8c1QE|eA(RX%rt|A zL1dH1k+MF4;>PDEcOELSzaPwAy*{wv`SB&$Rqvni@$8;Mdtk>~WDi4$)&etmu2-=^lrgsv}@O_c6>PzVF3k$t3rwEWKVEQ*>1lHQiM?_>J98H+ z#=pE$8Ywt0YWl*KU;pjrvHCdOI(XQ$ho4PGeDRq>0@ZQ`GfudFnEjG*L!N%~(p^0q z2UK!Yjxn~sRSwkDKPkidrYP5i(VcO(&w;0J{&2 + exit 1 +fi + +BUILD_NUMBER=$LATEST_BUILD_NUMBER + +if [[ $# -gt 0 && "$1" == "increment" ]]; then + NEW_BUILD_NUMBER=$((LATEST_BUILD_NUMBER + 1)) + NEW_TAG="$TAG_PREFIX$NEW_BUILD_NUMBER" + BUILD_NUMBER=$NEW_BUILD_NUMBER + + git tag $NEW_TAG + git push --quiet origin $NEW_TAG + gh release create "$NEW_TAG" -t "Build $BUILD_NUMBER" --verify-tag --generate-notes >/dev/null +fi + +if [[ -z $(grep $BUILD_NUMBER Apple/Configuration/Version.xcconfig 2>/dev/null) ]]; then + echo "CURRENT_PROJECT_VERSION = $BUILD_NUMBER" > Apple/Configuration/Version.xcconfig + git update-index --assume-unchanged Apple/Configuration/Version.xcconfig +fi + +if [[ $# -gt 0 && "$1" == "status" ]]; then + if [[ $CURRENT_BUILD_NUMBER -eq $LATEST_BUILD_NUMBER ]]; then + echo "clean" + else + echo "dirty" + fi + exit 0 +fi + +echo $BUILD_NUMBER From a97063f9b731cf73ffddc4c81826935f6fd1b978 Mon Sep 17 00:00:00 2001 From: "Kartikey S. Chauhan" Date: Sat, 2 Sep 2023 23:18:25 +0530 Subject: [PATCH 114/128] Initial website setup - Created project structure with necessary directories and files - Set up Next.js with Tailwind CSS and Font Awesome - Added base HTML structure and layout components - Configured routing and created the homepage - Styled the homepage with basic styling - Added FontAwesome icons - Configured font imports and styles - Integrated HackClub branding elements This commit establishes the foundation for our website, including the project structure, styling, and initial content. --- site/.eslintrc.json | 3 + site/.gitignore | 5 ++ site/.prettierignore | 6 ++ site/assets/Bold.woff2 | Bin 0 -> 25000 bytes site/assets/Italic.woff2 | Bin 0 -> 21744 bytes site/assets/Regular.woff2 | Bin 0 -> 22408 bytes site/layout/layout.tsx | 47 ++++++++++++ site/next.config.js | 6 ++ site/package.json | 36 +++++++++ site/pages/_app.tsx | 14 ++++ site/pages/_document.tsx | 13 ++++ site/pages/index.tsx | 154 ++++++++++++++++++++++++++++++++++++++ site/postcss.config.js | 6 ++ site/prettier.config.js | 3 + site/public/hackclub.svg | 66 ++++++++++++++++ site/static/globals.css | 3 + site/tailwind.config.ts | 28 +++++++ site/tsconfig.json | 27 +++++++ 18 files changed, 417 insertions(+) create mode 100644 site/.eslintrc.json create mode 100644 site/.gitignore create mode 100644 site/.prettierignore create mode 100644 site/assets/Bold.woff2 create mode 100644 site/assets/Italic.woff2 create mode 100644 site/assets/Regular.woff2 create mode 100644 site/layout/layout.tsx create mode 100644 site/next.config.js create mode 100644 site/package.json create mode 100644 site/pages/_app.tsx create mode 100644 site/pages/_document.tsx create mode 100644 site/pages/index.tsx create mode 100644 site/postcss.config.js create mode 100644 site/prettier.config.js create mode 100644 site/public/hackclub.svg create mode 100644 site/static/globals.css create mode 100644 site/tailwind.config.ts create mode 100644 site/tsconfig.json diff --git a/site/.eslintrc.json b/site/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/site/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..71b863e --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/out/ + +/.next/ +next-env.d.ts diff --git a/site/.prettierignore b/site/.prettierignore new file mode 100644 index 0000000..fcac576 --- /dev/null +++ b/site/.prettierignore @@ -0,0 +1,6 @@ +# Ignore artifacts: +build +coverage + +# Ignore all HTML files: +**/*.html \ No newline at end of file diff --git a/site/assets/Bold.woff2 b/site/assets/Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..8c00084a985116cb2ef26d1b79acd67af692ba53 GIT binary patch literal 25000 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 z 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 VarXY^{9pFMN%PJW>}6qK001pXiIM;S literal 0 HcmV?d00001 diff --git a/site/assets/Italic.woff2 b/site/assets/Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..056909c32a24bdfed5c6149656be0e42cc6b47ec GIT binary patch literal 21744 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 literal 0 HcmV?d00001 diff --git a/site/assets/Regular.woff2 b/site/assets/Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d7c3d52c693bb4dfd0786625f21ea2f4726125d5 GIT binary patch literal 22408 zcmXT-cQayOWME)m2=8DJ1krD2FffFxFfeR32Jz6blz{YF6`3|WwgiSA2@V_1Sz>}5 zTuldbnx<4RnlY&`_X}~ihcK`xuy&fU&x+w`b+q@pf}tkbm^-C96~0Ybz2K9BZ`cjrwrOc`d7A?F*UrPh&KL zuh;~|hHb44Qi-e;87+jIVOZRghCwsiSUv)^gK=IJ?!W>tv#szv}(0WaNE@9DSMZ2Rj8#W z+`eIV@=59;`zf>kKleKj{7PVnzz<#1%2&H4>A&+fygTzH&p|V5fm_Rh0*`3$oT*Cn zd-JcpbnR+`?2FHK_3*7Ry=YRR9qu3;^3ud0Pw`^SYvHRE4cS_=wg6p%qxIKft zb}urL_`M}3$1l-2?S#nOLtpwkrOrI~Z~v}x+qX&qRo@Vy3IPv?$fUD>K2Dh3)N_eN zpY_cj6_0N9$&C*hRu<s$KefMzUY@m z#idPEaLL*r(za6mck*eK`3ffAza6tb^7+-8N_$Ne6$gp#gy@FCbGHOO#+`VjC;HU% zY;Jh0#w^u_x8I9yOtu&FJ!2E8l6%qDMD*^rKRY&tuCicWstcH5-|tNw-Fy&BuA#%SWHctNO{shOE0NJ-_?`HT|gET<B==xos(*PEzP^F z*Bw^;@<5E8Pce|Md+U;Av)oJ=*UL4x38;9?_`bFO{I8wYb@FdaN?IPNot>)4Em-Dc z;bbsVdC8Ug+MoZ$d~I5KK5V;n*A)|w))1wt&!xAsEG-RV`F9np-T$ki)#13(fu}+B zTszhqR#`Om)oXlV{d9Z&8z06HgNjKSbIL^0Zg;=cbf{yGu`*#baB(|+C%^eawfXht zd=IP4!o$aYNtH#Oyt+4Rjjr+$oz+RP(SL)trs==E7RszCy>N-qt5y4YdQ-)%U$Sr5 zr5n^*^LEv*r#E-KpV%=uGP!Tg-nw^HPt{&Nd!eiUjqT<8jr%)}r#>`W;rLHF&F0g) zt(Q-IwUM`EKL2lXL7wG5|ETjH7c4o@$zb@1#Up^#QCG=Cj?J>`$^(v8h0e(yhyAKE ztX0|{G!ONLG z8JGN-ni?i-=*Za9eZAT)!%j*{db!N5+Ssp`7JRmNH{T}lW{aY^p#SVj!H>@lmp{_e zdaW<}xM}agkME*SJ>AXwQfrxxvPJ0QnQc}Docs$pqqe+kn`q!S`G5)QJjvo|d#7#+ zh?tpsYw2EF!!LoS^iNrBTDZyc=E65l{`U`DIB`Q|+4cIFGovG%q{XTi@f}!eQ4(+c zrekrR=X6Embu#OAJ=-Onx9jOP>%#9(=KYqdcro$#yoy&dt-eZ`xLsZt*&UX9tMtW{ zrP2LywZC{JOj2%i1STKzozY`i!+m7hFRIUfeA=_-tM2~8 z{Pusd>eh1=DQKK%XmoaPay-m(txnauuSVm>R1wWUzByA{+&O}+=Qg-nDaU-5&AYSO z{i%eCVTIGAe9OOAVr29<+DbZAEn4Q4$6qel5+r@qr9^j?tMBA3^Kz{O)a(ARa)_w- z?P2NUvxNGk_?_h1b$YKCIwqUMnxuDMUFEx*Z|+UjyH#IaUCuwSK=H8Ks|@A?M~>tO zzV~~ymw(B%+Ak-c{|F9W_%D0i`mPWAk~&UopY--|p2OzIZ_%D_wImN5_;q4isMOMq zDMrGfrpHzW#j_ig$1HiQd;Z_O*z}X~d;Vw}ZQVNYo=JpWS6e6#`+-GI-@XYx#~vXq zb$!Vui_~x4N#=8^H|%<5{Q5ulj^j5CUNiVE3<+|!U3i%Hd;QHH1-*0q<{KQ!3+wJG z$$ecR{B$GRipPiDOgFKXJu5GnyF7SCm(BW(pDP_>LIkIVEsZ+MG1>V~xu{iVNa2eu zXXkFu=W7?xRz1-&%jD0j#3S=vf9i;-dd)3A=qeg$xhiD!&E^k3& z_204GbnINdDB_fR+R8m27#SLN2bPF*3oQ*yd)jCHa7nPA_2V2{^#F$ZDifJg4Cdd` zO0r>DG}UrzZv<=V_H}O~CAun4>MnmWbEdsf!qVxx-B)coB;EV^dHBoA>s22sDQbSb zZE3Q_dj;$3?43WCPyF%wQc@b*g;NGOGm0#?{Iq7db>dGgXEk?Jnsw`$-IcltHuHXQ z?92CwYqZ~;G}T`A)~o0}=eiA-t6fRUj(%(KcemPwl@SXr{kd*C@$=!`J$sHn-4fY4 zb+3{6i@lbyP1`4}G5E3hK=h)yRX@|L1z+obt2Ng$p8jlK5zpz8z}PS1yUX)-WHa3; zl1)Fk^RR{L^L;0_U6@?Bd(zJ@T&*8>sPEIA-_a1itNC>Loy|P;eY2;(c3k@1SD-M= zvS@~P!tWIBf(3WQZnx@Nsd#O6(WGTH(EH@1F zJ&znMzW(a=PpESD@X*Q#;$rhZI4LaUq*jrM#-}IYVGA03+ZHt~S+c<5-8+X!1&)@bP7aZJ)6QIv zkTBc9J*Qd1rQpOowx=u3`0ovV>QLO~Byld3>vZe6iBGdcpX`X^t;{Q%>GW#Sq9qy+ zRrj{iKev}PnXWh3qvrVeT)Inqjq~Tb z`62a8A*_KtIT5NW{2Wa++Jzab3{@>X{U%M_FlkMz?qZdE?Hb#^Vd}HjJb$zy^UOhK zmyU@`RI-QRv4Ho0AGt z8w`GQS1H6?tuoruEd8%3;CgaulgZ4^!`cUVj4nC6Hr?u@B;Tj-PJP+m52hstrz?lg zt6}95)eKuy^3Rv2Ys>9h4Bvf>45r*IJzvXj^W{XczYXirE0bBDL~uUxdGTd08_%oD z`6o6AE)KYrvG=L^ml$=YGo0PWpH6t%v(5gF&?B!?`t?rvrw;nI2Ky{XshOmwMU6MurWuWi4zOL`#g* z)g$KY<*Qnt*LcV0rmcyxLG0#&De~d{zZU;=bo(d%|A(ag`{{o+-G9vg_4x1TzqbE+ zlX5qPywQ@pIw5OmmhZ}aQ#7A?zg>EA>4~72WcT3rYy0dX9NbW1K300q>SstTmx^CU>N2_M9+j;Pw@!O~^S;qOt z)>xj4n_TsLZ>hZ1#9(e!Ny}9!N!=d{XK#pc9?v!b~RUq5EpUx5W|%o^Hv`&$}PGxWmC=3)}tye zCYjU9A}Trlaw?pXPhm(n@Dyi}{apSym=<9b3BH>__+8^)g9E3RfyPs)kO{ zIDAl9K}m7(f<+6H6Ha|KDS2^e@%aYDf4BGi&OS7yL9$p(Cvs!j;c0U>=qj8w{wSO^ zU%7Ahqg|`l?R?pm9<}9k?#rIaOQw62RqwK1Yrf!{)>^uCV^Q~(V=>wGCDL6Py9Bzk zLhb$H7hd|$c=yq))9Y5$MlyQVpR!1u*z31);-fP2N-iU&WU-0KH)K8?I_<)3c0W2K zc*!cA|4dFhKd+E{uKU$6H)FDa(?g|Ir@dCL;d#(9OX)(ze!ITQM!vYx zLwJh3^3-Sl_h}!<&R`By)vZjGwq^2JxUfO`yQed2&jI}gwYpc+?VdaiRc_-MF>#4lbu*_6CD?!*~miv|DN}-+VELQWDudsT0@4|sAB8#);GwyWA4cTxw za<9X(xL0fIDn7W}{PDjy>#K$J^3LUkb3N=nXRPBhdpV4DDQ%tY8 zlB|%X5NxY{tA^L}*4qi+7>wNAWd0ey`x#mKQq8j9(y>P_c2(=&?2r_j@?N9hw4#)g zM`z@^cDLY?*p>1UAGI8h`U{;{y!WBNOCDV}^IP-1Ld?%aG9N4MOnTw-)=q2b#IA!3mkhtZTJ<}Cjp(}dheu6KWbJ1k%HLP6bdf3ob}2ytZ| z>y7J|Z*&&nk?wi>@n8{4g0#?^&3kgLU8r9;Vcz28-0GBBTX8u|E%-lRhy372!t;EH|`5L81XXM^Z zGm+_5k~Df|%(rlco7-8NU6QX7y#!8nh_0=hzKPM~;^lS1ywHHe-Vbo6qSk zm4+!-bzHqCGFOhj zQG7y{mF*v0&e<&byiM-cy`;pe%ePmlsqfC4^>f{yr2P?(wkn>k+j&GbKh%Mxf%J&MejqRGxt7_J@$Ul--B~sNFG17Hs(joZngL9b~>R?RL^mn*-bmU z`uLqKzxsq2T#oTB-bM1m&(4ls9Q>Yk!iEf+NO(UuR=T)W?#{ksNF1cq2+;dQ@=os&j-$f z%UR|N*7#NE1w3ro9uxQGt(mzY_I*WSlD4k{39RjsSW#nwKRP$a+uHd=|96-d&Y~sjTas_ z_Vqjbw8<#A(5AZjZqAMAS36_3Tf5%)z{*`S@(v-acPyA=VeXbwR>Lb)T?% zYq;i?ORcd@Soe)l^e2n)qRaMzKKr{)-Ep3zs%PxxR6cuT_4d%i zO7mw{&*BbzWo*1s&TsQ)yYx2g^V0A2RF7(%YAE`3^joi}wD-(h$(X5&PpNNhPH;OV zCb&w|iN(?3RFtx@^r^-b#ZD`gt}VErd@aj4pLuSWN>HEZoa{8)EgTY=FK*w>S+L#i zWKQk20BOJEb9>`fKD>G7y1>yBHoHr8QsgK27OeF5y;E4qb)NC^ehu~qHqK_V+;v#$ z)WRAn+U_xEwKJ}^XFvR*Lc}#!^&_idlVi@pMf#uk6ZKBq3g~+HW8vgKoie!>59dwu zvub9HGC8|6?XHNXv(J0swIK_6tzRg6%u0&dxI#9%a2aRKiUr3jkF5Rf_4fs@!>?^A z^PUyk86;FHTAq(-I2WbVmKd@i)5lmMcSd2C$6u~>ZHE=-7Pii|IvaC)@6nTUC(nI# zl9@Sx!AJ12&*E*8%R8A&TEv3v|@ ze0fMU>(i_EJ6W#e3IEluAK6-~a#!7TlzT49vf=4<@kgp$ zMu91EQVzf0ls`JszA!`XSnHW>^FCR+*F9+U(few^c{+6_+fSP#iMm(MI@ENXezdyz zr_$8tJ(2b{%DLx4S0$QlyD9f#viAe07s|gTh#l!We2Mw@!DYNRb&GALEOL6NGWEj3 zlUkxNw=Xc(2z+AHTT}kTc|}9f{_h*)Ps(L)cR!YDD8BL!`|X48#95>tZg2?n==Ev&$rSkW+%Yc834(K*n+w>gKC*Va+kR_4zvwl?>8gvb%{gh2($d?n zx8d!~8_f234=3I)UMAJOk?a62o}am{MhwA{bFvx~1uJh-NQYSlaA2`|n(k?Q*(xa;I&-{nyrRnBfV zzfI1Ovn{G{Q2Qn@FR*UkgAL2?{W|cY%YDx4V_8#ze|cE0>=n4cd`pn*8`* zW~B3kC+eH$hW~Q@_+s(w7Cx3J@2WS&s*Ag>aHk$Up5t?Q;i~g8G20@i9ly9z>xxs~ zpO86udsbd&54)Ridd~-4&EpoES6Qr@l3u6jzk{Xf&$p|_iH!S}?Rmdx?rfE`*JZQI z%lzi)Zk`u=e8B|wOuq*|xtbLpM}FPbvnArI*RfB(m$p7yWY@s-fqANFpQPX7n* z6@pKWPpJyky1L8h{T(x7u4_|1OJpwm!s~Ax=OFu3_3`5EjZg3HmW^gh$;rRcUq0h< zd;HDdb&;WQ*tb&r}AsueqPhmV&=29dHKv!uHP=&N#t-_^@b%bWMF^QP9=^uVd z>qQ(=zV@DFUbI@9!V+2Zb#2CJ9l7R=il7&fWi_R9-}H%6y6 zmwE{NdpgZ~ci!r{8iy9GkeC(zThE(WZ_c|pjpM3D?j0XP|JyOezkE;o?X=?va$atC zAzejvwYZ;5{`RtqdzOZs>1sT2VSo3aWIgKj0;`vuv2`u@a|qvKiEZ|TqTJX^}m z{+2J^y6@Do?~{FlT3Y+oZ1Op?OpNtyYUZ1W`u*=}p1Yo6{Cm|gOg?e%^95Zd0gt2C z&H9kE=+tEMi`Pp{k9=QO`*5x?$LoZXf2EiCscG(D3@mCexpQZtc0T7EW~I>WDGz6B zK4&fy$bXz6a=(|YF#E~$rsRL+?HBIM-)1o3?WV(D5;Y{)++$K_+)Rl0nOkQhbjVoAS)Get%1}`1RB1sCfU1ixZ}Wh3kK?`ExEO%PGsINa~?({q?v5N9EIQ zevRE9cSFWGNadF>_tVIjTQ}#cD9-7cu3W|+(!Md?+858osP@KCAY|kF zIZY17|JxkBzVe4dub(exi4HH{q7Mcli<|gonfvh8uiP^|qV)BfS@Sn8%9!PqvwfAo zs_!?$MCO*T+yCa6=F7eGYwxQ`Z|)}T_;5-ua?|>9{T)r`e(bDStNs4x;VJw3K1E;m zyBZcREFMzd9D3A|^}x*o=Hh!6q-t(n?R>fKeO2jWN9S2n%f72t<>$7SR5x#mI9C1T zSo^^xX=?FXm+(JapwfF$R_K(6+@>BslY2M+X`S}E`nJWRFy?#7A#RD-10gL!XWu!e z-Mgs3mM#2u+T(OaDNtjV#rpd8l-&ec%-|n8f?_Bq@{DpgBz=U>0&W+bvQMPWTZA~GKFGHq+da6uQjtptx>- z$(~EE_t+#_FOd1U^68&N^Si$Gq{{G_d2_WYug@ym`KcoLbAWjMqtojZ?iFb{swTJ< zYOy|?ZROwH)p2Xx4%4Gv2e(}{(dw2ct(ctMdhE7_ql;DVw!J=)IJ9F#z+zZ!Y{Q3QMbZ~|3 z?b7?9fj#_IRaH9{NzaevXTOrQyC;0x_p7z*szX+F`#qh0@zccH*LoLPy6Pxu@3^z+ zqlbkNz3)K8} z{o3(kMuoE+N8iVG{@~0%DSee@@d79Adn8}ERz6j+GSz0K&Vm&@$0k>*COu{GyHfOM zmdJU&V(G6{0{%@a12^&Bu6s76=?{0CMX~K&-3Q^jV+5xao}C`NNP7L+^34mPy05F{ z|BnuTzo|O-&a;hj;lJ0J8S$n3SyfQ@w8H<4*qh$z^VKE%|6Ki>9h3d^`DKQS(q9r6 z|NE)FSW?1lRh-}~wkN@t7~_>}Ze6>zJ1fJ{+oWT0$H~QwYnWeYW*m_I_?_t|d z*N;s9nE&x~l9_#Y#O|zOJAMB>+XSRe{hGb_BKtJir0q}Mnadb0i95x+e@^o9;^I%W zpY}=qbgcaHQ)S!o8+Atf`zJgOe-c$*zwhnmWyX8Vo7T2>^NX18eU`j;&xgGew^_gZ ze$hC*>eLR^A79!J|9E{{J;PdWZpDMAPyVhfcqbV1TSoqQ{Iy4y?AKWwDs?-Wwg2(5 z+V*p2X9TZ`iu>#SN@ihr<2qfDg^$)6RP8A@-O(bN=hN-}*2iwiwr1N(8?H;*s;6Z$ zH9mHg&YdIsoMX#&wLQiYc0X9w@hx_L#*F`aqYiew<$0!Gwc-bZZr{<&uK(vBJdM4v z?sVD&_M20+kF#I?BX#}%B%Rl@KCV>H+q3N+_r@rl#Mx52mOO0xtkJB^=N#;4z}*y_ z=yAqXvNG?-)^0VEq>T#%Co?|KkUGN4U-L=R;NRPYi`#`acsAaYIyw8am~Noc*NX+) zxLuT2x?PNZQ#J7#U%K`&?+cHY-2ZJlr$8pLK*caaouPO^WjfEK0s*%^@>p%jx_l>fyEhN?!NAaon2`ee>F^*%@C-kDaQWm|xzpwIiO-@$lKTjOu1dJW3y= zCr_Bi67e!hA=uzxntbpq8QEOLslv8#>Z`A(ZC_SUbGu@cg5>ecn$k<_?!&+G~PbC>ewuMYD| zz8olURg=TUSf=Mo+VS0Ylpki)lbd^ z6h8`AF!N5FD)yQEoTvN!<>iK9KL0%oc0Kr>esInIzs<|{v)*goe@#E`7iZnOg-XYk ziPiAglze$NZ~D2K4`KI(m;0T5{M$6V_k5Gd>D9rSvAg^&9o9E9_H|r}`*A$v`dce! z_ufTD90vZy4Ni)^=b4vY=sb;rX?(=c_LN zmdu%OYmX@Zj&b(f#bT^3}cZneTV zeM*~;frtc$pp4>S&y_zMrCjTk!YVngW|am?d`~y{d5OcU%A%q|e%pJ_pDh)hsjcy% zaZCmAA0iLWn|Xh#w8F77sbVUE`D@;E+Q<+$#bSu;HrtDQL(VKN~s z{fcLP%SZdpHxG~94L6&UeP)gCy4*W8W$$PA{@twe+~*pfYw;gOShxSSY+)}@}=!?l)ZTg&xepA{pf(l z({;4sUmUXS5Zje3Hif;~xlvGc+526$sy=t;M|PZe;L!P@=26qfNq4l5FZ~*L#mxJ@ z)y$?X$&b|9mOObd-B;+~jTxEK17<~@bvAWeSDo9)=s4l|zKQ!BmFF#}KB6n%`}0d9 zbKKm&oM8^j_&zu9__mbaR6sxU{{Gh+%l9n^o6-DlZT{iklRJ04*8TOn{_L(vrxXrU z_3)+$SQi)^n9?z4%Kt#tbyqh}D4nwScb$IR&3}#6voG8VWmjCr%*bnHhn7T4e3bM7O{9S{PPe$-k00bwbd)4||Jaf9cFv`_1Wm^XbKBpS3v@|24g} zJgM8hLB+l!();~?-F35)S4i-D?V4a+{A+pKd+n{P-k+l{#6@k52y=aQC#AkNcIAol zi-W@w7_V&SE6Mcgs=v4V;NwMs={_!x;zcfQT4vA9p&tF@HZGG_ zc3z?SYqvb#snPjm+iF({{->&q+GdN_N|zj8az1NITxSv6?WCPER`$+_J-hm!uD7q6 z{HYU77Z>&IV>LW(_xaGXhzB=gu3h`0Id^f~OLjdRodtTN!o|MI268nMFHox zbgz^5TziD(t?ctr`n_%DwOK3o-Muy2`%bKl&$W4`r=By}Z#o_%w152y)AcU62Y4xFU>XIkBt9dCbdNCe&t z37;FkzuTr@&b{|;dT%$HKXooD(w2LJmmnGT`pwRI~O&5vzf-~aO+N9B^% z{*LWe9%t(+YW;aP#oFV^p3Vo`3r^oH>s0-nX#HuI(6zPygWq15eBwvw9Wi-E<=;8K z{dP~X5%&DJQF)vCR=Xa)>d6PpxYM4d_&?X(^Iox1sMBYkrRqmZv7^kcX|>CK^4U8S zZNL0J_Q0D}5}MVD2fuFnIzfL&v2*uY)fZ~kEdHXJ9|X4=JUX8CdfvfToBB69nHz-| z%D;NS@O$B$#V0qNp8Q!hK2-Pd9~JfQY{G{2+UsWVm2MK|aj##~7aH!qKkT^L!P z*@AC{xzbz?IWs-H^J|u>>4U{*3v`k*rgz%b-r0Bm`^GC9mjC^3`+UB~y@QM|747~{ z5ZT|e`bspQ- zH%;em-7;J3Yg@Z~$&156zq#zMJZpUMjz2q{gWt4$awYc`vGbeXtuue5{9tQNScmz& z4v$UdN1{L1)gS+xt{ZbY@Pg*|zrl|mWEk2WYJPolN!G7BR}cHVI`Hi+^OaqP!=Gnd zRVlMQ<7}{ZTJYl^&+U|A6L%)9D33Y$GyKC#Vm*W`&kDTh|k`(EN1wSUYKa4gi{%jHcciz9(6Ikre_%uz= zs$b<1@Nnmm6a1-*slVoqT+-oUa`?Zz^g+*(ftU3Pp{ynCZuLQfk+bd zB?r)STZ^}IKJE&l6qbb`c|`}4oN zRSzl1wvl@8d5+mIt(_}q)SlYaSkuYdo0 z(Yf1Vw`xz{Ui>$GU-GqkUsCVS{Ql}y-WOFS+y45GM<3hv@Bbqk7MginV{@ebBiUO$ zL0WG!rY?TsbC;2+c$wb9+gY_r*NxxlSg;qBRUe!dweaGkb-WIW?^N{~6!KmOdGL!{ zv^f52|N8A!eP)?m{IP%8>esKw*KYr>^5JD3cOS7Yd-qJkWM#vqg(n}s z5?5Z=WFi;ZQ@O0{d=IBl=%gt*lCE9iDH~f#F3Fp|d+~DLNs0b-!OMg$NZh(>@~&Ox zl&Go&%= zCz{RXzMAE_wNj#U8{TdmyYY-aOj+$sI2&QkZhbFq31cl_*L@xU(c%%kff^K*~K zwx)h*dlzsjSX|=cY{zLbr(*!KYOo-vpd6z&r6){@_u<(9d2kavz3#Xk#g$6uM| zuNGCbFRB0X-}EEPc{p9|Pm9VdyilC?JNDJA`b#m(PUt@A;r}5P{_a@Z{@p%T67}UI zk1xyq=k{|)Q-$g$gQ&uNAyW*}%~zSnQQ`Vi;3U=9fjiKCV>Z)wZwO zR(j{_+qyG~^N)#$>$T6i<+k_ky|ZzD97?mFKahCAvv5+{W+~ID?NfDXB>jZl+of3J zKfKNrc-JV(bY%f=6z6;;ZmZ9yqgf)B{)=rqCgV1}#p~jN$Ww><8nYt5r_?#Rw@CQS zGD(~7y1L7P`3K`fR}1cK<&W>OYwg<=ux`ur?%lUON}XES{4Q(anZrx2{a9)%+&ZuC zY3`AbHOCiUPkcAyXxF0|Hx57AeSK}RWpzmP^vlis?kziv6^e%=vqd9!W7{WQKEx_ukM&xt-{m;K&mEvl{gT{LNZ|FmT`dzPlwChv7f=XBN6 z*gK(o!{)7}D=u@d+#{}ZZ33T2RW0c-Kl;&ce5rY%?Mp~NM^?EmjNdQw0EuV z;ZPI})}1E$tW43PcKTI8j-H5^=N!=qqLmxdoU9kxUQ7|Xxa|JQ3geTyD_oY_S9YHN zZe4SdFM9SB(`^r&KD_fBSdIUxfr@{(HLQsji>b7P$iMgdgv19vPdN z+%>h(&=ffv(InL|*_*%pWlY}2pY|`hJ+=CEzc_lB*P9+#b1vY7o5`2cm;6@7R=n;u z*n29g^kLU~=@t6MlBZhZL=@7P^VIHr*LRM;&k!CwFX7rfzi%g+@*>^`YFnNPjbHyv zQmIT@>X2gp&vvV4CAyCVAF2FHi+$VHS-!3d*pLnIX!t~>wv#fKp`2M`l?Jn7Q{9QrCo1$O8 z+5feMh92nrd+^spx4->I|1^62o>18(zHVWAC9lC=Ha)3L3r|0u{NeBRN88`Izt^$T z?#x;jhM1Q+F0f_nMth5WFy7tRn95|NaXaGjpF!u1a^xe)qAra+T-RUYG0> z?oS#{8y?}T`9JILl#6c|Gx^-5tM9+Bwl@{OwyBxx^t8Mh;oH>`^%r-U?r^KNlbn9; znXeC97N4J*hUoX#HdlGCPu{gP+1W}e_0fkn|8``Z+-Y5`{P9hnfJFcLo3EOGEokI? zlvt8`Z*szqNtQ<+iux^z=xkr?wlcWvZTsYXS31OYn9CS?T@K%GEtlOf59hm#k(LW;sw8pF1ir0JPk|7h-5AGo9- zF?a2XbFWQy1)cRXW||yU{>JCAkj-A>EuT;MC7Jv<)Ol{PoWcFyio1S1I{NeAu{Kq= z*{Wvt)&6;Ld}>e1Ixe~UP5!63@#Y+@ZKn>HJTlnQAt1iqbLp!5rn?WX8fQxH^hPx(Cca?J6qOTt=(`|Sji$bGj>gG)Z7iF z%WvzxDBDo1`9`}}^=-_RxVw+HSx7lbJzizn?{+L;x_w95o>@PxEq1xAyZ+Y=N0;)p z<=d*aW?Z`6zw_w1rykNe={G}ctnbg;YRCQb%07O3L(86M5l4kxy~$fyS9Yq*l%61< zktv_$F3X~1y|FICamGTo*2nBljeA-(-|BW%{5d+2K~d?yliz*CsulV&FCU+O{ch{M z->+V4WxHRm`xmt9UHR+xthQd6+P3PflcUQ$-u^lEd(~3g^?^FeV{;E(s9lx(6X4?N8Qw5_d z=GP9@{ZlNs*)H}=N=d-vwE5&4@!^N2`v!4vPG@MGx8P`*F1zLl=Ka$ZTvZod4E^SG zv3;VfHIup$pGf->0U<6%CPT;5n&OHq7LOfRlvrI^E9E^CgJsQkowCz7w&9=3&t%U3 zzv_N%`S458W8M21rVblA7d+8BHccq?#pb5{y4PZ^pHMbEZo)RZJ95r8llMm^%W%xv zxJGf#jgD6D?OhHY%TJ%&=X`YMj;RKo`$Vo@5qs5S?=r#3;}(a&lVpt-WxL-szAHOh z)!EVdtZY*5)_-ynFMJcx7AR(j67HnoCyxY>WrH8RwcRepTGmu!TFuP$h4F>f07mr&rvwS|7N1tX#Xb z);o8l@2=;qCRe_Ea%(L*<#*Ia|KrB={x7zL#t*A^TQ69;xh&r*_3x&ppE@qz@4UFX zJ+S(2ma*umg`r=o_jvr9$sS{}^~eW{CC8kk|7~lU|LkjQHQ&mYfA?j+`?v6-8k<3x zcbH5s>&E#0NuEoJuSmJ7d4w_Rcb=Oy;g{IVi>y0pPVE0X`OLY=+Z)q<-4odBX`nVs z`s(tQC$mlV_V@1Uoj>DD)4ADJ2d*4je^Th={>JpibF(H?@kXBb%+aYLzjos3R-GFR zk>ZnrMfXI$(777qn-cA@k9%h8cfqY!YE#<3Ojr0Rn7sDJq?dsY@2=~4&GpYUFC*{b zp6PwzKQfpu?|iR)`r6;8U-+l&ed>7mO~4{ue*eFL52E|R?AG=C4?ce6%f`}c`D@Os zJF|1WbXTn3GAq-58@Y{{x<(d9R!sl<&3n(rkH6)zWNJ&dysZ70o&0$2bFq3xfu9!+ z@<(aE8FqmujT^rz9}-U)@%C-tG@FZ|;|Ub*YWblAB^<9sVboVbbQyIqeJA zpL;pScE+^*1`pQ;9e(~`ht%$$vp^P}c(~eoQ_H>&KC|6N|=Koq(%rJK% zSAM|UnDpZu@3Kqh_E*J~GGA%7Dp+!H*WEga$gb(e0+AQ0tN$$8e9%3&V2bj_uv+b*4R0V^T@#UF%!(eUBe`AAW7;pSXNg#(#S@$q4^$ z65;20=<&uz^z|#g$l}DBeD3s5;U2$rc7J&MH}d1dc^S;=B{x$9FC7(RKk`u|vn+S= ziAHUK5S5~Re9=4CaG&NgyIf#?xaN~b1oNeAr5$B_KkrDcW0aLx8$D4rafSYMAJwD( zw1ef$BNxi%-C%s(QGI8tW7LMj(ph?I)1o~JE}YsN6>_8Ox9zkgug|P`X|y4X&oA81 zx#!>1SLUW}GuNgFKUu$I$MHnvwg^V0FP~4RSo^P!bDfsFlmFcnfdvO^-P9X1_sFNj zEbz&`^mmGXM~9>J`Pn`KJ1(lTdaRF~cr)Rk?qfddsq9wlyt9|9R0SQ`HSx+hgV)=p zZ(!t!c$y{=cei79_e4>4*Os($#|8S4`{zHL=XCUwNRqYf#U+k%pLCM;-8;eB@3AOL z&12Kaw9V2IABF59mP;B$xM~ZkcH8;1Wa#`=y;YQ`%O!fO&8_BAo&Qnq^6ARu3Uu=2g#^WpZJz$&gPn%7m3tsSuukN0ZP`~f6)%U0CLKWO@@Dmir!w#?48aHx|JqUOv z&Arv6VAi$Etm_+8OwRRZo!GvMS#ybCM$Y4j-*Z1+-pC;TRpCU!OO9vP;wGLe_1LyA zJm(^3G>7!M)~tJXn(p+jkP{Xy5EV{~nmu`~X6x&OfO9VQ0w2Ekv-t(@^ld*qV%~@6 z3eDS?z46YzH|~t7Y{}B|R@Y?9B2LqGrn) z-&;;lTvl=-?qyQO%DVD}?6o{83q9FiMjOsGzbB))?v8Jes9(_8%Bf`n&Vd~Ve<>*U zX8dFh{bt+l<|g7iXUpd$wxza+)&0OVd8bu!>kaNhz{qy$(dpcjW?|a8yN8cH5(-)-Yd-Y$t{m?3H-Nvit=eFL~ zepl9hv-0$bX@}?Xua(|Vy*xcz!f#Del3LN5GftBoHXPh3ouTGZ&QSN(cT#DMoI%E? zqzlJ;4=webt9WM3m%NRk(aINJ@2Wf(7`Crr?Y+o_%)7cSmw&GDaFWU^eRb{WMZN3W z*O@)`YhP*+a-FaBti@9Ie981R@05aWe|8j$diT&kYj;q~vd1wxqE$W1=G4gjE_GZz zXOV5CN#W#+ZzmSK?f<-xfA-atao=|ob5G0iPn*U&)zEQ!(n{@+U3?cfw_J(XBl76R z#yPW-1a9uLKks@*Emtx3{lLV)T(0R;EQ2mJdw$z`SeL`Pd9m}v)jwX|OtHV)e`=4A zXg|~at-e_b#`&jaPtZPk?~<*Je%}T8`5(6KQ#sE5J8(tYd=?*lF9v6M$$N2ojJp@t z{8Bz;xp3m+iYHdf8U}xu&%Smg zEa3CS%#>-4N{-RCX$ z3G~p~Zg#yW%Jxp^>w-&n=3Prlh?=Br@Tb!7PFv8c2WOjB^{Re3bH6Uvc$L*Ug*`{q z*Dt7K7&)gjC0%UY_5Ee4f@|?H<~*~i6RVGDs_=<^V83Gc zeBN_WiGGm_GoOp`&b6B&ze-3y`|0%UkE?RcoOZsjwz+fQ`|;eZTXaPuKkwcVGesaU zz<6eH=|nX{t|GsPZ=YJr!k>L!cl+P-^8GdLsznQ1j(L32;*_1A9sf*W?UBmV)U;Dm z{9fcekhxV;e(lIU4q3yC3)8~YADejFXcnIkm6$R`;>or@#RU!#`{t-GIlH3z@QU0$ zOiNUx*DF32zsuZl=e)z_EBt0om*XqMB9^nR@YCzeRd{$&a$Id zek~|Z{hM%HWy+q_A9&Zxht+l0q*^YrFcW!bSX5!EWL7#w=4&q7{C4Ow2^xR-S-9vatMI;cwGk_s=UU?5yvu5#RlEor8^_<@=Qd(XHa*GU8jpI$lhDuQct+ii@2JYTw%Y z)TfjhteTP-u_hq!szq8D2j4>b-P*@@IQDmDR7jg$mbCa91o?%luml6{%-LSAFbmm@C-owRev&2HmVxhs?(A*mpmFbOprCB33MCkWeA3pef{nH6E zay$2b^3xSp=lfT5St(BDkMD>7*>(C^9P`b5B-TYFoZx57=W|t*Q~O(g_OywQfp1;* zP4y!?j>?v=;_CBPmH93p@nx;peAh=??Jhb^Zn%@BVrI6h@rO#J65r>4MU|GBMGph7 zcDme-V!D$1##r|1t&Q_v?1{LX8`Yj`@g&aTM}C9h53$K_%7Uzpeet~N(pvp=_tk*k z^%K-4cOTH4w&Jq#OYMM8xkqaAGi}a!e>r|k#n2%9hgg(zg0b%T53ktrxehLL5Y}Ni zo#a$zQ=t)|Hl?(^<=^SBTeV;I3cD`iYj69{*kYo1v+IHSXI3LIms?Rkm=(R2WjWlu z5WcYVzwd8}VD_DByuR~jH`&jSW7v~(cjdG^YtGcr72Ez@*(zeHyS{3gw{EX`mf(fd z>IS=?(`sG{eNW>~m0CB6SNN&s^V%)kOS*Esru(tQuxsr1`JnLipn&Tawf|R@<0F`k z-F-iyaa*xYLLq-eKt0>Ok2~}~uZjx4F~@4%}2o4(qm6;s~6cRW~lx49^8p7AGxOU4`yf@{~_=K49o{YUPn zezVUXf4Cf~wpMa>xL^KI{gd$B##w?&q93@gZPUKdCB^^V)tzJK>bX0FzP4t3SaE5I z+MWyjcfWrU{{7+a>yK_%t{(K$P7vP5(EILglecon{M-9?ZtqMO=t)8QZ>8X4J>MUT znQ42aMPzLWWd6tWsISMi;FY(CVd;y~8%rd2ivI1qbI|_ovitR(a$(MkuJ&%_o-ils zsr1aN4k1pu2bM2if8ymo?rlPjS#4V_PxCKZVt;IrU_Z}!*~p~aJ!ul;`%wP%t$@i4*_nXbuM@xBaF>*cFDdPFibG=^c0+*xf*O^a$>$+yTb)>HN=fICH z`Io#77`@J4x-G-BtY!O$YL|s)ytd5how(oo(*t-;c(axDy+PHj&sQJHjk$Ye`Ti5EO>Em_S=*9sdt~2W_!Qbdr98MWjr($8 zL|TMFs6^;J_KmwNtvb?j(w18oobS7^P10uGO!msJpDwpgw~zblA*OV8lZ5)jo&RPn z|E;Nh-a&0oUTUVt%4g1A?hgYmMZdnFc6v?p@m+Z@!X8M5My-@6{-Ciiq5I+T9+#c3 z${hZ!3%h^WR6czBz3=fwZv?CNW;x4x`dAlNbFcgw;QG1k+O9q8qR*{frMc2M^?Bs! z&`VkAv-xTdoeWep%5a$S$(Kh%^6#Y=*JASddmj}GOq-O&RFT<||1$XhtRCqDeAdxN zVqe~~Wb+rYF#PfI(Z0)ZTjlp{UAUG(HFf@mg}kZ`^Cc?RUe%GAc(7$|W0)wb;Y=;7 z-b2}$7N^X*Wt=B3uF-p&Xy2~+&^2&I8cP&|YsT7}?qU&2W-%&rA|A|pr#$PMM|I#M zR?U}-4^~XsWZF6D>F(>novu|p?!gup5B@&+UG8~Q@!`nz%G39jy;3~8-hpl9~gwJ-Q;ieLn`RZ zv)LR@6DKLCM7n#e-z|3csE@>sS)xoq&G$Z@#}LD`WNz2anC^leNERwEBPd_AMp<5-ncVoi#muDJxf?LT&qw z6SMZ6d$8Q&>f(PNdhN`2D;~S|$XV7>e`2tP&DFHH(uhv?zgK#Dg|E-}d1hNkN7?QR z+mo#2zN~b~5L#LkuI+I-thRl%ecix(LiR1k{bq%w7v!Ah@qR9{H`gp!_&Y{|zsHdOv1Y=% z&F?qe_i$dR{4#Ry4};RVm2HRSFF*UG{A}dm32WCo8)QCz)NkG%RB}wKer?6|6rSpR zkGXf+rQh1#wSWJ_`EfN?_tva?t%D&9F_bJ3>#NUVJj-(k(atmg+mpcArq! z`sLYgTP2p|EU9G?>BttwtB{{)lyFJ&VtC9jmmDyyLCV4w||DghntfPAC}oL|60t)K4aab6vm%1tCkh;EM_e~@v!N+s<_97 zH8)QsGR)4o#u@weTe4m`ld2f!ttagY|90C5$6GdhKQjG9f4u94F@TEQ;Bbb*6L{aIc)2u?>#?VE3N$Bd%qiU zALq^cJX`ttx%yhU^|oT$&;8$Lz5agBtN-@%hpeOze?vy^EbJ-?>}ZuwE81=r|QgPWv*T8LYVpabEcO#hwn~^ZJjYK``)dF zs#gB8j6I>Bzb;L0dTRJ4|8jlxN|w(h!aVKuH%{(}lHe3**libIeB@a6)TW=?J9<8+ zGTi&8eAn9Uh5u#g!^@8>caLfiU}I=BE>}KKxxe96o@C4EP_ps)fLk7$2 z<96#$OOl3-Fw?4r?B~J{_-x?KXCS%OjzHX z?4}tD?p_H0{dhsaT)p3)J-SRAqU*E1ty1pky{x|P$&O4F(fJSd*W0b+$v6LRFu~Y7f-f^m;%WC2)(OetyZV<;Ddu9aetvJp zB#G$7TREOa&%BWkX>oJ?wD!j7r|hRiJzP3eU6R7f&1i@ZKG1!S}qzTWtGsLALn1N6L-{pR#a;iRbo7uZ)J#?yp)dV zR$TmM_v4q$oM{Xjciz#wKSScXWOt?zCPc@3hPR%#CBb zRU%U7q#Lq#FDkx#g6r|4V13=KM#<#{Yrh>itzTvN@`CSGw@18l%AXumdb{!J_X4+~ z9S3Ktaw$L4QCRZzOvHxm_HlV#Z@OklrbXqL-_7MUF5Mv_!Ru^3Lvyik)6t{Ws>@g3 zw%__pD6@A(`uFXRo!0anov(hV|NHFY`}Ww~*l!*teIY+y!x29rcjIYR>mpbCtVV5z$9p@D7v`y^ z6mfhAWA^Kc6Z>er-EH<4(`5=5H-x#TTxLJUJI6DaL9gvT!^yQjo*bL(AM?T?LC37s zqs#k*GFy(qf`WbX&t5t3qIG6Vv!!#-M`u^1M~jNwCb)iPczD7*Cs?y7aTRN*)}v-N zXRgB;tfe0<1g`7F%&up`?bqE6yQvxLr>hgKzOQ|^43r(5^I zk))ho^Lvf_HpY*wVTD?4FK_&ks-c z?`LRiV&N9l@@d^27FzXquS~FFWlXnR?4r9OkHa)vMU(g1n~6+c&82!{LG=24A|a}( zn%u{~e^Bbsm~`Tcv_Xajm;K@{w+o;4d+BUksuWt-u|DqABcmLbCd2KQ1^j1SnzXGX z!h3rBOV4;VBawKwp4k&JjFM-b2w|FeGDd9El&NbfSNm$2>qTUNWO)@t03u&iZvR!$ZU zHZJD&Ud^TAcz)4GwWzD7)Lu6IYrOls_j~{9*}L19hX?ywKbo*zxz9>1w#_GU(^Hj? zYho(*=FD#5viLSF)tE~+Ld7io+8#4+m&*U;8+~>@_Op07$mxE!&#zf4%gYtzO1R9eVd=0 zT5Z(#S!#Y7Yx9aFPXv~KQ_L!%1&8*srw2WJ$wC0Odpb8w(2u$Utv#VR(>t*E^;

Ra^9H{`RJM&`B<@1)md zLRJkoy5+7c%es5=-ASuwTSKm|e!cab<%RGWslUGcHo7(M`@Y0b_SdW5HwZ4v+j8oJ zRC@Tzjh0uQN1px0H2G^NyV}>6pH07JPQU+m@2pi(rCI)S*Unb^q^T^yzD%$DK#b?= zpWIzLErWTuPPTKeVXuqQ72Z&za^GXM*3K&F#Sfw-Umchjz-cER%I9*Rb~>Z1f%0?* zjn)>1#}RC%6Jj0ASffv-hc9bBbUfoRvl@??a@{5gR;x1??--q%C(V|+>E$BP$rp~d zez4S7t#D)6#k9^tzZR|S-?U+kZf;V$W%smlNn`tT`HZyHGZ)L8Pwlrozgdo5rh>6} zzV*4LR`m$KX?wLb{CEC+@Z9V7t<2?~o1e|SGqrrrWOlm`8*8qdDpI|7NuJ4mW=gXB z)PHh$i`btxi24{y^6k1}(O{O#qr()n^*o7 zbE(>N@N9J2eT#;J*=5X$!oeTE} z-k7)UtKHW6^9-h8vE5x-^&dDl-Gm1>*yn3=9nu8LQYBSmzk6 z@78D#WJqY3$LPn%z|g~U!I(kg!I!52oHqY1{nh=jPT{%DzQy);z8KiQYm)hGrhB0+ z`~FT1*K6IA7Ul*f@3$&*?2u}iw(@Pm$Nf@M|7v_V_u|b_wxeyQBF`(eH?!P({JO@M ktLMMn{5N$>s@vbz<)-Xu6W!CO^u65RTbIE + {children} + + ); +} diff --git a/site/next.config.js b/site/next.config.js new file mode 100644 index 0000000..a35bfad --- /dev/null +++ b/site/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "export", +}; + +module.exports = nextConfig; diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..5deb167 --- /dev/null +++ b/site/package.json @@ -0,0 +1,36 @@ +{ + "name": "site", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.4.2", + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-brands-svg-icons": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/react-fontawesome": "^0.2.0", + "@headlessui/react": "^1.7.17", + "@headlessui/tailwindcss": "^0.2.0", + "@types/node": "20.5.8", + "@types/react": "18.2.21", + "@types/react-dom": "18.2.7", + "autoprefixer": "10.4.15", + "eslint": "8.48.0", + "eslint-config-next": "13.4.19", + "next": "13.4.19", + "postcss": "8.4.29", + "react": "18.2.0", + "react-dom": "18.2.0", + "tailwindcss": "3.3.3", + "typescript": "5.2.2" + }, + "devDependencies": { + "prettier": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.5.4" + } +} diff --git a/site/pages/_app.tsx b/site/pages/_app.tsx new file mode 100644 index 0000000..c0572f6 --- /dev/null +++ b/site/pages/_app.tsx @@ -0,0 +1,14 @@ +import Layout from "@/layout/layout"; +import type { AppProps } from "next/app"; +import { config } from "@fortawesome/fontawesome-svg-core"; +import "@fortawesome/fontawesome-svg-core/styles.css"; +config.autoAddCss = false; +import "static/globals.css"; + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ); +} diff --git a/site/pages/_document.tsx b/site/pages/_document.tsx new file mode 100644 index 0000000..ce4b5e1 --- /dev/null +++ b/site/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from "next/document"; + +export default function Document() { + return ( + + + +

+ + + + ); +} diff --git a/site/pages/index.tsx b/site/pages/index.tsx new file mode 100644 index 0000000..73fbc33 --- /dev/null +++ b/site/pages/index.tsx @@ -0,0 +1,154 @@ +import { faGithub } from "@fortawesome/free-brands-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Head from "next/head"; +import { + faChevronDown, + faChevronUp, + faUpRightFromSquare, +} from "@fortawesome/free-solid-svg-icons"; +import { Menu, Transition } from "@headlessui/react"; +import { useState, useRef, useEffect } from "react"; +export default function Page() { + const [chevron, setChevron] = useState(false); + const menuButtonRef = useRef(null); + const toggleDropdown = () => { + setChevron(!chevron); + }; + const handleClickOutside = (event: MouseEvent) => { + if ( + menuButtonRef.current && + !menuButtonRef.current.contains(event.target as Node) + ) { + setChevron(false); + } + }; + useEffect(() => { + document.addEventListener("click", handleClickOutside); + + return () => { + document.removeEventListener("click", handleClickOutside); + }; + }, []); + return ( + <> + + Burrow + + + +
+
+

+ Burrow Through{" "} + Firewalls +

+
+

+ Burrow is an open source tool for burrowing through firewalls, + built by teenagers at{" "} + + + Hack Club. + + {" "} + + burrow + {" "} + is a Rust-based VPN for getting around restrictive Internet + censors. +

+
+
+
+ +
+ toggleDropdown()} + ref={menuButtonRef} + className="w-50 h-12 rounded-2xl bg-hackClubRed px-3 font-SpaceMono hover:scale-105 md:h-12 md:w-auto md:rounded-3xl md:text-xl 2xl:h-16 2xl:text-2xl " + > + Install for Linux + {chevron ? ( + + ) : ( + + )} + +
+ + +
+ + {({ active }) => ( + + Install for Windows + + )} + + + + Install for MacOS + + +
+
+
+
+ + + +
+ +
+ {/* Footer */} + {/* */} +
+
+ + ); +} diff --git a/site/postcss.config.js b/site/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/site/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/site/prettier.config.js b/site/prettier.config.js new file mode 100644 index 0000000..d573118 --- /dev/null +++ b/site/prettier.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["prettier-plugin-tailwindcss"], +}; diff --git a/site/public/hackclub.svg b/site/public/hackclub.svg new file mode 100644 index 0000000..38c2a68 --- /dev/null +++ b/site/public/hackclub.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/static/globals.css b/site/static/globals.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/site/static/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/site/tailwind.config.ts b/site/tailwind.config.ts new file mode 100644 index 0000000..3df6f5a --- /dev/null +++ b/site/tailwind.config.ts @@ -0,0 +1,28 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + backgroundBlack: "#17171D", + hackClubRed: "#EC3750", + hackClubBlueShade: "#32323D", + hackClubBlue: "#338EDA", + burrowStroke: "#595959", + burrowHover: "#3D3D3D", + }, + fontFamily: { + SpaceMono: ["var(--font-space-mono)"], + Poppins: ["var(--font-poppins)"], + PhantomSans: ["var(--font-phantom-sans)"], + }, + }, + }, + plugins: [require("@headlessui/tailwindcss")({ prefix: "ui" })], +}; +export default config; diff --git a/site/tsconfig.json b/site/tsconfig.json new file mode 100644 index 0000000..c714696 --- /dev/null +++ b/site/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From ec8cc533abf7502cb09ea6f8033124786afe5fb7 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 30 Mar 2024 17:17:52 -0700 Subject: [PATCH 115/128] Add apple-app-site-association file --- site/bun.lockb | Bin 0 -> 140507 bytes site/next.config.js | 9 +++++++- site/package.json | 2 +- .../.well-known/apple-app-site-association | 21 ++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100755 site/bun.lockb create mode 100644 site/public/.well-known/apple-app-site-association diff --git a/site/bun.lockb b/site/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..ea2d13747933513bfd419def1a5e0e9b8fb67bdc GIT binary patch literal 140507 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 z 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 literal 0 HcmV?d00001 diff --git a/site/next.config.js b/site/next.config.js index a35bfad..afbb70f 100644 --- a/site/next.config.js +++ b/site/next.config.js @@ -1,6 +1,13 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: "export", + headers() { + return [ + { + source: "/.well-known/apple-app-site-association", + headers: [{ key: "Content-Type", value: "application/json" }], + } + ]; + } }; module.exports = nextConfig; diff --git a/site/package.json b/site/package.json index 5deb167..4fcacc8 100644 --- a/site/package.json +++ b/site/package.json @@ -1,5 +1,5 @@ { - "name": "site", + "name": "burrow", "version": "0.1.0", "private": true, "scripts": { diff --git a/site/public/.well-known/apple-app-site-association b/site/public/.well-known/apple-app-site-association new file mode 100644 index 0000000..63262fb --- /dev/null +++ b/site/public/.well-known/apple-app-site-association @@ -0,0 +1,21 @@ +{ + "applinks": { + "details": [ + { + "appIDs": [ + "P6PV2R9443.com.hackclub.burrow" + ], + "components": [ + { + "/": "/callback/*" + } + ] + } + ] + }, + "webcredentials": { + "apps": [ + "P6PV2R9443.com.hackclub.burrow" + ] + } +} From df549d48e6d995f0efacd028f06f0d2e51595a7e Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 30 Mar 2024 16:47:59 -0700 Subject: [PATCH 116/128] Implement Slack authentication on iOS --- .github/actions/archive/action.yml | 1 - .github/actions/notarize/action.yml | 37 ++-- Apple/App/App-iOS.entitlements | 5 + Apple/App/App-macOS.entitlements | 5 + Apple/App/BurrowView.swift | 49 ++++- Apple/App/NetworkCarouselView.swift | 39 ++++ Apple/App/NetworkView.swift | 50 ----- Apple/App/OAuth2.swift | 280 +++++++++++++++++++++++++ Apple/App/TunnelButton.swift | 26 ++- Apple/Burrow.xcodeproj/project.pbxproj | 8 + 10 files changed, 419 insertions(+), 81 deletions(-) create mode 100644 Apple/App/NetworkCarouselView.swift create mode 100644 Apple/App/OAuth2.swift diff --git a/.github/actions/archive/action.yml b/.github/actions/archive/action.yml index 37282e1..e49eb0d 100644 --- a/.github/actions/archive/action.yml +++ b/.github/actions/archive/action.yml @@ -35,7 +35,6 @@ runs: -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -onlyUsePackageVersionsFromResolvedFile \ -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ -archivePath '${{ inputs.archive-path }}' \ diff --git a/.github/actions/notarize/action.yml b/.github/actions/notarize/action.yml index 290ed86..f3f98f2 100644 --- a/.github/actions/notarize/action.yml +++ b/.github/actions/notarize/action.yml @@ -15,10 +15,6 @@ inputs: export-path: description: The path to export the archive to required: true -outputs: - notarized-app: - description: The compressed and notarized app - value: ${{ steps.notarize.outputs.notarized-app }} runs: using: composite steps: @@ -28,31 +24,28 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - echo '{"destination":"upload","method":"developer-id"}' \ + echo '{"destination":"export","method":"developer-id"}' \ | plutil -convert xml1 -o ExportOptions.plist - - xcodebuild \ - -exportArchive \ + xcodebuild -exportArchive \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + -onlyUsePackageVersionsFromResolvedFile \ -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -archivePath '${{ inputs.archive-path }}' \ + -archivePath Wallet.xcarchive \ + -exportPath Release \ -exportOptionsPlist ExportOptions.plist - until xcodebuild \ - -exportNotarizedApp \ - -allowProvisioningUpdates \ - -allowProvisioningDeviceRegistration \ - -authenticationKeyID ${{ inputs.app-store-key-id }} \ - -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ - -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -archivePath '${{ inputs.archive-path }}' \ - -exportPath ${{ inputs.export-path }} - do - echo "Failed to export app, trying again in 10s..." - sleep 10 - done + ditto -c -k --keepParent Release/Wallet.app Upload.zip + SUBMISSION_ID=$(xcrun notarytool submit --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" Upload.zip | awk '/ id:/ { print $2; exit }') - rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist + xcrun notarytool wait $SUBMISSION_ID --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" + xcrun stapler staple Release/Wallet.app + + aa archive -a lzma -b 8m -d Release -subdir Wallet.app -o Wallet.app.aar + + rm -rf Upload.zip Release AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist diff --git a/Apple/App/App-iOS.entitlements b/Apple/App/App-iOS.entitlements index 02ee960..53fcbb7 100644 --- a/Apple/App/App-iOS.entitlements +++ b/Apple/App/App-iOS.entitlements @@ -2,6 +2,11 @@ + com.apple.developer.associated-domains + + applinks:burrow.rs?mode=developer + webcredentials:burrow.rs?mode=developer + com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Apple/App/App-macOS.entitlements b/Apple/App/App-macOS.entitlements index 02ee960..53fcbb7 100644 --- a/Apple/App/App-macOS.entitlements +++ b/Apple/App/App-macOS.entitlements @@ -2,6 +2,11 @@ + com.apple.developer.associated-domains + + applinks:burrow.rs?mode=developer + webcredentials:burrow.rs?mode=developer + com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Apple/App/BurrowView.swift b/Apple/App/BurrowView.swift index b78b1e1..8447592 100644 --- a/Apple/App/BurrowView.swift +++ b/Apple/App/BurrowView.swift @@ -1,9 +1,29 @@ +import AuthenticationServices import SwiftUI +#if !os(macOS) struct BurrowView: View { + @Environment(\.webAuthenticationSession) + private var webAuthenticationSession + var body: some View { NavigationStack { VStack { + HStack { + Text("Networks") + .font(.largeTitle) + .fontWeight(.bold) + Spacer() + Menu { + Button("Hack Club", action: addHackClubNetwork) + Button("WireGuard", action: addWireGuardNetwork) + } label: { + Image(systemName: "plus.circle.fill") + .font(.title) + .accessibilityLabel("Add") + } + } + .padding(.top) NetworkCarouselView() Spacer() TunnelStatusView() @@ -11,9 +31,35 @@ struct BurrowView: View { .padding(.bottom) } .padding() - .navigationTitle("Networks") + .handleOAuth2Callback() } } + + private func addHackClubNetwork() { + Task { + try await authenticateWithSlack() + } + } + + private func addWireGuardNetwork() { + + } + + private func authenticateWithSlack() async throws { + guard + let authorizationEndpoint = URL(string: "https://slack.com/openid/connect/authorize"), + let tokenEndpoint = URL(string: "https://slack.com/api/openid.connect.token"), + let redirectURI = URL(string: "https://burrow.rs/callback/oauth2") else { return } + let session = OAuth2.Session( + authorizationEndpoint: authorizationEndpoint, + tokenEndpoint: tokenEndpoint, + redirectURI: redirectURI, + scopes: ["openid", "profile"], + clientID: "2210535565.6884042183125", + clientSecret: "2793c8a5255cae38830934c664eeb62d" + ) + let response = try await session.authorize(webAuthenticationSession) + } } #if DEBUG @@ -24,3 +70,4 @@ struct NetworkView_Previews: PreviewProvider { } } #endif +#endif diff --git a/Apple/App/NetworkCarouselView.swift b/Apple/App/NetworkCarouselView.swift new file mode 100644 index 0000000..b120c60 --- /dev/null +++ b/Apple/App/NetworkCarouselView.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct NetworkCarouselView: View { + var networks: [any Network] = [ + HackClub(id: "1"), + HackClub(id: "2"), + WireGuard(id: "4"), + HackClub(id: "5"), + ] + + var body: some View { + ScrollView(.horizontal) { + LazyHStack { + ForEach(networks, id: \.id) { network in + NetworkView(network: network) + .containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center) + .scrollTransition(.interactive, axis: .horizontal) { content, phase in + content + .scaleEffect(1.0 - abs(phase.value) * 0.1) + } + } + } + } + .scrollTargetLayout() + .scrollClipDisabled() + .scrollIndicators(.hidden) + .defaultScrollAnchor(.center) + .scrollTargetBehavior(.viewAligned) + .containerRelativeFrame(.horizontal) + } +} + +#if DEBUG +struct NetworkCarouselView_Previews: PreviewProvider { + static var previews: some View { + NetworkCarouselView() + } +} +#endif diff --git a/Apple/App/NetworkView.swift b/Apple/App/NetworkView.swift index 290254c..b839d65 100644 --- a/Apple/App/NetworkView.swift +++ b/Apple/App/NetworkView.swift @@ -30,59 +30,9 @@ struct NetworkView: View { } } -struct AddNetworkView: View { - var body: some View { - Text("Add Network") - .frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175) - .background( - RoundedRectangle(cornerRadius: 10) - .stroke(style: .init(lineWidth: 2, dash: [6])) - ) - } -} - extension NetworkView where Content == AnyView { init(network: any Network) { color = network.backgroundColor content = { AnyView(network.label) } } } - -struct NetworkCarouselView: View { - var networks: [any Network] = [ - HackClub(id: "1"), - HackClub(id: "2"), - WireGuard(id: "4"), - HackClub(id: "5"), - ] - - var body: some View { - ScrollView(.horizontal) { - LazyHStack { - ForEach(networks, id: \.id) { network in - NetworkView(network: network) - .containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center) - .scrollTransition(.interactive, axis: .horizontal) { content, phase in - content - .scaleEffect(1.0 - abs(phase.value) * 0.1) - } - } - AddNetworkView() - } - .scrollTargetLayout() - } - .scrollClipDisabled() - .scrollIndicators(.hidden) - .defaultScrollAnchor(.center) - .scrollTargetBehavior(.viewAligned) - .containerRelativeFrame(.horizontal) - } -} - -#if DEBUG -struct NetworkCarouselView_Previews: PreviewProvider { - static var previews: some View { - NetworkCarouselView() - } -} -#endif diff --git a/Apple/App/OAuth2.swift b/Apple/App/OAuth2.swift new file mode 100644 index 0000000..dc8c62b --- /dev/null +++ b/Apple/App/OAuth2.swift @@ -0,0 +1,280 @@ +import AuthenticationServices +import SwiftUI +import Foundation + +enum OAuth2 { + enum Error: Swift.Error { + case unknown + case invalidAuthorizationURL + case invalidCallbackURL + case invalidRedirectURI + } + + struct Credential { + var accessToken: String + var refreshToken: String? + var expirationDate: Date? + } + + struct Session { + var authorizationEndpoint: URL + var tokenEndpoint: URL + var redirectURI: URL + var responseType = OAuth2.ResponseType.code + var scopes: Set + var clientID: String + var clientSecret: String + + fileprivate static var queue: [Int: CheckedContinuation] = [:] + + fileprivate static func handle(url: URL) { + let continuations = queue + queue.removeAll() + for (_, continuation) in continuations { + continuation.resume(returning: url) + } + } + + public init( + authorizationEndpoint: URL, + tokenEndpoint: URL, + redirectURI: URL, + scopes: Set, + clientID: String, + clientSecret: String + ) { + self.authorizationEndpoint = authorizationEndpoint + self.tokenEndpoint = tokenEndpoint + self.redirectURI = redirectURI + self.scopes = scopes + self.clientID = clientID + self.clientSecret = clientSecret + } + + private var authorizationURL: URL { + get throws { + var queryItems: [URLQueryItem] = [ + .init(name: "client_id", value: clientID), + .init(name: "response_type", value: responseType.rawValue), + .init(name: "redirect_uri", value: redirectURI.absoluteString), + ] + if !scopes.isEmpty { + queryItems.append(.init(name: "scope", value: scopes.joined(separator: ","))) + } + guard var components = URLComponents(url: authorizationEndpoint, resolvingAgainstBaseURL: false) else { + throw OAuth2.Error.invalidAuthorizationURL + } + components.queryItems = queryItems + guard let authorizationURL = components.url else { throw OAuth2.Error.invalidAuthorizationURL } + return authorizationURL + } + } + + private func handle(callbackURL: URL) async throws -> OAuth2.AccessTokenResponse { + switch responseType { + case .code: + guard let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false) else { + throw OAuth2.Error.invalidCallbackURL + } + return try await handle(response: try components.decode(OAuth2.CodeResponse.self)) + default: + throw OAuth2.Error.invalidCallbackURL + } + } + + private func handle(response: OAuth2.CodeResponse) async throws -> OAuth2.AccessTokenResponse { + var components = URLComponents() + components.queryItems = [ + .init(name: "client_id", value: clientID), + .init(name: "client_secret", value: clientSecret), + .init(name: "grant_type", value: GrantType.authorizationCode.rawValue), + .init(name: "code", value: response.code), + .init(name: "redirect_uri", value: redirectURI.absoluteString) + ] + let httpBody = Data(components.percentEncodedQuery!.utf8) + + var request = URLRequest(url: tokenEndpoint) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + request.httpBody = httpBody + + let session = URLSession(configuration: .ephemeral) + let (data, _) = try await session.data(for: request) + return try OAuth2.decoder.decode(OAuth2.AccessTokenResponse.self, from: data) + } + + func authorize(_ session: WebAuthenticationSession) async throws -> Credential { + let authorizationURL = try authorizationURL + let callbackURL = try await session.start( + url: authorizationURL, + redirectURI: redirectURI + ) + return try await handle(callbackURL: callbackURL).credential + } + } + + private struct CodeResponse: Codable { + var code: String + var state: String? + } + + private struct AccessTokenResponse: Codable { + var accessToken: String + var tokenType: TokenType + var expiresIn: Double? + var refreshToken: String? + + var credential: Credential { + .init(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expiresIn.map { Date.init(timeIntervalSinceNow: $0) }) + } + } + + enum TokenType: Codable, RawRepresentable { + case bearer + case unknown(String) + + init(rawValue: String) { + self = switch rawValue.lowercased() { + case "bearer": .bearer + default: .unknown(rawValue) + } + } + + var rawValue: String { + switch self { + case .bearer: "bearer" + case .unknown(let type): type + } + } + } + + enum GrantType: Codable, RawRepresentable { + case authorizationCode + case unknown(String) + + init(rawValue: String) { + self = switch rawValue.lowercased() { + case "authorization_code": .authorizationCode + default: .unknown(rawValue) + } + } + + var rawValue: String { + switch self { + case .authorizationCode: "authorization_code" + case .unknown(let type): type + } + } + } + + enum ResponseType: Codable, RawRepresentable { + case code + case idToken + case unknown(String) + + init(rawValue: String) { + self = switch rawValue.lowercased() { + case "code": .code + case "id_token": .idToken + default: .unknown(rawValue) + } + } + + var rawValue: String { + switch self { + case .code: "code" + case .idToken: "id_token" + case .unknown(let type): type + } + } + } + + fileprivate static var decoder: JSONDecoder { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + } + + fileprivate static var encoder: JSONEncoder { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + return encoder + } +} + +extension WebAuthenticationSession { + func start(url: URL, redirectURI: URL) async throws -> URL { + #if canImport(BrowserEngineKit) + if #available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) { + return try await authenticate( + using: url, + callback: try Self.callback(for: redirectURI), + additionalHeaderFields: [:] + ) + } + #endif + + return try await withThrowingTaskGroup(of: URL.self) { group in + group.addTask { + return try await authenticate(using: url, callbackURLScheme: redirectURI.scheme ?? "") + } + + let id = Int.random(in: 0.. ASWebAuthenticationSession.Callback { + switch redirectURI.scheme { + case "https": + guard let host = redirectURI.host else { throw OAuth2.Error.invalidRedirectURI } + return .https(host: host, path: redirectURI.path) + case "http": + throw OAuth2.Error.invalidRedirectURI + case .some(let scheme): + return .customScheme(scheme) + case .none: + throw OAuth2.Error.invalidRedirectURI + } + } + #endif +} + +extension View { + func handleOAuth2Callback() -> some View { + onOpenURL { url in OAuth2.Session.handle(url: url) } + } +} + +extension URLComponents { + fileprivate func decode(_ type: T.Type) throws -> T { + guard let queryItems else { + throw DecodingError.valueNotFound( + T.self, + .init(codingPath: [], debugDescription: "Missing query items") + ) + } + let data = try OAuth2.encoder.encode(try queryItems.values) + return try OAuth2.decoder.decode(T.self, from: data) + } +} + +extension Sequence where Element == URLQueryItem { + fileprivate var values: [String: String?] { + get throws { + try Dictionary(map { ($0.name, $0.value) }) { _, _ in + throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Duplicate query items")) + } + } + } +} diff --git a/Apple/App/TunnelButton.swift b/Apple/App/TunnelButton.swift index df8d7e6..1f5693e 100644 --- a/Apple/App/TunnelButton.swift +++ b/Apple/App/TunnelButton.swift @@ -4,16 +4,19 @@ struct TunnelButton: View { @Environment(\.tunnel) var tunnel: any Tunnel + private var action: Action? { tunnel.action } + var body: some View { - if let action = tunnel.action { - Button { + Button { + if let action { tunnel.perform(action) - } label: { - Text(action.description) } - .padding(.horizontal) - .buttonStyle(.floating) + } label: { + Text(action.description) } + .disabled(action.isDisabled) + .padding(.horizontal) + .buttonStyle(.floating) } } @@ -40,12 +43,21 @@ extension TunnelButton { } } -extension TunnelButton.Action { +extension TunnelButton.Action? { var description: LocalizedStringKey { switch self { case .enable: "Enable" case .start: "Start" case .stop: "Stop" + case .none: "Start" + } + } + + var isDisabled: Bool { + if case .none = self { + true + } else { + false } } } diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index b08c4b0..3ea58b3 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; }; + D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */; }; + D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363E2BB895FB00E582EC /* OAuth2.swift */; }; D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; @@ -78,6 +80,8 @@ 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; 0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = ""; }; + D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCarouselView.swift; sourceTree = ""; }; + D000363E2BB895FB00E582EC /* OAuth2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2.swift; sourceTree = ""; }; D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = ""; }; D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = ""; }; D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -247,7 +251,9 @@ D00AA8962A4669BC005C8102 /* AppDelegate.swift */, 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */, D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */, + D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */, D01A79302B81630D0024EC91 /* NetworkView.swift */, + D000363E2BB895FB00E582EC /* OAuth2.swift */, D032E64D2B8A69C90006B8AD /* Networks */, D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */, D0FAB5952B818B2900F6A84B /* TunnelButton.swift */, @@ -476,6 +482,7 @@ 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */, D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */, D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */, + D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */, D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */, D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */, D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */, @@ -484,6 +491,7 @@ D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */, D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */, D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */, + D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From abf1101484166ff434287056e8c0f5af727ef39e Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Mon, 22 Apr 2024 06:01:47 +0800 Subject: [PATCH 117/128] Wireguard Configuration in SQLite (#263) #241 --- .github/workflows/release-linux.yml | 34 ++++ .vscode/settings.json | 35 +++-- Apple/Burrow.xcodeproj/project.pbxproj | 24 +-- Apple/NetworkExtension/Client.swift | 60 -------- Apple/NetworkExtension/DataTypes.swift | 61 -------- .../PacketTunnelProvider.swift | 70 +++++---- Apple/NetworkExtension/libburrow/libburrow.h | 4 +- Apple/Shared/Client.swift | 106 +++++++++++++ Apple/Shared/Constants.swift | 10 ++ Apple/Shared/DataTypes.swift | 139 +++++++++++++++++ .../NWConnection+Async.swift | 0 .../NewlineProtocolFramer.swift | 0 Cargo.lock | 89 +++++++++++ Dockerfile | 26 +++- burrow-gtk/build-aux/Dockerfile | 4 +- burrow-gtk/build-aux/build_appimage.sh | 2 + burrow/Cargo.toml | 22 ++- burrow/burrow.db | Bin 0 -> 20480 bytes burrow/src/daemon/apple.rs | 14 +- burrow/src/daemon/instance.rs | 49 ++++-- burrow/src/daemon/mod.rs | 39 +++-- burrow/src/daemon/net/mod.rs | 11 +- burrow/src/daemon/net/unix.rs | 103 +++++++++---- burrow/src/daemon/rpc/mod.rs | 40 +++++ burrow/src/daemon/rpc/notification.rs | 11 ++ .../src/daemon/{command.rs => rpc/request.rs} | 9 ++ burrow/src/daemon/{ => rpc}/response.rs | 15 ++ ...equest__daemoncommand_serialization-2.snap | 5 + ...equest__daemoncommand_serialization-3.snap | 5 + ...equest__daemoncommand_serialization-4.snap | 5 + ...equest__daemoncommand_serialization-5.snap | 5 + ..._request__daemoncommand_serialization.snap | 5 + ...c__response__response_serialization-2.snap | 5 + ...c__response__response_serialization-3.snap | 5 + ...c__response__response_serialization-4.snap | 5 + ...rpc__response__response_serialization.snap | 5 + burrow/src/database.rs | 145 ++++++++++++++++++ burrow/src/lib.rs | 6 +- burrow/src/main.rs | 79 +++++----- burrow/src/wireguard/config.rs | 3 + burrow/src/wireguard/iface.rs | 36 +++-- burrow/src/wireguard/mod.rs | 2 +- tun/src/unix/apple/mod.rs | 20 +-- 43 files changed, 988 insertions(+), 325 deletions(-) create mode 100644 .github/workflows/release-linux.yml delete mode 100644 Apple/NetworkExtension/Client.swift delete mode 100644 Apple/NetworkExtension/DataTypes.swift create mode 100644 Apple/Shared/Client.swift create mode 100644 Apple/Shared/DataTypes.swift rename Apple/{NetworkExtension => Shared}/NWConnection+Async.swift (100%) rename Apple/{NetworkExtension => Shared}/NewlineProtocolFramer.swift (100%) create mode 100644 burrow/burrow.db create mode 100644 burrow/src/daemon/rpc/mod.rs create mode 100644 burrow/src/daemon/rpc/notification.rs rename burrow/src/daemon/{command.rs => rpc/request.rs} (82%) rename burrow/src/daemon/{ => rpc}/response.rs (89%) create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-2.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-3.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-4.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-5.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-2.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-3.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization.snap create mode 100644 burrow/src/database.rs diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml new file mode 100644 index 0000000..6709edb --- /dev/null +++ b/.github/workflows/release-linux.yml @@ -0,0 +1,34 @@ +name: Release (Linux) +on: + release: + types: + - created +jobs: + appimage: + name: Build AppImage + runs-on: ubuntu-latest + container: docker + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build AppImage + run: | + docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile + docker create --name temp appimage-builder + docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . + docker rm temp + - name: Get Build Number + id: version + shell: bash + run: | + echo "BUILD_NUMBER=$(Tools/version.sh)" >> $GITHUB_OUTPUT + - name: Attach Artifacts + uses: SierraSoftworks/gh-releases@v1.0.7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release_tag: builds/${{ steps.version.outputs.BUILD_NUMBER }} + overwrite: "true" + files: | + Burrow-x86_64.AppImage diff --git a/.vscode/settings.json b/.vscode/settings.json index 3c714be..a760137 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,19 @@ { - "files.autoSave": "onFocusChange", - "files.defaultLanguage": "rust", - "editor.formatOnPaste": true, - "editor.formatOnSave": true, - "files.trimTrailingWhitespace": true, - "editor.suggest.preview": true, - "editor.acceptSuggestionOnEnter": "on", - "rust-analyzer.restartServerOnConfigChange": true, - "rust-analyzer.cargo.features": "all", - "rust-analyzer.rustfmt.extraArgs": [ - "+nightly" - ], - "[rust]": { - "editor.defaultFormatter": "rust-lang.rust-analyzer", - }, - "rust-analyzer.inlayHints.typeHints.enable": false -} \ No newline at end of file + "files.autoSave": "onFocusChange", + "files.defaultLanguage": "rust", + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "files.trimTrailingWhitespace": true, + "editor.suggest.preview": true, + "editor.acceptSuggestionOnEnter": "on", + "rust-analyzer.restartServerOnConfigChange": true, + "rust-analyzer.cargo.features": "all", + "rust-analyzer.rustfmt.extraArgs": ["+nightly"], + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, + "rust-analyzer.inlayHints.typeHints.enable": false, + "rust-analyzer.linkedProjects": [ + "./burrow/Cargo.toml" + ] +} diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 3ea58b3..a3be02d 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -7,13 +7,13 @@ objects = { /* Begin PBXBuildFile section */ - 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; - 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; + 0BA6D73B2BA638D900BD4B55 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; + 0BA6D73C2BA6393200BD4B55 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; + 0BA6D73D2BA6393B00BD4B55 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; + 0BA6D73E2BA6394B00BD4B55 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; }; D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */; }; D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363E2BB895FB00E582EC /* OAuth2.swift */; }; - D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; - D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; @@ -158,6 +158,10 @@ D00117392B30341C00D87C25 /* Shared */ = { isa = PBXGroup; children = ( + 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, + D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */, + D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */, + 0B46E8DF2AC918CA00BA2A3C /* Client.swift */, D001173A2B30341C00D87C25 /* Logging.swift */, D08252752B5C9FC4005DA378 /* Constants.swift */, D00117422B30348D00D87C25 /* Shared.xcconfig */, @@ -199,10 +203,6 @@ isa = PBXGroup; children = ( D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */, - 0B46E8DF2AC918CA00BA2A3C /* Client.swift */, - 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, - D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */, - D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */, D020F65929E4A697002790F6 /* Info.plist */, D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */, D020F66629E4A95D002790F6 /* NetworkExtension-macOS.entitlements */, @@ -456,7 +456,11 @@ buildActionMask = 2147483647; files = ( D001173B2B30341C00D87C25 /* Logging.swift in Sources */, + 0BA6D73C2BA6393200BD4B55 /* NWConnection+Async.swift in Sources */, D08252762B5C9FC4005DA378 /* Constants.swift in Sources */, + 0BA6D73E2BA6394B00BD4B55 /* DataTypes.swift in Sources */, + 0BA6D73B2BA638D900BD4B55 /* Client.swift in Sources */, + 0BA6D73D2BA6393B00BD4B55 /* NewlineProtocolFramer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -464,10 +468,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */, - 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */, - D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */, - 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */, D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Apple/NetworkExtension/Client.swift b/Apple/NetworkExtension/Client.swift deleted file mode 100644 index e7c1bc8..0000000 --- a/Apple/NetworkExtension/Client.swift +++ /dev/null @@ -1,60 +0,0 @@ -import BurrowShared -import Foundation -import Network - -final class Client { - let connection: NWConnection - - private let logger = Logger.logger(for: Client.self) - private var generator = SystemRandomNumberGenerator() - - convenience init() throws { - self.init(url: try Constants.socketURL) - } - - init(url: URL) { - let endpoint: NWEndpoint - if url.isFileURL { - endpoint = .unix(path: url.path(percentEncoded: false)) - } else { - endpoint = .url(url) - } - - let parameters = NWParameters.tcp - parameters.defaultProtocolStack - .applicationProtocols - .insert(NWProtocolFramer.Options(definition: NewlineProtocolFramer.definition), at: 0) - connection = NWConnection(to: endpoint, using: parameters) - connection.start(queue: .global()) - } - - func request(_ request: any Request, type: U.Type = U.self) async throws -> U { - do { - var copy = request - copy.id = generator.next(upperBound: UInt.max) - let content = try JSONEncoder().encode(copy) - logger.debug("> \(String(decoding: content, as: UTF8.self))") - - try await self.connection.send(content: content) - let (response, _, _) = try await connection.receiveMessage() - - logger.debug("< \(String(decoding: response, as: UTF8.self))") - return try JSONDecoder().decode(U.self, from: response) - } catch { - logger.error("\(error, privacy: .public)") - throw error - } - } - - deinit { - connection.cancel() - } -} - -extension Constants { - static var socketURL: URL { - get throws { - try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory) - } - } -} diff --git a/Apple/NetworkExtension/DataTypes.swift b/Apple/NetworkExtension/DataTypes.swift deleted file mode 100644 index 1409fde..0000000 --- a/Apple/NetworkExtension/DataTypes.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation - -// swiftlint:disable identifier_name -enum BurrowError: Error { - case addrDoesntExist - case resultIsError - case cantParseResult - case resultIsNone -} - -protocol Request: Codable where Command: Codable { - associatedtype Command - - var id: UInt { get set } - var command: Command { get set } -} - -struct BurrowSingleCommand: Request { - var id: UInt - var command: String -} - -struct BurrowRequest: Request where T: Codable { - var id: UInt - var command: T -} - -struct BurrowStartRequest: Codable { - struct TunOptions: Codable { - let name: String? - let no_pi: Bool - let tun_excl: Bool - let tun_retrieve: Bool - let address: [String] - } - struct StartOptions: Codable { - let tun: TunOptions - } - let Start: StartOptions -} - -struct Response: Decodable where T: Decodable { - var id: UInt - var result: T -} - -struct BurrowResult: Codable where T: Codable { - var Ok: T? - var Err: String? -} - -struct ServerConfigData: Codable { - struct InternalConfig: Codable { - let address: [String] - let name: String? - let mtu: Int32? - } - let ServerConfig: InternalConfig -} - -// swiftlint:enable identifier_name diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index a07daa3..89e0de6 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -5,10 +5,14 @@ import os class PacketTunnelProvider: NEPacketTunnelProvider { private let logger = Logger.logger(for: PacketTunnelProvider.self) + private var client: Client? override init() { do { - libburrow.spawnInProcess(socketPath: try Constants.socketURL.path) + libburrow.spawnInProcess( + socketPath: try Constants.socketURL.path(percentEncoded: false), + dbPath: try Constants.dbURL.path(percentEncoded: false) + ) } catch { logger.error("Failed to spawn: \(error)") } @@ -17,33 +21,17 @@ class PacketTunnelProvider: NEPacketTunnelProvider { override func startTunnel(options: [String: NSObject]? = nil) async throws { do { let client = try Client() + self.client = client + register_events(client) - let command = BurrowRequest(id: 0, command: "ServerConfig") - let data = try await client.request(command, type: Response>.self) - - let encoded = try JSONEncoder().encode(data.result) - self.logger.log("Received final data: \(String(decoding: encoded, as: UTF8.self))") - guard let serverconfig = data.result.Ok else { - throw BurrowError.resultIsError - } - guard let tunNs = generateTunSettings(from: serverconfig) else { - throw BurrowError.addrDoesntExist - } - try await self.setTunnelNetworkSettings(tunNs) - self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") - - let startRequest = BurrowRequest( - id: .random(in: (.min)..<(.max)), - command: BurrowStartRequest( - Start: BurrowStartRequest.StartOptions( - tun: BurrowStartRequest.TunOptions( - name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: [] - ) - ) + _ = try await self.loadTunSettings() + let startRequest = Start( + tun: Start.TunOptions( + name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: [] ) ) - let response = try await client.request(startRequest, type: Response>.self) - self.logger.log("Received start server response: \(String(describing: response.result))") + let response = try await client.request(startRequest, type: BurrowResult.self) + self.logger.log("Received start server response: \(String(describing: response))") } catch { self.logger.error("Failed to start tunnel: \(error)") throw error @@ -53,20 +41,33 @@ class PacketTunnelProvider: NEPacketTunnelProvider { override func stopTunnel(with reason: NEProviderStopReason) async { do { let client = try Client() - let command = BurrowRequest(id: 0, command: "Stop") - let data = try await client.request(command, type: Response>.self) + _ = try await client.single_request("Stop", type: BurrowResult.self) self.logger.log("Stopped client.") } catch { self.logger.error("Failed to stop tunnel: \(error)") } } - - private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { - let cfig = from.ServerConfig + func loadTunSettings() async throws -> ServerConfig { + guard let client = self.client else { + throw BurrowError.noClient + } + let srvConfig = try await client.single_request("ServerConfig", type: BurrowResult.self) + guard let serverconfig = srvConfig.Ok else { + throw BurrowError.resultIsError + } + guard let tunNs = generateTunSettings(from: serverconfig) else { + throw BurrowError.addrDoesntExist + } + try await self.setTunnelNetworkSettings(tunNs) + self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") + return serverconfig + } + private func generateTunSettings(from: ServerConfig) -> NETunnelNetworkSettings? { + // Using a makeshift remote tunnel address let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") var v4Addresses = [String]() var v6Addresses = [String]() - for addr in cfig.address { + for addr in from.address { if IPv4Address(addr) != nil { v6Addresses.append(addr) } @@ -81,4 +82,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") return nst } + func register_events(_ client: Client) { + client.on_event(.ConfigChange) { (cfig: ServerConfig) in + self.logger.info("Config Change Notification: \(String(describing: cfig))") + self.setTunnelNetworkSettings(self.generateTunSettings(from: cfig)) + self.logger.info("Updated Tunnel Network Settings.") + } + } } diff --git a/Apple/NetworkExtension/libburrow/libburrow.h b/Apple/NetworkExtension/libburrow/libburrow.h index e500de4..2b578ab 100644 --- a/Apple/NetworkExtension/libburrow/libburrow.h +++ b/Apple/NetworkExtension/libburrow/libburrow.h @@ -1,2 +1,2 @@ -__attribute__((__swift_name__("spawnInProcess(socketPath:)"))) -extern void spawn_in_process(const char * __nullable path); +__attribute__((__swift_name__("spawnInProcess(socketPath:dbPath:)"))) +extern void spawn_in_process(const char * __nullable socket_path, const char * __nullable db_path); diff --git a/Apple/Shared/Client.swift b/Apple/Shared/Client.swift new file mode 100644 index 0000000..f643c6c --- /dev/null +++ b/Apple/Shared/Client.swift @@ -0,0 +1,106 @@ +import Foundation +import Network + +public final class Client { + let connection: NWConnection + + private let logger = Logger.logger(for: Client.self) + private var generator = SystemRandomNumberGenerator() + private var continuations: [UInt: UnsafeContinuation] = [:] + private var eventMap: [NotificationType: [(Data) throws -> Void]] = [:] + private var task: Task? + + public convenience init() throws { + self.init(url: try Constants.socketURL) + } + + public init(url: URL) { + let endpoint: NWEndpoint + if url.isFileURL { + endpoint = .unix(path: url.path(percentEncoded: false)) + } else { + endpoint = .url(url) + } + + let parameters = NWParameters.tcp + parameters.defaultProtocolStack + .applicationProtocols + .insert(NWProtocolFramer.Options(definition: NewlineProtocolFramer.definition), at: 0) + let connection = NWConnection(to: endpoint, using: parameters) + connection.start(queue: .global()) + self.connection = connection + self.task = Task { [weak self] in + while true { + let (data, _, _) = try await connection.receiveMessage() + let peek = try JSONDecoder().decode(MessagePeek.self, from: data) + switch peek.type { + case .Response: + let response = try JSONDecoder().decode(ResponsePeek.self, from: data) + self?.logger.info("Received response for \(response.id)") + guard let continuations = self?.continuations else {return} + self?.logger.debug("All keys in continuation table: \(continuations.keys)") + guard let continuation = self?.continuations[response.id] else { return } + self?.logger.debug("Got matching continuation") + continuation.resume(returning: data) + case .Notification: + let peek = try JSONDecoder().decode(NotificationPeek.self, from: data) + guard let handlers = self?.eventMap[peek.method] else { continue } + _ = try handlers.map { try $0(data) } + default: + continue + } + } + } + } + private func send(_ request: T) async throws -> U { + let data: Data = try await withUnsafeThrowingContinuation { continuation in + continuations[request.id] = continuation + do { + let data = try JSONEncoder().encode(request) + let completion: NWConnection.SendCompletion = .contentProcessed { error in + guard let error = error else { + return + } + continuation.resume(throwing: error) + } + connection.send(content: data, completion: completion) + } catch { + continuation.resume(throwing: error) + return + } + } + self.logger.debug("Got response data: \(String(describing: data.base64EncodedString()))") + let res = try JSONDecoder().decode(Response.self, from: data) + self.logger.debug("Got response data decoded: \(String(describing: res))") + return res.result + } + public func request(_ request: T, type: U.Type = U.self) async throws -> U { + let req = BurrowRequest( + id: generator.next(upperBound: UInt.max), + command: request + ) + return try await send(req) + } + public func single_request(_ request: String, type: U.Type = U.self) async throws -> U { + let req = BurrowSimpleRequest( + id: generator.next(upperBound: UInt.max), + command: request + ) + return try await send(req) + } + public func on_event(_ event: NotificationType, callable: @escaping (T) throws -> Void) { + let action = { data in + let decoded = try JSONDecoder().decode(Notification.self, from: data) + try callable(decoded.params) + } + if eventMap[event] != nil { + eventMap[event]?.append(action) + } else { + eventMap[event] = [action] + } + } + + deinit { + connection.cancel() + } +} diff --git a/Apple/Shared/Constants.swift b/Apple/Shared/Constants.swift index 634c500..a8207cd 100644 --- a/Apple/Shared/Constants.swift +++ b/Apple/Shared/Constants.swift @@ -20,4 +20,14 @@ public enum Constants { } return .success(groupContainerURL) }() + public static var socketURL: URL { + get throws { + try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory) + } + } + public static var dbURL: URL { + get throws { + try groupContainerURL.appending(component: "burrow.db", directoryHint: .notDirectory) + } + } } diff --git a/Apple/Shared/DataTypes.swift b/Apple/Shared/DataTypes.swift new file mode 100644 index 0000000..ac49abc --- /dev/null +++ b/Apple/Shared/DataTypes.swift @@ -0,0 +1,139 @@ +import Foundation + +// swiftlint:disable identifier_name raw_value_for_camel_cased_codable_enum +public enum BurrowError: Error { + case addrDoesntExist + case resultIsError + case cantParseResult + case resultIsNone + case noClient +} + +public protocol Request: Codable where Params: Codable { + associatedtype Params + + var id: UInt { get set } + var method: String { get set } + var params: Params? { get set } +} + +public enum MessageType: String, Codable { + case Request + case Response + case Notification +} + +public struct MessagePeek: Codable { + public var type: MessageType + public init(type: MessageType) { + self.type = type + } +} + +public struct BurrowSimpleRequest: Request { + public var id: UInt + public var method: String + public var params: String? + public init(id: UInt, command: String, params: String? = nil) { + self.id = id + self.method = command + self.params = params + } +} + +public struct BurrowRequest: Request where T: Codable { + public var id: UInt + public var method: String + public var params: T? + public init(id: UInt, command: T) { + self.id = id + self.method = "\(T.self)" + self.params = command + } +} + +public struct Response: Decodable where T: Decodable { + public var id: UInt + public var result: T + public init(id: UInt, result: T) { + self.id = id + self.result = result + } +} + +public struct ResponsePeek: Codable { + public var id: UInt + public init(id: UInt) { + self.id = id + } +} + +public enum NotificationType: String, Codable { + case ConfigChange +} + +public struct Notification: Codable where T: Codable { + public var method: NotificationType + public var params: T + public init(method: NotificationType, params: T) { + self.method = method + self.params = params + } +} + +public struct NotificationPeek: Codable { + public var method: NotificationType + public init(method: NotificationType) { + self.method = method + } +} + +public struct AnyResponseData: Codable { + public var type: String + public init(type: String) { + self.type = type + } +} + +public struct BurrowResult: Codable where T: Codable { + public var Ok: T? + public var Err: String? + public init(Ok: T, Err: String? = nil) { + self.Ok = Ok + self.Err = Err + } +} + +public struct ServerConfig: Codable { + public let address: [String] + public let name: String? + public let mtu: Int32? + public init(address: [String], name: String?, mtu: Int32?) { + self.address = address + self.name = name + self.mtu = mtu + } +} + +public struct Start: Codable { + public struct TunOptions: Codable { + public let name: String? + public let no_pi: Bool + public let tun_excl: Bool + public let tun_retrieve: Bool + public let address: [String] + public init(name: String?, no_pi: Bool, tun_excl: Bool, tun_retrieve: Bool, address: [String]) { + self.name = name + self.no_pi = no_pi + self.tun_excl = tun_excl + self.tun_retrieve = tun_retrieve + self.address = address + } + } + public let tun: TunOptions + public init(tun: TunOptions) { + self.tun = tun + } +} + +// swiftlint:enable identifier_name raw_value_for_camel_cased_codable_enum diff --git a/Apple/NetworkExtension/NWConnection+Async.swift b/Apple/Shared/NWConnection+Async.swift similarity index 100% rename from Apple/NetworkExtension/NWConnection+Async.swift rename to Apple/Shared/NWConnection+Async.swift diff --git a/Apple/NetworkExtension/NewlineProtocolFramer.swift b/Apple/Shared/NewlineProtocolFramer.swift similarity index 100% rename from Apple/NetworkExtension/NewlineProtocolFramer.swift rename to Apple/Shared/NewlineProtocolFramer.swift diff --git a/Cargo.lock b/Cargo.lock index a75bd28..628e996 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,18 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -47,6 +59,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "anstream" version = "0.6.11" @@ -334,6 +352,7 @@ dependencies = [ "rand", "rand_core", "ring", + "rusqlite", "schemars", "serde", "serde_json", @@ -743,6 +762,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.0.1" @@ -967,6 +998,19 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" +dependencies = [ + "hashbrown 0.14.3", +] [[package]] name = "hdrhistogram" @@ -1258,6 +1302,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libsystemd" version = "0.7.0" @@ -1877,6 +1932,20 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.4.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2949,6 +3018,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Dockerfile b/Dockerfile index 9f54478..afd51ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN set -eux && \ curl --proto '=https' --tlsv1.2 -sSf https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor --output $KEYRINGS/llvm.gpg && \ echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-$LLVM_VERSION main" > /etc/apt/sources.list.d/llvm.list && \ apt-get update && \ - apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION && \ + apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION build-essential sqlite3 libsqlite3-dev musl musl-tools musl-dev && \ ln -s clang-$LLVM_VERSION /usr/bin/clang && \ ln -s clang /usr/bin/clang++ && \ ln -s lld-$LLVM_VERSION /usr/bin/ld.lld && \ @@ -24,12 +24,30 @@ RUN set -eux && \ apt-get remove -y --auto-remove && \ rm -rf /var/lib/apt/lists/* +ARG SQLITE_VERSION=3400100 + RUN case $TARGETPLATFORM in \ - "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ - "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl MUSL_TARGET=aarch64-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl MUSL_TARGET=x86_64-linux-musl ;; \ *) exit 1 ;; \ esac && \ - rustup target add $LLVM_TARGET + rustup target add $LLVM_TARGET && \ + curl --proto '=https' --tlsv1.2 -sSfO https://www.sqlite.org/2022/sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ + tar xf sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ + rm sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ + cd sqlite-autoconf-$SQLITE_VERSION && \ + ./configure --disable-shared \ + CC="clang-$LLVM_VERSION -target $LLVM_TARGET" \ + CFLAGS="-I/usr/local/include -I/usr/include/$MUSL_TARGET" \ + LDFLAGS="-L/usr/local/lib -L/usr/lib/$MUSL_TARGET -L/lib/$MUSL_TARGET" && \ + make && \ + make install && \ + cd .. && \ + rm -rf sqlite-autoconf-$SQLITE_VERSION + +ENV SQLITE3_STATIC=1 \ + SQLITE3_INCLUDE_DIR=/usr/local/include \ + SQLITE3_LIB_DIR=/usr/local/lib ENV CC_x86_64_unknown_linux_musl=clang-$LLVM_VERSION \ AR_x86_64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ diff --git a/burrow-gtk/build-aux/Dockerfile b/burrow-gtk/build-aux/Dockerfile index df07c4a..4e71c05 100644 --- a/burrow-gtk/build-aux/Dockerfile +++ b/burrow-gtk/build-aux/Dockerfile @@ -4,7 +4,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN set -eux && \ dnf update -y && \ - dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file + dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file sqlite sqlite-devel RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal ENV PATH="/root/.cargo/bin:${PATH}" @@ -12,6 +12,8 @@ ENV PATH="/root/.cargo/bin:${PATH}" WORKDIR /app COPY . /app +ENV SQLITE3_STATIC=1 + RUN cd /app/burrow-gtk/ && \ ./build-aux/build_appimage.sh diff --git a/burrow-gtk/build-aux/build_appimage.sh b/burrow-gtk/build-aux/build_appimage.sh index cd58c17..f054cd9 100755 --- a/burrow-gtk/build-aux/build_appimage.sh +++ b/burrow-gtk/build-aux/build_appimage.sh @@ -22,6 +22,8 @@ elif [ "$ARCHITECTURE" == "aarch64" ]; then chmod a+x /tmp/linuxdeploy fi + +CFLAGS="-I/usr/local/include -I/usr/include/$MUSL_TARGET -fPIE" meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr --buildtype $BURROW_BUILD_TYPE meson compile -C $BURROW_GTK_BUILD DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 4e7688b..0c816f8 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,7 +10,15 @@ crate-type = ["lib", "staticlib"] [dependencies] anyhow = "1.0" -tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util", "rt-multi-thread", "time", "tracing"] } +tokio = { version = "1.21", features = [ + "rt", + "macros", + "sync", + "io-util", + "rt-multi-thread", + "time", + "tracing", +] } tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } clap = { version = "4.4", features = ["derive"] } tracing = "0.1" @@ -25,7 +33,10 @@ chacha20poly1305 = "0.10" rand = "0.8" rand_core = "0.6" aead = "0.5" -x25519-dalek = { version = "2.0", features = ["reusable_secrets", "static_secrets"] } +x25519-dalek = { version = "2.0", features = [ + "reusable_secrets", + "static_secrets", +] } ring = "0.17" parking_lot = "0.12" hmac = "0.12" @@ -37,9 +48,12 @@ async-channel = "2.1" schemars = "0.8" futures = "0.3.28" once_cell = "1.19" -console-subscriber = { version = "0.2.0" , optional = true } +console-subscriber = { version = "0.2.0", optional = true } console = "0.15.8" +[dependencies.rusqlite] +version = "0.31.0" + [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5" libsystemd = "0.7" @@ -47,6 +61,7 @@ tracing-journald = "0.3" [target.'cfg(target_vendor = "apple")'.dependencies] nix = { version = "0.27" } +rusqlite = { version = "0.31.0", features = ["bundled"] } [dev-dependencies] insta = { version = "1.32", features = ["yaml"] } @@ -62,3 +77,4 @@ pre_uninstall_script = "../package/rpm/pre_uninstall" [features] tokio-console = ["dep:console-subscriber"] +bundled = ["rusqlite/bundled"] diff --git a/burrow/burrow.db b/burrow/burrow.db new file mode 100644 index 0000000000000000000000000000000000000000..c5b6e2c614ecb4db4c264f50691b6f2cea99772a GIT binary patch literal 20480 zcmWFz^vNtqRY=P(%1ta$FlG>7U}R))P*7lCU=U|uU|?lH0A>aT1{MUDff0#~iz&{a zSJuG`(#R{q!1sc08t)Wd5nPH##YaP6Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONfZicc z$HFcyEzQ^%T#}fSlbV-WQl4Lw4W(F}gIpa$TopnboqSvspn?h-TnbQ-nOBlpl$MyB z8lRb>;OQ5l5ajCS8szHd>>8|4o*oaE*2qlJRPgsx2n}!n8RzU6?Cj{`3N}Wwv7Q<1 zfaXxJ1Ip9m3sO^ypcD&=1E7LbbAS%m1t7nq=A{(mXXceCgt$h8DERq@DENi?_#os9 zN|SOjljE~fD{-kv%*n|wPfdx>EGWjMq@XCZI3uwrH3e=C*nZ6bCN^HWN82kl9QqrXkB9 z2QfHiUEN)S6as=geI0`$6}(*|6&yoD{5}1ggIs-G{X!4{1#$w|{|KR+%;J*Ny!e9r zq7qOV0hxr5%q=O!6f7vpEK4j&g$EOs2uVyyDM~HI8Pq9xXi|`n2KCLkcwHFyck!3- z>+!wdTf`T`C&qh$w~N<>-uZ6SzR?gE4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R80;b7 z!o|VBz|4@U&dYEr$KN#|EG0iT)hDDP#M7y)%s3>n*efVK)xe{`($6khIH_U^2USdAr-~_TR567W$rN|LLQhA3=b(zL9R1|XAY71JH1mFA{7sYRJyiIoQ0`5rzQLB0iH+K#3bj)4Xl&R*Ju=HcZQ zhK~MaAtqSUX|Z`ug??_jc2R1Wt8borUSVowq<>&`m5YU0o{@HXWL|}#uVrO=rh!Ga zZ6hlS#2qXH?G9#$JD3OB9ZV2+Fb%LfSQyzjLFL%MIs?@IXAq!ac|B_MXb6mkz-S1J ihQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2B~hX4R^(?&G_ literal 0 HcmV?d00001 diff --git a/burrow/src/daemon/apple.rs b/burrow/src/daemon/apple.rs index 9460613..c60f131 100644 --- a/burrow/src/daemon/apple.rs +++ b/burrow/src/daemon/apple.rs @@ -18,7 +18,7 @@ static BURROW_NOTIFY: OnceCell> = OnceCell::new(); static BURROW_HANDLE: OnceCell = OnceCell::new(); #[no_mangle] -pub unsafe extern "C" fn spawn_in_process(path: *const c_char) { +pub unsafe extern "C" fn spawn_in_process(path: *const c_char, db_path: *const c_char) { crate::tracing::initialize(); let notify = BURROW_NOTIFY.get_or_init(|| Arc::new(Notify::new())); @@ -28,6 +28,11 @@ pub unsafe extern "C" fn spawn_in_process(path: *const c_char) { } else { Some(PathBuf::from(CStr::from_ptr(path).to_str().unwrap())) }; + let db_path_buf = if db_path.is_null() { + None + } else { + Some(PathBuf::from(CStr::from_ptr(db_path).to_str().unwrap())) + }; let sender = notify.clone(); let (handle_tx, handle_rx) = tokio::sync::oneshot::channel(); @@ -40,7 +45,12 @@ pub unsafe extern "C" fn spawn_in_process(path: *const c_char) { .unwrap(); handle_tx.send(runtime.handle().clone()).unwrap(); runtime.block_on(async { - let result = daemon_main(path_buf.as_deref(), Some(sender.clone())).await; + let result = daemon_main( + path_buf.as_deref(), + db_path_buf.as_deref(), + Some(sender.clone()), + ) + .await; if let Err(error) = result.as_ref() { error!("Burrow thread exited: {}", error); } diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index 0d3e726..bc506bd 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -1,4 +1,7 @@ -use std::sync::Arc; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use anyhow::Result; use tokio::{sync::RwLock, task::JoinHandle}; @@ -6,11 +9,16 @@ use tracing::{debug, info, warn}; use tun::tokio::TunInterface; use crate::{ - daemon::{ - command::DaemonCommand, - response::{DaemonResponse, DaemonResponseData, ServerConfig, ServerInfo}, + daemon::rpc::{ + DaemonCommand, + DaemonNotification, + DaemonResponse, + DaemonResponseData, + ServerConfig, + ServerInfo, }, - wireguard::Interface, + database::{get_connection, load_interface}, + wireguard::{Config, Interface}, }; enum RunState { @@ -21,8 +29,11 @@ enum RunState { pub struct DaemonInstance { rx: async_channel::Receiver, sx: async_channel::Sender, + subx: async_channel::Sender, tun_interface: Arc>>, wg_interface: Arc>, + config: Arc>, + db_path: Option, wg_state: RunState, } @@ -30,13 +41,19 @@ impl DaemonInstance { pub fn new( rx: async_channel::Receiver, sx: async_channel::Sender, + subx: async_channel::Sender, wg_interface: Arc>, + config: Arc>, + db_path: Option<&Path>, ) -> Self { Self { rx, sx, + subx, wg_interface, tun_interface: Arc::new(RwLock::new(None)), + config, + db_path: db_path.map(|p| p.to_owned()), wg_state: RunState::Idle, } } @@ -59,24 +76,13 @@ impl DaemonInstance { self.tun_interface = self.wg_interface.read().await.get_tun(); debug!("tun_interface set: {:?}", self.tun_interface); - debug!("Cloning wg_interface"); let tmp_wg = self.wg_interface.clone(); - debug!("wg_interface cloned"); - - debug!("Spawning run task"); let run_task = tokio::spawn(async move { - debug!("Running wg_interface"); let twlock = tmp_wg.read().await; - debug!("wg_interface read lock acquired"); twlock.run().await }); - debug!("Run task spawned: {:?}", run_task); - - debug!("Setting wg_state to Running"); self.wg_state = RunState::Running(run_task); - debug!("wg_state set to Running"); - info!("Daemon started tun interface"); } } @@ -99,6 +105,17 @@ impl DaemonInstance { DaemonCommand::ServerConfig => { Ok(DaemonResponseData::ServerConfig(ServerConfig::default())) } + DaemonCommand::ReloadConfig(interface_id) => { + let conn = get_connection(self.db_path.as_deref())?; + let cfig = load_interface(&conn, &interface_id)?; + *self.config.write().await = cfig; + self.subx + .send(DaemonNotification::ConfigChange(ServerConfig::try_from( + &self.config.read().await.to_owned(), + )?)) + .await?; + Ok(DaemonResponseData::None) + } } } diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs index 2a971dd..4469e90 100644 --- a/burrow/src/daemon/mod.rs +++ b/burrow/src/daemon/mod.rs @@ -1,40 +1,53 @@ use std::{path::Path, sync::Arc}; pub mod apple; -mod command; mod instance; mod net; -mod response; +pub mod rpc; use anyhow::Result; -pub use command::{DaemonCommand, DaemonStartOptions}; use instance::DaemonInstance; pub use net::{DaemonClient, Listener}; -pub use response::{DaemonResponse, DaemonResponseData, ServerInfo}; +pub use rpc::{DaemonCommand, DaemonResponseData, DaemonStartOptions}; use tokio::sync::{Notify, RwLock}; use tracing::{error, info}; -use crate::wireguard::{Config, Interface}; +use crate::{ + database::{get_connection, load_interface}, + wireguard::Interface, +}; -pub async fn daemon_main(path: Option<&Path>, notify_ready: Option>) -> Result<()> { +pub async fn daemon_main( + socket_path: Option<&Path>, + db_path: Option<&Path>, + notify_ready: Option>, +) -> Result<()> { let (commands_tx, commands_rx) = async_channel::unbounded(); let (response_tx, response_rx) = async_channel::unbounded(); + let (subscribe_tx, subscribe_rx) = async_channel::unbounded(); - let listener = if let Some(path) = path { + let listener = if let Some(path) = socket_path { info!("Creating listener... {:?}", path); - Listener::new_with_path(commands_tx, response_rx, path) + Listener::new_with_path(commands_tx, response_rx, subscribe_rx, path) } else { info!("Creating listener..."); - Listener::new(commands_tx, response_rx) + Listener::new(commands_tx, response_rx, subscribe_rx) }; if let Some(n) = notify_ready { n.notify_one() } let listener = listener?; - - let config = Config::default(); - let iface: Interface = config.try_into()?; - let mut instance = DaemonInstance::new(commands_rx, response_tx, Arc::new(RwLock::new(iface))); + let conn = get_connection(db_path)?; + let config = load_interface(&conn, "1")?; + let iface: Interface = config.clone().try_into()?; + let mut instance = DaemonInstance::new( + commands_rx, + response_tx, + subscribe_tx, + Arc::new(RwLock::new(iface)), + Arc::new(RwLock::new(config)), + db_path, + ); info!("Starting daemon..."); diff --git a/burrow/src/daemon/net/mod.rs b/burrow/src/daemon/net/mod.rs index fe35bae..242f479 100644 --- a/burrow/src/daemon/net/mod.rs +++ b/burrow/src/daemon/net/mod.rs @@ -1,6 +1,7 @@ -use serde::{Deserialize, Serialize}; -use super::DaemonCommand; + + + #[cfg(target_family = "unix")] mod unix; @@ -14,8 +15,4 @@ mod windows; #[cfg(target_os = "windows")] pub use windows::{DaemonClient, Listener}; -#[derive(Clone, Serialize, Deserialize)] -pub struct DaemonRequest { - pub id: u64, - pub command: DaemonCommand, -} + diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs index 26e901d..70c4207 100644 --- a/burrow/src/daemon/net/unix.rs +++ b/burrow/src/daemon/net/unix.rs @@ -10,8 +10,14 @@ use tokio::{ }; use tracing::{debug, error, info}; -use super::*; -use crate::daemon::{DaemonCommand, DaemonResponse, DaemonResponseData}; +use crate::daemon::rpc::{ + DaemonCommand, + DaemonMessage, + DaemonNotification, + DaemonRequest, + DaemonResponse, + DaemonResponseData, +}; #[cfg(not(target_vendor = "apple"))] const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; @@ -19,10 +25,17 @@ const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; #[cfg(target_vendor = "apple")] const UNIX_SOCKET_PATH: &str = "burrow.sock"; -#[derive(Debug)] +fn get_socket_path() -> String { + if std::env::var("BURROW_SOCKET_PATH").is_ok() { + return std::env::var("BURROW_SOCKET_PATH").unwrap(); + } + UNIX_SOCKET_PATH.to_string() +} + pub struct Listener { cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, + sub_chan: async_channel::Receiver, inner: UnixListener, } @@ -31,9 +44,11 @@ impl Listener { pub fn new( cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, + sub_chan: async_channel::Receiver, ) -> Self { - let path = Path::new(OsStr::new(UNIX_SOCKET_PATH)); - Self::new_with_path(cmd_tx, rsp_rx, path)? + let socket_path = get_socket_path(); + let path = Path::new(OsStr::new(&socket_path)); + Self::new_with_path(cmd_tx, rsp_rx, sub_chan, path)? } #[throws] @@ -41,10 +56,16 @@ impl Listener { pub fn new_with_path( cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, + sub_chan: async_channel::Receiver, path: &Path, ) -> Self { let inner = listener_from_path_or_fd(&path, raw_fd())?; - Self { cmd_tx, rsp_rx, inner } + Self { + cmd_tx, + rsp_rx, + sub_chan, + inner, + } } #[throws] @@ -52,10 +73,16 @@ impl Listener { pub fn new_with_path( cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, + sub_chan: async_channel::Receiver, path: &Path, ) -> Self { let inner = listener_from_path(path)?; - Self { cmd_tx, rsp_rx, inner } + Self { + cmd_tx, + rsp_rx, + inner, + sub_chan, + } } pub async fn run(&self) -> Result<()> { @@ -64,9 +91,10 @@ impl Listener { let (stream, _) = self.inner.accept().await?; let cmd_tx = self.cmd_tx.clone(); let rsp_rxc = self.rsp_rx.clone(); + let sub_chan = self.sub_chan.clone(); tokio::task::spawn(async move { info!("Got connection: {:?}", stream); - Self::stream(stream, cmd_tx, rsp_rxc).await; + Self::stream(stream, cmd_tx, rsp_rxc, sub_chan).await; }); } } @@ -75,34 +103,46 @@ impl Listener { stream: UnixStream, cmd_tx: async_channel::Sender, rsp_rxc: async_channel::Receiver, + sub_chan: async_channel::Receiver, ) { let mut stream = stream; let (mut read_stream, mut write_stream) = stream.split(); let buf_reader = BufReader::new(&mut read_stream); let mut lines = buf_reader.lines(); - while let Ok(Some(line)) = lines.next_line().await { - info!("Line: {}", line); - let mut res: DaemonResponse = DaemonResponseData::None.into(); - let req = match serde_json::from_str::(&line) { - Ok(req) => Some(req), - Err(e) => { - res.result = Err(e.to_string()); - error!("Failed to parse request: {}", e); - None - } - }; - let mut res = serde_json::to_string(&res).unwrap(); - res.push('\n'); + loop { + tokio::select! { + Ok(Some(line)) = lines.next_line() => { + info!("Line: {}", line); + let mut res: DaemonResponse = DaemonResponseData::None.into(); + let req = match serde_json::from_str::(&line) { + Ok(req) => Some(req), + Err(e) => { + res.result = Err(e.to_string()); + error!("Failed to parse request: {}", e); + None + } + }; - if let Some(req) = req { - cmd_tx.send(req.command).await.unwrap(); - let res = rsp_rxc.recv().await.unwrap().with_id(req.id); - let mut retres = serde_json::to_string(&res).unwrap(); - retres.push('\n'); - info!("Sending response: {}", retres); - write_stream.write_all(retres.as_bytes()).await.unwrap(); - } else { - write_stream.write_all(res.as_bytes()).await.unwrap(); + let res = serde_json::to_string(&DaemonMessage::from(res)).unwrap(); + + if let Some(req) = req { + cmd_tx.send(req.command).await.unwrap(); + let res = rsp_rxc.recv().await.unwrap().with_id(req.id); + let mut payload = serde_json::to_string(&DaemonMessage::from(res)).unwrap(); + payload.push('\n'); + info!("Sending response: {}", payload); + write_stream.write_all(payload.as_bytes()).await.unwrap(); + } else { + write_stream.write_all(res.as_bytes()).await.unwrap(); + } + } + Ok(cmd) = sub_chan.recv() => { + info!("Got subscription command: {:?}", cmd); + let msg = DaemonMessage::from(cmd); + let mut payload = serde_json::to_string(&msg).unwrap(); + payload.push('\n'); + write_stream.write_all(payload.as_bytes()).await.unwrap(); + } } } } @@ -176,7 +216,8 @@ pub struct DaemonClient { impl DaemonClient { pub async fn new() -> Result { - let path = Path::new(OsStr::new(UNIX_SOCKET_PATH)); + let socket_path = get_socket_path(); + let path = Path::new(OsStr::new(&socket_path)); Self::new_with_path(path).await } diff --git a/burrow/src/daemon/rpc/mod.rs b/burrow/src/daemon/rpc/mod.rs new file mode 100644 index 0000000..4146e71 --- /dev/null +++ b/burrow/src/daemon/rpc/mod.rs @@ -0,0 +1,40 @@ +pub mod notification; +pub mod request; +pub mod response; + +pub use notification::DaemonNotification; +pub use request::{DaemonCommand, DaemonRequest, DaemonStartOptions}; +pub use response::{DaemonResponse, DaemonResponseData, ServerConfig, ServerInfo}; +use serde::{Deserialize, Serialize}; + +/// The `Message` object contains either a `DaemonRequest` or a `DaemonResponse` to be serialized / deserialized +/// for our IPC communication. Our IPC protocol is based on jsonrpc (https://www.jsonrpc.org/specification#overview), +/// but deviates from it in a few ways: +/// - We differentiate Notifications from Requests explicitly. +/// - We have a "type" field to differentiate between a request, a response, and a notification. +/// - The params field may receive any json value(such as a string), not just an object or an array. +#[derive(Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum DaemonMessage { + Request(DaemonRequest), + Response(DaemonResponse), + Notification(DaemonNotification), +} + +impl From for DaemonMessage { + fn from(request: DaemonRequest) -> Self { + DaemonMessage::Request(request) + } +} + +impl From for DaemonMessage { + fn from(response: DaemonResponse) -> Self { + DaemonMessage::Response(response) + } +} + +impl From for DaemonMessage { + fn from(notification: DaemonNotification) -> Self { + DaemonMessage::Notification(notification) + } +} diff --git a/burrow/src/daemon/rpc/notification.rs b/burrow/src/daemon/rpc/notification.rs new file mode 100644 index 0000000..135b0e4 --- /dev/null +++ b/burrow/src/daemon/rpc/notification.rs @@ -0,0 +1,11 @@ +use rpc::ServerConfig; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::daemon::rpc; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "method", content = "params")] +pub enum DaemonNotification { + ConfigChange(ServerConfig), +} diff --git a/burrow/src/daemon/command.rs b/burrow/src/daemon/rpc/request.rs similarity index 82% rename from burrow/src/daemon/command.rs rename to burrow/src/daemon/rpc/request.rs index 53b4108..e9480aa 100644 --- a/burrow/src/daemon/command.rs +++ b/burrow/src/daemon/rpc/request.rs @@ -3,11 +3,13 @@ use serde::{Deserialize, Serialize}; use tun::TunOptions; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(tag="method", content="params")] pub enum DaemonCommand { Start(DaemonStartOptions), ServerInfo, ServerConfig, Stop, + ReloadConfig(String), } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -15,6 +17,13 @@ pub struct DaemonStartOptions { pub tun: TunOptions, } +#[derive(Clone, Serialize, Deserialize)] +pub struct DaemonRequest { + pub id: u64, + #[serde(flatten)] + pub command: DaemonCommand, +} + #[test] fn test_daemoncommand_serialization() { insta::assert_snapshot!(serde_json::to_string(&DaemonCommand::Start( diff --git a/burrow/src/daemon/response.rs b/burrow/src/daemon/rpc/response.rs similarity index 89% rename from burrow/src/daemon/response.rs rename to burrow/src/daemon/rpc/response.rs index 37ee5d9..61c9c50 100644 --- a/burrow/src/daemon/response.rs +++ b/burrow/src/daemon/rpc/response.rs @@ -2,6 +2,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tun::TunInterface; +use crate::wireguard::Config; + #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct DaemonResponse { // Error types can't be serialized, so this is the second best option. @@ -62,6 +64,18 @@ pub struct ServerConfig { pub mtu: Option, } +impl TryFrom<&Config> for ServerConfig { + type Error = anyhow::Error; + + fn try_from(config: &Config) -> anyhow::Result { + Ok(ServerConfig { + address: config.interface.address.clone(), + name: None, + mtu: config.interface.mtu.map(|mtu| mtu as i32), + }) + } +} + impl Default for ServerConfig { fn default() -> Self { Self { @@ -73,6 +87,7 @@ impl Default for ServerConfig { } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type")] pub enum DaemonResponseData { ServerInfo(ServerInfo), ServerConfig(ServerConfig), diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-2.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-2.snap new file mode 100644 index 0000000..01ec8a7 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-2.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions {\n tun: TunOptions { ..TunOptions::default() },\n })).unwrap()" +--- +{"method":"Start","params":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":[]}}} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-3.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-3.snap new file mode 100644 index 0000000..a6a0466 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-3.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::ServerInfo).unwrap()" +--- +{"method":"ServerInfo"} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-4.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-4.snap new file mode 100644 index 0000000..f930051 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-4.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::Stop).unwrap()" +--- +{"method":"Stop"} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-5.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-5.snap new file mode 100644 index 0000000..89dc42c --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-5.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::ServerConfig).unwrap()" +--- +{"method":"ServerConfig"} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization.snap new file mode 100644 index 0000000..aeca659 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap()" +--- +{"method":"Start","params":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":[]}}} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-2.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-2.snap new file mode 100644 index 0000000..d7bd712 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-2.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerInfo(ServerInfo {\n name: Some(\"burrow\".to_string()),\n ip: None,\n mtu: Some(1500),\n }))))?" +--- +{"result":{"Ok":{"type":"ServerInfo","name":"burrow","ip":null,"mtu":1500}},"id":0} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-3.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-3.snap new file mode 100644 index 0000000..30068f3 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-3.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Err::(\"error\".to_string())))?" +--- +{"result":{"Err":"error"},"id":0} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap new file mode 100644 index 0000000..c40db25 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig(ServerConfig::default()))))?" +--- +{"result":{"Ok":{"type":"ServerConfig","address":["10.13.13.2"],"name":null,"mtu":null}},"id":0} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization.snap new file mode 100644 index 0000000..31bd84b --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::None)))?" +--- +{"result":{"Ok":{"type":"None"}},"id":0} diff --git a/burrow/src/database.rs b/burrow/src/database.rs new file mode 100644 index 0000000..0047b01 --- /dev/null +++ b/burrow/src/database.rs @@ -0,0 +1,145 @@ +use std::path::Path; + +use anyhow::Result; +use rusqlite::{params, Connection}; + +use crate::wireguard::config::{Config, Interface, Peer}; + +#[cfg(target_vendor = "apple")] +const DB_PATH: &str = "burrow.db"; + +#[cfg(not(target_vendor = "apple"))] +const DB_PATH: &str = "/var/lib/burrow/burrow.db"; + +const CREATE_WG_INTERFACE_TABLE: &str = "CREATE TABLE IF NOT EXISTS wg_interface ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + listen_port INTEGER, + mtu INTEGER, + private_key TEXT NOT NULL, + address TEXT NOT NULL, + dns TEXT NOT NULL +)"; + +const CREATE_WG_PEER_TABLE: &str = "CREATE TABLE IF NOT EXISTS wg_peer ( + interface_id INT REFERENCES wg_interface(id) ON UPDATE CASCADE, + endpoint TEXT NOT NULL, + public_key TEXT NOT NULL, + allowed_ips TEXT NOT NULL, + preshared_key TEXT +)"; + +const CREATE_NETWORK_TABLE: &str = "CREATE TABLE IF NOT EXISTS network ( + interface_id INT REFERENCES wg_interface(id) ON UPDATE CASCADE +)"; + +pub fn initialize_tables(conn: &Connection) -> Result<()> { + conn.execute(CREATE_WG_INTERFACE_TABLE, [])?; + conn.execute(CREATE_WG_PEER_TABLE, [])?; + conn.execute(CREATE_NETWORK_TABLE, [])?; + Ok(()) +} + +fn parse_lst(s: &str) -> Vec { + if s.is_empty() { + return vec![]; + } + s.split(',').map(|s| s.to_string()).collect() +} + +fn to_lst(v: &Vec) -> String { + v.iter() + .map(|s| s.to_string()) + .collect::>() + .join(",") +} + +pub fn load_interface(conn: &Connection, interface_id: &str) -> Result { + let iface = conn.query_row( + "SELECT private_key, dns, address, listen_port, mtu FROM wg_interface WHERE id = ?", + [&interface_id], + |row| { + let dns_rw: String = row.get(1)?; + let dns = parse_lst(&dns_rw); + let address_rw: String = row.get(2)?; + let address = parse_lst(&address_rw); + Ok(Interface { + private_key: row.get(0)?, + dns, + address, + mtu: row.get(4)?, + listen_port: row.get(3)?, + }) + }, + )?; + let mut peers_stmt = conn.prepare("SELECT public_key, preshared_key, allowed_ips, endpoint FROM wg_peer WHERE interface_id = ?")?; + let peers = peers_stmt + .query_map([&interface_id], |row| { + let preshared_key: Option = row.get(1)?; + let allowed_ips_rw: String = row.get(2)?; + let allowed_ips: Vec = + allowed_ips_rw.split(',').map(|s| s.to_string()).collect(); + Ok(Peer { + public_key: row.get(0)?, + preshared_key, + allowed_ips, + endpoint: row.get(3)?, + persistent_keepalive: None, + name: None, + }) + })? + .collect::>>()?; + Ok(Config { interface: iface, peers }) +} + +pub fn dump_interface(conn: &Connection, config: &Config) -> Result<()> { + let mut stmt = conn.prepare("INSERT INTO wg_interface (private_key, dns, address, listen_port, mtu) VALUES (?, ?, ?, ?, ?)")?; + let cif = &config.interface; + stmt.execute(params![ + cif.private_key, + to_lst(&cif.dns), + to_lst(&cif.address), + cif.listen_port, + cif.mtu + ])?; + let interface_id = conn.last_insert_rowid(); + let mut stmt = conn.prepare("INSERT INTO wg_peer (interface_id, public_key, preshared_key, allowed_ips, endpoint) VALUES (?, ?, ?, ?, ?)")?; + for peer in &config.peers { + stmt.execute(params![ + &interface_id, + &peer.public_key, + &peer.preshared_key, + &peer.allowed_ips.join(","), + &peer.endpoint + ])?; + } + Ok(()) +} + +pub fn get_connection(path: Option<&Path>) -> Result { + let p = path.unwrap_or_else(|| std::path::Path::new(DB_PATH)); + if !p.exists() { + let conn = Connection::open(p)?; + initialize_tables(&conn)?; + dump_interface(&conn, &Config::default())?; + return Ok(conn); + } + Ok(Connection::open(p)?) +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::*; + + #[test] + fn test_db() { + let conn = Connection::open_in_memory().unwrap(); + initialize_tables(&conn).unwrap(); + let config = Config::default(); + dump_interface(&conn, &config).unwrap(); + let loaded = load_interface(&conn, "1").unwrap(); + assert_eq!(config, loaded); + } +} diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index c5406b2..d9ebf7e 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -3,16 +3,18 @@ pub mod wireguard; #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod daemon; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +pub mod database; pub(crate) mod tracing; #[cfg(target_vendor = "apple")] pub use daemon::apple::spawn_in_process; #[cfg(any(target_os = "linux", target_vendor = "apple"))] pub use daemon::{ + rpc::DaemonResponse, + rpc::ServerInfo, DaemonClient, DaemonCommand, - DaemonResponse, DaemonResponseData, DaemonStartOptions, - ServerInfo, }; diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 71d1c02..295373a 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -14,6 +14,9 @@ use tun::TunOptions; #[cfg(any(target_os = "linux", target_vendor = "apple"))] use crate::daemon::DaemonResponseData; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +pub mod database; + #[derive(Parser)] #[command(name = "Burrow")] #[command(author = "Hack Club ")] @@ -42,6 +45,14 @@ enum Commands { ServerInfo, /// Server config ServerConfig, + /// Reload Config + ReloadConfig(ReloadConfigArgs), +} + +#[derive(Args)] +struct ReloadConfigArgs { + #[clap(long, short)] + interface_id: String, } #[derive(Args)] @@ -69,13 +80,8 @@ async fn try_stop() -> Result<()> { } #[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_serverinfo() -> Result<()> { - let mut client = DaemonClient::new().await?; - let res = client.send_command(DaemonCommand::ServerInfo).await?; - match res.result { - Ok(DaemonResponseData::ServerInfo(si)) => { - println!("Got Result! {:?}", si); - } +fn handle_unexpected(res: Result) { + match res { Ok(DaemonResponseData::None) => { println!("Server not started.") } @@ -86,6 +92,17 @@ async fn try_serverinfo() -> Result<()> { println!("Error when retrieving from server: {}", e) } } +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_serverinfo() -> Result<()> { + let mut client = DaemonClient::new().await?; + let res = client.send_command(DaemonCommand::ServerInfo).await?; + if let Ok(DaemonResponseData::ServerInfo(si)) = res.result { + println!("Got Result! {:?}", si); + } else { + handle_unexpected(res.result); + } Ok(()) } @@ -93,40 +110,25 @@ async fn try_serverinfo() -> Result<()> { async fn try_serverconfig() -> Result<()> { let mut client = DaemonClient::new().await?; let res = client.send_command(DaemonCommand::ServerConfig).await?; - match res.result { - Ok(DaemonResponseData::ServerConfig(cfig)) => { - println!("Got Result! {:?}", cfig); - } - Ok(DaemonResponseData::None) => { - println!("Server not started.") - } - Ok(res) => { - println!("Unexpected Response: {:?}", res) - } - Err(e) => { - println!("Error when retrieving from server: {}", e) - } + if let Ok(DaemonResponseData::ServerConfig(cfig)) = res.result { + println!("Got Result! {:?}", cfig); + } else { + handle_unexpected(res.result); } Ok(()) } -#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] -async fn try_start() -> Result<()> { - Ok(()) -} - -#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] -async fn try_stop() -> Result<()> { - Ok(()) -} - -#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] -async fn try_serverinfo() -> Result<()> { - Ok(()) -} - -#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] -async fn try_serverconfig() -> Result<()> { +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_reloadconfig(interface_id: String) -> Result<()> { + let mut client = DaemonClient::new().await?; + let res = client + .send_command(DaemonCommand::ReloadConfig(interface_id)) + .await?; + if let Ok(DaemonResponseData::ServerConfig(cfig)) = res.result { + println!("Got Result! {:?}", cfig); + } else { + handle_unexpected(res.result); + } Ok(()) } @@ -139,9 +141,10 @@ async fn main() -> Result<()> { match &cli.command { Commands::Start(..) => try_start().await?, Commands::Stop => try_stop().await?, - Commands::Daemon(_) => daemon::daemon_main(None, None).await?, + Commands::Daemon(_) => daemon::daemon_main(None, None, None).await?, Commands::ServerInfo => try_serverinfo().await?, Commands::ServerConfig => try_serverconfig().await?, + Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?, } Ok(()) diff --git a/burrow/src/wireguard/config.rs b/burrow/src/wireguard/config.rs index ed7b3cd..bd86a9f 100644 --- a/burrow/src/wireguard/config.rs +++ b/burrow/src/wireguard/config.rs @@ -31,6 +31,7 @@ fn parse_public_key(string: &str) -> PublicKey { /// A raw version of Peer Config that can be used later to reflect configuration files. /// This should be later converted to a `WgPeer`. /// Refers to https://github.com/pirate/wireguard-docs?tab=readme-ov-file#overview +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Peer { pub public_key: String, pub preshared_key: Option, @@ -40,6 +41,7 @@ pub struct Peer { pub name: Option, } +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Interface { pub private_key: String, pub address: Vec, @@ -48,6 +50,7 @@ pub struct Interface { pub mtu: Option, } +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Config { pub peers: Vec, pub interface: Interface, // Support for multiple interfaces? diff --git a/burrow/src/wireguard/iface.rs b/burrow/src/wireguard/iface.rs index 6097082..84b5489 100755 --- a/burrow/src/wireguard/iface.rs +++ b/burrow/src/wireguard/iface.rs @@ -1,21 +1,26 @@ -use std::{net::IpAddr, sync::Arc}; -use std::ops::Deref; +use std::{net::IpAddr, ops::Deref, sync::Arc}; use anyhow::Error; use fehler::throws; use futures::future::join_all; use ip_network_table::IpNetworkTable; -use tokio::sync::{RwLock, Notify}; +use tokio::sync::{Notify, RwLock}; use tracing::{debug, error}; use tun::tokio::TunInterface; use super::{noise::Tunnel, Peer, PeerPcb}; -struct IndexedPcbs { +pub struct IndexedPcbs { pcbs: Vec>, allowed_ips: IpNetworkTable, } +impl Default for IndexedPcbs { + fn default() -> Self { + Self::new() + } +} + impl IndexedPcbs { pub fn new() -> Self { Self { @@ -49,12 +54,12 @@ impl FromIterator for IndexedPcbs { enum IfaceStatus { Running, - Idle + Idle, } pub struct Interface { - tun: Arc>>, - pcbs: Arc, + pub tun: Arc>>, + pub pcbs: Arc, status: Arc>, stop_notifier: Arc, } @@ -73,7 +78,12 @@ impl Interface { .collect::>()?; let pcbs = Arc::new(pcbs); - Self { pcbs, tun: Arc::new(RwLock::new(None)), status: Arc::new(RwLock::new(IfaceStatus::Idle)), stop_notifier: Arc::new(Notify::new()) } + Self { + pcbs, + tun: Arc::new(RwLock::new(None)), + status: Arc::new(RwLock::new(IfaceStatus::Idle)), + stop_notifier: Arc::new(Notify::new()), + } } pub async fn set_tun(&self, tun: TunInterface) { @@ -87,7 +97,7 @@ impl Interface { self.tun.clone() } - pub async fn remove_tun(&self){ + pub async fn remove_tun(&self) { let mut st = self.status.write().await; self.stop_notifier.notify_waiters(); *st = IfaceStatus::Idle; @@ -95,9 +105,7 @@ impl Interface { pub async fn run(&self) -> anyhow::Result<()> { let pcbs = self.pcbs.clone(); - let tun = self - .tun - .clone(); + let tun = self.tun.clone(); let status = self.status.clone(); let stop_notifier = self.stop_notifier.clone(); log::info!("Starting interface"); @@ -153,9 +161,7 @@ impl Interface { }; let mut tsks = vec![]; - let tun = self - .tun - .clone(); + let tun = self.tun.clone(); let outgoing = tokio::task::spawn(outgoing); tsks.push(outgoing); debug!("preparing to spawn read tasks"); diff --git a/burrow/src/wireguard/mod.rs b/burrow/src/wireguard/mod.rs index 15563fb..4c70a7f 100755 --- a/burrow/src/wireguard/mod.rs +++ b/burrow/src/wireguard/mod.rs @@ -1,4 +1,4 @@ -mod config; +pub mod config; mod iface; mod noise; mod pcb; diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 6e859ca..74e93eb 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,11 +1,13 @@ -use std::{io::{Error, IoSlice}, mem, net::{Ipv4Addr, SocketAddrV4}, os::fd::{AsRawFd, FromRawFd, RawFd}, ptr}; -use std::net::{IpAddr, Ipv6Addr, SocketAddrV6}; -use std::ptr::addr_of; +use std::{ + io::{Error, IoSlice}, + mem, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4}, + os::fd::{AsRawFd, FromRawFd, RawFd}, +}; use byteorder::{ByteOrder, NetworkEndian}; use fehler::throws; -use libc::{c_char, iovec, writev, AF_INET, AF_INET6, sockaddr_in6}; -use nix::sys::socket::SockaddrIn6; +use libc::{c_char, iovec, writev, AF_INET, AF_INET6}; use socket2::{Domain, SockAddr, Socket, Type}; use tracing::{self, instrument}; @@ -69,11 +71,11 @@ impl TunInterface { #[throws] fn configure(&self, options: TunOptions) { - for addr in options.address{ + for addr in options.address { if let Ok(addr) = addr.parse::() { match addr { - IpAddr::V4(addr) => {self.set_ipv4_addr(addr)?} - IpAddr::V6(addr) => {self.set_ipv6_addr(addr)?} + IpAddr::V4(addr) => self.set_ipv4_addr(addr)?, + IpAddr::V6(addr) => self.set_ipv6_addr(addr)?, } } } @@ -146,7 +148,7 @@ impl TunInterface { } #[throws] - pub fn set_ipv6_addr(&self, addr: Ipv6Addr) { + pub fn set_ipv6_addr(&self, _addr: Ipv6Addr) { // let addr = SockAddr::from(SocketAddrV6::new(addr, 0, 0, 0)); // println!("addr: {:?}", addr); // let mut iff = self.in6_ifreq()?; From bca07c33b8bf9ef44f4a57e7a1fa6fdc15ba4790 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 25 May 2024 09:06:53 -0700 Subject: [PATCH 118/128] Start authentication flow --- .gitignore | 1 + .../NetworkExtension/libburrow/build-rust.sh | 2 +- Cargo.lock | 1153 +++++++++++------ Cargo.toml | 5 + Dockerfile | 55 +- burrow/Cargo.toml | 15 +- burrow/src/auth/client.rs | 24 + burrow/src/auth/mod.rs | 2 + burrow/src/auth/server/db.rs | 89 ++ burrow/src/auth/server/mod.rs | 62 + burrow/src/auth/server/providers/mod.rs | 8 + burrow/src/auth/server/providers/slack.rs | 102 ++ burrow/src/lib.rs | 2 + burrow/src/main.rs | 12 +- tun/Cargo.toml | 9 +- 15 files changed, 1104 insertions(+), 437 deletions(-) create mode 100644 burrow/src/auth/client.rs create mode 100644 burrow/src/auth/mod.rs create mode 100644 burrow/src/auth/server/db.rs create mode 100644 burrow/src/auth/server/mod.rs create mode 100644 burrow/src/auth/server/providers/mod.rs create mode 100644 burrow/src/auth/server/providers/slack.rs diff --git a/.gitignore b/.gitignore index dc886ed..96b2507 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ xcuserdata # Rust target/ +.env .DS_STORE .idea/ diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index fffa0d0..e7204a5 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -70,7 +70,7 @@ fi # Run cargo without the various environment variables set by Xcode. # Those variables can confuse cargo and the build scripts it runs. -env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" cargo build "${CARGO_ARGS[@]}" +env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}" mkdir -p "${BUILT_PRODUCTS_DIR}" diff --git a/Cargo.lock b/Cargo.lock index 628e996..ce263f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "once_cell", @@ -52,62 +52,57 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "anstream" -version = "0.6.11" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -115,18 +110,17 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "async-channel" -version = "2.1.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -151,25 +145,25 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -178,13 +172,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.29", "itoa", "matchit", "memchr", @@ -193,12 +187,46 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower", "tower-layer", "tower-service", ] +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.4.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-core" version = "0.3.4" @@ -208,8 +236,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", @@ -217,10 +245,31 @@ dependencies = [ ] [[package]] -name = "backtrace" -version = "0.3.69" +name = "axum-core" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -237,6 +286,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -284,7 +339,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.48", + "syn 2.0.68", "which", ] @@ -296,9 +351,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "blake2" @@ -320,9 +375,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "burrow" @@ -331,13 +386,15 @@ dependencies = [ "aead", "anyhow", "async-channel", - "base64", + "axum 0.7.5", + "base64 0.21.7", "blake2", "caps", "chacha20poly1305", "clap", "console", "console-subscriber", + "dotenv", "fehler", "futures", "hmac", @@ -351,6 +408,7 @@ dependencies = [ "parking_lot", "rand", "rand_core", + "reqwest 0.12.5", "ring", "rusqlite", "schemars", @@ -374,9 +432,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bzip2" @@ -411,12 +469,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -471,20 +530,20 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.1", + "libloading 0.8.4", ] [[package]] name = "clap" -version = "4.4.18" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" dependencies = [ "clap_builder", "clap_derive", @@ -492,9 +551,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.18" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" dependencies = [ "anstream", "anstyle", @@ -504,33 +563,33 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.7" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -618,27 +677,27 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" @@ -653,15 +712,14 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.1" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "fiat-crypto", - "platforms", "rustc_version", "subtle", "zeroize", @@ -675,7 +733,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -699,16 +757,22 @@ dependencies = [ ] [[package]] -name = "dyn-clone" -version = "1.0.16" +name = "dotenv" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" @@ -718,9 +782,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -733,9 +797,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -743,9 +807,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "4.0.3" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", @@ -754,9 +818,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ "event-listener", "pin-project-lite", @@ -776,9 +840,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fehler" @@ -802,15 +866,15 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.5" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -902,7 +966,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -947,9 +1011,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -958,9 +1022,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -970,17 +1034,17 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap 2.1.0", + "http 0.2.12", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -995,21 +1059,20 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", - "allocator-api2", ] [[package]] name = "hashlink" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1018,7 +1081,7 @@ version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" dependencies = [ - "base64", + "base64 0.21.7", "byteorder", "flate2", "nom", @@ -1027,15 +1090,15 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1063,9 +1126,20 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -1079,15 +1153,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -1103,17 +1200,17 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -1125,13 +1222,51 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.4.0", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-timeout" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.29", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -1144,12 +1279,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.29", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.4.0", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "idna" version = "0.5.0" @@ -1172,12 +1327,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1191,16 +1346,15 @@ dependencies = [ [[package]] name = "insta" -version = "1.34.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" dependencies = [ "console", "lazy_static", "linked-hash-map", "serde", "similar", - "yaml-rust", ] [[package]] @@ -1232,43 +1386,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] -name = "itertools" -version = "0.11.0" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -1278,9 +1438,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -1294,12 +1454,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets 0.52.6", ] [[package]] @@ -1339,15 +1499,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1355,9 +1515,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matchers" @@ -1376,9 +1536,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -1391,9 +1551,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -1418,7 +1578,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -1435,18 +1595,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -1455,11 +1615,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -1490,10 +1649,10 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "libc", - "memoffset 0.9.0", + "memoffset 0.9.1", ] [[package]] @@ -1517,10 +1676,16 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.17" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1537,9 +1702,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -1552,17 +1717,17 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.63" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -1579,7 +1744,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -1590,9 +1755,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.99" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -1614,9 +1779,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1624,15 +1789,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1672,29 +1837,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1704,15 +1869,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" - -[[package]] -name = "platforms" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "poly1305" @@ -1739,28 +1898,28 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.16" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", "prost-derive", @@ -1768,31 +1927,78 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "prost-types" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ "prost", ] [[package]] -name = "quote" -version = "1.0.35" +name = "quinn" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1829,23 +2035,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.3" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.5", - "regex-syntax 0.8.2", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -1859,13 +2065,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.4", ] [[package]] @@ -1876,25 +2082,25 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.29", "hyper-tls", "ipnet", "js-sys", @@ -1904,9 +2110,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", @@ -1915,21 +2123,64 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.4.0", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg 0.52.0", ] [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1938,7 +2189,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1948,9 +2199,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -1969,11 +2220,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1981,16 +2232,66 @@ dependencies = [ ] [[package]] -name = "rustversion" -version = "1.0.14" +name = "rustls" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustls-webpki" +version = "0.102.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" @@ -2003,9 +2304,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", @@ -2015,14 +2316,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] @@ -2033,11 +2334,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -2046,9 +2347,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -2056,52 +2357,62 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] name = "serde_json" -version = "1.0.112" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2163,10 +2474,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "similar" -version = "2.4.0" +name = "signal-hook-registry" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "similar" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "slab" @@ -2179,18 +2499,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2205,7 +2525,7 @@ version = "9.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082" dependencies = [ - "base64", + "base64 0.21.7", "digest", "hex", "miette", @@ -2217,15 +2537,15 @@ dependencies = [ [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -2240,9 +2560,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -2255,6 +2575,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "system-configuration" version = "0.5.1" @@ -2278,42 +2604,41 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -2321,11 +2646,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "num-conv", "powerfmt", "serde", "time-core", @@ -2339,9 +2665,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22" dependencies = [ "tinyvec_macros", ] @@ -2354,9 +2680,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -2364,6 +2690,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "tracing", @@ -2382,13 +2709,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -2402,10 +2729,21 @@ dependencies = [ ] [[package]] -name = "tokio-stream" -version = "0.1.14" +name = "tokio-rustls" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -2414,16 +2752,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -2434,13 +2771,13 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ "async-stream", "async-trait", - "axum", - "base64", + "axum 0.6.20", + "base64 0.21.7", "bytes", "h2", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.29", "hyper-timeout", "percent-encoding", "pin-project", @@ -2491,6 +2828,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2504,7 +2842,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -2603,7 +2941,7 @@ dependencies = [ "libloading 0.7.4", "log", "nix 0.26.4", - "reqwest", + "reqwest 0.11.27", "schemars", "serde", "socket2", @@ -2636,18 +2974,18 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "universal-hash" @@ -2667,9 +3005,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -2678,15 +3016,15 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.7.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ "serde", ] @@ -2726,9 +3064,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2736,24 +3074,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2763,9 +3101,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2773,33 +3111,42 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -2814,9 +3161,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi" @@ -2864,7 +3211,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -2884,17 +3231,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -2905,9 +3253,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -2917,9 +3265,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -2929,9 +3277,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -2941,9 +3295,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -2953,9 +3307,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -2965,9 +3319,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -2977,9 +3331,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winreg" @@ -2992,10 +3346,20 @@ dependencies = [ ] [[package]] -name = "x25519-dalek" -version = "2.0.0" +name = "winreg" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", "rand_core", @@ -3005,44 +3369,35 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] +checksum = "63658493314859b4dfdf3fb8c1defd61587839def09582db50b8a4e93afca6bb" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] @@ -3055,7 +3410,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -3099,9 +3454,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 44981a2..362ba2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,3 +2,8 @@ members = ["burrow", "tun"] resolver = "2" exclude = ["burrow-gtk"] + +[profile.release] +lto = true +panic = "abort" +opt-level = "z" diff --git a/Dockerfile b/Dockerfile index afd51ea..e55eb58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.76.0-slim-bookworm AS builder +FROM docker.io/library/rust:1.77-slim-bookworm AS builder ARG TARGETPLATFORM ARG LLVM_VERSION=16 @@ -8,7 +8,7 @@ ENV KEYRINGS /etc/apt/keyrings RUN set -eux && \ mkdir -p $KEYRINGS && \ apt-get update && \ - apt-get install --no-install-recommends -y gpg curl musl-dev && \ + apt-get install --no-install-recommends -y gpg curl busybox make musl-dev && \ curl --proto '=https' --tlsv1.2 -sSf https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor --output $KEYRINGS/llvm.gpg && \ echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-$LLVM_VERSION main" > /etc/apt/sources.list.d/llvm.list && \ apt-get update && \ @@ -24,30 +24,31 @@ RUN set -eux && \ apt-get remove -y --auto-remove && \ rm -rf /var/lib/apt/lists/* -ARG SQLITE_VERSION=3400100 +RUN case $TARGETPLATFORM in \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ + *) exit 1 ;; \ + esac && \ + rustup target add $LLVM_TARGET + +ARG SQLITE_VERSION=3460000 RUN case $TARGETPLATFORM in \ - "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl MUSL_TARGET=aarch64-linux-musl ;; \ - "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl MUSL_TARGET=x86_64-linux-musl ;; \ - *) exit 1 ;; \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl MUSL_TARGET=aarch64-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl MUSL_TARGET=x86_64-linux-musl ;; \ + *) exit 1 ;; \ esac && \ - rustup target add $LLVM_TARGET && \ - curl --proto '=https' --tlsv1.2 -sSfO https://www.sqlite.org/2022/sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ + curl --proto '=https' --tlsv1.2 -sSfO https://www.sqlite.org/2024/sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ tar xf sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ - rm sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ cd sqlite-autoconf-$SQLITE_VERSION && \ - ./configure --disable-shared \ - CC="clang-$LLVM_VERSION -target $LLVM_TARGET" \ - CFLAGS="-I/usr/local/include -I/usr/include/$MUSL_TARGET" \ - LDFLAGS="-L/usr/local/lib -L/usr/lib/$MUSL_TARGET -L/lib/$MUSL_TARGET" && \ + ./configure --disable-shared --disable-dependency-tracking \ + CC="clang-$LLVM_VERSION -target $LLVM_TARGET" \ + CFLAGS="-I/usr/local/include -I/usr/include/$MUSL_TARGET" \ + LDFLAGS="-L/usr/local/lib -L/usr/lib/$MUSL_TARGET -L/lib/$MUSL_TARGET" && \ make && \ make install && \ cd .. && \ - rm -rf sqlite-autoconf-$SQLITE_VERSION - -ENV SQLITE3_STATIC=1 \ - SQLITE3_INCLUDE_DIR=/usr/local/include \ - SQLITE3_LIB_DIR=/usr/local/lib + rm -rf sqlite-autoconf-$SQLITE_VERSION sqlite-autoconf-$SQLITE_VERSION.tar.gz ENV CC_x86_64_unknown_linux_musl=clang-$LLVM_VERSION \ AR_x86_64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ @@ -55,14 +56,17 @@ ENV CC_x86_64_unknown_linux_musl=clang-$LLVM_VERSION \ AR_aarch64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/x86_64-linux-musl -L/lib/x86_64-linux-musl -C linker=rust-lld" \ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/aarch64-linux-musl -L/lib/aarch64-linux-musl -C linker=rust-lld" \ - CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse + CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \ + SQLITE3_STATIC=1 \ + SQLITE3_INCLUDE_DIR=/usr/local/include \ + SQLITE3_LIB_DIR=/usr/local/lib COPY . . RUN case $TARGETPLATFORM in \ - "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ - "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ - *) exit 1 ;; \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ + *) exit 1 ;; \ esac && \ cargo install --path burrow --target $LLVM_TARGET @@ -71,7 +75,8 @@ WORKDIR /tmp/rootfs RUN set -eux && \ mkdir -p ./bin ./etc ./tmp ./data && \ mv /usr/local/cargo/bin/burrow ./bin/burrow && \ - echo 'burrow:x:10001:10001::/tmp:/sbin/nologin' > ./etc/passwd && \ + cp /bin/busybox ./bin/busybox && \ + echo 'burrow:x:10001:10001::/tmp:/bin/busybox' > ./etc/passwd && \ echo 'burrow:x:10001:' > ./etc/group && \ chown -R 10001:10001 ./tmp ./data && \ chmod 0777 ./tmp @@ -90,4 +95,6 @@ USER 10001:10001 COPY --from=builder /tmp/rootfs / WORKDIR /data -ENTRYPOINT ["/bin/burrow"] +EXPOSE 8080 + +CMD ["/bin/burrow", "auth-server"] diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 0c816f8..0fb63a5 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,12 +10,13 @@ crate-type = ["lib", "staticlib"] [dependencies] anyhow = "1.0" -tokio = { version = "1.21", features = [ +tokio = { version = "1.37", features = [ "rt", "macros", "sync", "io-util", "rt-multi-thread", + "signal", "time", "tracing", ] } @@ -24,7 +25,7 @@ clap = { version = "4.4", features = ["derive"] } tracing = "0.1" tracing-log = "0.1" tracing-oslog = { git = "https://github.com/Stormshield-robinc/tracing-oslog" } -tracing-subscriber = { version = "0.3" , features = ["std", "env-filter"] } +tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1.0" @@ -50,9 +51,13 @@ futures = "0.3.28" once_cell = "1.19" console-subscriber = { version = "0.2.0", optional = true } console = "0.15.8" - -[dependencies.rusqlite] -version = "0.31.0" +axum = "0.7.4" +reqwest = { version = "0.12", default-features = false, features = [ + "json", + "rustls-tls", +] } +rusqlite = "0.31.0" +dotenv = "0.15.0" [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5" diff --git a/burrow/src/auth/client.rs b/burrow/src/auth/client.rs new file mode 100644 index 0000000..e9721f3 --- /dev/null +++ b/burrow/src/auth/client.rs @@ -0,0 +1,24 @@ +use std::env::var; + +use anyhow::Result; +use reqwest::Url; + +pub async fn login() -> Result<()> { + let state = "vt :P"; + let nonce = "no"; + + let mut url = Url::parse("https://slack.com/openid/connect/authorize")?; + let mut q = url.query_pairs_mut(); + q.append_pair("response_type", "code"); + q.append_pair("scope", "openid profile email"); + q.append_pair("client_id", &var("CLIENT_ID")?); + q.append_pair("state", state); + q.append_pair("team", &var("SLACK_TEAM_ID")?); + q.append_pair("nonce", nonce); + q.append_pair("redirect_uri", "https://burrow.rs/callback"); + drop(q); + + println!("Continue auth in your browser:\n{}", url.as_str()); + + Ok(()) +} diff --git a/burrow/src/auth/mod.rs b/burrow/src/auth/mod.rs new file mode 100644 index 0000000..c07f47e --- /dev/null +++ b/burrow/src/auth/mod.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod server; diff --git a/burrow/src/auth/server/db.rs b/burrow/src/auth/server/db.rs new file mode 100644 index 0000000..b74f7ce --- /dev/null +++ b/burrow/src/auth/server/db.rs @@ -0,0 +1,89 @@ +use anyhow::Result; + +pub static PATH: &str = "./server.sqlite3"; + +pub fn init_db() -> Result<()> { + let conn = rusqlite::Connection::open(PATH)?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS user ( + id PRIMARY KEY, + created_at TEXT NOT NULL + )", + (), + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS user_connection ( + user_id INTEGER REFERENCES user(id) ON DELETE CASCADE, + openid_provider TEXT NOT NULL, + openid_user_id TEXT NOT NULL, + openid_user_name TEXT NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT, + PRIMARY KEY (openid_provider, openid_user_id) + )", + (), + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS device ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + public_key TEXT NOT NULL, + apns_token TEXT UNIQUE, + user_id INT REFERENCES user(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (datetime('now')) CHECK(created_at IS datetime(created_at)), + ipv4 TEXT NOT NULL UNIQUE, + ipv6 TEXT NOT NULL UNIQUE, + access_token TEXT NOT NULL UNIQUE, + refresh_token TEXT NOT NULL UNIQUE, + expires_at TEXT NOT NULL DEFAULT (datetime('now', '+7 days')) CHECK(expires_at IS datetime(expires_at)) + )", + () + ).unwrap(); + + Ok(()) +} + +pub fn store_connection( + openid_user: super::providers::OpenIdUser, + openid_provider: &str, + access_token: &str, + refresh_token: Option<&str>, +) -> Result<()> { + log::debug!("Storing openid user {:#?}", openid_user); + let conn = rusqlite::Connection::open(PATH)?; + + conn.execute( + "INSERT OR IGNORE INTO user (id, created_at) VALUES (?, datetime('now'))", + (&openid_user.sub,), + )?; + conn.execute( + "INSERT INTO user_connection (user_id, openid_provider, openid_user_id, openid_user_name, access_token, refresh_token) VALUES ( + (SELECT id FROM user WHERE id = ?), + ?, + ?, + ?, + ?, + ? + )", + (&openid_user.sub, &openid_provider, &openid_user.sub, &openid_user.name, access_token, refresh_token), + )?; + + Ok(()) +} + +pub fn store_device( + openid_user: super::providers::OpenIdUser, + openid_provider: &str, + access_token: &str, + refresh_token: Option<&str>, +) -> Result<()> { + log::debug!("Storing openid user {:#?}", openid_user); + let conn = rusqlite::Connection::open(PATH)?; + + // TODO + + Ok(()) +} diff --git a/burrow/src/auth/server/mod.rs b/burrow/src/auth/server/mod.rs new file mode 100644 index 0000000..88b3ff3 --- /dev/null +++ b/burrow/src/auth/server/mod.rs @@ -0,0 +1,62 @@ +pub mod db; +pub mod providers; + +use anyhow::Result; +use axum::{http::StatusCode, routing::post, Router}; +use providers::slack::auth; +use tokio::signal; + +pub async fn serve() -> Result<()> { + db::init_db()?; + + let app = Router::new() + .route("/slack-auth", post(auth)) + .route("/device/new", post(device_new)); + + let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); + log::info!("Starting auth server on port 8080"); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); + + Ok(()) +} + +async fn device_new() -> StatusCode { + StatusCode::OK +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} + +// mod db { +// use rusqlite::{Connection, Result}; + +// #[derive(Debug)] +// struct User { +// id: i32, +// created_at: String, +// } +// } diff --git a/burrow/src/auth/server/providers/mod.rs b/burrow/src/auth/server/providers/mod.rs new file mode 100644 index 0000000..36ff0bd --- /dev/null +++ b/burrow/src/auth/server/providers/mod.rs @@ -0,0 +1,8 @@ +pub mod slack; +pub use super::db; + +#[derive(serde::Deserialize, Default, Debug)] +pub struct OpenIdUser { + pub sub: String, + pub name: String, +} diff --git a/burrow/src/auth/server/providers/slack.rs b/burrow/src/auth/server/providers/slack.rs new file mode 100644 index 0000000..581cd1e --- /dev/null +++ b/burrow/src/auth/server/providers/slack.rs @@ -0,0 +1,102 @@ +use anyhow::Result; +use axum::{ + extract::Json, + http::StatusCode, + routing::{get, post}, +}; +use reqwest::header::AUTHORIZATION; +use serde::Deserialize; + +use super::db::store_connection; + +#[derive(Deserialize)] +pub struct SlackToken { + slack_token: String, +} +pub async fn auth(Json(payload): Json) -> (StatusCode, String) { + let slack_user = match fetch_slack_user(&payload.slack_token).await { + Ok(user) => user, + Err(e) => { + log::error!("Failed to fetch Slack user: {:?}", e); + return (StatusCode::UNAUTHORIZED, String::new()); + } + }; + + log::info!( + "Slack user {} ({}) logged in.", + slack_user.name, + slack_user.sub + ); + + let conn = match store_connection(slack_user, "slack", &payload.slack_token, None) { + Ok(user) => user, + Err(e) => { + log::error!("Failed to fetch Slack user: {:?}", e); + return (StatusCode::UNAUTHORIZED, String::new()); + } + }; + + (StatusCode::OK, String::new()) +} + +async fn fetch_slack_user(access_token: &str) -> Result { + let client = reqwest::Client::new(); + let res = client + .get("https://slack.com/api/openid.connect.userInfo") + .header(AUTHORIZATION, format!("Bearer {}", access_token)) + .send() + .await? + .json::() + .await?; + + let res_ok = res + .get("ok") + .and_then(|v| v.as_bool()) + .ok_or(anyhow::anyhow!("Slack user object not ok!"))?; + + if !res_ok { + return Err(anyhow::anyhow!("Slack user object not ok!")); + } + + Ok(serde_json::from_value(res)?) +} + +// async fn fetch_save_slack_user_data(query: Query) -> anyhow::Result<()> { +// let client = reqwest::Client::new(); +// log::trace!("Code was {}", &query.code); +// let mut url = Url::parse("https://slack.com/api/openid.connect.token")?; + +// { +// let mut q = url.query_pairs_mut(); +// q.append_pair("client_id", &var("CLIENT_ID")?); +// q.append_pair("client_secret", &var("CLIENT_SECRET")?); +// q.append_pair("code", &query.code); +// q.append_pair("grant_type", "authorization_code"); +// q.append_pair("redirect_uri", "https://burrow.rs/callback"); +// } + +// let data = client +// .post(url) +// .send() +// .await? +// .json::() +// .await?; + +// if !data.ok { +// return Err(anyhow::anyhow!("Slack code exchange response not ok!")); +// } + +// if let Some(access_token) = data.access_token { +// log::trace!("Access token is {access_token}"); +// let user = slack::fetch_slack_user(&access_token) +// .await +// .map_err(|err| anyhow::anyhow!("Failed to fetch Slack user info {:#?}", err))?; + +// db::store_user(user, access_token, String::new()) +// .map_err(|_| anyhow::anyhow!("Failed to store user in db"))?; + +// Ok(()) +// } else { +// Err(anyhow::anyhow!("Access token not found in response")) +// } +// } diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index d9ebf7e..6aae1fb 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -5,6 +5,8 @@ pub mod wireguard; mod daemon; #[cfg(any(target_os = "linux", target_vendor = "apple"))] pub mod database; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +mod auth; pub(crate) mod tracing; #[cfg(target_vendor = "apple")] diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 295373a..ff07d4c 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -7,6 +7,9 @@ pub(crate) mod tracing; #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod wireguard; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +mod auth; + #[cfg(any(target_os = "linux", target_vendor = "apple"))] use daemon::{DaemonClient, DaemonCommand, DaemonStartOptions}; use tun::TunOptions; @@ -47,12 +50,15 @@ enum Commands { ServerConfig, /// Reload Config ReloadConfig(ReloadConfigArgs), + /// Authentication server + AuthServer, } #[derive(Args)] struct ReloadConfigArgs { #[clap(long, short)] interface_id: String, + } #[derive(Args)] @@ -133,9 +139,10 @@ async fn try_reloadconfig(interface_id: String) -> Result<()> { } #[cfg(any(target_os = "linux", target_vendor = "apple"))] -#[tokio::main(flavor = "current_thread")] +#[tokio::main] async fn main() -> Result<()> { tracing::initialize(); + dotenv::dotenv().ok(); let cli = Cli::parse(); match &cli.command { @@ -145,6 +152,7 @@ async fn main() -> Result<()> { Commands::ServerInfo => try_serverinfo().await?, Commands::ServerConfig => try_serverconfig().await?, Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?, + Commands::AuthServer => crate::auth::server::serve().await?, } Ok(()) @@ -152,5 +160,5 @@ async fn main() -> Result<()> { #[cfg(not(any(target_os = "linux", target_vendor = "apple")))] pub fn main() { - eprintln!("This platform is not supported currently.") + eprintln!("This platform is not supported") } diff --git a/tun/Cargo.toml b/tun/Cargo.toml index 7413f65..1b07833 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -8,7 +8,7 @@ libc = "0.2" fehler = "1.0" nix = { version = "0.26", features = ["ioctl"] } socket2 = "0.5" -tokio = { version = "1.28", features = [] } +tokio = { version = "1.37", default-features = false, optional = true } byteorder = "1.4" tracing = "0.1" log = "0.4" @@ -19,10 +19,7 @@ futures = { version = "0.3.28", optional = true } [features] serde = ["dep:serde", "dep:schemars"] -tokio = ["tokio/net", "dep:futures"] - -[target.'cfg(feature = "tokio")'.dev-dependencies] -tokio = { features = ["rt", "macros"] } +tokio = ["tokio/net", "dep:tokio", "dep:futures"] [target.'cfg(windows)'.dependencies] lazy_static = "1.4" @@ -37,7 +34,7 @@ windows = { version = "0.48", features = [ [target.'cfg(windows)'.build-dependencies] anyhow = "1.0" bindgen = "0.65" -reqwest = { version = "0.11", features = ["native-tls"] } +reqwest = { version = "0.11" } ssri = { version = "9.0", default-features = false } tokio = { version = "1.28", features = ["rt", "macros"] } zip = { version = "0.6", features = ["deflate"] } From 3c70bc2a5c4a012c0fc31f12f1f5e75fbde67586 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 6 Jul 2024 10:50:14 -0700 Subject: [PATCH 119/128] Remove SwiftLint from Xcode project --- Apple/Burrow.xcodeproj/project.pbxproj | 45 ---------- .../xcshareddata/swiftpm/Package.resolved | 86 ------------------- 2 files changed, 131 deletions(-) delete mode 100644 Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index a3be02d..5c5e80b 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -294,7 +294,6 @@ buildRules = ( ); dependencies = ( - D082527D2B5DEB80005DA378 /* PBXTargetDependency */, ); name = Shared; productName = Shared; @@ -313,7 +312,6 @@ buildRules = ( ); dependencies = ( - D08252792B5DEB78005DA378 /* PBXTargetDependency */, D00117492B30373500D87C25 /* PBXTargetDependency */, ); name = NetworkExtension; @@ -334,7 +332,6 @@ buildRules = ( ); dependencies = ( - D082527B2B5DEB7D005DA378 /* PBXTargetDependency */, D00117472B30373100D87C25 /* PBXTargetDependency */, D020F65C29E4A697002790F6 /* PBXTargetDependency */, ); @@ -374,7 +371,6 @@ ); mainGroup = D05B9F6929E39EEC008CB1F9; packageReferences = ( - D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */, ); productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */; projectDirPath = ""; @@ -513,18 +509,6 @@ target = D020F65229E4A697002790F6 /* NetworkExtension */; targetProxy = D020F65B29E4A697002790F6 /* PBXContainerItemProxy */; }; - D08252792B5DEB78005DA378 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = D08252782B5DEB78005DA378 /* SwiftLintPlugin */; - }; - D082527B2B5DEB7D005DA378 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = D082527A2B5DEB7D005DA378 /* SwiftLintPlugin */; - }; - D082527D2B5DEB80005DA378 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = D082527C2B5DEB80005DA378 /* SwiftLintPlugin */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -624,35 +608,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/realm/SwiftLint.git"; - requirement = { - branch = main; - kind = branch; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - D08252782B5DEB78005DA378 /* SwiftLintPlugin */ = { - isa = XCSwiftPackageProductDependency; - package = D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; - productName = "plugin:SwiftLintPlugin"; - }; - D082527A2B5DEB7D005DA378 /* SwiftLintPlugin */ = { - isa = XCSwiftPackageProductDependency; - package = D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; - productName = "plugin:SwiftLintPlugin"; - }; - D082527C2B5DEB80005DA378 /* SwiftLintPlugin */ = { - isa = XCSwiftPackageProductDependency; - package = D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; - productName = "plugin:SwiftLintPlugin"; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = D05B9F6A29E39EEC008CB1F9 /* Project object */; } diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 9378372..0000000 --- a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,86 +0,0 @@ -{ - "pins" : [ - { - "identity" : "collectionconcurrencykit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", - "state" : { - "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", - "version" : "0.2.0" - } - }, - { - "identity" : "cryptoswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", - "state" : { - "revision" : "7892a123f7e8d0fe62f9f03728b17bbd4f94df5c", - "version" : "1.8.1" - } - }, - { - "identity" : "sourcekitten", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/SourceKitten.git", - "state" : { - "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", - "version" : "0.34.1" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" - } - }, - { - "identity" : "swiftlint", - "kind" : "remoteSourceControl", - "location" : "https://github.com/realm/SwiftLint.git", - "state" : { - "branch" : "main", - "revision" : "7595ad3fafc1a31086dc40ba01fd898bf6b42d5f" - } - }, - { - "identity" : "swiftytexttable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", - "state" : { - "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", - "version" : "0.9.0" - } - }, - { - "identity" : "swxmlhash", - "kind" : "remoteSourceControl", - "location" : "https://github.com/drmohundro/SWXMLHash.git", - "state" : { - "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", - "version" : "7.0.2" - } - }, - { - "identity" : "yams", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/Yams.git", - "state" : { - "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", - "version" : "5.0.6" - } - } - ], - "version" : 2 -} From 3dedca4de308a16f1782f3fdc30c6cce5056c8d3 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 6 Jul 2024 17:20:46 -0700 Subject: [PATCH 120/128] Update build settings --- .github/actions/notarize/action.yml | 34 +- .github/workflows/build-apple.yml | 4 +- .github/workflows/build-rpm.yml | 2 +- .github/workflows/build-rust.yml | 10 +- .github/workflows/release-apple.yml | 41 +- Apple/App/AppDelegate.swift | 3 +- Apple/App/MainMenu.xib | 4 +- Cargo.lock | 754 ++++++++++++++-------------- Dockerfile | 3 +- 9 files changed, 407 insertions(+), 448 deletions(-) diff --git a/.github/actions/notarize/action.yml b/.github/actions/notarize/action.yml index f3f98f2..efd2159 100644 --- a/.github/actions/notarize/action.yml +++ b/.github/actions/notarize/action.yml @@ -9,12 +9,6 @@ inputs: app-store-key-issuer-id: description: App Store key issuer ID required: true - archive-path: - description: Xcode archive path - required: true - export-path: - description: The path to export the archive to - required: true runs: using: composite steps: @@ -24,28 +18,8 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - echo '{"destination":"export","method":"developer-id"}' \ - | plutil -convert xml1 -o ExportOptions.plist - + ditto -c -k --keepParent Release/Burrow.app Upload.zip + xcrun notarytool submit --wait --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" Upload.zip + xcrun stapler staple Release/Burrow.app - xcodebuild -exportArchive \ - -allowProvisioningUpdates \ - -allowProvisioningDeviceRegistration \ - -skipPackagePluginValidation \ - -skipMacroValidation \ - -onlyUsePackageVersionsFromResolvedFile \ - -authenticationKeyID ${{ inputs.app-store-key-id }} \ - -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ - -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -archivePath Wallet.xcarchive \ - -exportPath Release \ - -exportOptionsPlist ExportOptions.plist - - ditto -c -k --keepParent Release/Wallet.app Upload.zip - SUBMISSION_ID=$(xcrun notarytool submit --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" Upload.zip | awk '/ id:/ { print $2; exit }') - - xcrun notarytool wait $SUBMISSION_ID --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" - xcrun stapler staple Release/Wallet.app - - aa archive -a lzma -b 8m -d Release -subdir Wallet.app -o Wallet.app.aar - - rm -rf Upload.zip Release AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist + rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 Release diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 00b6bec..84cc03a 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -24,7 +24,7 @@ jobs: rust-targets: - aarch64-apple-ios - scheme: App - destination: platform=iOS Simulator,OS=17.2,name=iPhone 15 Pro + destination: platform=iOS Simulator,OS=18.0,name=iPhone 15 Pro platform: iOS Simulator sdk-name: iphonesimulator rust-targets: @@ -38,7 +38,7 @@ jobs: - x86_64-apple-darwin - aarch64-apple-darwin env: - DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml index e0ce8df..029bf16 100644 --- a/.github/workflows/build-rpm.yml +++ b/.github/workflows/build-rpm.yml @@ -5,7 +5,7 @@ jobs: name: Build RPM runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Install RPM run: cargo install cargo-generate-rpm diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 3255fc7..76ce9f2 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -22,14 +22,18 @@ jobs: targets: - aarch64-unknown-linux-gnu - os: macos-12 - platform: macOS + platform: macOS (Intel) test-targets: - x86_64-apple-darwin targets: + - x86_64-apple-ios + - os: macos-14 + platform: macOS + test-targets: - aarch64-apple-darwin + targets: - aarch64-apple-ios - aarch64-apple-ios-sim - - x86_64-apple-ios - os: windows-2022 platform: Windows test-targets: @@ -38,7 +42,7 @@ jobs: - aarch64-pc-windows-msvc runs-on: ${{ matrix.os }} env: - DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer CARGO_INCREMENTAL: 0 CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc RUST_BACKTRACE: short diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index 786fb54..1883008 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -13,7 +13,8 @@ jobs: fail-fast: false matrix: include: - - destination: generic/platform=iOS + - + destination: generic/platform=iOS platform: iOS rust-targets: - aarch64-apple-ios @@ -23,7 +24,7 @@ jobs: - x86_64-apple-darwin - aarch64-apple-darwin env: - DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer steps: - name: Checkout uses: actions/checkout@v4 @@ -50,25 +51,23 @@ jobs: app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive - - name: Notarize (macOS) - if: ${{ matrix.platform == 'macOS' }} - uses: ./.github/actions/notarize - with: - app-store-key: ${{ secrets.APPSTORE_KEY }} - app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} - app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} - archive-path: Burrow.xcarchive - - name: Export IPA (iOS) - if: ${{ matrix.platform == 'iOS' }} + - name: Export uses: ./.github/actions/export with: - method: ad-hoc + method: ${{ matrix.platform == 'macOS' && 'developer-id' || 'ad-hoc' }} destination: export app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive export-path: Release + - name: Notarize + if: ${{ matrix.platform == 'macOS' }} + uses: ./.github/actions/notarize + with: + app-store-key: ${{ secrets.APPSTORE_KEY }} + app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} + app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} - name: Compress (iOS) if: ${{ matrix.platform == 'iOS' }} shell: bash @@ -83,27 +82,17 @@ jobs: aa archive -a lzma -b 8m -d Apple/Release -subdir Burrow.app -o Burrow.app.aar aa archive -a lzma -b 8m -d Apple -subdir Burrow.xcarchive -o Burrow-${{ matrix.platform }}.xcarchive.aar rm -rf Apple/Release - - name: Upload to GitHub (iOS) - if: ${{ matrix.platform == 'iOS' }} + - name: Upload to GitHub uses: SierraSoftworks/gh-releases@v1.0.7 with: token: ${{ secrets.GITHUB_TOKEN }} release_tag: ${{ github.ref_name }} overwrite: 'true' files: | - Burrow.ipa - Burrow-${{ matrix.platform }}.xcarchive.aar - - name: Upload to GitHub (macOS) - if: ${{ matrix.platform == 'macOS' }} - uses: SierraSoftworks/gh-releases@v1.0.7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - release_tag: ${{ github.ref_name }} - overwrite: 'true' - files: | - Burrow.aap.aar + ${{ matrix.platform == 'macOS' && 'Burrow.aap.aar' || 'Burrow.ipa' }} Burrow-${{ matrix.platform }}.xcarchive.aar - name: Upload to App Store Connect + if: ${{ matrix.platform == 'iOS' }} uses: ./.github/actions/export with: method: app-store diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index 6085d85..bd76a2f 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -2,8 +2,7 @@ import AppKit import SwiftUI -@MainActor -@NSApplicationMain +@MainActor @main class AppDelegate: NSObject, NSApplicationDelegate { private let quitItem: NSMenuItem = { let quitItem = NSMenuItem( diff --git a/Apple/App/MainMenu.xib b/Apple/App/MainMenu.xib index 8933f30..587f6c4 100644 --- a/Apple/App/MainMenu.xib +++ b/Apple/App/MainMenu.xib @@ -1,7 +1,7 @@ - + - + diff --git a/Cargo.lock b/Cargo.lock index ce263f9..5ef886c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" dependencies = [ "cfg-if", "once_cell", @@ -52,57 +52,62 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] -name = "anstream" -version = "0.6.14" +name = "allocator-api2" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -110,17 +115,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "async-channel" -version = "2.3.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -145,25 +151,25 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "autocfg" -version = "1.3.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" @@ -176,9 +182,9 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.28", "itoa", "matchit", "memchr", @@ -236,7 +242,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", "mime", "rustversion", @@ -267,9 +273,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -339,7 +345,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.68", + "syn 2.0.48", "which", ] @@ -351,9 +357,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "blake2" @@ -375,9 +381,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "burrow" @@ -432,9 +438,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bzip2" @@ -469,13 +475,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.104" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", "libc", - "once_cell", ] [[package]] @@ -530,20 +535,20 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.8.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", - "libloading 0.8.4", + "libloading 0.8.1", ] [[package]] name = "clap" -version = "4.5.8" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", @@ -551,9 +556,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.8" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -563,33 +568,33 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.8" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" -version = "2.5.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] @@ -677,27 +682,27 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -712,14 +717,15 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.3" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "fiat-crypto", + "platforms", "rustc_version", "subtle", "zeroize", @@ -733,7 +739,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -764,15 +770,15 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "either" -version = "1.13.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encode_unicode" @@ -782,9 +788,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -797,9 +803,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", @@ -807,9 +813,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.1" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", @@ -818,9 +824,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ "event-listener", "pin-project-lite", @@ -840,9 +846,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fehler" @@ -866,15 +872,15 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.9" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -966,7 +972,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -1011,9 +1017,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -1022,9 +1028,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -1034,17 +1040,17 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.26" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.12", - "indexmap 2.2.6", + "http 0.2.11", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -1059,20 +1065,21 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ "ahash", + "allocator-api2", ] [[package]] name = "hashlink" -version = "0.9.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.14.3", ] [[package]] @@ -1090,15 +1097,15 @@ dependencies = [ [[package]] name = "heck" -version = "0.5.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "hex" @@ -1126,9 +1133,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.12" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1153,7 +1160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http 0.2.12", + "http 0.2.11", "pin-project-lite", ] @@ -1182,9 +1189,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -1200,16 +1207,16 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", "httparse", "httpdate", @@ -1266,7 +1273,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.29", + "hyper 0.14.28", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -1279,7 +1286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.29", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", @@ -1327,12 +1334,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.14.3", ] [[package]] @@ -1346,15 +1353,16 @@ dependencies = [ [[package]] name = "insta" -version = "1.39.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" dependencies = [ "console", "lazy_static", "linked-hash-map", "serde", "similar", + "yaml-rust", ] [[package]] @@ -1385,50 +1393,44 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "is_terminal_polyfill" -version = "1.70.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" - [[package]] name = "itertools" -version = "0.12.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" @@ -1438,9 +1440,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -1454,12 +1456,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-sys 0.48.0", ] [[package]] @@ -1499,15 +1501,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1515,9 +1517,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "matchers" @@ -1536,9 +1538,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -1551,9 +1553,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.9.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -1578,7 +1580,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -1595,18 +1597,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.11" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", @@ -1615,10 +1617,11 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ + "lazy_static", "libc", "log", "openssl", @@ -1649,10 +1652,10 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.4.2", "cfg-if", "libc", - "memoffset 0.9.1", + "memoffset 0.9.0", ] [[package]] @@ -1675,17 +1678,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-traits" -version = "0.2.19" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -1702,9 +1699,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -1717,17 +1714,17 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", @@ -1744,7 +1741,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -1755,9 +1752,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ "cc", "libc", @@ -1779,9 +1776,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", @@ -1789,15 +1786,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -1837,29 +1834,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1869,9 +1866,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" + +[[package]] +name = "platforms" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" [[package]] name = "poly1305" @@ -1898,28 +1901,28 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.20" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.6" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", "prost-derive", @@ -1927,22 +1930,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.6" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "prost-types" -version = "0.12.6" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ "prost", ] @@ -1996,9 +1999,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2035,23 +2038,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags 2.6.0", + "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.10.5" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", ] [[package]] @@ -2065,13 +2068,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.2", ] [[package]] @@ -2082,15 +2085,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64 0.21.7", "bytes", @@ -2098,9 +2101,9 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.28", "hyper-tls", "ipnet", "js-sys", @@ -2110,11 +2113,9 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", @@ -2151,7 +2152,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", - "rustls-pemfile 2.1.2", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -2170,17 +2171,16 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", - "cfg-if", "getrandom", "libc", "spin", "untrusted", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -2189,7 +2189,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.4.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2199,9 +2199,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" @@ -2220,11 +2220,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -2245,15 +2245,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -2283,15 +2274,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" @@ -2304,9 +2295,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.21" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "dyn-clone", "schemars_derive", @@ -2316,14 +2307,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.21" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.68", + "syn 1.0.109", ] [[package]] @@ -2334,11 +2325,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags 2.6.0", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -2347,9 +2338,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -2357,46 +2348,46 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 1.0.109", ] [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" dependencies = [ "itoa", "ryu", @@ -2484,9 +2475,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.5.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" [[package]] name = "slab" @@ -2499,18 +2490,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -2537,15 +2528,15 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.6.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -2560,9 +2551,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2604,41 +2595,42 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", + "redox_syscall", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ "cfg-if", "once_cell", @@ -2646,12 +2638,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", - "num-conv", "powerfmt", "serde", "time-core", @@ -2665,9 +2656,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tinyvec" -version = "1.7.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] @@ -2715,7 +2706,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -2741,9 +2732,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -2752,15 +2743,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", + "tracing", ] [[package]] @@ -2775,9 +2767,9 @@ dependencies = [ "base64 0.21.7", "bytes", "h2", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.28", "hyper-timeout", "percent-encoding", "pin-project", @@ -2842,7 +2834,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -2941,7 +2933,7 @@ dependencies = [ "libloading 0.7.4", "log", "nix 0.26.4", - "reqwest 0.11.27", + "reqwest 0.11.23", "schemars", "serde", "socket2", @@ -2974,18 +2966,18 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "universal-hash" @@ -3005,9 +2997,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -3016,15 +3008,15 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.9.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "serde", ] @@ -3064,9 +3056,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3074,24 +3066,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -3101,9 +3093,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3111,28 +3103,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -3161,9 +3153,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.1.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" [[package]] name = "winapi" @@ -3211,7 +3203,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.52.0", ] [[package]] @@ -3231,18 +3223,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -3253,9 +3244,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" @@ -3265,9 +3256,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" @@ -3277,15 +3268,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" @@ -3295,9 +3280,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" @@ -3307,9 +3292,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" @@ -3319,9 +3304,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" @@ -3331,9 +3316,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winreg" @@ -3357,9 +3342,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ "curve25519-dalek", "rand_core", @@ -3369,35 +3354,44 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.11" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63658493314859b4dfdf3fb8c1defd61587839def09582db50b8a4e93afca6bb" +checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -3410,7 +3404,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -3454,9 +3448,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.12+zstd.1.5.6" +version = "2.0.9+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" dependencies = [ "cc", "pkg-config", diff --git a/Dockerfile b/Dockerfile index e55eb58..8e17812 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.77-slim-bookworm AS builder +FROM docker.io/library/rust:1.79-slim-bookworm AS builder ARG TARGETPLATFORM ARG LLVM_VERSION=16 @@ -56,7 +56,6 @@ ENV CC_x86_64_unknown_linux_musl=clang-$LLVM_VERSION \ AR_aarch64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/x86_64-linux-musl -L/lib/x86_64-linux-musl -C linker=rust-lld" \ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/aarch64-linux-musl -L/lib/aarch64-linux-musl -C linker=rust-lld" \ - CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \ SQLITE3_STATIC=1 \ SQLITE3_INCLUDE_DIR=/usr/local/include \ SQLITE3_LIB_DIR=/usr/local/lib From 951b4ddae2f33e15c7d1976337de64001797d0e9 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sat, 13 Jul 2024 18:09:09 -0700 Subject: [PATCH 121/128] add protobuf definition file --- proto/burrow.proto | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 proto/burrow.proto diff --git a/proto/burrow.proto b/proto/burrow.proto new file mode 100644 index 0000000..3e15219 --- /dev/null +++ b/proto/burrow.proto @@ -0,0 +1,72 @@ +syntax = "proto3"; +package burrow; + +import "google/protobuf/timestamp.proto"; + +service Tunnel { + rpc TunnelConfiguration (Empty) returns (TunnelConfigurationResponse); + rpc TunnelStart (Empty) returns (Empty); + rpc TunnelStop (Empty) returns (Empty); + rpc TunnelStatus (Empty) returns (stream TunnelStatusResponse); +} + +service Networks { + rpc NetworkAdd (Empty) returns (Empty); + rpc NetworkList (Empty) returns (stream NetworkListResponse); + rpc NetworkReorder (NetworkReorderRequest) returns (Empty); + rpc NetworkDelete (NetworkDeleteRequest) returns (Empty); +} + +message NetworkReorderRequest { + int32 id = 1; + int32 index = 2; +} + +message WireGuardPeer { + string endpoint = 1; + repeated string subnet = 2; +} + +message WireGuardNetwork { + string address = 1; + string dns = 2; + repeated WireGuardPeer peer = 3; +} + +message NetworkDeleteRequest { + int32 id = 1; +} + +message Network { + int32 id = 1; + NetworkType type = 2; + bytes payload = 3; +} + +enum NetworkType { + WireGuard = 0; + HackClub = 1; +} + +message NetworkListResponse { + repeated Network network = 1; +} + +message Empty { + +} + +enum State { + Stopped = 0; + Running = 1; +} + +message TunnelStatusResponse { + State state = 1; + optional google.protobuf.Timestamp start = 2; +} + +message TunnelConfigurationResponse { + repeated string addresses = 1; + int32 mtu = 2; +} From aa634d03e2560dbaf500b19f584d19696abb7161 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sat, 13 Jul 2024 18:14:00 -0700 Subject: [PATCH 122/128] update protobuf definition file --- proto/burrow.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/burrow.proto b/proto/burrow.proto index 3e15219..2d29c78 100644 --- a/proto/burrow.proto +++ b/proto/burrow.proto @@ -4,7 +4,7 @@ package burrow; import "google/protobuf/timestamp.proto"; service Tunnel { - rpc TunnelConfiguration (Empty) returns (TunnelConfigurationResponse); + rpc TunnelConfiguration (Empty) returns (stream TunnelConfigurationResponse); rpc TunnelStart (Empty) returns (Empty); rpc TunnelStop (Empty) returns (Empty); rpc TunnelStatus (Empty) returns (stream TunnelStatusResponse); From 62a5739d86feb8c2d23ec3d6187d2f1c6dffc9d3 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 7 Sep 2024 17:01:17 -0700 Subject: [PATCH 123/128] Update pipelines with various fixes --- .github/actions/download-profiles/action.yml | 27 ++++++++++++ .github/workflows/build-appimage.yml | 3 ++ .github/workflows/build-docker.yml | 3 ++ .github/workflows/build-rust.yml | 2 +- .github/workflows/lint-swift.yml | 2 +- .github/workflows/release-appimage.yml | 29 ------------- .github/workflows/release-apple.yml | 5 ++- .github/workflows/release-if-needed.yaml | 2 + .github/workflows/release-linux.yml | 43 +++++++++----------- 9 files changed, 59 insertions(+), 57 deletions(-) create mode 100644 .github/actions/download-profiles/action.yml delete mode 100644 .github/workflows/release-appimage.yml diff --git a/.github/actions/download-profiles/action.yml b/.github/actions/download-profiles/action.yml new file mode 100644 index 0000000..98961aa --- /dev/null +++ b/.github/actions/download-profiles/action.yml @@ -0,0 +1,27 @@ +name: Download Provisioning Profiles +inputs: + app-store-key: + description: App Store key in PEM PKCS#8 format + required: true + app-store-key-id: + description: App Store key ID + required: true + app-store-key-issuer-id: + description: App Store key issuer ID + required: true +runs: + using: composite + steps: + - shell: bash + run: | + cat << EOF > api-key.json + { + "key_id": "${{ inputs.app-store-key-id }}", + "issuer_id": "${{ inputs.app-store-key-issuer-id }}", + "key": "${{ inputs.app-store-key }}" + } + EOF + + fastlane sigh download_all --api_key_path api-key.json --download_xcode_profiles + + rm -rf api-key.json diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index bb510fb..bd29b07 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -6,6 +6,9 @@ on: pull_request: branches: - "*" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: appimage: name: Build AppImage diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 307a93c..6a3dae1 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -6,6 +6,9 @@ on: pull_request: branches: - "*" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: build: name: Build Docker Image diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 76ce9f2..11ff60d 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -58,7 +58,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y ${{ join(matrix.packages, ' ') }} - - name: Install Windows Deps + - name: Configure LLVM if: matrix.os == 'windows-2022' shell: bash run: echo "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\Llvm\x64\bin" >> $GITHUB_PATH diff --git a/.github/workflows/lint-swift.yml b/.github/workflows/lint-swift.yml index a2cc96a..857f575 100644 --- a/.github/workflows/lint-swift.yml +++ b/.github/workflows/lint-swift.yml @@ -13,4 +13,4 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Lint - run: swiftlint lint --reporter github-actions-logging + run: swiftlint lint --strict --reporter github-actions-logging diff --git a/.github/workflows/release-appimage.yml b/.github/workflows/release-appimage.yml deleted file mode 100644 index e566186..0000000 --- a/.github/workflows/release-appimage.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Release (AppImage) -on: - release: - types: - - created -jobs: - appimage: - name: Build AppImage - runs-on: ubuntu-latest - container: docker - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Build - run: | - docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile - docker create --name temp appimage-builder - docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . - docker rm temp - - name: Upload to GitHub - uses: SierraSoftworks/gh-releases@v1.0.7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - release_tag: ${{ github.ref_name }} - overwrite: 'true' - files: | - Burrow-x86_64.AppImage diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index 1883008..f1ee5dd 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -24,7 +24,7 @@ jobs: - x86_64-apple-darwin - aarch64-apple-darwin env: - DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - name: Checkout uses: actions/checkout@v4 @@ -40,8 +40,9 @@ jobs: with: targets: ${{ join(matrix.rust-targets, ', ') }} - name: Configure Version + id: version shell: bash - run: Tools/version.sh + run: echo "BUILD_NUMBER=$(Tools/version.sh)" >> $GITHUB_OUTPUT - name: Archive uses: ./.github/actions/archive with: diff --git a/.github/workflows/release-if-needed.yaml b/.github/workflows/release-if-needed.yaml index 0d2eb97..79f0d63 100644 --- a/.github/workflows/release-if-needed.yaml +++ b/.github/workflows/release-if-needed.yaml @@ -9,6 +9,8 @@ jobs: create: name: Create Release If Needed runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 6709edb..7db9bcf 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -2,33 +2,28 @@ name: Release (Linux) on: release: types: - - created + - created jobs: appimage: name: Build AppImage runs-on: ubuntu-latest container: docker steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Build AppImage - run: | - docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile - docker create --name temp appimage-builder - docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . - docker rm temp - - name: Get Build Number - id: version - shell: bash - run: | - echo "BUILD_NUMBER=$(Tools/version.sh)" >> $GITHUB_OUTPUT - - name: Attach Artifacts - uses: SierraSoftworks/gh-releases@v1.0.7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - release_tag: builds/${{ steps.version.outputs.BUILD_NUMBER }} - overwrite: "true" - files: | - Burrow-x86_64.AppImage + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build AppImage + run: | + docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile + docker create --name temp appimage-builder + docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . + docker rm temp + - name: Attach Artifacts + uses: SierraSoftworks/gh-releases@v1.0.7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release_tag: ${{ github.ref_name }} + overwrite: "true" + files: | + Burrow-x86_64.AppImage From fa1ef6fcda7acf4f9a0cf66d811baf1e626ac2a4 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 7 Sep 2024 17:08:02 -0700 Subject: [PATCH 124/128] Download provisioning profiles in release pipeline --- .github/actions/download-profiles/action.yml | 7 +++++-- .github/actions/export/action.yml | 10 +++------- .github/workflows/build-rust.yml | 2 +- .github/workflows/release-apple.yml | 21 ++++++++++++-------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/actions/download-profiles/action.yml b/.github/actions/download-profiles/action.yml index 98961aa..32b615c 100644 --- a/.github/actions/download-profiles/action.yml +++ b/.github/actions/download-profiles/action.yml @@ -13,15 +13,18 @@ runs: using: composite steps: - shell: bash + env: + FASTLANE_OPT_OUT_USAGE: 'YES' run: | + APP_STORE_KEY=$(echo "${{ inputs.app-store-key }}" | jq -sR .) cat << EOF > api-key.json { "key_id": "${{ inputs.app-store-key-id }}", "issuer_id": "${{ inputs.app-store-key-issuer-id }}", - "key": "${{ inputs.app-store-key }}" + "key": $APP_STORE_KEY } EOF - fastlane sigh download_all --api_key_path api-key.json --download_xcode_profiles + fastlane sigh download_all --api_key_path api-key.json rm -rf api-key.json diff --git a/.github/actions/export/action.yml b/.github/actions/export/action.yml index 8f891be..75b748f 100644 --- a/.github/actions/export/action.yml +++ b/.github/actions/export/action.yml @@ -12,11 +12,8 @@ inputs: archive-path: description: Xcode archive path required: true - destination: - description: The Xcode export destination. This can either be "export" or "upload" - required: true - method: - description: The Xcode export method. This can be one of app-store, validation, ad-hoc, package, enterprise, development, developer-id, or mac-application. + export-options: + description: The export options in JSON format required: true export-path: description: The path to export the archive to @@ -29,8 +26,7 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - echo '{"destination":"${{ inputs.destination }}","method":"${{ inputs.method }}"}' \ - | plutil -convert xml1 -o ExportOptions.plist - + echo '${{ inputs.export-options }}' | plutil -convert xml1 -o ExportOptions.plist - xcodebuild \ -exportArchive \ diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 11ff60d..22bf83a 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -42,7 +42,7 @@ jobs: - aarch64-pc-windows-msvc runs-on: ${{ matrix.os }} env: - DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer CARGO_INCREMENTAL: 0 CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc RUST_BACKTRACE: short diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index f1ee5dd..bb9c15a 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -13,13 +13,10 @@ jobs: fail-fast: false matrix: include: - - - destination: generic/platform=iOS - platform: iOS + - platform: iOS rust-targets: - aarch64-apple-ios - - destination: generic/platform=macOS - platform: macOS + - platform: macOS rust-targets: - x86_64-apple-darwin - aarch64-apple-darwin @@ -35,6 +32,12 @@ jobs: with: certificate: ${{ secrets.DEVELOPER_CERT }} password: ${{ secrets.DEVELOPER_CERT_PASSWORD }} + - name: Download Provisioning Profiles + uses: ./.github/actions/download-profiles + with: + app-store-key: ${{ secrets.APPSTORE_KEY }} + app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} + app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} - name: Install Rust uses: dtolnay/rust-toolchain@stable with: @@ -47,7 +50,7 @@ jobs: uses: ./.github/actions/archive with: scheme: App - destination: ${{ matrix.destination }} + destination: generic/platform=${{ matrix.platform }} app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} @@ -61,6 +64,8 @@ jobs: app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive + export-options: | + {"teamID":"P6PV2R9443","destination":"export","method":"developer-id","provisioningProfiles":{"com.hackclub.burrow":"Burrow Developer ID","com.hackclub.burrow.network":"Burrow Network Developer ID"},"signingCertificate":"Developer ID Application","signingStyle":"manual"} export-path: Release - name: Notarize if: ${{ matrix.platform == 'macOS' }} @@ -96,10 +101,10 @@ jobs: if: ${{ matrix.platform == 'iOS' }} uses: ./.github/actions/export with: - method: app-store - destination: upload app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive + export-options: | + {"method": "app-store", "destination": "upload"} export-path: Release From 3fbb520a106101761ca3cff49ce62029a88408fa Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 7 Sep 2024 17:36:48 -0700 Subject: [PATCH 125/128] Fix SwiftLint errors --- .github/actions/build-for-testing/action.yml | 2 + .github/workflows/build-rust.yml | 6 ++- Apple/App/AppDelegate.swift | 3 +- Apple/App/BurrowView.swift | 1 - Apple/App/OAuth2.swift | 46 +++++++++++--------- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index 084ba81..185c4ab 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -27,7 +27,9 @@ runs: Apple/DerivedData key: ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }} restore-keys: | + ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }} ${{ runner.os }}-${{ inputs.scheme }}- + ${{ runner.os }}- - name: Build shell: bash working-directory: Apple diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 22bf83a..84ac9d8 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -21,14 +21,16 @@ jobs: - x86_64-unknown-linux-gnu targets: - aarch64-unknown-linux-gnu - - os: macos-12 + - os: macos-13 platform: macOS (Intel) + xcode: /Applications/Xcode_15.2.app test-targets: - x86_64-apple-darwin targets: - x86_64-apple-ios - os: macos-14 platform: macOS + xcode: /Applications/Xcode_16.0.app test-targets: - aarch64-apple-darwin targets: @@ -42,7 +44,7 @@ jobs: - aarch64-pc-windows-msvc runs-on: ${{ matrix.os }} env: - DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer + DEVELOPER_DIR: ${{ matrix.xcode }}/Contents/Developer CARGO_INCREMENTAL: 0 CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc RUST_BACKTRACE: short diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index bd76a2f..b0c5546 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -2,7 +2,8 @@ import AppKit import SwiftUI -@MainActor @main +@main +@MainActor class AppDelegate: NSObject, NSApplicationDelegate { private let quitItem: NSMenuItem = { let quitItem = NSMenuItem( diff --git a/Apple/App/BurrowView.swift b/Apple/App/BurrowView.swift index 8447592..3a53762 100644 --- a/Apple/App/BurrowView.swift +++ b/Apple/App/BurrowView.swift @@ -42,7 +42,6 @@ struct BurrowView: View { } private func addWireGuardNetwork() { - } private func authenticateWithSlack() async throws { diff --git a/Apple/App/OAuth2.swift b/Apple/App/OAuth2.swift index dc8c62b..9a930c9 100644 --- a/Apple/App/OAuth2.swift +++ b/Apple/App/OAuth2.swift @@ -1,6 +1,6 @@ import AuthenticationServices -import SwiftUI import Foundation +import SwiftUI enum OAuth2 { enum Error: Swift.Error { @@ -35,7 +35,7 @@ enum OAuth2 { } } - public init( + init( authorizationEndpoint: URL, tokenEndpoint: URL, redirectURI: URL, @@ -125,7 +125,11 @@ enum OAuth2 { var refreshToken: String? var credential: Credential { - .init(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expiresIn.map { Date.init(timeIntervalSinceNow: $0) }) + .init( + accessToken: accessToken, + refreshToken: refreshToken, + expirationDate: expiresIn.map { Date(timeIntervalSinceNow: $0) } + ) } } @@ -203,7 +207,24 @@ enum OAuth2 { } extension WebAuthenticationSession { - func start(url: URL, redirectURI: URL) async throws -> URL { +#if canImport(BrowserEngineKit) + @available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) + fileprivate static func callback(for redirectURI: URL) throws -> ASWebAuthenticationSession.Callback { + switch redirectURI.scheme { + case "https": + guard let host = redirectURI.host else { throw OAuth2.Error.invalidRedirectURI } + return .https(host: host, path: redirectURI.path) + case "http": + throw OAuth2.Error.invalidRedirectURI + case .some(let scheme): + return .customScheme(scheme) + case .none: + throw OAuth2.Error.invalidRedirectURI + } + } +#endif + + fileprivate func start(url: URL, redirectURI: URL) async throws -> URL { #if canImport(BrowserEngineKit) if #available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) { return try await authenticate( @@ -231,23 +252,6 @@ extension WebAuthenticationSession { return url } } - - #if canImport(BrowserEngineKit) - @available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) - fileprivate static func callback(for redirectURI: URL) throws -> ASWebAuthenticationSession.Callback { - switch redirectURI.scheme { - case "https": - guard let host = redirectURI.host else { throw OAuth2.Error.invalidRedirectURI } - return .https(host: host, path: redirectURI.path) - case "http": - throw OAuth2.Error.invalidRedirectURI - case .some(let scheme): - return .customScheme(scheme) - case .none: - throw OAuth2.Error.invalidRedirectURI - } - } - #endif } extension View { From e4b0f1660bff2112d0e20316c228926bed47f270 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sat, 13 Jul 2024 17:32:49 -0700 Subject: [PATCH 126/128] GRPC Server Support - Deprecates old json-rpc system - Add GRPC daemon over uds --- .github/workflows/build-apple.yml | 9 +- .github/workflows/build-rust.yml | 7 +- .gitignore | 5 + .vscode/settings.json | 9 +- .../NetworkExtension/libburrow/build-rust.sh | 2 + Cargo.lock | 341 +++++++++++++++++- Dockerfile | 2 +- Makefile | 6 + burrow/Cargo.toml | 18 +- burrow/build.rs | 4 + burrow/burrow.db | Bin 20480 -> 0 bytes burrow/src/auth/server/db.rs | 2 + burrow/src/daemon/instance.rs | 300 ++++++++++----- burrow/src/daemon/mod.rs | 72 ++-- burrow/src/daemon/net/mod.rs | 9 +- burrow/src/daemon/net/unix.rs | 4 +- burrow/src/daemon/rpc/client.rs | 31 ++ burrow/src/daemon/rpc/grpc_defs.rs | 5 + burrow/src/daemon/rpc/mod.rs | 3 + burrow/src/database.rs | 109 +++++- burrow/src/main.rs | 156 +++++++- burrow/src/wireguard/config.rs | 94 ++++- burrow/src/wireguard/iface.rs | 14 +- burrow/src/wireguard/inifield.rs | 81 +++++ burrow/src/wireguard/mod.rs | 1 + ...guard__config__tests__tst_config_toml.snap | 16 + burrow/tmp/conrd.conf | 8 + proto/burrow.proto | 2 +- 28 files changed, 1110 insertions(+), 200 deletions(-) create mode 100644 burrow/build.rs delete mode 100644 burrow/burrow.db create mode 100644 burrow/src/daemon/rpc/client.rs create mode 100644 burrow/src/daemon/rpc/grpc_defs.rs create mode 100644 burrow/src/wireguard/inifield.rs create mode 100644 burrow/src/wireguard/snapshots/burrow__wireguard__config__tests__tst_config_toml.snap create mode 100644 burrow/tmp/conrd.conf diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 84cc03a..b628001 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -1,7 +1,7 @@ name: Build Apple Apps on: push: - branches: + branches: - main pull_request: branches: @@ -39,6 +39,7 @@ jobs: - aarch64-apple-darwin env: DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer + PROTOC_VERSION: 3.25.1 steps: - name: Checkout uses: actions/checkout@v3 @@ -54,6 +55,10 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: ${{ join(matrix.rust-targets, ', ') }} + - name: Install protoc + uses: taiki-e/install-action@v2 + with: + tool: protoc@${{ env.PROTOC_VERSION }} - name: Build id: build uses: ./.github/actions/build-for-testing @@ -82,4 +87,4 @@ jobs: destination: ${{ matrix.destination }} test-plan: ${{ matrix.xcode-ui-test }} artifact-prefix: ui-tests-${{ matrix.sdk-name }} - check-name: Xcode UI Tests (${{ matrix.platform }}) + check-name: Xcode UI Tests (${{ matrix.platform }}) \ No newline at end of file diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 84ac9d8..95fc628 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -48,6 +48,7 @@ jobs: CARGO_INCREMENTAL: 0 CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc RUST_BACKTRACE: short + PROTOC_VERSION: 3.25.1 steps: - name: Checkout uses: actions/checkout@v3 @@ -64,6 +65,10 @@ jobs: if: matrix.os == 'windows-2022' shell: bash run: echo "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\Llvm\x64\bin" >> $GITHUB_PATH + - name: Install protoc + uses: taiki-e/install-action@v2 + with: + tool: protoc@${{ env.PROTOC_VERSION }} - name: Install Rust uses: dtolnay/rust-toolchain@stable with: @@ -77,4 +82,4 @@ jobs: run: cargo build --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} --target ${{ join(matrix.test-targets, ' --target ') }} - name: Test shell: bash - run: cargo test --verbose --workspace --all-features --target ${{ join(matrix.test-targets, ' --target ') }} + run: cargo test --verbose --workspace --all-features --target ${{ join(matrix.test-targets, ' --target ') }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 96b2507..997d4d5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,8 @@ target/ .DS_STORE .idea/ + +tmp/ + +*.db +*.sock \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a760137..eb85504 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,12 @@ "rust-analyzer.inlayHints.typeHints.enable": false, "rust-analyzer.linkedProjects": [ "./burrow/Cargo.toml" - ] + ], + "[yaml]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.autoIndent": "advanced", + "diffEditor.ignoreTrimWhitespace": false, + "editor.formatOnSave": false + } } diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index e7204a5..00c3652 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -68,6 +68,8 @@ else CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin" fi +CARGO_PATH="$(dirname $(readlink -f $(which protoc))):$CARGO_PATH" + # Run cargo without the various environment variables set by Xcode. # Those variables can confuse cargo and the build scripts it runs. env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}" diff --git a/Cargo.lock b/Cargo.lock index 5ef886c..309fc08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,17 +132,38 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-stream" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5" +dependencies = [ + "async-stream-impl 0.2.1", + "futures-core", +] + [[package]] name = "async-stream" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ - "async-stream-impl", + "async-stream-impl 0.3.5", "futures-core", "pin-project-lite", ] +[[package]] +name = "async-stream-impl" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "async-stream-impl" version = "0.3.5" @@ -165,6 +186,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.1.0" @@ -392,6 +419,8 @@ dependencies = [ "aead", "anyhow", "async-channel", + "async-stream 0.2.1", + "async-stream 0.2.1", "axum 0.7.5", "base64 0.21.7", "blake2", @@ -404,6 +433,7 @@ dependencies = [ "fehler", "futures", "hmac", + "hyper-util", "insta", "ip_network", "ip_network_table", @@ -412,15 +442,24 @@ dependencies = [ "nix 0.27.1", "once_cell", "parking_lot", + "prost 0.13.1", + "prost-types 0.13.1", + "prost 0.13.2", + "prost-types 0.13.2", "rand", "rand_core", "reqwest 0.12.5", "ring", "rusqlite", + "rust-ini", "schemars", "serde", "serde_json", "tokio", + "tokio-stream", + "tonic 0.12.2", + "tonic-build", + "tower", "tracing", "tracing-journald", "tracing-log 0.1.4", @@ -619,9 +658,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" dependencies = [ "futures-core", - "prost", - "prost-types", - "tonic", + "prost 0.12.3", + "prost-types 0.12.3", + "tonic 0.10.2", "tracing-core", ] @@ -637,18 +676,38 @@ dependencies = [ "futures-task", "hdrhistogram", "humantime", - "prost-types", + "prost-types 0.12.3", "serde", "serde_json", "thread_local", "tokio", "tokio-stream", - "tonic", + "tonic 0.10.2", "tracing", "tracing-core", "tracing-subscriber", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -704,6 +763,12 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -762,6 +827,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -876,6 +950,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.28" @@ -1057,6 +1137,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1215,7 +1314,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.24", "http 0.2.11", "http-body 0.4.6", "httparse", @@ -1238,6 +1337,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -1279,6 +1379,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.4.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1615,6 +1728,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + [[package]] name = "native-tls" version = "0.2.11" @@ -1762,6 +1881,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.3", +] + [[package]] name = "overload" version = "0.1.1" @@ -1832,6 +1961,16 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.1.0", +] + [[package]] name = "pin-project" version = "1.1.4" @@ -1925,7 +2064,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.12.3", +] + +[[package]] +name = "prost" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +dependencies = [ + "bytes", + "prost-derive 0.13.2", +] + +[[package]] +name = "prost-build" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" +dependencies = [ + "bytes", + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.13.2", + "prost-types 0.13.2", + "regex", + "syn 2.0.48", + "tempfile", ] [[package]] @@ -1941,13 +2111,35 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "prost-derive" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "prost-types" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ - "prost", + "prost 0.12.3", +] + +[[package]] +name = "prost-types" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" +dependencies = [ + "prost 0.13.2", ] [[package]] @@ -2100,7 +2292,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.24", "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", @@ -2197,6 +2389,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d625ed57d8f49af6cfa514c42e1a71fadcff60eb0b1c517ff82fe41aa025b41" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2404,6 +2607,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2654,6 +2866,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2755,25 +2976,59 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ - "async-stream", + "async-stream 0.3.5", "async-trait", "axum 0.6.20", "base64 0.21.7", "bytes", - "h2", + "h2 0.3.24", "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", - "hyper-timeout", + "hyper-timeout 0.4.1", "percent-encoding", "pin-project", - "prost", + "prost 0.12.3", "tokio", "tokio-stream", "tower", @@ -2782,6 +3037,49 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f6ba989e4b2c58ae83d862d3a3e27690b6e3ae630d0deb59f3697f32aa88ad" +dependencies = [ + "async-stream 0.3.5", + "async-trait", + "axum 0.7.5", + "base64 0.22.1", + "bytes", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.4.0", + "hyper-timeout 0.5.1", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.2", + "socket2", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4ee8877250136bd7e3d2331632810a4df4ea5e004656990d8d66d2f5ee8a67" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn 2.0.48", +] + [[package]] name = "tower" version = "0.4.13" @@ -2913,6 +3211,12 @@ dependencies = [ "tracing-log 0.2.0", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "try-lock" version = "0.2.5" @@ -3320,6 +3624,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Dockerfile b/Dockerfile index 8e17812..404179b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN set -eux && \ curl --proto '=https' --tlsv1.2 -sSf https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor --output $KEYRINGS/llvm.gpg && \ echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-$LLVM_VERSION main" > /etc/apt/sources.list.d/llvm.list && \ apt-get update && \ - apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION build-essential sqlite3 libsqlite3-dev musl musl-tools musl-dev && \ + apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION build-essential sqlite3 libsqlite3-dev musl musl-tools musl-dev protobuf-compiler libprotobuf-dev && \ ln -s clang-$LLVM_VERSION /usr/bin/clang && \ ln -s clang /usr/bin/clang++ && \ ln -s lld-$LLVM_VERSION /usr/bin/ld.lld && \ diff --git a/Makefile b/Makefile index d0c9bd9..6563ab1 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,12 @@ start: stop: @$(cargo_norm) stop +status: + @$(cargo_norm) server-status + +tunnel-config: + @$(cargo_norm) tunnel-config + test-dns: @sudo route delete 8.8.8.8 @sudo route add 8.8.8.8 -interface $(tun) diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 0fb63a5..d5e56c1 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -19,6 +19,7 @@ tokio = { version = "1.37", features = [ "signal", "time", "tracing", + "fs", ] } tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } clap = { version = "4.4", features = ["derive"] } @@ -56,8 +57,17 @@ reqwest = { version = "0.12", default-features = false, features = [ "json", "rustls-tls", ] } -rusqlite = "0.31.0" +rusqlite = { version = "0.31.0", features = ["blob"] } dotenv = "0.15.0" +tonic = "0.12.0" +prost = "0.13.1" +prost-types = "0.13.1" +tokio-stream = "0.1" +async-stream = "0.2" +tower = "0.4.13" +hyper-util = "0.1.6" +toml = "0.8.15" +rust-ini = "0.21.0" [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5" @@ -66,7 +76,7 @@ tracing-journald = "0.3" [target.'cfg(target_vendor = "apple")'.dependencies] nix = { version = "0.27" } -rusqlite = { version = "0.31.0", features = ["bundled"] } +rusqlite = { version = "0.31.0", features = ["bundled", "blob"] } [dev-dependencies] insta = { version = "1.32", features = ["yaml"] } @@ -83,3 +93,7 @@ pre_uninstall_script = "../package/rpm/pre_uninstall" [features] tokio-console = ["dep:console-subscriber"] bundled = ["rusqlite/bundled"] + + +[build-dependencies] +tonic-build = "0.12.0" diff --git a/burrow/build.rs b/burrow/build.rs new file mode 100644 index 0000000..8eea5dc --- /dev/null +++ b/burrow/build.rs @@ -0,0 +1,4 @@ +fn main() -> Result<(), Box> { + tonic_build::compile_protos("../proto/burrow.proto")?; + Ok(()) +} diff --git a/burrow/burrow.db b/burrow/burrow.db deleted file mode 100644 index c5b6e2c614ecb4db4c264f50691b6f2cea99772a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmWFz^vNtqRY=P(%1ta$FlG>7U}R))P*7lCU=U|uU|?lH0A>aT1{MUDff0#~iz&{a zSJuG`(#R{q!1sc08t)Wd5nPH##YaP6Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONfZicc z$HFcyEzQ^%T#}fSlbV-WQl4Lw4W(F}gIpa$TopnboqSvspn?h-TnbQ-nOBlpl$MyB z8lRb>;OQ5l5ajCS8szHd>>8|4o*oaE*2qlJRPgsx2n}!n8RzU6?Cj{`3N}Wwv7Q<1 zfaXxJ1Ip9m3sO^ypcD&=1E7LbbAS%m1t7nq=A{(mXXceCgt$h8DERq@DENi?_#os9 zN|SOjljE~fD{-kv%*n|wPfdx>EGWjMq@XCZI3uwrH3e=C*nZ6bCN^HWN82kl9QqrXkB9 z2QfHiUEN)S6as=geI0`$6}(*|6&yoD{5}1ggIs-G{X!4{1#$w|{|KR+%;J*Ny!e9r zq7qOV0hxr5%q=O!6f7vpEK4j&g$EOs2uVyyDM~HI8Pq9xXi|`n2KCLkcwHFyck!3- z>+!wdTf`T`C&qh$w~N<>-uZ6SzR?gE4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R80;b7 z!o|VBz|4@U&dYEr$KN#|EG0iT)hDDP#M7y)%s3>n*efVK)xe{`($6khIH_U^2USdAr-~_TR567W$rN|LLQhA3=b(zL9R1|XAY71JH1mFA{7sYRJyiIoQ0`5rzQLB0iH+K#3bj)4Xl&R*Ju=HcZQ zhK~MaAtqSUX|Z`ug??_jc2R1Wt8borUSVowq<>&`m5YU0o{@HXWL|}#uVrO=rh!Ga zZ6hlS#2qXH?G9#$JD3OB9ZV2+Fb%LfSQyzjLFL%MIs?@IXAq!ac|B_MXb6mkz-S1J ihQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2B~hX4R^(?&G_ diff --git a/burrow/src/auth/server/db.rs b/burrow/src/auth/server/db.rs index b74f7ce..995e64b 100644 --- a/burrow/src/auth/server/db.rs +++ b/burrow/src/auth/server/db.rs @@ -1,5 +1,7 @@ use anyhow::Result; +use crate::daemon::rpc::grpc_defs::{Network, NetworkType}; + pub static PATH: &str = "./server.sqlite3"; pub fn init_db() -> Result<()> { diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index bc506bd..ce96fa5 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -1,13 +1,30 @@ use std::{ + ops::Deref, path::{Path, PathBuf}, sync::Arc, + time::Duration, }; use anyhow::Result; -use tokio::{sync::RwLock, task::JoinHandle}; +use rusqlite::Connection; +use tokio::sync::{mpsc, watch, Notify, RwLock}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::{Request, Response, Status as RspStatus}; use tracing::{debug, info, warn}; -use tun::tokio::TunInterface; +use tun::{tokio::TunInterface, TunOptions}; +use super::rpc::grpc_defs::{ + networks_server::Networks, + tunnel_server::Tunnel, + Empty, + Network, + NetworkDeleteRequest, + NetworkListResponse, + NetworkReorderRequest, + State as RPCTunnelState, + TunnelConfigurationResponse, + TunnelStatusResponse, +}; use crate::{ daemon::rpc::{ DaemonCommand, @@ -17,114 +34,223 @@ use crate::{ ServerConfig, ServerInfo, }, - database::{get_connection, load_interface}, + database::{ + add_network, + delete_network, + get_connection, + list_networks, + load_interface, + reorder_network, + }, wireguard::{Config, Interface}, }; +#[derive(Debug, Clone)] enum RunState { - Running(JoinHandle>), + Running, Idle, } -pub struct DaemonInstance { - rx: async_channel::Receiver, - sx: async_channel::Sender, - subx: async_channel::Sender, +impl RunState { + pub fn to_rpc(&self) -> RPCTunnelState { + match self { + RunState::Running => RPCTunnelState::Running, + RunState::Idle => RPCTunnelState::Stopped, + } + } +} + +#[derive(Clone)] +pub struct DaemonRPCServer { tun_interface: Arc>>, wg_interface: Arc>, config: Arc>, db_path: Option, - wg_state: RunState, + wg_state_chan: (watch::Sender, watch::Receiver), + network_update_chan: (watch::Sender<()>, watch::Receiver<()>), } -impl DaemonInstance { +impl DaemonRPCServer { pub fn new( - rx: async_channel::Receiver, - sx: async_channel::Sender, - subx: async_channel::Sender, wg_interface: Arc>, config: Arc>, db_path: Option<&Path>, - ) -> Self { - Self { - rx, - sx, - subx, - wg_interface, + ) -> Result { + Ok(Self { tun_interface: Arc::new(RwLock::new(None)), + wg_interface, config, db_path: db_path.map(|p| p.to_owned()), - wg_state: RunState::Idle, - } + wg_state_chan: watch::channel(RunState::Idle), + network_update_chan: watch::channel(()), + }) } - async fn proc_command(&mut self, command: DaemonCommand) -> Result { - info!("Daemon got command: {:?}", command); - match command { - DaemonCommand::Start(st) => { - match self.wg_state { - RunState::Running(_) => { - warn!("Got start, but tun interface already up."); - } - RunState::Idle => { - let tun_if = st.tun.open()?; - debug!("Setting tun on wg_interface"); - self.wg_interface.read().await.set_tun(tun_if).await; - debug!("tun set on wg_interface"); - - debug!("Setting tun_interface"); - self.tun_interface = self.wg_interface.read().await.get_tun(); - debug!("tun_interface set: {:?}", self.tun_interface); - - debug!("Cloning wg_interface"); - let tmp_wg = self.wg_interface.clone(); - let run_task = tokio::spawn(async move { - let twlock = tmp_wg.read().await; - twlock.run().await - }); - self.wg_state = RunState::Running(run_task); - info!("Daemon started tun interface"); - } - } - Ok(DaemonResponseData::None) - } - DaemonCommand::ServerInfo => match &self.tun_interface.read().await.as_ref() { - None => Ok(DaemonResponseData::None), - Some(ti) => { - info!("{:?}", ti); - Ok(DaemonResponseData::ServerInfo(ServerInfo::try_from( - ti.inner.get_ref(), - )?)) - } - }, - DaemonCommand::Stop => { - self.wg_interface.read().await.remove_tun().await; - self.wg_state = RunState::Idle; - Ok(DaemonResponseData::None) - } - DaemonCommand::ServerConfig => { - Ok(DaemonResponseData::ServerConfig(ServerConfig::default())) - } - DaemonCommand::ReloadConfig(interface_id) => { - let conn = get_connection(self.db_path.as_deref())?; - let cfig = load_interface(&conn, &interface_id)?; - *self.config.write().await = cfig; - self.subx - .send(DaemonNotification::ConfigChange(ServerConfig::try_from( - &self.config.read().await.to_owned(), - )?)) - .await?; - Ok(DaemonResponseData::None) - } - } + pub fn get_connection(&self) -> Result { + get_connection(self.db_path.as_deref()).map_err(proc_err) } - pub async fn run(&mut self) -> Result<()> { - while let Ok(command) = self.rx.recv().await { - let response = self.proc_command(command).await; - info!("Daemon response: {:?}", response); - self.sx.send(DaemonResponse::new(response)).await?; - } - Ok(()) + async fn set_wg_state(&self, state: RunState) -> Result<(), RspStatus> { + self.wg_state_chan.0.send(state).map_err(proc_err) + } + + async fn get_wg_state(&self) -> RunState { + self.wg_state_chan.1.borrow().to_owned() + } + + async fn notify_network_update(&self) -> Result<(), RspStatus> { + self.network_update_chan.0.send(()).map_err(proc_err) + } +} + +#[tonic::async_trait] +impl Tunnel for DaemonRPCServer { + type TunnelConfigurationStream = ReceiverStream>; + type TunnelStatusStream = ReceiverStream>; + + async fn tunnel_configuration( + &self, + _request: Request, + ) -> Result, RspStatus> { + let (tx, rx) = mpsc::channel(10); + tokio::spawn(async move { + let serv_config = ServerConfig::default(); + tx.send(Ok(TunnelConfigurationResponse { + mtu: serv_config.mtu.unwrap_or(1000), + addresses: serv_config.address, + })) + .await + }); + Ok(Response::new(ReceiverStream::new(rx))) + } + + async fn tunnel_start(&self, _request: Request) -> Result, RspStatus> { + let wg_state = self.get_wg_state().await; + match wg_state { + RunState::Idle => { + let tun_if = TunOptions::new().open()?; + debug!("Setting tun on wg_interface"); + self.tun_interface.write().await.replace(tun_if); + self.wg_interface + .write() + .await + .set_tun_ref(self.tun_interface.clone()) + .await; + debug!("tun set on wg_interface"); + + debug!("Setting tun_interface"); + debug!("tun_interface set: {:?}", self.tun_interface); + + debug!("Cloning wg_interface"); + let tmp_wg = self.wg_interface.clone(); + let run_task = tokio::spawn(async move { + let twlock = tmp_wg.read().await; + twlock.run().await + }); + self.set_wg_state(RunState::Running).await?; + } + + RunState::Running => { + warn!("Got start, but tun interface already up."); + } + } + + return Ok(Response::new(Empty {})); + } + + async fn tunnel_stop(&self, _request: Request) -> Result, RspStatus> { + self.wg_interface.write().await.remove_tun().await; + self.set_wg_state(RunState::Idle).await?; + return Ok(Response::new(Empty {})); + } + + async fn tunnel_status( + &self, + _request: Request, + ) -> Result, RspStatus> { + let (tx, rx) = mpsc::channel(10); + let mut state_rx = self.wg_state_chan.1.clone(); + tokio::spawn(async move { + let cur = state_rx.borrow_and_update().to_owned(); + tx.send(Ok(status_rsp(cur))).await; + loop { + state_rx.changed().await.unwrap(); + let cur = state_rx.borrow().to_owned(); + let res = tx.send(Ok(status_rsp(cur))).await; + if res.is_err() { + eprintln!("Tunnel status channel closed"); + break; + } + } + }); + Ok(Response::new(ReceiverStream::new(rx))) + } +} + +#[tonic::async_trait] +impl Networks for DaemonRPCServer { + type NetworkListStream = ReceiverStream>; + + async fn network_add(&self, request: Request) -> Result, RspStatus> { + let conn = self.get_connection()?; + let network = request.into_inner(); + add_network(&conn, &network).map_err(proc_err)?; + self.notify_network_update().await?; + Ok(Response::new(Empty {})) + } + + async fn network_list( + &self, + _request: Request, + ) -> Result, RspStatus> { + debug!("Mock network_list called"); + let (tx, rx) = mpsc::channel(10); + let conn = self.get_connection()?; + let mut sub = self.network_update_chan.1.clone(); + tokio::spawn(async move { + loop { + let networks = list_networks(&conn) + .map(|res| NetworkListResponse { network: res }) + .map_err(proc_err); + let res = tx.send(networks).await; + if res.is_err() { + eprintln!("Network list channel closed"); + break; + } + sub.changed().await.unwrap(); + } + }); + Ok(Response::new(ReceiverStream::new(rx))) + } + + async fn network_reorder( + &self, + request: Request, + ) -> Result, RspStatus> { + let conn = self.get_connection()?; + reorder_network(&conn, request.into_inner()).map_err(proc_err)?; + self.notify_network_update().await?; + Ok(Response::new(Empty {})) + } + + async fn network_delete( + &self, + request: Request, + ) -> Result, RspStatus> { + let conn = self.get_connection()?; + delete_network(&conn, request.into_inner()).map_err(proc_err)?; + self.notify_network_update().await?; + Ok(Response::new(Empty {})) + } +} + +fn proc_err(err: impl ToString) -> RspStatus { + RspStatus::internal(err.to_string()) +} + +fn status_rsp(state: RunState) -> TunnelStatusResponse { + TunnelStatusResponse { + state: state.to_rpc().into(), + start: None, // TODO: Add timestamp } } diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs index 4469e90..f6b973f 100644 --- a/burrow/src/daemon/mod.rs +++ b/burrow/src/daemon/mod.rs @@ -5,14 +5,20 @@ mod instance; mod net; pub mod rpc; -use anyhow::Result; -use instance::DaemonInstance; -pub use net::{DaemonClient, Listener}; +use anyhow::{Error as AhError, Result}; +use instance::DaemonRPCServer; +pub use net::{get_socket_path, DaemonClient}; pub use rpc::{DaemonCommand, DaemonResponseData, DaemonStartOptions}; -use tokio::sync::{Notify, RwLock}; +use tokio::{ + net::UnixListener, + sync::{Notify, RwLock}, +}; +use tokio_stream::wrappers::UnixListenerStream; +use tonic::transport::Server; use tracing::{error, info}; use crate::{ + daemon::rpc::grpc_defs::{networks_server::NetworksServer, tunnel_server::TunnelServer}, database::{get_connection, load_interface}, wireguard::Interface, }; @@ -22,52 +28,36 @@ pub async fn daemon_main( db_path: Option<&Path>, notify_ready: Option>, ) -> Result<()> { - let (commands_tx, commands_rx) = async_channel::unbounded(); - let (response_tx, response_rx) = async_channel::unbounded(); - let (subscribe_tx, subscribe_rx) = async_channel::unbounded(); - - let listener = if let Some(path) = socket_path { - info!("Creating listener... {:?}", path); - Listener::new_with_path(commands_tx, response_rx, subscribe_rx, path) - } else { - info!("Creating listener..."); - Listener::new(commands_tx, response_rx, subscribe_rx) - }; if let Some(n) = notify_ready { n.notify_one() } - let listener = listener?; let conn = get_connection(db_path)?; let config = load_interface(&conn, "1")?; - let iface: Interface = config.clone().try_into()?; - let mut instance = DaemonInstance::new( - commands_rx, - response_tx, - subscribe_tx, - Arc::new(RwLock::new(iface)), + let burrow_server = DaemonRPCServer::new( + Arc::new(RwLock::new(config.clone().try_into()?)), Arc::new(RwLock::new(config)), - db_path, - ); + db_path.clone(), + )?; + let spp = socket_path.clone(); + let tmp = get_socket_path(); + let sock_path = spp.unwrap_or(Path::new(tmp.as_str())); + if sock_path.exists() { + std::fs::remove_file(sock_path)?; + } + let uds = UnixListener::bind(sock_path)?; + let serve_job = tokio::spawn(async move { + let uds_stream = UnixListenerStream::new(uds); + let _srv = Server::builder() + .add_service(TunnelServer::new(burrow_server.clone())) + .add_service(NetworksServer::new(burrow_server)) + .serve_with_incoming(uds_stream) + .await?; + Ok::<(), AhError>(()) + }); info!("Starting daemon..."); - let main_job = tokio::spawn(async move { - let result = instance.run().await; - if let Err(e) = result.as_ref() { - error!("Instance exited: {}", e); - } - result - }); - - let listener_job = tokio::spawn(async move { - let result = listener.run().await; - if let Err(e) = result.as_ref() { - error!("Listener exited: {}", e); - } - result - }); - - tokio::try_join!(main_job, listener_job) + tokio::try_join!(serve_job) .map(|_| ()) .map_err(|e| e.into()) } diff --git a/burrow/src/daemon/net/mod.rs b/burrow/src/daemon/net/mod.rs index 242f479..eb45335 100644 --- a/burrow/src/daemon/net/mod.rs +++ b/burrow/src/daemon/net/mod.rs @@ -1,18 +1,11 @@ - - - - - #[cfg(target_family = "unix")] mod unix; #[cfg(target_family = "unix")] -pub use unix::{DaemonClient, Listener}; +pub use unix::{get_socket_path, DaemonClient, Listener}; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] pub use windows::{DaemonClient, Listener}; - - diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs index 70c4207..975c470 100644 --- a/burrow/src/daemon/net/unix.rs +++ b/burrow/src/daemon/net/unix.rs @@ -25,7 +25,7 @@ const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; #[cfg(target_vendor = "apple")] const UNIX_SOCKET_PATH: &str = "burrow.sock"; -fn get_socket_path() -> String { +pub fn get_socket_path() -> String { if std::env::var("BURROW_SOCKET_PATH").is_ok() { return std::env::var("BURROW_SOCKET_PATH").unwrap(); } @@ -36,7 +36,7 @@ pub struct Listener { cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, sub_chan: async_channel::Receiver, - inner: UnixListener, + pub inner: UnixListener, } impl Listener { diff --git a/burrow/src/daemon/rpc/client.rs b/burrow/src/daemon/rpc/client.rs new file mode 100644 index 0000000..862e34c --- /dev/null +++ b/burrow/src/daemon/rpc/client.rs @@ -0,0 +1,31 @@ +use anyhow::Result; +use hyper_util::rt::TokioIo; +use tokio::net::UnixStream; +use tonic::transport::{Endpoint, Uri}; +use tower::service_fn; + +use super::grpc_defs::{networks_client::NetworksClient, tunnel_client::TunnelClient}; +use crate::daemon::get_socket_path; + +pub struct BurrowClient { + pub networks_client: NetworksClient, + pub tunnel_client: TunnelClient, +} + +impl BurrowClient { + #[cfg(any(target_os = "linux", target_vendor = "apple"))] + pub async fn from_uds() -> Result { + let channel = Endpoint::try_from("http://[::]:50051")? // NOTE: this is a hack(?) + .connect_with_connector(service_fn(|_: Uri| async { + let sock_path = get_socket_path(); + Ok::<_, std::io::Error>(TokioIo::new(UnixStream::connect(sock_path).await?)) + })) + .await?; + let nw_client = NetworksClient::new(channel.clone()); + let tun_client = TunnelClient::new(channel.clone()); + Ok(BurrowClient { + networks_client: nw_client, + tunnel_client: tun_client, + }) + } +} diff --git a/burrow/src/daemon/rpc/grpc_defs.rs b/burrow/src/daemon/rpc/grpc_defs.rs new file mode 100644 index 0000000..f3085ee --- /dev/null +++ b/burrow/src/daemon/rpc/grpc_defs.rs @@ -0,0 +1,5 @@ +pub use burrowgrpc::*; + +mod burrowgrpc { + tonic::include_proto!("burrow"); +} diff --git a/burrow/src/daemon/rpc/mod.rs b/burrow/src/daemon/rpc/mod.rs index 4146e71..512662c 100644 --- a/burrow/src/daemon/rpc/mod.rs +++ b/burrow/src/daemon/rpc/mod.rs @@ -1,7 +1,10 @@ +pub mod client; +pub mod grpc_defs; pub mod notification; pub mod request; pub mod response; +pub use client::BurrowClient; pub use notification::DaemonNotification; pub use request::{DaemonCommand, DaemonRequest, DaemonStartOptions}; pub use response::{DaemonResponse, DaemonResponseData, ServerConfig, ServerInfo}; diff --git a/burrow/src/database.rs b/burrow/src/database.rs index 0047b01..9a9aac3 100644 --- a/burrow/src/database.rs +++ b/burrow/src/database.rs @@ -3,7 +3,15 @@ use std::path::Path; use anyhow::Result; use rusqlite::{params, Connection}; -use crate::wireguard::config::{Config, Interface, Peer}; +use crate::{ + daemon::rpc::grpc_defs::{ + Network as RPCNetwork, + NetworkDeleteRequest, + NetworkReorderRequest, + NetworkType, + }, + wireguard::config::{Config, Interface, Peer}, +}; #[cfg(target_vendor = "apple")] const DB_PATH: &str = "burrow.db"; @@ -30,8 +38,20 @@ const CREATE_WG_PEER_TABLE: &str = "CREATE TABLE IF NOT EXISTS wg_peer ( )"; const CREATE_NETWORK_TABLE: &str = "CREATE TABLE IF NOT EXISTS network ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + payload BLOB, + idx INTEGER, interface_id INT REFERENCES wg_interface(id) ON UPDATE CASCADE -)"; +); +CREATE TRIGGER IF NOT EXISTS increment_network_idx +AFTER INSERT ON network +BEGIN + UPDATE network + SET idx = (SELECT COALESCE(MAX(idx), 0) + 1 FROM network) + WHERE id = NEW.id; +END; +"; pub fn initialize_tables(conn: &Connection) -> Result<()> { conn.execute(CREATE_WG_INTERFACE_TABLE, [])?; @@ -40,20 +60,6 @@ pub fn initialize_tables(conn: &Connection) -> Result<()> { Ok(()) } -fn parse_lst(s: &str) -> Vec { - if s.is_empty() { - return vec![]; - } - s.split(',').map(|s| s.to_string()).collect() -} - -fn to_lst(v: &Vec) -> String { - v.iter() - .map(|s| s.to_string()) - .collect::>() - .join(",") -} - pub fn load_interface(conn: &Connection, interface_id: &str) -> Result { let iface = conn.query_row( "SELECT private_key, dns, address, listen_port, mtu FROM wg_interface WHERE id = ?", @@ -99,7 +105,7 @@ pub fn dump_interface(conn: &Connection, config: &Config) -> Result<()> { cif.private_key, to_lst(&cif.dns), to_lst(&cif.address), - cif.listen_port, + cif.listen_port.unwrap_or(51820), cif.mtu ])?; let interface_id = conn.last_insert_rowid(); @@ -127,10 +133,75 @@ pub fn get_connection(path: Option<&Path>) -> Result { Ok(Connection::open(p)?) } +pub fn add_network(conn: &Connection, network: &RPCNetwork) -> Result<()> { + let mut stmt = conn.prepare("INSERT INTO network (id, type, payload) VALUES (?, ?, ?)")?; + stmt.execute(params![ + network.id, + network.r#type().as_str_name(), + &network.payload + ])?; + if network.r#type() == NetworkType::WireGuard { + let payload_str = String::from_utf8(network.payload.clone())?; + let wg_config = Config::from_content_fmt(&payload_str, "ini")?; + dump_interface(conn, &wg_config)?; + } + Ok(()) +} + +pub fn list_networks(conn: &Connection) -> Result> { + let mut stmt = conn.prepare("SELECT id, type, payload FROM network ORDER BY idx")?; + let networks: Vec = stmt + .query_map([], |row| { + println!("row: {:?}", row); + let network_id: i32 = row.get(0)?; + let network_type: String = row.get(1)?; + let network_type = NetworkType::from_str_name(network_type.as_str()) + .ok_or(rusqlite::Error::InvalidQuery)?; + let payload: Vec = row.get(2)?; + Ok(RPCNetwork { + id: network_id, + r#type: network_type.into(), + payload: payload.into(), + }) + })? + .collect::, rusqlite::Error>>()?; + Ok(networks) +} + +pub fn reorder_network(conn: &Connection, req: NetworkReorderRequest) -> Result<()> { + let mut stmt = conn.prepare("UPDATE network SET idx = ? WHERE id = ?")?; + let res = stmt.execute(params![req.index, req.id])?; + if res == 0 { + return Err(anyhow::anyhow!("No such network exists")); + } + Ok(()) +} + +pub fn delete_network(conn: &Connection, req: NetworkDeleteRequest) -> Result<()> { + let mut stmt = conn.prepare("DELETE FROM network WHERE id = ?")?; + let res = stmt.execute(params![req.id])?; + if res == 0 { + return Err(anyhow::anyhow!("No such network exists")); + } + Ok(()) +} + +fn parse_lst(s: &str) -> Vec { + if s.is_empty() { + return vec![]; + } + s.split(',').map(|s| s.to_string()).collect() +} + +fn to_lst(v: &Vec) -> String { + v.iter() + .map(|s| s.to_string()) + .collect::>() + .join(",") +} + #[cfg(test)] mod tests { - use std::path::Path; - use super::*; #[test] diff --git a/burrow/src/main.rs b/burrow/src/main.rs index ff07d4c..e87b4c9 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -11,8 +11,7 @@ mod wireguard; mod auth; #[cfg(any(target_os = "linux", target_vendor = "apple"))] -use daemon::{DaemonClient, DaemonCommand, DaemonStartOptions}; -use tun::TunOptions; +use daemon::{DaemonClient, DaemonCommand}; #[cfg(any(target_os = "linux", target_vendor = "apple"))] use crate::daemon::DaemonResponseData; @@ -20,6 +19,9 @@ use crate::daemon::DaemonResponseData; #[cfg(any(target_os = "linux", target_vendor = "apple"))] pub mod database; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +use crate::daemon::rpc::{grpc_defs::Empty, BurrowClient}; + #[derive(Parser)] #[command(name = "Burrow")] #[command(author = "Hack Club ")] @@ -52,13 +54,24 @@ enum Commands { ReloadConfig(ReloadConfigArgs), /// Authentication server AuthServer, + /// Server Status + ServerStatus, + /// Tunnel Config + TunnelConfig, + /// Add Network + NetworkAdd(NetworkAddArgs), + /// List Networks + NetworkList, + /// Reorder Network + NetworkReorder(NetworkReorderArgs), + /// Delete Network + NetworkDelete(NetworkDeleteArgs), } #[derive(Args)] struct ReloadConfigArgs { #[clap(long, short)] interface_id: String, - } #[derive(Args)] @@ -67,21 +80,132 @@ struct StartArgs {} #[derive(Args)] struct DaemonArgs {} +#[derive(Args)] +struct NetworkAddArgs { + id: i32, + network_type: i32, + payload_path: String, +} + +#[derive(Args)] +struct NetworkReorderArgs { + id: i32, + index: i32, +} + +#[derive(Args)] +struct NetworkDeleteArgs { + id: i32, +} + #[cfg(any(target_os = "linux", target_vendor = "apple"))] async fn try_start() -> Result<()> { - let mut client = DaemonClient::new().await?; - client - .send_command(DaemonCommand::Start(DaemonStartOptions { - tun: TunOptions::new().address(vec!["10.13.13.2", "::2"]), - })) - .await - .map(|_| ()) + let mut client = BurrowClient::from_uds().await?; + let res = client.tunnel_client.tunnel_start(Empty {}).await?; + println!("Got results! {:?}", res); + Ok(()) } #[cfg(any(target_os = "linux", target_vendor = "apple"))] async fn try_stop() -> Result<()> { - let mut client = DaemonClient::new().await?; - client.send_command(DaemonCommand::Stop).await?; + let mut client = BurrowClient::from_uds().await?; + let res = client.tunnel_client.tunnel_stop(Empty {}).await?; + println!("Got results! {:?}", res); + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_serverstatus() -> Result<()> { + let mut client = BurrowClient::from_uds().await?; + let mut res = client + .tunnel_client + .tunnel_status(Empty {}) + .await? + .into_inner(); + if let Some(st) = res.message().await? { + println!("Server Status: {:?}", st); + } else { + println!("Server Status is None"); + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_tun_config() -> Result<()> { + let mut client = BurrowClient::from_uds().await?; + let mut res = client + .tunnel_client + .tunnel_configuration(Empty {}) + .await? + .into_inner(); + if let Some(config) = res.message().await? { + println!("Tunnel Config: {:?}", config); + } else { + println!("Tunnel Config is None"); + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_network_add(id: i32, network_type: i32, payload_path: &str) -> Result<()> { + use tokio::{fs::File, io::AsyncReadExt}; + + use crate::daemon::rpc::grpc_defs::Network; + + let mut file = File::open(payload_path).await?; + let mut payload = Vec::new(); + file.read_to_end(&mut payload).await?; + + let mut client = BurrowClient::from_uds().await?; + let network = Network { + id, + r#type: network_type, + payload, + }; + let res = client.networks_client.network_add(network).await?; + println!("Network Add Response: {:?}", res); + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_network_list() -> Result<()> { + let mut client = BurrowClient::from_uds().await?; + let mut res = client + .networks_client + .network_list(Empty {}) + .await? + .into_inner(); + while let Some(network_list) = res.message().await? { + println!("Network List: {:?}", network_list); + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_network_reorder(id: i32, index: i32) -> Result<()> { + use crate::daemon::rpc::grpc_defs::NetworkReorderRequest; + + let mut client = BurrowClient::from_uds().await?; + let reorder_request = NetworkReorderRequest { id, index }; + let res = client + .networks_client + .network_reorder(reorder_request) + .await?; + println!("Network Reorder Response: {:?}", res); + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_network_delete(id: i32) -> Result<()> { + use crate::daemon::rpc::grpc_defs::NetworkDeleteRequest; + + let mut client = BurrowClient::from_uds().await?; + let delete_request = NetworkDeleteRequest { id }; + let res = client + .networks_client + .network_delete(delete_request) + .await?; + println!("Network Delete Response: {:?}", res); Ok(()) } @@ -153,6 +277,14 @@ async fn main() -> Result<()> { Commands::ServerConfig => try_serverconfig().await?, Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?, Commands::AuthServer => crate::auth::server::serve().await?, + Commands::ServerStatus => try_serverstatus().await?, + Commands::TunnelConfig => try_tun_config().await?, + Commands::NetworkAdd(args) => { + try_network_add(args.id, args.network_type, &args.payload_path).await? + } + Commands::NetworkList => try_network_list().await?, + Commands::NetworkReorder(args) => try_network_reorder(args.id, args.index).await?, + Commands::NetworkDelete(args) => try_network_delete(args.id).await?, } Ok(()) diff --git a/burrow/src/wireguard/config.rs b/burrow/src/wireguard/config.rs index bd86a9f..5766675 100644 --- a/burrow/src/wireguard/config.rs +++ b/burrow/src/wireguard/config.rs @@ -3,9 +3,12 @@ use std::{net::ToSocketAddrs, str::FromStr}; use anyhow::{anyhow, Error, Result}; use base64::{engine::general_purpose, Engine}; use fehler::throws; +use ini::{Ini, Properties}; use ip_network::IpNetwork; +use serde::{Deserialize, Serialize}; use x25519_dalek::{PublicKey, StaticSecret}; +use super::inifield::IniField; use crate::wireguard::{Interface as WgInterface, Peer as WgPeer}; #[throws] @@ -31,7 +34,7 @@ fn parse_public_key(string: &str) -> PublicKey { /// A raw version of Peer Config that can be used later to reflect configuration files. /// This should be later converted to a `WgPeer`. /// Refers to https://github.com/pirate/wireguard-docs?tab=readme-ov-file#overview -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Peer { pub public_key: String, pub preshared_key: Option, @@ -41,17 +44,18 @@ pub struct Peer { pub name: Option, } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Interface { pub private_key: String, pub address: Vec, - pub listen_port: u32, + pub listen_port: Option, pub dns: Vec, pub mtu: Option, } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Config { + #[serde(rename = "Peer")] pub peers: Vec, pub interface: Interface, // Support for multiple interfaces? } @@ -98,7 +102,7 @@ impl Default for Config { interface: Interface { private_key: "OEPVdomeLTxTIBvv3TYsJRge0Hp9NMiY0sIrhT8OWG8=".into(), address: vec!["10.13.13.2/24".into()], - listen_port: 51820, + listen_port: Some(51820), dns: Default::default(), mtu: Default::default(), }, @@ -113,3 +117,83 @@ impl Default for Config { } } } + +fn props_get(props: &Properties, key: &str) -> Result +where + T: TryFrom, +{ + IniField::try_from(props.get(key))?.try_into() +} + +impl TryFrom<&Properties> for Interface { + type Error = anyhow::Error; + + fn try_from(props: &Properties) -> Result { + Ok(Self { + private_key: props_get(props, "PrivateKey")?, + address: props_get(props, "Address")?, + listen_port: props_get(props, "ListenPort")?, + dns: props_get(props, "DNS")?, + mtu: props_get(props, "MTU")?, + }) + } +} + +impl TryFrom<&Properties> for Peer { + type Error = anyhow::Error; + + fn try_from(props: &Properties) -> Result { + Ok(Self { + public_key: props_get(props, "PublicKey")?, + preshared_key: props_get(props, "PresharedKey")?, + allowed_ips: props_get(props, "AllowedIPs")?, + endpoint: props_get(props, "Endpoint")?, + persistent_keepalive: props_get(props, "PersistentKeepalive")?, + name: props_get(props, "Name")?, + }) + } +} + +impl Config { + pub fn from_toml(toml: &str) -> Result { + toml::from_str(toml).map_err(Into::into) + } + + pub fn from_ini(ini: &str) -> Result { + let ini = Ini::load_from_str(ini)?; + let interface = ini + .section(Some("Interface")) + .ok_or(anyhow!("Interface section not found"))?; + let peers = ini.section_all(Some("Peer")); + Ok(Self { + interface: Interface::try_from(interface)?, + peers: peers + .into_iter() + .map(|v| Peer::try_from(v)) + .collect::>>()?, + }) + } + + pub fn from_content_fmt(content: &str, fmt: &str) -> Result { + match fmt { + "toml" => Self::from_toml(content), + "ini" | "conf" => Self::from_ini(content), + _ => Err(anyhow::anyhow!("Unsupported format: {}", fmt)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tst_config_toml() { + let cfig = Config::default(); + let toml = toml::to_string(&cfig).unwrap(); + println!("{}", &toml); + insta::assert_snapshot!(toml); + let cfig2: Config = toml::from_str(&toml).unwrap(); + assert_eq!(cfig, cfig2); + } +} diff --git a/burrow/src/wireguard/iface.rs b/burrow/src/wireguard/iface.rs index 84b5489..321801b 100755 --- a/burrow/src/wireguard/iface.rs +++ b/burrow/src/wireguard/iface.rs @@ -93,6 +93,12 @@ impl Interface { *st = IfaceStatus::Running; } + pub async fn set_tun_ref(&mut self, tun: Arc>>) { + self.tun = tun; + let mut st = self.status.write().await; + *st = IfaceStatus::Running; + } + pub fn get_tun(&self) -> Arc>> { self.tun.clone() } @@ -135,7 +141,7 @@ impl Interface { Some(addr) => addr, None => { debug!("No destination found"); - continue + continue; } }; @@ -154,7 +160,7 @@ impl Interface { } Err(e) => { log::error!("Failed to send packet {}", e); - continue + continue; } }; } @@ -175,7 +181,7 @@ impl Interface { let main_tsk = async move { if let Err(e) = pcb.open_if_closed().await { log::error!("failed to open pcb: {}", e); - return + return; } let r2 = pcb.run(tun).await; if let Err(e) = r2 { @@ -195,7 +201,7 @@ impl Interface { Ok(..) => (), Err(e) => { error!("Failed to update timers: {}", e); - return + return; } } } diff --git a/burrow/src/wireguard/inifield.rs b/burrow/src/wireguard/inifield.rs new file mode 100644 index 0000000..946868d --- /dev/null +++ b/burrow/src/wireguard/inifield.rs @@ -0,0 +1,81 @@ +use std::str::FromStr; + +use anyhow::{Error, Result}; + +pub struct IniField(String); + +impl FromStr for IniField { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_string())) + } +} + +impl TryFrom for Vec { + type Error = Error; + + fn try_from(field: IniField) -> Result { + Ok(field.0.split(',').map(|s| s.trim().to_string()).collect()) + } +} + +impl TryFrom for u32 { + type Error = Error; + + fn try_from(value: IniField) -> Result { + value.0.parse().map_err(Error::from) + } +} + +impl TryFrom for Option { + type Error = Error; + + fn try_from(value: IniField) -> Result { + if value.0.is_empty() { + Ok(None) + } else { + value.0.parse().map(Some).map_err(Error::from) + } + } +} + +impl TryFrom for String { + type Error = Error; + + fn try_from(value: IniField) -> Result { + Ok(value.0) + } +} + +impl TryFrom for Option { + type Error = Error; + + fn try_from(value: IniField) -> Result { + if value.0.is_empty() { + Ok(None) + } else { + Ok(Some(value.0)) + } + } +} + +impl TryFrom> for IniField +where + T: ToString, +{ + type Error = Error; + + fn try_from(value: Option) -> Result { + Ok(match value { + Some(v) => Self(v.to_string()), + None => Self(String::new()), + }) + } +} + +impl IniField { + fn new(value: &str) -> Self { + Self(value.to_string()) + } +} diff --git a/burrow/src/wireguard/mod.rs b/burrow/src/wireguard/mod.rs index 4c70a7f..cfb4585 100755 --- a/burrow/src/wireguard/mod.rs +++ b/burrow/src/wireguard/mod.rs @@ -1,5 +1,6 @@ pub mod config; mod iface; +mod inifield; mod noise; mod pcb; mod peer; diff --git a/burrow/src/wireguard/snapshots/burrow__wireguard__config__tests__tst_config_toml.snap b/burrow/src/wireguard/snapshots/burrow__wireguard__config__tests__tst_config_toml.snap new file mode 100644 index 0000000..3800647 --- /dev/null +++ b/burrow/src/wireguard/snapshots/burrow__wireguard__config__tests__tst_config_toml.snap @@ -0,0 +1,16 @@ +--- +source: burrow/src/wireguard/config.rs +expression: toml +--- +[[Peer]] +public_key = "8GaFjVO6c4luCHG4ONO+1bFG8tO+Zz5/Gy+Geht1USM=" +preshared_key = "ha7j4BjD49sIzyF9SNlbueK0AMHghlj6+u0G3bzC698=" +allowed_ips = ["8.8.8.8/32", "0.0.0.0/0"] +endpoint = "wg.burrow.rs:51820" + +[interface] +private_key = "OEPVdomeLTxTIBvv3TYsJRge0Hp9NMiY0sIrhT8OWG8=" +address = ["10.13.13.2/24"] +listen_port = 51820 +dns = [] + diff --git a/burrow/tmp/conrd.conf b/burrow/tmp/conrd.conf new file mode 100644 index 0000000..52572d1 --- /dev/null +++ b/burrow/tmp/conrd.conf @@ -0,0 +1,8 @@ +[Interface] +PrivateKey = gAaK0KFGOpxY7geGo59XXDufcxeoSNXXNC12mCQmlVs= +Address = 10.1.11.2/32 +DNS = 10.1.11.1 +[Peer] +PublicKey = Ab6V2mgPHiCXaAZfQrNts8ha8RkEzC49VnmMQfe5Yg4= +AllowedIPs = 10.1.11.1/32,10.1.11.2/32,0.0.0.0/0 +Endpoint = 172.251.163.175:51820 \ No newline at end of file diff --git a/proto/burrow.proto b/proto/burrow.proto index 2d29c78..2355b8d 100644 --- a/proto/burrow.proto +++ b/proto/burrow.proto @@ -11,7 +11,7 @@ service Tunnel { } service Networks { - rpc NetworkAdd (Empty) returns (Empty); + rpc NetworkAdd (Network) returns (Empty); rpc NetworkList (Empty) returns (stream NetworkListResponse); rpc NetworkReorder (NetworkReorderRequest) returns (Empty); rpc NetworkDelete (NetworkDeleteRequest) returns (Empty); From 25a0f7c42158831ceb5f6bbe7defe3c067eb586c Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 7 Sep 2024 20:35:28 -0700 Subject: [PATCH 127/128] Add Developer ID Profiles to build --- .github/workflows/release-apple.yml | 5 +++++ .../Burrow_Developer_ID.provisionprofile | Bin 0 -> 13091 bytes ...Burrow_Network_Developer_ID.provisionprofile | Bin 0 -> 13027 bytes 3 files changed, 5 insertions(+) create mode 100644 Apple/Profiles/Burrow_Developer_ID.provisionprofile create mode 100644 Apple/Profiles/Burrow_Network_Developer_ID.provisionprofile diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index bb9c15a..c0a34a9 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -38,6 +38,11 @@ jobs: app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} + - name: Install Provisioning Profiles + shell: bash + run: | + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles/ + cp -f Apple/Profiles/* ~/Library/MobileDevice/Provisioning\ Profiles/ - name: Install Rust uses: dtolnay/rust-toolchain@stable with: diff --git a/Apple/Profiles/Burrow_Developer_ID.provisionprofile b/Apple/Profiles/Burrow_Developer_ID.provisionprofile new file mode 100644 index 0000000000000000000000000000000000000000..3ecd831fe2614bb8b5aea636ca5c080a48d99a98 GIT binary patch literal 13091 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 Uf<>W=OI4qBvCm4;3e_kF0D*cnOaK4? literal 0 HcmV?d00001 From 85640ffce18eac6ac1b6fa85ff278a457c955198 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 13 Jul 2024 18:08:43 -0700 Subject: [PATCH 128/128] Switch to gRPC client in Swift app --- .github/workflows/build-apple.yml | 9 +- .github/workflows/release-apple.yml | 4 + .gitignore | 3 + .swiftlint.yml | 1 - Apple/App/AppDelegate.swift | 1 + Apple/App/BurrowApp.swift | 3 +- Apple/App/MainMenu.xib | 4 +- Apple/App/Networks/Network.swift | 10 - Apple/App/Tunnel.swift | 50 - Apple/Burrow.xcodeproj/project.pbxproj | 814 +++++++++---- .../xcshareddata/swiftpm/Package.resolved | 123 ++ .../xcshareddata/xcschemes/App.xcscheme | 5 +- .../xcschemes/NetworkExtension.xcscheme | 6 +- Apple/Configuration/App.xcconfig | 6 +- Apple/Configuration/Compiler.xcconfig | 41 +- .../Configuration.xcconfig} | 5 +- .../Constants/Constants.h | 0 .../Constants}/Constants.swift | 31 +- .../Constants/module.modulemap | 2 +- Apple/Configuration/Debug.xcconfig | 26 + Apple/Configuration/Extension.xcconfig | 6 +- Apple/Configuration/Framework.xcconfig | 14 + Apple/Core/Client.swift | 32 + Apple/Core/Client/burrow.proto | 1 + Apple/Core/Client/grpc-swift-config.json | 11 + Apple/Core/Client/swift-protobuf-config.json | 10 + Apple/{Shared => Core}/Logging.swift | 2 +- .../PacketTunnelProvider.swift | 108 +- .../NetworkExtension/libburrow/build-rust.sh | 5 +- Apple/NetworkExtension/libburrow/libburrow.h | 2 +- Apple/Shared/Client.swift | 106 -- Apple/Shared/DataTypes.swift | 139 --- Apple/Shared/NWConnection+Async.swift | 32 - Apple/Shared/NewlineProtocolFramer.swift | 54 - .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/100.png | Bin .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/114.png | Bin .../AppIcon.appiconset/120.png | Bin .../AppIcon.appiconset/128.png | Bin .../AppIcon.appiconset/144.png | Bin .../AppIcon.appiconset/152.png | Bin .../Assets.xcassets/AppIcon.appiconset/16.png | Bin .../AppIcon.appiconset/167.png | Bin .../AppIcon.appiconset/172.png | Bin .../AppIcon.appiconset/180.png | Bin .../AppIcon.appiconset/196.png | Bin .../Assets.xcassets/AppIcon.appiconset/20.png | Bin .../AppIcon.appiconset/216.png | Bin .../AppIcon.appiconset/256.png | Bin .../Assets.xcassets/AppIcon.appiconset/29.png | Bin .../Assets.xcassets/AppIcon.appiconset/32.png | Bin .../Assets.xcassets/AppIcon.appiconset/40.png | Bin .../Assets.xcassets/AppIcon.appiconset/48.png | Bin .../Assets.xcassets/AppIcon.appiconset/50.png | Bin .../AppIcon.appiconset/512.png | Bin .../Assets.xcassets/AppIcon.appiconset/55.png | Bin .../Assets.xcassets/AppIcon.appiconset/57.png | Bin .../Assets.xcassets/AppIcon.appiconset/58.png | Bin .../Assets.xcassets/AppIcon.appiconset/60.png | Bin .../Assets.xcassets/AppIcon.appiconset/64.png | Bin .../Assets.xcassets/AppIcon.appiconset/72.png | Bin .../Assets.xcassets/AppIcon.appiconset/76.png | Bin .../Assets.xcassets/AppIcon.appiconset/80.png | Bin .../Assets.xcassets/AppIcon.appiconset/87.png | Bin .../Assets.xcassets/AppIcon.appiconset/88.png | Bin .../AppIcon.appiconset/Contents.json | 0 .../{App => UI}/Assets.xcassets/Contents.json | 0 .../HackClub.colorset/Contents.json | 0 .../HackClub.imageset/Contents.json | 0 .../flag-standalone-wtransparent.pdf | Bin .../WireGuard.colorset/Contents.json | 0 .../WireGuard.imageset/Contents.json | 0 .../WireGuard.imageset/WireGuard.svg | 0 .../WireGuardTitle.imageset/Contents.json | 0 .../WireGuardTitle.svg | 0 Apple/{App => UI}/BurrowView.swift | 7 +- Apple/{App => UI}/FloatingButtonStyle.swift | 0 Apple/{App => UI}/MenuItemToggleView.swift | 11 +- Apple/{App => UI}/NetworkCarouselView.swift | 8 +- .../{App => UI}/NetworkExtension+Async.swift | 6 +- .../{App => UI}/NetworkExtensionTunnel.swift | 72 +- Apple/{App => UI}/NetworkView.swift | 0 Apple/{App => UI}/Networks/HackClub.swift | 8 +- Apple/UI/Networks/Network.swift | 36 + Apple/{App => UI}/Networks/WireGuard.swift | 8 +- Apple/{App => UI}/OAuth2.swift | 21 +- Apple/UI/Tunnel.swift | 61 + Apple/{App => UI}/TunnelButton.swift | 2 +- Apple/{App => UI}/TunnelStatusView.swift | 2 +- Apple/UI/UI.xcconfig | 3 + Cargo.lock | 1080 +++++++++-------- burrow-gtk/build-aux/Dockerfile | 2 +- 93 files changed, 1666 insertions(+), 1327 deletions(-) delete mode 100644 Apple/App/Networks/Network.swift delete mode 100644 Apple/App/Tunnel.swift create mode 100644 Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename Apple/{Shared/Shared.xcconfig => Configuration/Configuration.xcconfig} (65%) rename Apple/{Shared => Configuration}/Constants/Constants.h (100%) rename Apple/{Shared => Configuration/Constants}/Constants.swift (61%) rename Apple/{Shared => Configuration}/Constants/module.modulemap (66%) create mode 100644 Apple/Configuration/Debug.xcconfig create mode 100644 Apple/Configuration/Framework.xcconfig create mode 100644 Apple/Core/Client.swift create mode 120000 Apple/Core/Client/burrow.proto create mode 100644 Apple/Core/Client/grpc-swift-config.json create mode 100644 Apple/Core/Client/swift-protobuf-config.json rename Apple/{Shared => Core}/Logging.swift (88%) delete mode 100644 Apple/Shared/Client.swift delete mode 100644 Apple/Shared/DataTypes.swift delete mode 100644 Apple/Shared/NWConnection+Async.swift delete mode 100644 Apple/Shared/NewlineProtocolFramer.swift rename Apple/{App => UI}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/100.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/1024.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/114.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/120.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/128.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/144.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/152.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/16.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/167.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/172.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/180.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/196.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/20.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/216.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/256.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/29.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/32.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/40.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/48.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/50.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/512.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/55.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/57.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/58.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/60.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/64.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/72.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/76.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/80.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/87.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/88.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/HackClub.colorset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/HackClub.imageset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuard.colorset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuard.imageset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuard.imageset/WireGuard.svg (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuardTitle.imageset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg (100%) rename Apple/{App => UI}/BurrowView.swift (95%) rename Apple/{App => UI}/FloatingButtonStyle.swift (100%) rename Apple/{App => UI}/MenuItemToggleView.swift (87%) rename Apple/{App => UI}/NetworkCarouselView.swift (90%) rename Apple/{App => UI}/NetworkExtension+Async.swift (82%) rename Apple/{App => UI}/NetworkExtensionTunnel.swift (67%) rename Apple/{App => UI}/NetworkView.swift (100%) rename Apple/{App => UI}/Networks/HackClub.swift (76%) create mode 100644 Apple/UI/Networks/Network.swift rename Apple/{App => UI}/Networks/WireGuard.swift (82%) rename Apple/{App => UI}/OAuth2.swift (94%) create mode 100644 Apple/UI/Tunnel.swift rename Apple/{App => UI}/TunnelButton.swift (95%) rename Apple/{App => UI}/TunnelStatusView.swift (95%) create mode 100644 Apple/UI/UI.xcconfig diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index b628001..7ae8c4c 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -39,7 +39,7 @@ jobs: - aarch64-apple-darwin env: DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer - PROTOC_VERSION: 3.25.1 + PROTOC_PATH: /opt/homebrew/bin/protoc steps: - name: Checkout uses: actions/checkout@v3 @@ -55,10 +55,9 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: ${{ join(matrix.rust-targets, ', ') }} - - name: Install protoc - uses: taiki-e/install-action@v2 - with: - tool: protoc@${{ env.PROTOC_VERSION }} + - name: Install Protobuf + shell: bash + run: brew install protobuf - name: Build id: build uses: ./.github/actions/build-for-testing diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index c0a34a9..c869d6a 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -22,6 +22,7 @@ jobs: - aarch64-apple-darwin env: DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer + PROTOC_PATH: /opt/homebrew/bin/protoc steps: - name: Checkout uses: actions/checkout@v4 @@ -47,6 +48,9 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: ${{ join(matrix.rust-targets, ', ') }} + - name: Install Protobuf + shell: bash + run: brew install protobuf - name: Configure Version id: version shell: bash diff --git a/.gitignore b/.gitignore index 997d4d5..1b300b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Xcode xcuserdata +# Swift +Apple/Package/.swiftpm/ + # Rust target/ .env diff --git a/.swiftlint.yml b/.swiftlint.yml index 22ef035..8efc85e 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -30,7 +30,6 @@ opt_in_rules: - function_default_parameter_at_end - ibinspectable_in_extension - identical_operands -- implicitly_unwrapped_optional - indentation_width - joined_default_parameter - last_where diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index b0c5546..0ea93f4 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -1,5 +1,6 @@ #if os(macOS) import AppKit +import BurrowUI import SwiftUI @main diff --git a/Apple/App/BurrowApp.swift b/Apple/App/BurrowApp.swift index 21ebf84..838ef54 100644 --- a/Apple/App/BurrowApp.swift +++ b/Apple/App/BurrowApp.swift @@ -1,6 +1,7 @@ +#if !os(macOS) +import BurrowUI import SwiftUI -#if !os(macOS) @MainActor @main struct BurrowApp: App { diff --git a/Apple/App/MainMenu.xib b/Apple/App/MainMenu.xib index 587f6c4..50ba431 100644 --- a/Apple/App/MainMenu.xib +++ b/Apple/App/MainMenu.xib @@ -1,7 +1,7 @@ - + - + diff --git a/Apple/App/Networks/Network.swift b/Apple/App/Networks/Network.swift deleted file mode 100644 index d441d24..0000000 --- a/Apple/App/Networks/Network.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -protocol Network { - associatedtype Label: View - - var id: String { get } - var backgroundColor: Color { get } - - var label: Label { get } -} diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift deleted file mode 100644 index 8db366f..0000000 --- a/Apple/App/Tunnel.swift +++ /dev/null @@ -1,50 +0,0 @@ -import SwiftUI - -protocol Tunnel { - var status: TunnelStatus { get } - - func start() - func stop() - func enable() -} - -enum TunnelStatus: Equatable, Hashable { - case unknown - case permissionRequired - case disabled - case connecting - case connected(Date) - case disconnecting - case disconnected - case reasserting - case invalid - case configurationReadWriteFailed -} - -struct TunnelKey: EnvironmentKey { - static let defaultValue: any Tunnel = NetworkExtensionTunnel() -} - -extension EnvironmentValues { - var tunnel: any Tunnel { - get { self[TunnelKey.self] } - set { self[TunnelKey.self] = newValue } - } -} - -#if DEBUG -@Observable -class PreviewTunnel: Tunnel { - var status: TunnelStatus = .permissionRequired - - func start() { - status = .connected(.now) - } - func stop() { - status = .disconnected - } - func enable() { - status = .disconnected - } -} -#endif diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 5c5e80b..617b88f 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -7,52 +7,50 @@ objects = { /* Begin PBXBuildFile section */ - 0BA6D73B2BA638D900BD4B55 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; - 0BA6D73C2BA6393200BD4B55 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; - 0BA6D73D2BA6393B00BD4B55 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; - 0BA6D73E2BA6394B00BD4B55 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; - 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; }; - D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */; }; - D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363E2BB895FB00E582EC /* OAuth2.swift */; }; - D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; - D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; - D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; }; - D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A79302B81630D0024EC91 /* NetworkView.swift */; }; D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - D032E6522B8A79C20006B8AD /* HackClub.swift in Sources */ = {isa = PBXBuildFile; fileRef = D032E6512B8A79C20006B8AD /* HackClub.swift */; }; - D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D032E6532B8A79DA0006B8AD /* WireGuard.swift */; }; + D03383AD2C8E67E300F7C44E /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = D078F7E22C8DA375008A8CEC /* SwiftProtobuf */; }; + D03383AE2C8E67E300F7C44E /* NIO in Frameworks */ = {isa = PBXBuildFile; productRef = D044EE902C8DAB2000778185 /* NIO */; }; + D03383AF2C8E67E300F7C44E /* NIOConcurrencyHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = D044EE922C8DAB2000778185 /* NIOConcurrencyHelpers */; }; + D03383B02C8E67E300F7C44E /* NIOTransportServices in Frameworks */ = {isa = PBXBuildFile; productRef = D044EE952C8DAB2800778185 /* NIOTransportServices */; }; D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; - D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */; }; - D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D05B9F7929E39EED008CB1F9 /* Assets.xcassets */; }; - D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */; }; - D08252762B5C9FC4005DA378 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08252752B5C9FC4005DA378 /* Constants.swift */; }; - D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D09150412B9D2AF700BE3CB0 /* MainMenu.xib */; }; - D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */; }; - D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B98FC629FDC5B5004E7149 /* Tunnel.swift */; }; + D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D09150412B9D2AF700BE3CB0 /* MainMenu.xib */; platformFilters = (macos, ); }; + D0B1D1102C436152004B7823 /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = D0B1D10F2C436152004B7823 /* AsyncAlgorithms */; }; D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BCC6032A09535900AD070D /* libburrow.a */; }; - D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */; }; - D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5952B818B2900F6A84B /* TunnelButton.swift */; }; - D0FAB5982B818B8200F6A84B /* TunnelStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */; }; - D0FAB59A2B818B9600F6A84B /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5992B818B9600F6A84B /* Network.swift */; }; + D0BF09522C8E66F6000D8DEC /* BurrowConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; }; + D0BF09552C8E66FD000D8DEC /* BurrowConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; }; + D0D4E53A2C8D996F007F820A /* BurrowCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D4E56B2C8D9C2F007F820A /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E49A2C8D921A007F820A /* Logging.swift */; }; + D0D4E5702C8D9C62007F820A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; }; + D0D4E5712C8D9C6F007F820A /* HackClub.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E49D2C8D921A007F820A /* HackClub.swift */; }; + D0D4E5722C8D9C6F007F820A /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E49E2C8D921A007F820A /* Network.swift */; }; + D0D4E5732C8D9C6F007F820A /* WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E49F2C8D921A007F820A /* WireGuard.swift */; }; + D0D4E5742C8D9C6F007F820A /* BurrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A22C8D921A007F820A /* BurrowView.swift */; }; + D0D4E5752C8D9C6F007F820A /* FloatingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A32C8D921A007F820A /* FloatingButtonStyle.swift */; }; + D0D4E5762C8D9C6F007F820A /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A42C8D921A007F820A /* MenuItemToggleView.swift */; }; + D0D4E5772C8D9C6F007F820A /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A52C8D921A007F820A /* NetworkCarouselView.swift */; }; + D0D4E5782C8D9C6F007F820A /* NetworkExtension+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A62C8D921A007F820A /* NetworkExtension+Async.swift */; }; + D0D4E5792C8D9C6F007F820A /* NetworkExtensionTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A72C8D921A007F820A /* NetworkExtensionTunnel.swift */; }; + D0D4E57A2C8D9C6F007F820A /* NetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A82C8D921A007F820A /* NetworkView.swift */; }; + D0D4E57B2C8D9C6F007F820A /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A92C8D921A007F820A /* OAuth2.swift */; }; + D0D4E57C2C8D9C6F007F820A /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4AA2C8D921A007F820A /* Tunnel.swift */; }; + D0D4E57D2C8D9C6F007F820A /* TunnelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4AB2C8D921A007F820A /* TunnelButton.swift */; }; + D0D4E57E2C8D9C6F007F820A /* TunnelStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4AC2C8D921A007F820A /* TunnelStatusView.swift */; }; + D0D4E5892C8D9C94007F820A /* BurrowUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */; }; + D0D4E58A2C8D9C9E007F820A /* BurrowUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D4E58B2C8D9CA4007F820A /* BurrowConfiguration.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D4E5922C8D9D15007F820A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E58F2C8D9D0A007F820A /* Constants.swift */; }; + D0D4E5A62C8D9E65007F820A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; }; + D0F4FAD32C8DC79C0068730A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; }; + D0F7594E2C8DAB6B00126CF3 /* GRPC in Frameworks */ = {isa = PBXBuildFile; productRef = D078F7E02C8DA375008A8CEC /* GRPC */; }; + D0F759612C8DB24B00126CF3 /* grpc-swift-config.json in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4962C8D921A007F820A /* grpc-swift-config.json */; }; + D0F759622C8DB24B00126CF3 /* swift-protobuf-config.json in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4972C8D921A007F820A /* swift-protobuf-config.json */; }; + D0F7597E2C8DB30500126CF3 /* CGRPCZlib in Frameworks */ = {isa = PBXBuildFile; productRef = D0F7597D2C8DB30500126CF3 /* CGRPCZlib */; }; + D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4992C8D921A007F820A /* Client.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - D00117462B30373100D87C25 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; - proxyType = 1; - remoteGlobalIDString = D00117372B30341C00D87C25; - remoteInfo = Shared; - }; - D00117482B30373500D87C25 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; - proxyType = 1; - remoteGlobalIDString = D00117372B30341C00D87C25; - remoteInfo = Shared; - }; D020F65B29E4A697002790F6 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; @@ -60,6 +58,48 @@ remoteGlobalIDString = D020F65229E4A697002790F6; remoteInfo = BurrowNetworkExtension; }; + D0BF09502C8E66F1000D8DEC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E55A2C8D9BF4007F820A; + remoteInfo = Configuration; + }; + D0BF09532C8E66FA000D8DEC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E55A2C8D9BF4007F820A; + remoteInfo = Configuration; + }; + D0D4E56E2C8D9C5D007F820A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E5302C8D996F007F820A; + remoteInfo = Core; + }; + D0D4E57F2C8D9C78007F820A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E5302C8D996F007F820A; + remoteInfo = Core; + }; + D0D4E5872C8D9C88007F820A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E5502C8D9BF2007F820A; + remoteInfo = UI; + }; + D0F4FAD12C8DC7960068730A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E5302C8D996F007F820A; + remoteInfo = Core; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -74,22 +114,24 @@ name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; + D0D4E53F2C8D996F007F820A /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D0D4E58B2C8D9CA4007F820A /* BurrowConfiguration.framework in Embed Frameworks */, + D0D4E58A2C8D9C9E007F820A /* BurrowUI.framework in Embed Frameworks */, + D0D4E53A2C8D996F007F820A /* BurrowCore.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; - 0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; - 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = ""; }; - D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCarouselView.swift; sourceTree = ""; }; - D000363E2BB895FB00E582EC /* OAuth2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2.swift; sourceTree = ""; }; - D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = ""; }; - D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = ""; }; - D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; }; - D001173A2B30341C00D87C25 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; - D00117412B30347800D87C25 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - D00117422B30348D00D87C25 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; + D00117422B30348D00D87C25 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Configuration.xcconfig; sourceTree = ""; }; D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - D01A79302B81630D0024EC91 /* NetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkView.swift; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = ""; }; D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -104,43 +146,54 @@ D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "NetworkExtension-iOS.entitlements"; sourceTree = ""; }; D020F66829E4AA74002790F6 /* App-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "App-iOS.entitlements"; sourceTree = ""; }; D020F66929E4AA74002790F6 /* App-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "App-macOS.entitlements"; sourceTree = ""; }; - D032E6512B8A79C20006B8AD /* HackClub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackClub.swift; sourceTree = ""; }; - D032E6532B8A79DA0006B8AD /* WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuard.swift; sourceTree = ""; }; D04A3E1D2BAF465F0043EC85 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; }; D05B9F7229E39EEC008CB1F9 /* Burrow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Burrow.app; sourceTree = BUILT_PRODUCTS_DIR; }; D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowApp.swift; sourceTree = ""; }; - D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowView.swift; sourceTree = ""; }; - D05B9F7929E39EED008CB1F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingButtonStyle.swift; sourceTree = ""; }; - D08252742B5C9DEB005DA378 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; - D08252752B5C9FC4005DA378 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; D09150412B9D2AF700BE3CB0 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; D0B98FBF29FD8072004E7149 /* build-rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-rust.sh"; sourceTree = ""; }; - D0B98FC629FDC5B5004E7149 /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; D0B98FD829FDDB6F004E7149 /* libburrow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libburrow.h; sourceTree = ""; }; D0B98FDC29FDDDCF004E7149 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkExtension+Async.swift"; sourceTree = ""; }; D0BCC6032A09535900AD070D /* libburrow.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libburrow.a; sourceTree = BUILT_PRODUCTS_DIR; }; - D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionTunnel.swift; sourceTree = ""; }; - D0FAB5952B818B2900F6A84B /* TunnelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelButton.swift; sourceTree = ""; }; - D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusView.swift; sourceTree = ""; }; - D0FAB5992B818B9600F6A84B /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + D0BF09582C8E6789000D8DEC /* UI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UI.xcconfig; sourceTree = ""; }; + D0D4E4952C8D921A007F820A /* burrow.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = burrow.proto; sourceTree = ""; }; + D0D4E4962C8D921A007F820A /* grpc-swift-config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "grpc-swift-config.json"; sourceTree = ""; }; + D0D4E4972C8D921A007F820A /* swift-protobuf-config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "swift-protobuf-config.json"; sourceTree = ""; }; + D0D4E4992C8D921A007F820A /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; + D0D4E49A2C8D921A007F820A /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; + D0D4E49D2C8D921A007F820A /* HackClub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackClub.swift; sourceTree = ""; }; + D0D4E49E2C8D921A007F820A /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + D0D4E49F2C8D921A007F820A /* WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuard.swift; sourceTree = ""; }; + D0D4E4A12C8D921A007F820A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D0D4E4A22C8D921A007F820A /* BurrowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowView.swift; sourceTree = ""; }; + D0D4E4A32C8D921A007F820A /* FloatingButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingButtonStyle.swift; sourceTree = ""; }; + D0D4E4A42C8D921A007F820A /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = ""; }; + D0D4E4A52C8D921A007F820A /* NetworkCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCarouselView.swift; sourceTree = ""; }; + D0D4E4A62C8D921A007F820A /* NetworkExtension+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkExtension+Async.swift"; sourceTree = ""; }; + D0D4E4A72C8D921A007F820A /* NetworkExtensionTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionTunnel.swift; sourceTree = ""; }; + D0D4E4A82C8D921A007F820A /* NetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkView.swift; sourceTree = ""; }; + D0D4E4A92C8D921A007F820A /* OAuth2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2.swift; sourceTree = ""; }; + D0D4E4AA2C8D921A007F820A /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; + D0D4E4AB2C8D921A007F820A /* TunnelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelButton.swift; sourceTree = ""; }; + D0D4E4AC2C8D921A007F820A /* TunnelStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusView.swift; sourceTree = ""; }; + D0D4E4F62C8D932D007F820A /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + D0D4E4F72C8D941D007F820A /* Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Framework.xcconfig; sourceTree = ""; }; + D0D4E5312C8D996F007F820A /* BurrowCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BurrowCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BurrowUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BurrowConfiguration.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0D4E58E2C8D9D0A007F820A /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; + D0D4E58F2C8D9D0A007F820A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + D0D4E5902C8D9D0A007F820A /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - D00117352B30341C00D87C25 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; D020F65029E4A697002790F6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */, + D0BF09522C8E66F6000D8DEC /* BurrowConfiguration.framework in Frameworks */, + D0D4E5A62C8D9E65007F820A /* BurrowCore.framework in Frameworks */, D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */, + D0B1D1102C436152004B7823 /* AsyncAlgorithms in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -148,37 +201,36 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */, + D0BF09552C8E66FD000D8DEC /* BurrowConfiguration.framework in Frameworks */, + D0F4FAD32C8DC79C0068730A /* BurrowCore.framework in Frameworks */, + D0D4E5892C8D9C94007F820A /* BurrowUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D078F7CF2C8DA213008A8CEC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D03383B02C8E67E300F7C44E /* NIOTransportServices in Frameworks */, + D03383AF2C8E67E300F7C44E /* NIOConcurrencyHelpers in Frameworks */, + D03383AE2C8E67E300F7C44E /* NIO in Frameworks */, + D03383AD2C8E67E300F7C44E /* SwiftProtobuf in Frameworks */, + D0F7594E2C8DAB6B00126CF3 /* GRPC in Frameworks */, + D0F7597E2C8DB30500126CF3 /* CGRPCZlib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D4E5532C8D9BF2007F820A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0D4E5702C8D9C62007F820A /* BurrowCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - D00117392B30341C00D87C25 /* Shared */ = { - isa = PBXGroup; - children = ( - 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, - D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */, - D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */, - 0B46E8DF2AC918CA00BA2A3C /* Client.swift */, - D001173A2B30341C00D87C25 /* Logging.swift */, - D08252752B5C9FC4005DA378 /* Constants.swift */, - D00117422B30348D00D87C25 /* Shared.xcconfig */, - D001173F2B30347800D87C25 /* Constants */, - ); - path = Shared; - sourceTree = ""; - }; - D001173F2B30347800D87C25 /* Constants */ = { - isa = PBXGroup; - children = ( - D08252742B5C9DEB005DA378 /* Constants.h */, - D00117412B30347800D87C25 /* module.modulemap */, - ); - path = Constants; - sourceTree = ""; - }; D00117432B30372900D87C25 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -192,9 +244,13 @@ D020F63D29E4A1FF002790F6 /* Identity.xcconfig */, D020F64A29E4A452002790F6 /* App.xcconfig */, D020F66329E4A703002790F6 /* Extension.xcconfig */, + D0D4E4F72C8D941D007F820A /* Framework.xcconfig */, D020F64029E4A1FF002790F6 /* Compiler.xcconfig */, + D0D4E4F62C8D932D007F820A /* Debug.xcconfig */, D04A3E1D2BAF465F0043EC85 /* Version.xcconfig */, D020F64229E4A1FF002790F6 /* Info.plist */, + D0D4E5912C8D9D0A007F820A /* Constants */, + D00117422B30348D00D87C25 /* Configuration.xcconfig */, ); path = Configuration; sourceTree = ""; @@ -212,22 +268,13 @@ path = NetworkExtension; sourceTree = ""; }; - D032E64D2B8A69C90006B8AD /* Networks */ = { - isa = PBXGroup; - children = ( - D0FAB5992B818B9600F6A84B /* Network.swift */, - D032E6512B8A79C20006B8AD /* HackClub.swift */, - D032E6532B8A79DA0006B8AD /* WireGuard.swift */, - ); - path = Networks; - sourceTree = ""; - }; D05B9F6929E39EEC008CB1F9 = { isa = PBXGroup; children = ( D05B9F7429E39EEC008CB1F9 /* App */, D020F65629E4A697002790F6 /* NetworkExtension */, - D00117392B30341C00D87C25 /* Shared */, + D0D4E49C2C8D921A007F820A /* Core */, + D0D4E4AD2C8D921A007F820A /* UI */, D020F63C29E4A1FF002790F6 /* Configuration */, D05B9F7329E39EEC008CB1F9 /* Products */, D00117432B30372900D87C25 /* Frameworks */, @@ -239,7 +286,10 @@ children = ( D05B9F7229E39EEC008CB1F9 /* Burrow.app */, D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */, - D00117382B30341C00D87C25 /* libBurrowShared.a */, + D0BCC6032A09535900AD070D /* libburrow.a */, + D0D4E5312C8D996F007F820A /* BurrowCore.framework */, + D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */, + D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */, ); name = Products; sourceTree = ""; @@ -249,19 +299,6 @@ children = ( D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */, D00AA8962A4669BC005C8102 /* AppDelegate.swift */, - 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */, - D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */, - D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */, - D01A79302B81630D0024EC91 /* NetworkView.swift */, - D000363E2BB895FB00E582EC /* OAuth2.swift */, - D032E64D2B8A69C90006B8AD /* Networks */, - D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */, - D0FAB5952B818B2900F6A84B /* TunnelButton.swift */, - D0B98FC629FDC5B5004E7149 /* Tunnel.swift */, - D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */, - D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */, - D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */, - D05B9F7929E39EED008CB1F9 /* Assets.xcassets */, D09150412B9D2AF700BE3CB0 /* MainMenu.xib */, D020F66829E4AA74002790F6 /* App-iOS.entitlements */, D020F66929E4AA74002790F6 /* App-macOS.entitlements */, @@ -276,30 +313,74 @@ D0B98FBF29FD8072004E7149 /* build-rust.sh */, D0B98FDC29FDDDCF004E7149 /* module.modulemap */, D0B98FD829FDDB6F004E7149 /* libburrow.h */, - D0BCC6032A09535900AD070D /* libburrow.a */, ); path = libburrow; sourceTree = ""; }; + D0D4E4982C8D921A007F820A /* Client */ = { + isa = PBXGroup; + children = ( + D0D4E4952C8D921A007F820A /* burrow.proto */, + D0D4E4962C8D921A007F820A /* grpc-swift-config.json */, + D0D4E4972C8D921A007F820A /* swift-protobuf-config.json */, + ); + path = Client; + sourceTree = ""; + }; + D0D4E49C2C8D921A007F820A /* Core */ = { + isa = PBXGroup; + children = ( + D0D4E49A2C8D921A007F820A /* Logging.swift */, + D0D4E4992C8D921A007F820A /* Client.swift */, + D0D4E4982C8D921A007F820A /* Client */, + ); + path = Core; + sourceTree = ""; + }; + D0D4E4A02C8D921A007F820A /* Networks */ = { + isa = PBXGroup; + children = ( + D0D4E49D2C8D921A007F820A /* HackClub.swift */, + D0D4E49E2C8D921A007F820A /* Network.swift */, + D0D4E49F2C8D921A007F820A /* WireGuard.swift */, + ); + path = Networks; + sourceTree = ""; + }; + D0D4E4AD2C8D921A007F820A /* UI */ = { + isa = PBXGroup; + children = ( + D0D4E4A22C8D921A007F820A /* BurrowView.swift */, + D0D4E4A02C8D921A007F820A /* Networks */, + D0D4E4A32C8D921A007F820A /* FloatingButtonStyle.swift */, + D0D4E4A42C8D921A007F820A /* MenuItemToggleView.swift */, + D0D4E4A52C8D921A007F820A /* NetworkCarouselView.swift */, + D0D4E4A62C8D921A007F820A /* NetworkExtension+Async.swift */, + D0D4E4A72C8D921A007F820A /* NetworkExtensionTunnel.swift */, + D0D4E4A82C8D921A007F820A /* NetworkView.swift */, + D0D4E4A92C8D921A007F820A /* OAuth2.swift */, + D0D4E4AA2C8D921A007F820A /* Tunnel.swift */, + D0D4E4AB2C8D921A007F820A /* TunnelButton.swift */, + D0D4E4AC2C8D921A007F820A /* TunnelStatusView.swift */, + D0D4E4A12C8D921A007F820A /* Assets.xcassets */, + D0BF09582C8E6789000D8DEC /* UI.xcconfig */, + ); + path = UI; + sourceTree = ""; + }; + D0D4E5912C8D9D0A007F820A /* Constants */ = { + isa = PBXGroup; + children = ( + D0D4E58E2C8D9D0A007F820A /* Constants.h */, + D0D4E58F2C8D9D0A007F820A /* Constants.swift */, + D0D4E5902C8D9D0A007F820A /* module.modulemap */, + ); + path = Constants; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - D00117372B30341C00D87C25 /* Shared */ = { - isa = PBXNativeTarget; - buildConfigurationList = D001173C2B30341C00D87C25 /* Build configuration list for PBXNativeTarget "Shared" */; - buildPhases = ( - D00117342B30341C00D87C25 /* Sources */, - D00117352B30341C00D87C25 /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Shared; - productName = Shared; - productReference = D00117382B30341C00D87C25 /* libBurrowShared.a */; - productType = "com.apple.product-type.library.static"; - }; D020F65229E4A697002790F6 /* NetworkExtension */ = { isa = PBXNativeTarget; buildConfigurationList = D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */; @@ -307,12 +388,12 @@ D0BCC60B2A09A0C100AD070D /* Compile Rust */, D020F64F29E4A697002790F6 /* Sources */, D020F65029E4A697002790F6 /* Frameworks */, - D020F65129E4A697002790F6 /* Resources */, ); buildRules = ( ); dependencies = ( - D00117492B30373500D87C25 /* PBXTargetDependency */, + D0BF09512C8E66F1000D8DEC /* PBXTargetDependency */, + D0D4E5802C8D9C78007F820A /* PBXTargetDependency */, ); name = NetworkExtension; productName = BurrowNetworkExtension; @@ -323,16 +404,18 @@ isa = PBXNativeTarget; buildConfigurationList = D05B9F8129E39EED008CB1F9 /* Build configuration list for PBXNativeTarget "App" */; buildPhases = ( - D04A3E232BAF4AE50043EC85 /* Update Build Number */, D05B9F6E29E39EEC008CB1F9 /* Sources */, D05B9F6F29E39EEC008CB1F9 /* Frameworks */, D05B9F7029E39EEC008CB1F9 /* Resources */, + D0D4E53F2C8D996F007F820A /* Embed Frameworks */, D020F66129E4A697002790F6 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( - D00117472B30373100D87C25 /* PBXTargetDependency */, + D0BF09542C8E66FA000D8DEC /* PBXTargetDependency */, + D0F4FAD22C8DC7960068730A /* PBXTargetDependency */, + D0D4E5882C8D9C88007F820A /* PBXTargetDependency */, D020F65C29E4A697002790F6 /* PBXTargetDependency */, ); name = App; @@ -340,6 +423,71 @@ productReference = D05B9F7229E39EEC008CB1F9 /* Burrow.app */; productType = "com.apple.product-type.application"; }; + D0D4E5302C8D996F007F820A /* Core */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0D4E53C2C8D996F007F820A /* Build configuration list for PBXNativeTarget "Core" */; + buildPhases = ( + D0D4E52D2C8D996F007F820A /* Sources */, + D078F7CF2C8DA213008A8CEC /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D0F7598A2C8DB34200126CF3 /* PBXTargetDependency */, + D0F7595E2C8DB24400126CF3 /* PBXTargetDependency */, + D0F759602C8DB24400126CF3 /* PBXTargetDependency */, + ); + name = Core; + packageProductDependencies = ( + D078F7E02C8DA375008A8CEC /* GRPC */, + D078F7E22C8DA375008A8CEC /* SwiftProtobuf */, + D044EE902C8DAB2000778185 /* NIO */, + D044EE922C8DAB2000778185 /* NIOConcurrencyHelpers */, + D044EE952C8DAB2800778185 /* NIOTransportServices */, + D0F7597D2C8DB30500126CF3 /* CGRPCZlib */, + ); + productName = Core; + productReference = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; + productType = "com.apple.product-type.framework"; + }; + D0D4E5502C8D9BF2007F820A /* UI */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0D4E5552C8D9BF2007F820A /* Build configuration list for PBXNativeTarget "UI" */; + buildPhases = ( + D0D4E5522C8D9BF2007F820A /* Sources */, + D0D4E5532C8D9BF2007F820A /* Frameworks */, + D0D4E5542C8D9BF2007F820A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D0D4E56F2C8D9C5D007F820A /* PBXTargetDependency */, + ); + name = UI; + packageProductDependencies = ( + ); + productName = Core; + productReference = D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */; + productType = "com.apple.product-type.framework"; + }; + D0D4E55A2C8D9BF4007F820A /* Configuration */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0D4E55F2C8D9BF4007F820A /* Build configuration list for PBXNativeTarget "Configuration" */; + buildPhases = ( + D0F759912C8DB49E00126CF3 /* Configure Version */, + D0D4E55C2C8D9BF4007F820A /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Configuration; + packageProductDependencies = ( + ); + productName = Core; + productReference = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -347,18 +495,18 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1510; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1520; TargetAttributes = { - D00117372B30341C00D87C25 = { - CreatedOnToolsVersion = 15.1; - }; D020F65229E4A697002790F6 = { CreatedOnToolsVersion = 14.3; }; D05B9F7129E39EEC008CB1F9 = { CreatedOnToolsVersion = 14.3; }; + D0D4E5302C8D996F007F820A = { + CreatedOnToolsVersion = 16.0; + }; }; }; buildConfigurationList = D05B9F6D29E39EEC008CB1F9 /* Build configuration list for PBXProject "Burrow" */; @@ -371,6 +519,11 @@ ); mainGroup = D05B9F6929E39EEC008CB1F9; packageReferences = ( + D0B1D10E2C436152004B7823 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, + D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */, + D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */, + D044EE8F2C8DAB2000778185 /* XCRemoteSwiftPackageReference "swift-nio" */, + D044EE942C8DAB2800778185 /* XCRemoteSwiftPackageReference "swift-nio-transport-services" */, ); productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */; projectDirPath = ""; @@ -378,52 +531,32 @@ targets = ( D05B9F7129E39EEC008CB1F9 /* App */, D020F65229E4A697002790F6 /* NetworkExtension */, - D00117372B30341C00D87C25 /* Shared */, + D0D4E5502C8D9BF2007F820A /* UI */, + D0D4E5302C8D996F007F820A /* Core */, + D0D4E55A2C8D9BF4007F820A /* Configuration */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - D020F65129E4A697002790F6 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; D05B9F7029E39EEC008CB1F9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */, D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; + D0D4E5542C8D9BF2007F820A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - D04A3E232BAF4AE50043EC85 /* Update Build Number */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(PROJECT_DIR)/../Tools/version.sh", - "$(PROJECT_DIR)/../.git", - ); - name = "Update Build Number"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(PROJECT_DIR)/Configuration/Version.xcconfig", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$PROJECT_DIR/../Tools/version.sh\"\n"; - }; D0BCC60B2A09A0C100AD070D /* Compile Rust */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -444,22 +577,31 @@ shellScript = "\"${PROJECT_DIR}/NetworkExtension/libburrow/build-rust.sh\"\n"; showEnvVarsInLog = 0; }; + D0F759912C8DB49E00126CF3 /* Configure Version */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(PROJECT_DIR)/../Tools/version.sh", + "$(PROJECT_DIR)/../.git", + ); + name = "Configure Version"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(PROJECT_DIR)/Configuration/Version.xcconfig", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$PROJECT_DIR/../Tools/version.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - D00117342B30341C00D87C25 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D001173B2B30341C00D87C25 /* Logging.swift in Sources */, - 0BA6D73C2BA6393200BD4B55 /* NWConnection+Async.swift in Sources */, - D08252762B5C9FC4005DA378 /* Constants.swift in Sources */, - 0BA6D73E2BA6394B00BD4B55 /* DataTypes.swift in Sources */, - 0BA6D73B2BA638D900BD4B55 /* Client.swift in Sources */, - 0BA6D73D2BA6393B00BD4B55 /* NewlineProtocolFramer.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; D020F64F29E4A697002790F6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -472,60 +614,104 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D0FAB59A2B818B9600F6A84B /* Network.swift in Sources */, - D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */, - D0FAB5982B818B8200F6A84B /* TunnelStatusView.swift in Sources */, - 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */, - D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */, - D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */, - D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */, - D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */, D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */, - D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */, - D032E6522B8A79C20006B8AD /* HackClub.swift in Sources */, D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */, - D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */, - D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */, - D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */, - D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D4E52D2C8D996F007F820A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0F759612C8DB24B00126CF3 /* grpc-swift-config.json in Sources */, + D0F759622C8DB24B00126CF3 /* swift-protobuf-config.json in Sources */, + D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */, + D0D4E56B2C8D9C2F007F820A /* Logging.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D4E5522C8D9BF2007F820A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0D4E5712C8D9C6F007F820A /* HackClub.swift in Sources */, + D0D4E5722C8D9C6F007F820A /* Network.swift in Sources */, + D0D4E5732C8D9C6F007F820A /* WireGuard.swift in Sources */, + D0D4E5742C8D9C6F007F820A /* BurrowView.swift in Sources */, + D0D4E5752C8D9C6F007F820A /* FloatingButtonStyle.swift in Sources */, + D0D4E5762C8D9C6F007F820A /* MenuItemToggleView.swift in Sources */, + D0D4E5772C8D9C6F007F820A /* NetworkCarouselView.swift in Sources */, + D0D4E5782C8D9C6F007F820A /* NetworkExtension+Async.swift in Sources */, + D0D4E5792C8D9C6F007F820A /* NetworkExtensionTunnel.swift in Sources */, + D0D4E57A2C8D9C6F007F820A /* NetworkView.swift in Sources */, + D0D4E57B2C8D9C6F007F820A /* OAuth2.swift in Sources */, + D0D4E57C2C8D9C6F007F820A /* Tunnel.swift in Sources */, + D0D4E57D2C8D9C6F007F820A /* TunnelButton.swift in Sources */, + D0D4E57E2C8D9C6F007F820A /* TunnelStatusView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D4E55C2C8D9BF4007F820A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0D4E5922C8D9D15007F820A /* Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - D00117472B30373100D87C25 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D00117372B30341C00D87C25 /* Shared */; - targetProxy = D00117462B30373100D87C25 /* PBXContainerItemProxy */; - }; - D00117492B30373500D87C25 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D00117372B30341C00D87C25 /* Shared */; - targetProxy = D00117482B30373500D87C25 /* PBXContainerItemProxy */; - }; D020F65C29E4A697002790F6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D020F65229E4A697002790F6 /* NetworkExtension */; targetProxy = D020F65B29E4A697002790F6 /* PBXContainerItemProxy */; }; + D0BF09512C8E66F1000D8DEC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E55A2C8D9BF4007F820A /* Configuration */; + targetProxy = D0BF09502C8E66F1000D8DEC /* PBXContainerItemProxy */; + }; + D0BF09542C8E66FA000D8DEC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E55A2C8D9BF4007F820A /* Configuration */; + targetProxy = D0BF09532C8E66FA000D8DEC /* PBXContainerItemProxy */; + }; + D0D4E56F2C8D9C5D007F820A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E5302C8D996F007F820A /* Core */; + targetProxy = D0D4E56E2C8D9C5D007F820A /* PBXContainerItemProxy */; + }; + D0D4E5802C8D9C78007F820A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E5302C8D996F007F820A /* Core */; + targetProxy = D0D4E57F2C8D9C78007F820A /* PBXContainerItemProxy */; + }; + D0D4E5882C8D9C88007F820A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E5502C8D9BF2007F820A /* UI */; + targetProxy = D0D4E5872C8D9C88007F820A /* PBXContainerItemProxy */; + }; + D0F4FAD22C8DC7960068730A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E5302C8D996F007F820A /* Core */; + targetProxy = D0F4FAD12C8DC7960068730A /* PBXContainerItemProxy */; + }; + D0F7595E2C8DB24400126CF3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D0F7595D2C8DB24400126CF3 /* GRPCSwiftPlugin */; + }; + D0F759602C8DB24400126CF3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D0F7595F2C8DB24400126CF3 /* SwiftProtobufPlugin */; + }; + D0F7598A2C8DB34200126CF3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D0F759892C8DB34200126CF3 /* GRPC */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - D001173D2B30341C00D87C25 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = D00117422B30348D00D87C25 /* Shared.xcconfig */; - buildSettings = { - }; - name = Debug; - }; - D001173E2B30341C00D87C25 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = D00117422B30348D00D87C25 /* Shared.xcconfig */; - buildSettings = { - }; - name = Release; - }; D020F65F29E4A697002790F6 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */; @@ -568,18 +754,51 @@ }; name = Release; }; + D0D4E53D2C8D996F007F820A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0D4E4F72C8D941D007F820A /* Framework.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + D0D4E53E2C8D996F007F820A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0D4E4F72C8D941D007F820A /* Framework.xcconfig */; + buildSettings = { + }; + name = Release; + }; + D0D4E5562C8D9BF2007F820A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0BF09582C8E6789000D8DEC /* UI.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + D0D4E5572C8D9BF2007F820A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0BF09582C8E6789000D8DEC /* UI.xcconfig */; + buildSettings = { + }; + name = Release; + }; + D0D4E5602C8D9BF4007F820A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D00117422B30348D00D87C25 /* Configuration.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + D0D4E5612C8D9BF4007F820A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D00117422B30348D00D87C25 /* Configuration.xcconfig */; + buildSettings = { + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - D001173C2B30341C00D87C25 /* Build configuration list for PBXNativeTarget "Shared" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D001173D2B30341C00D87C25 /* Debug */, - D001173E2B30341C00D87C25 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -607,7 +826,130 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D0D4E53C2C8D996F007F820A /* Build configuration list for PBXNativeTarget "Core" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0D4E53D2C8D996F007F820A /* Debug */, + D0D4E53E2C8D996F007F820A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D0D4E5552C8D9BF2007F820A /* Build configuration list for PBXNativeTarget "UI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0D4E5562C8D9BF2007F820A /* Debug */, + D0D4E5572C8D9BF2007F820A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D0D4E55F2C8D9BF4007F820A /* Build configuration list for PBXNativeTarget "Configuration" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0D4E5602C8D9BF4007F820A /* Debug */, + D0D4E5612C8D9BF4007F820A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + D044EE8F2C8DAB2000778185 /* XCRemoteSwiftPackageReference "swift-nio" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-nio.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.72.0; + }; + }; + D044EE942C8DAB2800778185 /* XCRemoteSwiftPackageReference "swift-nio-transport-services" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-nio-transport-services.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.21.0; + }; + }; + D0B1D10E2C436152004B7823 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-async-algorithms.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.1; + }; + }; + D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/grpc/grpc-swift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.23.0; + }; + }; + D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-protobuf.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.28.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D044EE902C8DAB2000778185 /* NIO */ = { + isa = XCSwiftPackageProductDependency; + package = D044EE8F2C8DAB2000778185 /* XCRemoteSwiftPackageReference "swift-nio" */; + productName = NIO; + }; + D044EE922C8DAB2000778185 /* NIOConcurrencyHelpers */ = { + isa = XCSwiftPackageProductDependency; + package = D044EE8F2C8DAB2000778185 /* XCRemoteSwiftPackageReference "swift-nio" */; + productName = NIOConcurrencyHelpers; + }; + D044EE952C8DAB2800778185 /* NIOTransportServices */ = { + isa = XCSwiftPackageProductDependency; + package = D044EE942C8DAB2800778185 /* XCRemoteSwiftPackageReference "swift-nio-transport-services" */; + productName = NIOTransportServices; + }; + D078F7E02C8DA375008A8CEC /* GRPC */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = GRPC; + }; + D078F7E22C8DA375008A8CEC /* SwiftProtobuf */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */; + productName = SwiftProtobuf; + }; + D0B1D10F2C436152004B7823 /* AsyncAlgorithms */ = { + isa = XCSwiftPackageProductDependency; + package = D0B1D10E2C436152004B7823 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */; + productName = AsyncAlgorithms; + }; + D0F7595D2C8DB24400126CF3 /* GRPCSwiftPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = "plugin:GRPCSwiftPlugin"; + }; + D0F7595F2C8DB24400126CF3 /* SwiftProtobufPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */; + productName = "plugin:SwiftProtobufPlugin"; + }; + D0F7597D2C8DB30500126CF3 /* CGRPCZlib */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = CGRPCZlib; + }; + D0F759892C8DB34200126CF3 /* GRPC */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = GRPC; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = D05B9F6A29E39EEC008CB1F9 /* Project object */; } diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..739b77c --- /dev/null +++ b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,123 @@ +{ + "originHash" : "fa512b990383b7e309c5854a5279817052294a8191a6d3c55c49cfb38e88c0c3", + "pins" : [ + { + "identity" : "grpc-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift.git", + "state" : { + "revision" : "6a90b7e77e29f9bda6c2b3a4165a40d6c02cfda1", + "version" : "1.23.0" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "6ae9a051f76b81cc668305ceed5b0e0a7fd93d20", + "version" : "1.0.1" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "9746cf80e29edfef2a39924a66731249223f42a3", + "version" : "2.72.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "d1ead62745cc3269e482f1c51f27608057174379", + "version" : "1.24.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "b5f7062b60e4add1e8c343ba4eb8da2e324b3a94", + "version" : "1.34.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "7b84abbdcef69cc3be6573ac12440220789dcd69", + "version" : "2.27.2" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "38ac8221dd20674682148d6451367f89c2652980", + "version" : "1.21.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5", + "version" : "1.28.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "d2ba781702a1d8285419c15ee62fd734a9437ff5", + "version" : "1.3.2" + } + } + ], + "version" : 3 +} diff --git a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme index 670823d..a524e87 100644 --- a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme +++ b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -1,10 +1,11 @@ + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> = { - guard let groupContainerURL = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else { - return .failure(.invalidAppGroupIdentifier) - } - return .success(groupContainerURL) - }() public static var socketURL: URL { get throws { try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory) } } - public static var dbURL: URL { + public static var databaseURL: URL { get throws { try groupContainerURL.appending(component: "burrow.db", directoryHint: .notDirectory) } } + + private static var groupContainerURL: URL { + get throws { try _groupContainerURL.get() } + } + private static let _groupContainerURL: Result = { + switch FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) { + case .some(let url): .success(url) + case .none: .failure(.invalidAppGroupIdentifier) + } + }() +} + +extension Logger { + @_dynamicReplacement(for: subsystem) + public static var subsystem: String { Constants.bundleIdentifier } } diff --git a/Apple/Shared/Constants/module.modulemap b/Apple/Configuration/Constants/module.modulemap similarity index 66% rename from Apple/Shared/Constants/module.modulemap rename to Apple/Configuration/Constants/module.modulemap index 7ee21fc..0e60f32 100644 --- a/Apple/Shared/Constants/module.modulemap +++ b/Apple/Configuration/Constants/module.modulemap @@ -1,4 +1,4 @@ -module Constants { +module CConstants { header "Constants.h" export * } diff --git a/Apple/Configuration/Debug.xcconfig b/Apple/Configuration/Debug.xcconfig new file mode 100644 index 0000000..9529dbd --- /dev/null +++ b/Apple/Configuration/Debug.xcconfig @@ -0,0 +1,26 @@ +// Release +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +SWIFT_COMPILATION_MODE = wholemodule +SWIFT_OPTIMIZATION_LEVEL = -Osize +LLVM_LTO = YES +DEAD_CODE_STRIPPING = YES +STRIP_INSTALLED_PRODUCT = YES +STRIP_SWIFT_SYMBOLS = YES +COPY_PHASE_STRIP = NO +VALIDATE_PRODUCT = YES +ENABLE_MODULE_VERIFIER = YES + +// Debug +ONLY_ACTIVE_ARCH[config=Debug] = YES +DEBUG_INFORMATION_FORMAT[config=Debug] = dwarf +ENABLE_TESTABILITY[config=Debug] = YES +GCC_PREPROCESSOR_DEFINITIONS[config=Debug] = DEBUG=1 $(inherited) +SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug] = DEBUG +SWIFT_COMPILATION_MODE[config=Debug] = singlefile +LLVM_LTO[config=Debug] = NO +DEAD_CODE_STRIPPING[config=Debug] = NO +VALIDATE_PRODUCT[config=Debug] = NO +STRIP_INSTALLED_PRODUCT[config=Debug] = NO +STRIP_SWIFT_SYMBOLS[config=Debug] = NO +ENABLE_MODULE_VERIFIER[config=Debug] = NO diff --git a/Apple/Configuration/Extension.xcconfig b/Apple/Configuration/Extension.xcconfig index f8d90a3..5885c31 100644 --- a/Apple/Configuration/Extension.xcconfig +++ b/Apple/Configuration/Extension.xcconfig @@ -1,4 +1,6 @@ -MERGED_BINARY_TYPE = manual +LD_EXPORT_SYMBOLS = NO + +OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -disable-autolink-framework -Xfrontend UIKit -Xfrontend -disable-autolink-framework -Xfrontend AppKit -Xfrontend -disable-autolink-framework -Xfrontend SwiftUI LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks -LD_RUNPATH_SEARCH_PATHS[sdk=macos*] = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks +LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks diff --git a/Apple/Configuration/Framework.xcconfig b/Apple/Configuration/Framework.xcconfig new file mode 100644 index 0000000..6fa4f19 --- /dev/null +++ b/Apple/Configuration/Framework.xcconfig @@ -0,0 +1,14 @@ +PRODUCT_NAME = Burrow$(TARGET_NAME:c99extidentifier) +PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).$(TARGET_NAME:c99extidentifier) +APPLICATION_EXTENSION_API_ONLY = YES +SWIFT_INSTALL_OBJC_HEADER = NO +SWIFT_SKIP_AUTOLINKING_FRAMEWORKS = YES +SWIFT_SKIP_AUTOLINKING_LIBRARIES = YES + +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks +LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks + +DYLIB_INSTALL_NAME_BASE = @rpath +DYLIB_COMPATIBILITY_VERSION = 1 +DYLIB_CURRENT_VERSION = 1 +VERSIONING_SYSTEM = diff --git a/Apple/Core/Client.swift b/Apple/Core/Client.swift new file mode 100644 index 0000000..8874e3b --- /dev/null +++ b/Apple/Core/Client.swift @@ -0,0 +1,32 @@ +import GRPC +import NIOTransportServices + +public typealias TunnelClient = Burrow_TunnelAsyncClient +public typealias NetworksClient = Burrow_NetworksAsyncClient + +public protocol Client { + init(channel: GRPCChannel) +} + +extension Client { + public static func unix(socketURL: URL) -> Self { + let group = NIOTSEventLoopGroup() + let configuration = ClientConnection.Configuration.default( + target: .unixDomainSocket(socketURL.path), + eventLoopGroup: group + ) + return Self(channel: ClientConnection(configuration: configuration)) + } +} + +extension TunnelClient: Client { + public init(channel: any GRPCChannel) { + self.init(channel: channel, defaultCallOptions: .init(), interceptors: .none) + } +} + +extension NetworksClient: Client { + public init(channel: any GRPCChannel) { + self.init(channel: channel, defaultCallOptions: .init(), interceptors: .none) + } +} diff --git a/Apple/Core/Client/burrow.proto b/Apple/Core/Client/burrow.proto new file mode 120000 index 0000000..03e86a5 --- /dev/null +++ b/Apple/Core/Client/burrow.proto @@ -0,0 +1 @@ +../../../proto/burrow.proto \ No newline at end of file diff --git a/Apple/Core/Client/grpc-swift-config.json b/Apple/Core/Client/grpc-swift-config.json new file mode 100644 index 0000000..2d89698 --- /dev/null +++ b/Apple/Core/Client/grpc-swift-config.json @@ -0,0 +1,11 @@ +{ + "invocations": [ + { + "protoFiles": [ + "burrow.proto", + ], + "server": false, + "visibility": "public" + } + ] +} diff --git a/Apple/Core/Client/swift-protobuf-config.json b/Apple/Core/Client/swift-protobuf-config.json new file mode 100644 index 0000000..87aaec3 --- /dev/null +++ b/Apple/Core/Client/swift-protobuf-config.json @@ -0,0 +1,10 @@ +{ + "invocations": [ + { + "protoFiles": [ + "burrow.proto", + ], + "visibility": "public" + } + ] +} diff --git a/Apple/Shared/Logging.swift b/Apple/Core/Logging.swift similarity index 88% rename from Apple/Shared/Logging.swift rename to Apple/Core/Logging.swift index 36f024c..ba40888 100644 --- a/Apple/Shared/Logging.swift +++ b/Apple/Core/Logging.swift @@ -4,7 +4,7 @@ import os extension Logger { private static let loggers: OSAllocatedUnfairLock<[String: Logger]> = OSAllocatedUnfairLock(initialState: [:]) - public static let subsystem = Constants.bundleIdentifier + public dynamic static var subsystem: String { "com.hackclub.burrow" } public static func logger(for type: Any.Type) -> Logger { let category = String(describing: type) diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 89e0de6..a8e42e0 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -1,92 +1,74 @@ -import BurrowShared +import AsyncAlgorithms +import BurrowConfiguration +import BurrowCore import libburrow import NetworkExtension import os class PacketTunnelProvider: NEPacketTunnelProvider { + enum Error: Swift.Error { + case missingTunnelConfiguration + } + private let logger = Logger.logger(for: PacketTunnelProvider.self) - private var client: Client? + + private var client: TunnelClient { + get throws { try _client.get() } + } + private let _client: Result = Result { + try TunnelClient.unix(socketURL: Constants.socketURL) + } override init() { do { libburrow.spawnInProcess( socketPath: try Constants.socketURL.path(percentEncoded: false), - dbPath: try Constants.dbURL.path(percentEncoded: false) + databasePath: try Constants.databaseURL.path(percentEncoded: false) ) } catch { - logger.error("Failed to spawn: \(error)") + logger.error("Failed to spawn networking thread: \(error)") } } override func startTunnel(options: [String: NSObject]? = nil) async throws { do { - let client = try Client() - self.client = client - register_events(client) - - _ = try await self.loadTunSettings() - let startRequest = Start( - tun: Start.TunOptions( - name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: [] - ) - ) - let response = try await client.request(startRequest, type: BurrowResult.self) - self.logger.log("Received start server response: \(String(describing: response))") + let configuration = try await Array(client.tunnelConfiguration(.init()).prefix(1)).first + guard let settings = configuration?.settings else { + throw Error.missingTunnelConfiguration + } + try await setTunnelNetworkSettings(settings) + _ = try await client.tunnelStart(.init()) + logger.log("Started tunnel with network settings: \(settings)") } catch { - self.logger.error("Failed to start tunnel: \(error)") + logger.error("Failed to start tunnel: \(error)") throw error } } override func stopTunnel(with reason: NEProviderStopReason) async { do { - let client = try Client() - _ = try await client.single_request("Stop", type: BurrowResult.self) - self.logger.log("Stopped client.") + _ = try await client.tunnelStop(.init()) + logger.log("Stopped client") } catch { - self.logger.error("Failed to stop tunnel: \(error)") - } - } - func loadTunSettings() async throws -> ServerConfig { - guard let client = self.client else { - throw BurrowError.noClient - } - let srvConfig = try await client.single_request("ServerConfig", type: BurrowResult.self) - guard let serverconfig = srvConfig.Ok else { - throw BurrowError.resultIsError - } - guard let tunNs = generateTunSettings(from: serverconfig) else { - throw BurrowError.addrDoesntExist - } - try await self.setTunnelNetworkSettings(tunNs) - self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") - return serverconfig - } - private func generateTunSettings(from: ServerConfig) -> NETunnelNetworkSettings? { - // Using a makeshift remote tunnel address - let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") - var v4Addresses = [String]() - var v6Addresses = [String]() - for addr in from.address { - if IPv4Address(addr) != nil { - v6Addresses.append(addr) - } - if IPv6Address(addr) != nil { - v4Addresses.append(addr) - } - } - nst.ipv4Settings = NEIPv4Settings(addresses: v4Addresses, subnetMasks: v4Addresses.map { _ in - "255.255.255.0" - }) - nst.ipv6Settings = NEIPv6Settings(addresses: v6Addresses, networkPrefixLengths: v6Addresses.map { _ in 64 }) - logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") - return nst - } - func register_events(_ client: Client) { - client.on_event(.ConfigChange) { (cfig: ServerConfig) in - self.logger.info("Config Change Notification: \(String(describing: cfig))") - self.setTunnelNetworkSettings(self.generateTunSettings(from: cfig)) - self.logger.info("Updated Tunnel Network Settings.") + logger.error("Failed to stop tunnel: \(error)") } } } + +extension Burrow_TunnelConfigurationResponse { + fileprivate var settings: NEPacketTunnelNetworkSettings { + let ipv6Addresses = addresses.filter { IPv6Address($0) != nil } + + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") + settings.mtu = NSNumber(value: mtu) + settings.ipv4Settings = NEIPv4Settings( + addresses: addresses.filter { IPv4Address($0) != nil }, + subnetMasks: ["255.255.255.0"] + ) + settings.ipv6Settings = NEIPv6Settings( + addresses: ipv6Addresses, + networkPrefixLengths: ipv6Addresses.map { _ in 64 } + ) + return settings + } +} diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index 00c3652..6f455a9 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -68,11 +68,12 @@ else CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin" fi -CARGO_PATH="$(dirname $(readlink -f $(which protoc))):$CARGO_PATH" +PROTOC=$(readlink -f $(which protoc)) +CARGO_PATH="$(dirname $PROTOC):$CARGO_PATH" # Run cargo without the various environment variables set by Xcode. # Those variables can confuse cargo and the build scripts it runs. -env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}" +env -i PATH="$CARGO_PATH" PROTOC="$PROTOC" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}" mkdir -p "${BUILT_PRODUCTS_DIR}" diff --git a/Apple/NetworkExtension/libburrow/libburrow.h b/Apple/NetworkExtension/libburrow/libburrow.h index 2b578ab..59b4734 100644 --- a/Apple/NetworkExtension/libburrow/libburrow.h +++ b/Apple/NetworkExtension/libburrow/libburrow.h @@ -1,2 +1,2 @@ -__attribute__((__swift_name__("spawnInProcess(socketPath:dbPath:)"))) +__attribute__((__swift_name__("spawnInProcess(socketPath:databasePath:)"))) extern void spawn_in_process(const char * __nullable socket_path, const char * __nullable db_path); diff --git a/Apple/Shared/Client.swift b/Apple/Shared/Client.swift deleted file mode 100644 index f643c6c..0000000 --- a/Apple/Shared/Client.swift +++ /dev/null @@ -1,106 +0,0 @@ -import Foundation -import Network - -public final class Client { - let connection: NWConnection - - private let logger = Logger.logger(for: Client.self) - private var generator = SystemRandomNumberGenerator() - private var continuations: [UInt: UnsafeContinuation] = [:] - private var eventMap: [NotificationType: [(Data) throws -> Void]] = [:] - private var task: Task? - - public convenience init() throws { - self.init(url: try Constants.socketURL) - } - - public init(url: URL) { - let endpoint: NWEndpoint - if url.isFileURL { - endpoint = .unix(path: url.path(percentEncoded: false)) - } else { - endpoint = .url(url) - } - - let parameters = NWParameters.tcp - parameters.defaultProtocolStack - .applicationProtocols - .insert(NWProtocolFramer.Options(definition: NewlineProtocolFramer.definition), at: 0) - let connection = NWConnection(to: endpoint, using: parameters) - connection.start(queue: .global()) - self.connection = connection - self.task = Task { [weak self] in - while true { - let (data, _, _) = try await connection.receiveMessage() - let peek = try JSONDecoder().decode(MessagePeek.self, from: data) - switch peek.type { - case .Response: - let response = try JSONDecoder().decode(ResponsePeek.self, from: data) - self?.logger.info("Received response for \(response.id)") - guard let continuations = self?.continuations else {return} - self?.logger.debug("All keys in continuation table: \(continuations.keys)") - guard let continuation = self?.continuations[response.id] else { return } - self?.logger.debug("Got matching continuation") - continuation.resume(returning: data) - case .Notification: - let peek = try JSONDecoder().decode(NotificationPeek.self, from: data) - guard let handlers = self?.eventMap[peek.method] else { continue } - _ = try handlers.map { try $0(data) } - default: - continue - } - } - } - } - private func send(_ request: T) async throws -> U { - let data: Data = try await withUnsafeThrowingContinuation { continuation in - continuations[request.id] = continuation - do { - let data = try JSONEncoder().encode(request) - let completion: NWConnection.SendCompletion = .contentProcessed { error in - guard let error = error else { - return - } - continuation.resume(throwing: error) - } - connection.send(content: data, completion: completion) - } catch { - continuation.resume(throwing: error) - return - } - } - self.logger.debug("Got response data: \(String(describing: data.base64EncodedString()))") - let res = try JSONDecoder().decode(Response.self, from: data) - self.logger.debug("Got response data decoded: \(String(describing: res))") - return res.result - } - public func request(_ request: T, type: U.Type = U.self) async throws -> U { - let req = BurrowRequest( - id: generator.next(upperBound: UInt.max), - command: request - ) - return try await send(req) - } - public func single_request(_ request: String, type: U.Type = U.self) async throws -> U { - let req = BurrowSimpleRequest( - id: generator.next(upperBound: UInt.max), - command: request - ) - return try await send(req) - } - public func on_event(_ event: NotificationType, callable: @escaping (T) throws -> Void) { - let action = { data in - let decoded = try JSONDecoder().decode(Notification.self, from: data) - try callable(decoded.params) - } - if eventMap[event] != nil { - eventMap[event]?.append(action) - } else { - eventMap[event] = [action] - } - } - - deinit { - connection.cancel() - } -} diff --git a/Apple/Shared/DataTypes.swift b/Apple/Shared/DataTypes.swift deleted file mode 100644 index ac49abc..0000000 --- a/Apple/Shared/DataTypes.swift +++ /dev/null @@ -1,139 +0,0 @@ -import Foundation - -// swiftlint:disable identifier_name raw_value_for_camel_cased_codable_enum -public enum BurrowError: Error { - case addrDoesntExist - case resultIsError - case cantParseResult - case resultIsNone - case noClient -} - -public protocol Request: Codable where Params: Codable { - associatedtype Params - - var id: UInt { get set } - var method: String { get set } - var params: Params? { get set } -} - -public enum MessageType: String, Codable { - case Request - case Response - case Notification -} - -public struct MessagePeek: Codable { - public var type: MessageType - public init(type: MessageType) { - self.type = type - } -} - -public struct BurrowSimpleRequest: Request { - public var id: UInt - public var method: String - public var params: String? - public init(id: UInt, command: String, params: String? = nil) { - self.id = id - self.method = command - self.params = params - } -} - -public struct BurrowRequest: Request where T: Codable { - public var id: UInt - public var method: String - public var params: T? - public init(id: UInt, command: T) { - self.id = id - self.method = "\(T.self)" - self.params = command - } -} - -public struct Response: Decodable where T: Decodable { - public var id: UInt - public var result: T - public init(id: UInt, result: T) { - self.id = id - self.result = result - } -} - -public struct ResponsePeek: Codable { - public var id: UInt - public init(id: UInt) { - self.id = id - } -} - -public enum NotificationType: String, Codable { - case ConfigChange -} - -public struct Notification: Codable where T: Codable { - public var method: NotificationType - public var params: T - public init(method: NotificationType, params: T) { - self.method = method - self.params = params - } -} - -public struct NotificationPeek: Codable { - public var method: NotificationType - public init(method: NotificationType) { - self.method = method - } -} - -public struct AnyResponseData: Codable { - public var type: String - public init(type: String) { - self.type = type - } -} - -public struct BurrowResult: Codable where T: Codable { - public var Ok: T? - public var Err: String? - public init(Ok: T, Err: String? = nil) { - self.Ok = Ok - self.Err = Err - } -} - -public struct ServerConfig: Codable { - public let address: [String] - public let name: String? - public let mtu: Int32? - public init(address: [String], name: String?, mtu: Int32?) { - self.address = address - self.name = name - self.mtu = mtu - } -} - -public struct Start: Codable { - public struct TunOptions: Codable { - public let name: String? - public let no_pi: Bool - public let tun_excl: Bool - public let tun_retrieve: Bool - public let address: [String] - public init(name: String?, no_pi: Bool, tun_excl: Bool, tun_retrieve: Bool, address: [String]) { - self.name = name - self.no_pi = no_pi - self.tun_excl = tun_excl - self.tun_retrieve = tun_retrieve - self.address = address - } - } - public let tun: TunOptions - public init(tun: TunOptions) { - self.tun = tun - } -} - -// swiftlint:enable identifier_name raw_value_for_camel_cased_codable_enum diff --git a/Apple/Shared/NWConnection+Async.swift b/Apple/Shared/NWConnection+Async.swift deleted file mode 100644 index c21fdc0..0000000 --- a/Apple/Shared/NWConnection+Async.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import Network - -extension NWConnection { - // swiftlint:disable:next large_tuple - func receiveMessage() async throws -> (Data, NWConnection.ContentContext?, Bool) { - try await withUnsafeThrowingContinuation { continuation in - receiveMessage { completeContent, contentContext, isComplete, error in - if let error { - continuation.resume(throwing: error) - } else { - guard let completeContent = completeContent else { - fatalError("Both error and completeContent were nil") - } - continuation.resume(returning: (completeContent, contentContext, isComplete)) - } - } - } - } - - func send(content: Data) async throws { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - send(content: content, completion: .contentProcessed { error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: ()) - } - }) - } - } -} diff --git a/Apple/Shared/NewlineProtocolFramer.swift b/Apple/Shared/NewlineProtocolFramer.swift deleted file mode 100644 index d2f71e5..0000000 --- a/Apple/Shared/NewlineProtocolFramer.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation -import Network - -final class NewlineProtocolFramer: NWProtocolFramerImplementation { - private static let delimeter: UInt8 = 10 // `\n` - - static let definition = NWProtocolFramer.Definition(implementation: NewlineProtocolFramer.self) - static let label = "Lines" - - init(framer: NWProtocolFramer.Instance) { } - - func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { .ready } - func stop(framer: NWProtocolFramer.Instance) -> Bool { true } - - func wakeup(framer: NWProtocolFramer.Instance) { } - func cleanup(framer: NWProtocolFramer.Instance) { } - - func handleInput(framer: NWProtocolFramer.Instance) -> Int { - while true { - var result: [Data] = [] - let parsed = framer.parseInput(minimumIncompleteLength: 1, maximumLength: 16_000) { buffer, _ in - guard let buffer else { return 0 } - var lines = buffer - .split(separator: Self.delimeter, omittingEmptySubsequences: false) - .map { Data($0) } - guard lines.count > 1 else { return 0 } - _ = lines.popLast() - - result = lines - return lines.reduce(lines.count) { $0 + $1.count } - } - - guard parsed && !result.isEmpty else { break } - - for line in result { - framer.deliverInput(data: line, message: .init(instance: framer), isComplete: true) - } - } - return 0 - } - - func handleOutput( - framer: NWProtocolFramer.Instance, - message: NWProtocolFramer.Message, - messageLength: Int, - isComplete: Bool - ) { - do { - try framer.writeOutputNoCopy(length: messageLength) - framer.writeOutput(data: [Self.delimeter]) - } catch { - } - } -} diff --git a/Apple/App/Assets.xcassets/AccentColor.colorset/Contents.json b/Apple/UI/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/AccentColor.colorset/Contents.json rename to Apple/UI/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/100.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/100.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/100.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/100.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/114.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/114.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/114.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/114.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/120.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/120.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/120.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/120.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/128.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/128.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/128.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/128.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/144.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/144.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/144.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/144.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/152.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/152.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/152.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/152.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/16.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/16.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/16.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/16.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/167.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/167.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/167.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/167.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/172.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/172.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/172.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/172.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/180.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/180.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/180.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/180.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/196.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/196.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/196.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/196.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/20.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/20.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/20.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/20.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/216.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/216.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/216.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/216.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/256.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/256.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/256.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/256.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/29.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/29.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/29.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/29.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/32.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/32.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/32.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/32.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/40.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/40.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/40.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/40.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/48.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/48.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/48.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/48.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/50.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/50.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/50.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/50.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/512.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/512.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/512.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/512.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/55.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/55.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/55.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/55.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/57.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/57.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/57.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/57.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/58.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/58.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/58.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/58.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/60.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/60.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/60.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/60.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/64.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/64.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/64.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/64.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/72.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/72.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/72.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/72.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/76.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/76.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/76.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/76.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/80.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/80.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/80.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/80.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/87.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/87.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/87.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/87.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/88.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/88.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/88.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/88.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/Apple/UI/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Apple/App/Assets.xcassets/Contents.json b/Apple/UI/Assets.xcassets/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/Contents.json rename to Apple/UI/Assets.xcassets/Contents.json diff --git a/Apple/App/Assets.xcassets/HackClub.colorset/Contents.json b/Apple/UI/Assets.xcassets/HackClub.colorset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/HackClub.colorset/Contents.json rename to Apple/UI/Assets.xcassets/HackClub.colorset/Contents.json diff --git a/Apple/App/Assets.xcassets/HackClub.imageset/Contents.json b/Apple/UI/Assets.xcassets/HackClub.imageset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/HackClub.imageset/Contents.json rename to Apple/UI/Assets.xcassets/HackClub.imageset/Contents.json diff --git a/Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf b/Apple/UI/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf similarity index 100% rename from Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf rename to Apple/UI/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf diff --git a/Apple/App/Assets.xcassets/WireGuard.colorset/Contents.json b/Apple/UI/Assets.xcassets/WireGuard.colorset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/WireGuard.colorset/Contents.json rename to Apple/UI/Assets.xcassets/WireGuard.colorset/Contents.json diff --git a/Apple/App/Assets.xcassets/WireGuard.imageset/Contents.json b/Apple/UI/Assets.xcassets/WireGuard.imageset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/WireGuard.imageset/Contents.json rename to Apple/UI/Assets.xcassets/WireGuard.imageset/Contents.json diff --git a/Apple/App/Assets.xcassets/WireGuard.imageset/WireGuard.svg b/Apple/UI/Assets.xcassets/WireGuard.imageset/WireGuard.svg similarity index 100% rename from Apple/App/Assets.xcassets/WireGuard.imageset/WireGuard.svg rename to Apple/UI/Assets.xcassets/WireGuard.imageset/WireGuard.svg diff --git a/Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json b/Apple/UI/Assets.xcassets/WireGuardTitle.imageset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json rename to Apple/UI/Assets.xcassets/WireGuardTitle.imageset/Contents.json diff --git a/Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg b/Apple/UI/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg similarity index 100% rename from Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg rename to Apple/UI/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg diff --git a/Apple/App/BurrowView.swift b/Apple/UI/BurrowView.swift similarity index 95% rename from Apple/App/BurrowView.swift rename to Apple/UI/BurrowView.swift index 3a53762..96467c7 100644 --- a/Apple/App/BurrowView.swift +++ b/Apple/UI/BurrowView.swift @@ -2,11 +2,11 @@ import AuthenticationServices import SwiftUI #if !os(macOS) -struct BurrowView: View { +public struct BurrowView: View { @Environment(\.webAuthenticationSession) private var webAuthenticationSession - var body: some View { + public var body: some View { NavigationStack { VStack { HStack { @@ -35,6 +35,9 @@ struct BurrowView: View { } } + public init() { + } + private func addHackClubNetwork() { Task { try await authenticateWithSlack() diff --git a/Apple/App/FloatingButtonStyle.swift b/Apple/UI/FloatingButtonStyle.swift similarity index 100% rename from Apple/App/FloatingButtonStyle.swift rename to Apple/UI/FloatingButtonStyle.swift diff --git a/Apple/App/MenuItemToggleView.swift b/Apple/UI/MenuItemToggleView.swift similarity index 87% rename from Apple/App/MenuItemToggleView.swift rename to Apple/UI/MenuItemToggleView.swift index 07db51d..ef5e8ee 100644 --- a/Apple/App/MenuItemToggleView.swift +++ b/Apple/UI/MenuItemToggleView.swift @@ -7,11 +7,11 @@ import SwiftUI -struct MenuItemToggleView: View { +public struct MenuItemToggleView: View { @Environment(\.tunnel) var tunnel: Tunnel - var body: some View { + public var body: some View { HStack { VStack(alignment: .leading) { Text("Burrow") @@ -30,10 +30,13 @@ struct MenuItemToggleView: View { .padding(10) .frame(minWidth: 300, minHeight: 32, maxHeight: 32) } + + public init() { + } } extension Tunnel { - fileprivate var toggleDisabled: Bool { + @MainActor fileprivate var toggleDisabled: Bool { switch status { case .disconnected, .permissionRequired, .connected, .disconnecting: false @@ -42,7 +45,7 @@ extension Tunnel { } } - var toggleIsOn: Binding { + @MainActor var toggleIsOn: Binding { Binding { switch status { case .connecting, .reasserting, .connected: diff --git a/Apple/App/NetworkCarouselView.swift b/Apple/UI/NetworkCarouselView.swift similarity index 90% rename from Apple/App/NetworkCarouselView.swift rename to Apple/UI/NetworkCarouselView.swift index b120c60..f969356 100644 --- a/Apple/App/NetworkCarouselView.swift +++ b/Apple/UI/NetworkCarouselView.swift @@ -2,10 +2,10 @@ import SwiftUI struct NetworkCarouselView: View { var networks: [any Network] = [ - HackClub(id: "1"), - HackClub(id: "2"), - WireGuard(id: "4"), - HackClub(id: "5"), + HackClub(id: 1), + HackClub(id: 2), + WireGuard(id: 4), + HackClub(id: 5) ] var body: some View { diff --git a/Apple/App/NetworkExtension+Async.swift b/Apple/UI/NetworkExtension+Async.swift similarity index 82% rename from Apple/App/NetworkExtension+Async.swift rename to Apple/UI/NetworkExtension+Async.swift index 4833efb..5820e7f 100644 --- a/Apple/App/NetworkExtension+Async.swift +++ b/Apple/UI/NetworkExtension+Async.swift @@ -1,6 +1,6 @@ import NetworkExtension -extension NEVPNManager { +extension NEVPNManager: @unchecked @retroactive Sendable { func remove() async throws { _ = try await withUnsafeThrowingContinuation { continuation in removeFromPreferences(completionHandler: completion(continuation)) @@ -14,7 +14,7 @@ extension NEVPNManager { } } -extension NETunnelProviderManager { +extension NETunnelProviderManager: @unchecked @retroactive Sendable { class var managers: [NETunnelProviderManager] { get async throws { try await withUnsafeThrowingContinuation { continuation in @@ -34,7 +34,7 @@ private func completion(_ continuation: UnsafeContinuation) -> (Err } } -private func completion(_ continuation: UnsafeContinuation) -> (T?, Error?) -> Void { +private func completion(_ continuation: UnsafeContinuation) -> (T?, Error?) -> Void { return { value, error in if let error { continuation.resume(throwing: error) diff --git a/Apple/App/NetworkExtensionTunnel.swift b/Apple/UI/NetworkExtensionTunnel.swift similarity index 67% rename from Apple/App/NetworkExtensionTunnel.swift rename to Apple/UI/NetworkExtensionTunnel.swift index 08002de..7aaa3b1 100644 --- a/Apple/App/NetworkExtensionTunnel.swift +++ b/Apple/UI/NetworkExtensionTunnel.swift @@ -1,22 +1,23 @@ -import BurrowShared +import BurrowCore import NetworkExtension @Observable -class NetworkExtensionTunnel: Tunnel { - @MainActor private(set) var status: TunnelStatus = .unknown - private var error: NEVPNError? +public final class NetworkExtensionTunnel: Tunnel { + @MainActor public private(set) var status: TunnelStatus = .unknown + @MainActor private var error: NEVPNError? private let logger = Logger.logger(for: Tunnel.self) private let bundleIdentifier: String - private var tasks: [Task] = [] + private let configurationChanged: Task + private let statusChanged: Task // Each manager corresponds to one entry in the Settings app. // Our goal is to maintain a single manager, so we create one if none exist and delete any extra. - private var managers: [NEVPNManager]? { + @MainActor private var managers: [NEVPNManager]? { didSet { Task { await updateStatus() } } } - private var currentStatus: TunnelStatus { + @MainActor private var currentStatus: TunnelStatus { guard let managers = managers else { guard let error = error else { return .unknown @@ -41,35 +42,40 @@ class NetworkExtensionTunnel: Tunnel { return manager.connection.tunnelStatus } - convenience init() { - self.init(Constants.networkExtensionBundleIdentifier) - } - - init(_ bundleIdentifier: String) { + public init(bundleIdentifier: String) { self.bundleIdentifier = bundleIdentifier let center = NotificationCenter.default - let configurationChanged = Task { [weak self] in - for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) { - await self?.update() + let tunnel: OSAllocatedUnfairLock = .init(initialState: .none) + configurationChanged = Task { + for try await _ in center.notifications(named: .NEVPNConfigurationChange) { + try Task.checkCancellation() + await tunnel.withLock { $0 }?.update() } } - let statusChanged = Task { [weak self] in - for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { - await self?.updateStatus() + statusChanged = Task { + for try await _ in center.notifications(named: .NEVPNStatusDidChange) { + try Task.checkCancellation() + await tunnel.withLock { $0 }?.updateStatus() } } - tasks = [configurationChanged, statusChanged] + tunnel.withLock { $0 = self } Task { await update() } } private func update() async { do { - managers = try await NETunnelProviderManager.managers + let result = try await NETunnelProviderManager.managers + await MainActor.run { + managers = result + status = currentStatus + } await self.updateStatus() } catch let vpnError as NEVPNError { - error = vpnError + await MainActor.run { + error = vpnError + } } catch { logger.error("Failed to update VPN configurations: \(error)") } @@ -82,12 +88,7 @@ class NetworkExtensionTunnel: Tunnel { } func configure() async throws { - if managers == nil { - await update() - } - - guard let managers = managers else { return } - + let managers = try await NETunnelProviderManager.managers if managers.count > 1 { try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in for manager in managers.suffix(from: 1) { @@ -110,9 +111,9 @@ class NetworkExtensionTunnel: Tunnel { try await manager.save() } - func start() { - guard let manager = managers?.first else { return } + public func start() { Task { + guard let manager = try await NETunnelProviderManager.managers.first else { return } do { if !manager.isEnabled { manager.isEnabled = true @@ -125,12 +126,14 @@ class NetworkExtensionTunnel: Tunnel { } } - func stop() { - guard let manager = managers?.first else { return } - manager.connection.stopVPNTunnel() + public func stop() { + Task { + guard let manager = try await NETunnelProviderManager.managers.first else { return } + manager.connection.stopVPNTunnel() + } } - func enable() { + public func enable() { Task { do { try await configure() @@ -141,7 +144,8 @@ class NetworkExtensionTunnel: Tunnel { } deinit { - tasks.forEach { $0.cancel() } + configurationChanged.cancel() + statusChanged.cancel() } } diff --git a/Apple/App/NetworkView.swift b/Apple/UI/NetworkView.swift similarity index 100% rename from Apple/App/NetworkView.swift rename to Apple/UI/NetworkView.swift diff --git a/Apple/App/Networks/HackClub.swift b/Apple/UI/Networks/HackClub.swift similarity index 76% rename from Apple/App/Networks/HackClub.swift rename to Apple/UI/Networks/HackClub.swift index f7df674..b1c2023 100644 --- a/Apple/App/Networks/HackClub.swift +++ b/Apple/UI/Networks/HackClub.swift @@ -1,10 +1,14 @@ +import BurrowCore import SwiftUI struct HackClub: Network { - var id: String + typealias NetworkType = Burrow_WireGuardNetwork + static let type: Burrow_NetworkType = .hackClub + + var id: Int32 var backgroundColor: Color { .init("HackClub") } - var label: some View { + @MainActor var label: some View { GeometryReader { reader in VStack(alignment: .leading) { Image("HackClub") diff --git a/Apple/UI/Networks/Network.swift b/Apple/UI/Networks/Network.swift new file mode 100644 index 0000000..c6d5fba --- /dev/null +++ b/Apple/UI/Networks/Network.swift @@ -0,0 +1,36 @@ +import Atomics +import BurrowCore +import SwiftProtobuf +import SwiftUI + +protocol Network { + associatedtype NetworkType: Message + associatedtype Label: View + + static var type: Burrow_NetworkType { get } + + var id: Int32 { get } + var backgroundColor: Color { get } + + @MainActor var label: Label { get } +} + +@Observable +@MainActor +final class NetworkViewModel: Sendable { + private(set) var networks: [Burrow_Network] = [] + + private var task: Task! + + init(socketURL: URL) { + task = Task { [weak self] in + let client = NetworksClient.unix(socketURL: socketURL) + for try await networks in client.networkList(.init()) { + guard let viewModel = self else { continue } + Task { @MainActor in + viewModel.networks = networks.network + } + } + } + } +} diff --git a/Apple/App/Networks/WireGuard.swift b/Apple/UI/Networks/WireGuard.swift similarity index 82% rename from Apple/App/Networks/WireGuard.swift rename to Apple/UI/Networks/WireGuard.swift index 499288a..cba67ef 100644 --- a/Apple/App/Networks/WireGuard.swift +++ b/Apple/UI/Networks/WireGuard.swift @@ -1,10 +1,14 @@ +import BurrowCore import SwiftUI struct WireGuard: Network { - var id: String + typealias NetworkType = Burrow_WireGuardNetwork + static let type: BurrowCore.Burrow_NetworkType = .wireGuard + + var id: Int32 var backgroundColor: Color { .init("WireGuard") } - var label: some View { + @MainActor var label: some View { GeometryReader { reader in VStack(alignment: .leading) { HStack { diff --git a/Apple/App/OAuth2.swift b/Apple/UI/OAuth2.swift similarity index 94% rename from Apple/App/OAuth2.swift rename to Apple/UI/OAuth2.swift index 9a930c9..0fafc8d 100644 --- a/Apple/App/OAuth2.swift +++ b/Apple/UI/OAuth2.swift @@ -1,5 +1,6 @@ import AuthenticationServices import Foundation +import os import SwiftUI enum OAuth2 { @@ -25,11 +26,16 @@ enum OAuth2 { var clientID: String var clientSecret: String - fileprivate static var queue: [Int: CheckedContinuation] = [:] + fileprivate static let queue: OSAllocatedUnfairLock<[Int: CheckedContinuation]> = { + .init(initialState: [:]) + }() fileprivate static func handle(url: URL) { - let continuations = queue - queue.removeAll() + let continuations = queue.withLock { continuations in + let copy = continuations + continuations.removeAll() + return copy + } for (_, continuation) in continuations { continuation.resume(returning: url) } @@ -56,7 +62,7 @@ enum OAuth2 { var queryItems: [URLQueryItem] = [ .init(name: "client_id", value: clientID), .init(name: "response_type", value: responseType.rawValue), - .init(name: "redirect_uri", value: redirectURI.absoluteString), + .init(name: "redirect_uri", value: redirectURI.absoluteString) ] if !scopes.isEmpty { queryItems.append(.init(name: "scope", value: scopes.joined(separator: ","))) @@ -206,6 +212,9 @@ enum OAuth2 { } } +extension WebAuthenticationSession: @unchecked @retroactive Sendable { +} + extension WebAuthenticationSession { #if canImport(BrowserEngineKit) @available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) @@ -243,12 +252,12 @@ extension WebAuthenticationSession { let id = Int.random(in: 0..