commit dbd4d57222a827184422d147314aad07513aa927 Author: _Frky <3105926+Frky@users.noreply.github.com> Date: Wed Dec 8 10:08:38 2021 +0100 Publication of masscanned v0.2.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f07cd65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/target/ +Cargo.lock +**/*.rs.bk + +# Vim temporary files +*.swp +*.swo + +*__pycache__* +test/res/* diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..789f87f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,43 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned 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. +# +# Masscanned 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 Masscanned. If not, see . + +[package] +name = "masscanned" +version = "0.2.0" +authors = ["_Frky <3105926+Frky@users.noreply.github.com>"] +edition = "2018" + +[dependencies] +pcap = "0.7.0" +pcap-file = "1.1.1" +pnet = "0.26.0" +# pnet = { path = "libpnet" } +clap = "2.33.3" +log = "0.4.11" +stderrlog = "0.5.0" +itertools = "0.9.0" +rand = "0.7.3" +dns-parser = "0.8.0" +netdevice = "0.1.1" +bitflags = "1.2.1" +lazy_static = "1.4.0" +siphasher = "0.3" +chrono = "0.4.19" +byteorder = "1.4.3" + +[[bin]] +name = "masscanned" +path = "src/masscanned.rs" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + 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 +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f5a1f6 --- /dev/null +++ b/README.md @@ -0,0 +1,292 @@ +# Masscanned + +**Masscanned** (name inspired, of course, by [masscan](https://github.com/robertdavidgraham/masscan)) +is a network responder. Its purpose is to provide generic answers to as many protocols as possible, +and with as few asumptions as possible on the client's intentions. + +> *Let them talk first.* + +Just like [masscan](https://github.com/robertdavidgraham/masscan), **masscanned** implements its own, +userland network stack, similarly to [honeyd](http://honeyd.org/). It is designed to interact +with scanners and opportunistic bots as far as possible, and to support as many protocols as possible. + +For example, when it receives network packets: + +* **masscanned** answers to `ARP who is-at` with `ARP is-at` (for its IP addresses), +* **masscanned** answers to `ICMP Echo Request` with `ICMP Echo Reply`, +* **masscanned** answers to `TCP SYN` (any port) with `TCP SYN/ACK` on any port, +* **masscanned** answers to `HTTP` requests (any verb) over `TCP/UDP` (any port) with a `HTTP 401` web page. + +![demo](doc/demo.gif) + +**Masscanned** currently supports most common protocols at layers 2-3-4, and a few application +protocols: + +* `Eth::ARP::REQ`, +* `Eth::IPv{4,6}::ICMP::ECHO-REQ`, +* `Eth::IPv{4,6}::TCP::SYN` (all ports), +* `Eth::IPv{4,6}::TCP::PSHACK` (all ports), +* `Eth::IPv6::ICMP::ND_NS`. +* `Eth::IPv{4,6}::{TCP,UDP}::HTTP` (all HTTP verbs), +* `Eth::IPv{4,6}::{TCP,UDP}::STUN`, +* `Eth::IPv{4,6}::{TCP,UDP}::SSH` (Server Protocol only). + +## Try it locally + +1. Build **masscanned** +``` +$ cargo build +``` +2. Create a new net namespace +``` +# ip netns add masscanned +``` +3. Create veth between the two namespaces +``` +# ip link add vethmasscanned type veth peer veth netns masscanned +# ip link set vethmasscanned up +# ip -n masscanned link set veth up +``` +4. Set IP on local veth to have a route for outgoing packets +``` +# ip addr add dev vethmasscanned 192.168.0.0/31 +``` +5. Run **masscanned** in the namespace +``` +# ip netns exec masscanned ./target/debug/masscanned --iface veth -v[vv] +``` +6. With another terminal, send packets to **masscanned** +``` +# arping 192.168.0.1 +# ping 192.168.0.1 +# nc -n -v 192.168.0.1 80 +# nc -n -v -u 192.168.0.1 80 +... +``` + +## Protocols + +### Layer 2 + +#### ARP + +`masscanned` anwsers to `ARP` requests, for requests that target an `IPv4` address +that is handled by `masscanned` (*i.e.*, an address that is in the +IP address file given with option `-f`). + +The answer contains the first of the following possible `MAC` addresses: + +* the `MAC` address specified with `-a` in command line if any, +* the `MAC` address of the interface specified with `-i` in command line if any, +* or the `masscanned` default `MAC` address, *i.e.*, `c0:ff:ee:c0:ff:ee`. + +#### Ethernet + +`masscanned` answers to `Ethernet` frames, if and only if the following requirements are met: + +* the destination address of the frame should be handled by `masscanned`, which means: + + * `masscanned` own `MAC` address, + * the broadcast `MAC` address `ff:ff:ff:ff:ff:ff`, + * a multicast `MAC` address corresponding to one of the `IPv4` addresses handled by `masscanned` ([RFC 1112](https://datatracker.ietf.org/doc/html/rfc1112)), + * a multicast `MAC` address corresponding to one of the `IPv6` addresses handled by `masscanned` ; + +* `EtherType` field is one of `ARP`, `IPv4` or `IPv6`. + +**Note:** even for a non-multicast IP address, `masscanned` will respond to L2 frames addressed to the corresponding multicast `MAC` address. +For instance, if `masscanned` handles `10.11.12.13`, it will answer to frames addressed to `01:00:5e:0b:0c:0d`. + +### Layer 3 + +#### IPv4/IPv6 + +`masscanned` answers to `IPv4` and `IPv6` packets, only if: + +* no `IP` address is specified in a file (*i.e.*, no `-f` option is specified or the file is empty), + +**or** + +* the destination IP address of the incoming packet is one of the IP addresses handled by `masscanned`. + +An additionnal requirement is that the next layer protocol is supported - see below. + +#### IPv4 + +The following L4 protocols are suppported for an `IPv4` packet: + +* `ICMPv4` +* `UDP` +* `TCP` + +If the next layer protocol is not one of them, the packet is dropped. + +#### IPv6 + +The following L4 protocols are suppported for an `IPv6` packet: + +* `ICMPv6` +* `UDP` +* `TCP` + +If the next layer protocol is not one of them, the packet is dropped. + +### Layer 3+/4 + +#### ICMPv4 + +`masscanned` answers to `ICMPv4` packets if and only if: + +* the `ICMP` type of the incoming packet is `EchoRequest` (`8`), +* the `ICMP` code of the incoming packet is `0`. + +If these conditions are met, `masscanned` answers with an `ICMP` packet of type `EchoReply` (`0`), +code `0` and the same payload as the incoming packet, as specified by [RFC 792](https://datatracker.ietf.org/doc/html/rfc792). + +#### ICMPv6 + +`masscanned` answers to `ICMPv6` packets if and only if: + +* the `ICMP` type is `NeighborSol` (`135`) **and**: + * no IP (v4 or v6) was speficied for `masscanned` + * **or** the target address of the Neighbor Solicitation is one of `masccanned` + +*In that case, the answer is a `Neighbor Advertisement` (`136`) packet with `masscanned` `MAC` address* + +**or** + +* the `ICMP` type is `EchoRequest` (`128`) + +*In that case, the answer is a `EchoReply` (`129`) packet.* + +#### TCP + +`masscanned` answers to the following `TCP` packets: + +* if the received packet has flags `PSH` and `ACK`, `masscanned` checks the **SYNACK-cookie**, and if valid answers at least a `ACK`, or a `PSH-ACK` if +a supported protocol (Layer 5/6/7) has been detected, +* if the received packet has flag `ACK`, it is ignored, +* if the received packet has flag `RST` or `FIN-ACK`, it is ignored, +* if the received packet has flag `SYN`, then `masscanned` answers with a `SYN-ACK` packet, setting a **SYNACK-cookie** in the sequence number. + +#### UDP + +`masscanned` answers to an `UDP` packet if and only if the upper-layer protocol +is handled and provides an answer. + +### Protocols + +#### HTTP + +#### STUN + +#### SSH + +`masscanned` answers to `SSH` `Client: Protocol` messages with the following `Server: Protocol` message: + +``` +SSH-2.0-1\r\n +``` + +## Internals + +### Tests + +#### Unit tests + +``` +$ cargo test + Compiling masscanned v0.2.0 (/zdata/workdir/masscanned) + Finished test [unoptimized + debuginfo] target(s) in 2.34s + Running target/debug/deps/masscanned-b86211a090e50323 + +running 36 tests +test client::client_info::tests::test_client_info_eq ... ok +test layer_2::arp::tests::test_arp_reply ... ok +test layer_3::ipv4::tests::test_ipv4_reply ... ok +test layer_3::ipv6::tests::test_ipv6_reply ... ok +test layer_4::icmpv6::tests::test_icmpv6_reply ... ok +test layer_2::tests::test_eth_reply ... ok +test layer_4::icmpv6::tests::test_nd_na_reply ... ok +test layer_4::tcp::tests::test_synack_cookie_ipv4 ... ok +test layer_4::icmpv4::tests::test_icmpv4_reply ... ok +test layer_4::tcp::tests::test_synack_cookie_ipv6 ... ok +test proto::http::test_http_request_field ... ok +test proto::http::test_http_request_no_field ... ok +test proto::http::test_http_request_line ... ok +test proto::http::test_http_verb ... ok +test proto::stun::tests::test_change_request_port ... ok +test proto::stun::tests::test_proto_stun_ipv6 ... ok +test proto::stun::tests::test_proto_stun_ipv4 ... ok +test proto::stun::tests::test_change_request_port_overflow ... ok +test smack::smack::tests::test_anchor_end ... ok +test smack::smack::tests::test_anchor_begin ... ok +test smack::smack::tests::test_multiple_matches ... ok +test smack::smack::tests::test_http_banner ... ok +test smack::smack::tests::test_multiple_matches_wildcard ... ok +test smack::smack::tests::test_proto ... ok +test smack::smack::tests::test_wildcard ... ok +test proto::tests::test_proto_dispatch_ssh ... ok +test proto::tests::test_proto_dispatch_stun ... ok +test synackcookie::tests::test_clientinfo ... ok +test synackcookie::tests::test_ip4_dst ... ok +test synackcookie::tests::test_ip4_src ... ok +test synackcookie::tests::test_ip4 ... ok +test synackcookie::tests::test_ip6 ... ok +test synackcookie::tests::test_key ... ok +test synackcookie::tests::test_tcp_dst ... ok +test synackcookie::tests::test_tcp_src ... ok +test smack::smack::tests::test_pattern ... ok + +test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +``` + +#### Functional tests + +``` +# ./test/test_masscanned.py +tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes +INFO test_arp_req......................................OK +INFO test_arp_req_other_ip.............................OK +INFO test_ipv4_req.....................................OK +INFO test_eth_req_other_mac............................OK +INFO test_ipv4_req_other_ip............................OK +INFO test_icmpv4_echo_req..............................OK +INFO test_icmpv6_neighbor_solicitation.................OK +INFO test_icmpv6_neighbor_solicitation_other_ip........OK +INFO test_icmpv6_echo_req..............................OK +INFO test_tcp_syn......................................OK +INFO test_ipv4_tcp_psh_ack.............................OK +INFO test_ipv6_tcp_psh_ack.............................OK +INFO test_ipv4_tcp_http................................OK +INFO test_ipv6_tcp_http................................OK +INFO test_ipv4_udp_http................................OK +INFO test_ipv6_udp_http................................OK +INFO test_ipv4_tcp_http_ko.............................OK +INFO test_ipv4_udp_http_ko.............................OK +INFO test_ipv6_tcp_http_ko.............................OK +INFO test_ipv6_udp_http_ko.............................OK +INFO test_ipv4_udp_stun................................OK +INFO test_ipv6_udp_stun................................OK +INFO test_ipv4_udp_stun_change_port....................OK +INFO test_ipv6_udp_stun_change_port....................OK +INFO test_ipv4_tcp_ssh.................................OK +INFO test_ipv4_udp_ssh.................................OK +INFO test_ipv6_tcp_ssh.................................OK +INFO test_ipv6_udp_ssh.................................OK +tcpdump: pcap_loop: The interface disappeared +604 packets captured +604 packets received by filter +0 packets dropped by kernel +``` + +### Logging Policy + +* `ERR`: any error - will always be displayed. +* `WARN`, `-v`: responses sent by `masscanned`. +* `INFO`, `-vv`: packets not handled, packets ignored. +* `DEBUG`, `-vvv`: all packets received and sent by `masscanned`. + +## To Do + +* Drop incoming packets if checksum is incorrect +* Fix source address when answering to multicast packets. diff --git a/doc/demo.gif b/doc/demo.gif new file mode 100644 index 0000000..a83f921 Binary files /dev/null and b/doc/demo.gif differ diff --git a/src/client/client_info.rs b/src/client/client_info.rs new file mode 100644 index 0000000..3005a8e --- /dev/null +++ b/src/client/client_info.rs @@ -0,0 +1,182 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use std::fmt::{Display, Error}; +use std::hash::Hash; +use std::net::IpAddr; + +use pnet::packet::ip::IpNextHeaderProtocol; +use pnet::util::MacAddr; + +#[derive(PartialEq, Hash, Copy, Clone)] +pub struct ClientInfoSrcDst { + pub src: Option, + pub dst: Option, +} + +/* Structure to describe useful information + * about a client connection, such as: + * - source mac address + * - source and dest. IP address + * - transport layer protocol + * - source and dest. transport port + * - syn cookie + **/ +#[derive(Copy, Clone)] +pub struct ClientInfo { + pub mac: ClientInfoSrcDst, + pub ip: ClientInfoSrcDst, + pub transport: Option, + pub port: ClientInfoSrcDst, + pub cookie: Option, +} + +impl ClientInfo { + pub fn new() -> Self { + ClientInfo { + mac: ClientInfoSrcDst:: { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst:: { + src: None, + dst: None, + }, + transport: None, + port: ClientInfoSrcDst:: { + src: None, + dst: None, + }, + cookie: None, + } + } +} + +impl PartialEq for ClientInfo { + fn eq(&self, other: &Self) -> bool { + if self.mac != other.mac { + return false; + } + if self.ip != other.ip { + return false; + } + if self.transport != other.transport { + return false; + } + if self.port != other.port { + return false; + } + /* this next case should never occur with TCP and UDP, + * but this implementation tries to remain transport-protocol-agnostic + **/ + if self.cookie != other.cookie { + return false; + } + true + } +} + +impl Eq for ClientInfo {} + +impl Display for ClientInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), Error> { + write!( + f, + "{:>width_ip$}:{: {:>width_port$}:{: Self { + ClientInfo { + mac: ClientInfoSrcDst { + src: Some(MacAddr::new(0, 0, 0, 0, 0, 0)), + dst: Some(MacAddr::new(0, 0, 0, 0, 0, 0)), + }, + ip: ClientInfoSrcDst { + src: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), + dst: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), + }, + transport: Some(IpNextHeaderProtocols::Tcp), + port: ClientInfoSrcDst { + src: Some(0), + dst: Some(0), + }, + cookie: Some(0), + } + } + } + + #[test] + fn test_client_info_eq() { + let client_ref = ClientInfo::new_test(); + /* two clients with different mac addr should be different */ + let mut client_test = ClientInfo::new_test(); + assert!(client_test == client_ref); + client_test.mac.src = Some(MacAddr::new(1, 0, 0, 0, 0, 0)); + assert!(client_test != client_ref); + client_test.mac.src = Some(MacAddr::new(0, 0, 0, 0, 0, 0)); + client_test.mac.dst = Some(MacAddr::new(1, 0, 0, 0, 0, 0)); + assert!(client_test != client_ref); + client_test.mac.dst = Some(MacAddr::new(0, 0, 0, 0, 0, 0)); + assert!(client_test == client_ref); + /* two clients with different ip addr should be different */ + let mut client_test = ClientInfo::new_test(); + assert!(client_test == client_ref); + client_test.ip.src = Some(IpAddr::V4(Ipv4Addr::new(1, 0, 0, 0))); + assert!(client_test != client_ref); + client_test.ip.src = Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); + client_test.ip.dst = Some(IpAddr::V4(Ipv4Addr::new(1, 0, 0, 0))); + assert!(client_test != client_ref); + /* two clients with different tranport layer should be different */ + let mut client_test = ClientInfo::new_test(); + assert!(client_test == client_ref); + client_test.transport = Some(IpNextHeaderProtocols::Udp); + assert!(client_test != client_ref); + client_test.transport = Some(IpNextHeaderProtocols::Tcp); + assert!(client_test == client_ref); + /* two clients with different tranport ports should be different */ + let mut client_test = ClientInfo::new_test(); + assert!(client_test == client_ref); + client_test.port.src = Some(1); + assert!(client_test != client_ref); + client_test.port.src = Some(0); + client_test.port.dst = Some(1); + assert!(client_test != client_ref); + client_test.port.dst = Some(0); + assert!(client_test == client_ref); + /* two clients with different cookies should be different */ + let mut client_test = ClientInfo::new_test(); + assert!(client_test == client_ref); + client_test.cookie = Some(1); + assert!(client_test != client_ref); + client_test.cookie = Some(0); + assert!(client_test == client_ref); + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..a7994fb --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,19 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +mod client_info; + +pub use client_info::{ClientInfo, ClientInfoSrcDst}; diff --git a/src/layer_2/arp.rs b/src/layer_2/arp.rs new file mode 100644 index 0000000..4798c22 --- /dev/null +++ b/src/layer_2/arp.rs @@ -0,0 +1,116 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; +use std::net::IpAddr; + +use pnet::packet::{ + arp::{ArpHardwareTypes, ArpOperations, ArpPacket, MutableArpPacket}, + /* Import needed for traits */ + Packet as _, +}; + +use crate::Masscanned; + +pub fn repl<'a, 'b>( + arp_req: &'a ArpPacket, + masscanned: &Masscanned, +) -> Option> { + let mut arp_repl = + MutableArpPacket::owned(arp_req.packet().to_vec()).expect("error parsing ARP packet"); + /* Build ARP answer depending of the type of request */ + match arp_req.get_operation() { + ArpOperations::Request => { + let ip = IpAddr::V4(arp_req.get_target_proto_addr()); + /* Ignore ARP requests for IP addresses not handled by masscanned */ + if let Some(ip_addr_list) = masscanned.ip_addresses { + if !ip_addr_list.contains(&ip) { + info!( + "Ignoring ARP request from {} for IP {}", + arp_req.get_sender_hw_addr(), + ip + ); + return None; + } + } + /* Fill ARP reply */ + arp_repl.set_operation(ArpOperations::Reply); + arp_repl.set_hardware_type(ArpHardwareTypes::Ethernet); + arp_repl.set_sender_hw_addr(masscanned.mac); + arp_repl.set_target_hw_addr(arp_req.get_sender_hw_addr().to_owned()); + arp_repl.set_target_proto_addr(arp_req.get_sender_proto_addr().to_owned()); + arp_repl.set_sender_proto_addr(arp_req.get_target_proto_addr().to_owned()); + warn!( + "ARP-Reply to {} for IP {}", + arp_req.get_sender_hw_addr(), + arp_repl.get_sender_proto_addr() + ); + } + _ => { + info!("ARP Operation not handled: {:?}", arp_repl.get_operation()); + return None; + } + }; + Some(arp_repl) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::net::Ipv4Addr; + use std::str::FromStr; + + use pnet::util::MacAddr; + + #[test] + fn test_arp_reply() { + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(Ipv4Addr::new(0, 1, 2, 3))); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + let mut arp_req = + MutableArpPacket::owned([0; 28].to_vec()).expect("error constructing ARP request"); + arp_req.set_hardware_type(ArpHardwareTypes::Ethernet); + arp_req.set_operation(ArpOperations::Request); + arp_req.set_sender_hw_addr( + MacAddr::from_str("55:44:33:22:11:00").expect("error parsing MAC address"), + ); + arp_req.set_target_hw_addr( + MacAddr::from_str("00:00:00:00:00:00").expect("error parsing MAC address"), + ); + arp_req.set_sender_proto_addr(Ipv4Addr::new(3, 2, 1, 0)); + /* Test getting an ARP reply for a legitimate IP address */ + arp_req.set_target_proto_addr(Ipv4Addr::new(0, 1, 2, 3)); + if let Some(arp_repl) = repl(&arp_req.to_immutable(), &masscanned) { + assert!(arp_repl.get_hardware_type() == ArpHardwareTypes::Ethernet); + assert!(arp_repl.get_operation() == ArpOperations::Reply); + assert!(arp_repl.get_sender_hw_addr() == masscanned.mac); + assert!(arp_repl.get_sender_proto_addr() == Ipv4Addr::new(0, 1, 2, 3)); + } else { + panic!("Expected ARP reply - got None"); + } + /* Ensure no response is returned for an other IP address */ + arp_req.set_target_proto_addr(Ipv4Addr::new(1, 1, 2, 3)); + let arp_repl = repl(&arp_req.to_immutable(), &masscanned); + assert!(arp_repl == None); + } +} diff --git a/src/layer_2/mod.rs b/src/layer_2/mod.rs new file mode 100644 index 0000000..75d4af8 --- /dev/null +++ b/src/layer_2/mod.rs @@ -0,0 +1,260 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; +use std::collections::HashSet; +use std::net::IpAddr; + +use pnet::packet::{ + arp::ArpPacket, + ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket}, + ipv4::checksum as ipv4_checksum, + ipv4::Ipv4Packet, + ipv6::Ipv6Packet, + Packet as Pkt, +}; +use pnet::util::MacAddr; + +use crate::client::ClientInfo; +use crate::layer_3; +use crate::Masscanned; + +pub mod arp; + +/* representation of a 6-bytes Ethernet address */ +type EtherAddr = [u8; 6]; + +/* This function builds the list of layer-2 destination addresses + * that masscanned is authorized to answer to. It includes: + * - masscanned own MAC address, + * - layer 2 broadcast MAC addresses, + * - layer 2 IPv6 multicast MAC address, + * - layer 2 IPv6 solicited-node multicast addresses for each IPv6 address + * of masscanned + **/ +pub fn get_authorized_eth_addr( + mac: &MacAddr, + ip_addresses: Option<&HashSet>, +) -> HashSet { + let mut auth_addr = HashSet::new(); + auth_addr.insert(MacAddr::broadcast()); + auth_addr.insert(*mac); + /* add IPv6 multicast addr */ + auth_addr.insert( + "33:33:00:00:00:01" + .parse() + .expect("error parsing generic MAC address"), + ); + /* Add: + * - IPv4 multicast address for every IPv4 + * - IPv6 Solicited-Node multicast address for every IPv6 + **/ + if let Some(ip_addr) = ip_addresses { + for addr in ip_addr { + match addr { + IpAddr::V4(ipv4) => { + let mut eth_ma: EtherAddr = [0; 6]; + eth_ma[0] = 0x01; + eth_ma[1] = 0x00; + eth_ma[2] = 0x5e; + /* RFC 1112 - https://datatracker.ietf.org/doc/html/rfc1112 + * Section 6.4: + * An IP host group address is mapped to an Ethernet multicast address + * by placing the low-order 23-bits of the IP address into the low-order + * 23 bits of the Ethernet multicast address 01-00-5E-00-00-00 (hex). + **/ + eth_ma[3] = ipv4.octets()[1] & 0x7f; + eth_ma[4] = ipv4.octets()[2]; + eth_ma[5] = ipv4.octets()[3]; + auth_addr.insert(MacAddr::from(eth_ma)); + } + IpAddr::V6(ipv6) => { + let mut eth_snma: EtherAddr = [0; 6]; + eth_snma[0] = 0x33; + eth_snma[1] = 0x33; + /* multicast MAC address corresponding to solicited-node + * multicast IPv6 address */ + eth_snma[2] = 0xff; + eth_snma[3] = ipv6.octets()[13]; + eth_snma[4] = ipv6.octets()[14]; + eth_snma[5] = ipv6.octets()[15]; + auth_addr.insert(MacAddr::from(eth_snma)); + } + } + } + } + auth_addr +} + +pub fn reply<'a, 'b>( + eth_req: &'a EthernetPacket, + masscanned: &Masscanned, + mut client_info: &mut ClientInfo, +) -> Option> { + debug!("receiving Ethernet packet: {:?}", eth_req); + let mut eth_repl; + /* First, check if the destination MAC address is one of those masscanned + * is authorized to answer to (avoid answering to packets addressed to + * other machines) + **/ + if !get_authorized_eth_addr(&masscanned.mac, masscanned.ip_addresses) + .contains(ð_req.get_destination()) + { + info!( + "Ignoring Ethernet packet from {} to {}", + eth_req.get_source(), + eth_req.get_destination(), + ); + return None; + } + /* Fill client information for this packet with MAC addresses (src and dst) */ + client_info.mac.src = Some(eth_req.get_source()); + client_info.mac.dst = Some(eth_req.get_destination()); + /* Build next layer payload for answer depending on the incoming packet */ + match eth_req.get_ethertype() { + /* Construct answer to ARP request */ + EtherTypes::Arp => { + let arp_req = ArpPacket::new(eth_req.payload()).expect("error parsing ARP packet"); + if let Some(arp_repl) = arp::repl(&arp_req, masscanned) { + let arp_len = arp_repl.packet().len(); + let eth_len = EthernetPacket::minimum_packet_size() + arp_len; + eth_repl = MutableEthernetPacket::owned(vec![0; eth_len]) + .expect("error constructing an Ethernet Packet"); + eth_repl.set_ethertype(EtherTypes::Arp); + eth_repl.set_payload(arp_repl.packet()); + } else { + return None; + } + } + /* Construct answer to IPv4 packet */ + EtherTypes::Ipv4 => { + let ipv4_req = if let Some(p) = Ipv4Packet::new(eth_req.payload()) { + p + } else { + warn!("error parsing IPv4 packet"); + return None; + }; + if let Some(mut ipv4_repl) = + layer_3::ipv4::repl(&ipv4_req, masscanned, &mut client_info) + { + ipv4_repl.set_checksum(ipv4_checksum(&ipv4_repl.to_immutable())); + let ipv4_len = ipv4_repl.packet().len(); + let eth_len = EthernetPacket::minimum_packet_size() + ipv4_len; + eth_repl = MutableEthernetPacket::owned(vec![0; eth_len]) + .expect("error constructing an Ethernet Packet"); + eth_repl.set_ethertype(EtherTypes::Ipv4); + eth_repl.set_payload(ipv4_repl.packet()); + } else { + return None; + } + } + /* Construct answer to IPv6 packet */ + EtherTypes::Ipv6 => { + let ipv6_req = Ipv6Packet::new(eth_req.payload()).expect("error parsing IPv6 packet"); + if let Some(ipv6_repl) = layer_3::ipv6::repl(&ipv6_req, masscanned, &mut client_info) { + let ipv6_len = ipv6_repl.packet().len(); + let eth_len = EthernetPacket::minimum_packet_size() + ipv6_len; + eth_repl = MutableEthernetPacket::owned(vec![0; eth_len]) + .expect("error constructing an Ethernet Packet"); + eth_repl.set_ethertype(EtherTypes::Ipv6); + eth_repl.set_payload(ipv6_repl.packet()); + } else { + return None; + } + } + /* Log & drop unknown network protocol */ + _ => { + info!("Ethernet type not handled: {:?}", eth_req.get_ethertype()); + return None; + } + }; + eth_repl.set_source(masscanned.mac); + eth_repl.set_destination(eth_req.get_source()); + debug!("sending Ethernet packet: {:?}", eth_repl); + Some(eth_repl) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{Ipv4Addr, Ipv6Addr}; + use std::str::FromStr; + + #[test] + fn test_eth_reply() { + /* test payload is IP(src="3.2.1.0", dst=".".join(str(b) for b in [0xaa, 0x99, + * 0x88, 0x77]))/ICMP() */ + let payload = b"E\x00\x00\x1c\x00\x01\x00\x00@\x01C\xce\x03\x02\x01\x00\xaa\x99\x88w\x08\x00\xf7\xff\x00\x00\x00\x00"; + let test_mac_addr = + MacAddr::from_str("55:44:33:22:11:00").expect("error parsing MAC address"); + let mut client_info = ClientInfo::new(); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(Ipv4Addr::new(0xaa, 0x99, 0x88, 0x77))); + ips.insert(IpAddr::V6(Ipv6Addr::new( + 0x7777, 0x7777, 0x7777, 0x7777, 0x7777, 0x7777, 0xaabb, 0xccdd, + ))); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + let mut eth_req = MutableEthernetPacket::owned(vec![ + 0; + EthernetPacket::minimum_packet_size() + + payload.len() + ]) + .expect("error constructing ethernet packet"); + eth_req.set_source(test_mac_addr); + eth_req.set_payload(payload); + /* Test answer to legitimate dest. */ + let dest_mac = [ + masscanned.mac, + MacAddr::from_str("ff:ff:ff:ff:ff:ff").unwrap(), + MacAddr::from_str("01:00:5e:19:88:77").unwrap(), + MacAddr::from_str("33:33:ff:bb:cc:dd").unwrap(), + ]; + for mac in dest_mac.iter() { + println!("testing mac: {:?}", mac); + eth_req.set_ethertype(EtherTypes::Ipv4); + eth_req.set_destination(*mac); + if let Some(eth_repl) = reply(ð_req.to_immutable(), &masscanned, &mut client_info) { + assert!(eth_repl.get_source() == masscanned.mac); + assert!(eth_repl.get_destination() == test_mac_addr); + assert!(eth_repl.get_ethertype() == EtherTypes::Ipv4); + } else { + panic!("expected an Ethernet answer, got None"); + } + } + /* Test answer to non-legitimate dest. */ + let dest_mac = [ + MacAddr::from_str("aa:bb:cc:dd:ee:ff").unwrap(), + MacAddr::from_str("ff:ff:ff:ff:ff:fe").unwrap(), + MacAddr::from_str("01:00:5e:00:11:22").unwrap(), + MacAddr::from_str("33:33:aa:bb:cc:de").unwrap(), + MacAddr::from_str("01:00:5e:99:88:77").unwrap(), + MacAddr::from_str("33:33:aa:bb:cc:dd").unwrap(), + ]; + for mac in dest_mac.iter() { + println!("testing mac: {:?}", mac); + eth_req.set_ethertype(EtherTypes::Ipv4); + eth_req.set_destination(*mac); + let eth_repl = reply(ð_req.to_immutable(), &masscanned, &mut client_info); + assert!(eth_repl == None); + } + } +} diff --git a/src/layer_3/ipv4.rs b/src/layer_3/ipv4.rs new file mode 100644 index 0000000..6be97ba --- /dev/null +++ b/src/layer_3/ipv4.rs @@ -0,0 +1,210 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; +use std::convert::TryInto; +use std::net::IpAddr; + +use pnet::packet::{ + icmp::checksum as ipv4_checksum_icmp, + icmp::IcmpPacket, + ip::IpNextHeaderProtocols, + ipv4::{Ipv4Flags, Ipv4Packet, MutableIpv4Packet}, + tcp::ipv4_checksum as ipv4_checksum_tcp, + tcp::TcpPacket, + udp::ipv4_checksum as ipv4_checksum_udp, + udp::UdpPacket, + Packet, +}; + +use crate::client::ClientInfo; +use crate::layer_4; +use crate::Masscanned; + +pub fn repl<'a, 'b>( + ip_req: &'a Ipv4Packet, + masscanned: &Masscanned, + mut client_info: &mut ClientInfo, +) -> Option> { + debug!("receiving IPv4 packet: {:?}", ip_req); + /* If masscanned is configured with IP addresses, then + * check that the dest. IP address of the packet is one of + * those handled by masscanned - otherwise, drop the packet. + **/ + if let Some(ip_addr_list) = masscanned.ip_addresses { + if !ip_addr_list.contains(&IpAddr::V4(ip_req.get_destination())) { + info!( + "Ignoring IP packet from {} for {}", + ip_req.get_source(), + ip_req.get_destination() + ); + return None; + } + } + /* Fill client info with source and dest. IP addresses */ + client_info.ip.src = Some(IpAddr::V4(ip_req.get_source())); + client_info.ip.dst = Some(IpAddr::V4(ip_req.get_destination())); + /* Fill client info with transport layer procotol */ + client_info.transport = Some(ip_req.get_next_level_protocol()); + let mut ip_repl; + match ip_req.get_next_level_protocol() { + /* Answer to an ICMP packet */ + IpNextHeaderProtocols::Icmp => { + let icmp_req = IcmpPacket::new(ip_req.payload()).expect("error parsing ICMP packet"); + if let Some(mut icmp_repl) = layer_4::icmpv4::repl(&icmp_req, masscanned, &client_info) + { + icmp_repl.set_checksum(ipv4_checksum_icmp(&icmp_repl.to_immutable())); + let icmp_len = icmp_repl.packet().len(); + let ip_len = MutableIpv4Packet::minimum_packet_size() + icmp_len; + ip_repl = MutableIpv4Packet::owned(vec![0; ip_len]) + .expect("error constructing an IPv4 packet"); + ip_repl.set_total_length(ip_len as u16); + // FIXME + ip_repl.set_header_length(5); + ip_repl.set_payload(icmp_repl.packet()); + ip_repl.set_next_level_protocol(IpNextHeaderProtocols::Icmp); + } else { + return None; + } + } + /* Answer to a TCP packet */ + IpNextHeaderProtocols::Tcp => { + let tcp_req = TcpPacket::new(ip_req.payload()).expect("error parsing TCP packet"); + if let Some(mut tcp_repl) = layer_4::tcp::repl(&tcp_req, masscanned, &mut client_info) { + tcp_repl.set_checksum(ipv4_checksum_tcp( + &tcp_repl.to_immutable(), + &ip_req.get_destination(), + &ip_req.get_source(), + )); + let tcp_len = tcp_repl.packet().len(); + let ip_len = Ipv4Packet::minimum_packet_size() + tcp_len; + ip_repl = MutableIpv4Packet::owned(vec![0; ip_len]) + .expect("error constructing an IPv4 packet"); + ip_repl.set_total_length(ip_len as u16); + // FIXME + ip_repl.set_header_length(5); + ip_repl.set_payload(tcp_repl.packet()); + ip_repl.set_next_level_protocol(IpNextHeaderProtocols::Tcp); + } else { + return None; + } + } + /* Answer to an UDP packet */ + IpNextHeaderProtocols::Udp => { + let udp_req = UdpPacket::new(ip_req.payload()).expect("error parsing UDP packet"); + if let Some(mut udp_repl) = layer_4::udp::repl(&udp_req, masscanned, &mut client_info) { + udp_repl.set_checksum(ipv4_checksum_udp( + &udp_repl.to_immutable(), + &ip_req.get_destination(), + &ip_req.get_source(), + )); + let udp_len = udp_repl.packet().len(); + udp_repl.set_length(udp_len.try_into().unwrap()); + debug!("udp len: {}", udp_len); + let ip_len = Ipv4Packet::minimum_packet_size() + udp_len; + ip_repl = MutableIpv4Packet::owned(vec![0; ip_len]) + .expect("error constructing an IPv4 packet"); + ip_repl.set_total_length(ip_len as u16); + // FIXME + ip_repl.set_header_length(5); + ip_repl.set_payload(udp_repl.packet()); + ip_repl.set_next_level_protocol(IpNextHeaderProtocols::Udp); + } else { + return None; + } + } + /* Next layer protocol not handled (yet) - dropping packet */ + _ => { + info!( + "IPv4 upper layer not handled: {:?}", + ip_req.get_next_level_protocol() + ); + return None; + } + }; + /* Set IP packet fields before sending */ + ip_repl.set_version(4); + ip_repl.set_ttl(64); + ip_repl.set_identification(0); + /* These values are already initialized with 0s + * ip_repl.set_dscp(0); + * ip_repl.set_ecn(0); + * ip_repl.set_identification(0); + **/ + /* Do not fragment packet */ + ip_repl.set_flags(Ipv4Flags::DontFragment); + /* Set source and dest. IP address */ + /* FIXME when dest. was a multicast IP address */ + ip_repl.set_source(ip_req.get_destination()); + ip_repl.set_destination(ip_req.get_source()); + debug!("sending IPv4 packet: {:?}", ip_repl); + Some(ip_repl) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::net::Ipv4Addr; + use std::str::FromStr; + + use pnet::util::MacAddr; + + #[test] + fn test_ipv4_reply() { + /* test payload is scapy> ICMP() */ + let payload = b"\x08\x00\xf7\xff\x00\x00\x00\x00"; + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + let mut ip_req = + MutableIpv4Packet::owned(vec![0; Ipv4Packet::minimum_packet_size() + payload.len()]) + .expect("error constructing IPv4 packet"); + ip_req.set_version(4); + ip_req.set_ttl(64); + ip_req.set_identification(0); + ip_req.set_flags(Ipv4Flags::DontFragment); + ip_req.set_source(test_ip_addr); + ip_req.set_header_length(5); + /* Set test payload for layer 4 */ + ip_req.set_total_length(ip_req.packet().len() as u16); + ip_req.set_payload(payload); + /* Set next protocol */ + ip_req.set_next_level_protocol(IpNextHeaderProtocols::Icmp); + /* Send to a legitimate IP address */ + ip_req.set_destination(masscanned_ip_addr); + if let Some(ip_repl) = repl(&ip_req.to_immutable(), &masscanned, &mut client_info) { + assert!(ip_repl.get_destination() == test_ip_addr); + assert!(ip_repl.get_source() == masscanned_ip_addr); + assert!(ip_repl.get_next_level_protocol() == IpNextHeaderProtocols::Icmp); + assert!(ip_repl.get_total_length() == ip_repl.packet().len() as u16); + } else { + panic!("expected an IP answer, got None"); + } + /* Send to a non-legitimate IP address */ + ip_req.set_destination(Ipv4Addr::new(2, 2, 2, 2)); + assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None); + } +} diff --git a/src/layer_3/ipv6.rs b/src/layer_3/ipv6.rs new file mode 100644 index 0000000..edc5390 --- /dev/null +++ b/src/layer_3/ipv6.rs @@ -0,0 +1,217 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; +use std::net::IpAddr; + +use pnet::packet::{ + icmpv6::{checksum as icmpv6_checksum, Icmpv6Packet, Icmpv6Types}, + ip::IpNextHeaderProtocols, + ipv6::{Ipv6Packet, MutableIpv6Packet}, + tcp::{ipv6_checksum as ipv6_checksum_tcp, TcpPacket}, + udp::{ipv6_checksum as ipv6_checksum_udp, UdpPacket}, + Packet, +}; + +use crate::client::ClientInfo; +use crate::layer_4; +use crate::Masscanned; + +pub fn repl<'a, 'b>( + ip_req: &'a Ipv6Packet, + masscanned: &Masscanned, + mut client_info: &mut ClientInfo, +) -> Option> { + debug!("receiving IPv6 packet: {:?}", ip_req); + let src = ip_req.get_source(); + let mut dst = ip_req.get_destination(); + /* If masscanned is configured with IP addresses, check that + * the dest. IP address corresponds to one of those + * Otherwise, drop the packet. + **/ + if let Some(ip_addr_list) = masscanned.ip_addresses { + if !ip_addr_list.contains(&IpAddr::V6(dst)) + && ip_req.get_next_header() != IpNextHeaderProtocols::Icmpv6 + { + info!("Ignoring IP packet from {} for {}", &src, &dst); + return None; + } + } + /* Fill client info with source and dest. IP address */ + client_info.ip.src = Some(IpAddr::V6(ip_req.get_source())); + client_info.ip.dst = Some(IpAddr::V6(ip_req.get_destination())); + /* Fill client info with transport layer procotol */ + client_info.transport = Some(ip_req.get_next_header()); + let mut ip_repl; + match ip_req.get_next_header() { + /* Answer to ICMPv6 */ + IpNextHeaderProtocols::Icmpv6 => { + let icmp_req = + Icmpv6Packet::new(ip_req.payload()).expect("error parsing ICMPv6 packet"); + if let (Some(mut icmp_repl), dst_addr) = + layer_4::icmpv6::repl(&icmp_req, masscanned, &client_info) + { + if let Some(ip) = dst_addr { + dst = ip; + } + /* Compute checksum of upper layer */ + icmp_repl.set_checksum(icmpv6_checksum(&icmp_repl.to_immutable(), &src, &dst)); + /* Compute answer length */ + let icmp_len = icmp_repl.packet().len(); + let ip_len = MutableIpv6Packet::minimum_packet_size() + icmp_len; + /* Create answer packet */ + ip_repl = MutableIpv6Packet::owned(vec![0; ip_len]) + .expect("error constructing an IPv6 packet"); + /* Set next header protocol and payload */ + ip_repl.set_next_header(IpNextHeaderProtocols::Icmpv6); + ip_repl.set_payload_length(icmp_len as u16); + ip_repl.set_payload(&icmp_repl.packet().to_vec()); + /* Special value of hlim for ICMP */ + if let Icmpv6Types::NeighborAdvert = icmp_repl.get_icmpv6_type() { + ip_repl.set_hop_limit(255); + }; + } else { + return None; + } + } + /* Answer to TCP */ + IpNextHeaderProtocols::Tcp => { + let tcp_req = TcpPacket::new(ip_req.payload()).expect("error parsing TCP packet"); + if let Some(mut tcp_repl) = layer_4::tcp::repl(&tcp_req, masscanned, &mut client_info) { + /* Compute and set TCP checksum */ + tcp_repl.set_checksum(ipv6_checksum_tcp( + &tcp_repl.to_immutable(), + &ip_req.get_destination(), + &ip_req.get_source(), + )); + /* Compute answer length */ + let tcp_len = tcp_repl.packet().len(); + let ip_len = Ipv6Packet::minimum_packet_size() + tcp_len; + /* Create answer packet */ + ip_repl = MutableIpv6Packet::owned(vec![0; ip_len]) + .expect("error constructing an IPv6 packet"); + /* Set next header protocol and payload */ + ip_repl.set_next_header(IpNextHeaderProtocols::Tcp); + ip_repl.set_payload_length(tcp_len as u16); + ip_repl.set_payload(&tcp_repl.packet()); + } else { + return None; + } + } + /* Answer to UDP */ + IpNextHeaderProtocols::Udp => { + let udp_req = UdpPacket::new(ip_req.payload()).expect("error parsing UDP packet"); + if let Some(mut udp_repl) = layer_4::udp::repl(&udp_req, masscanned, &mut client_info) { + /* Compute and set UDP checksum */ + udp_repl.set_checksum(ipv6_checksum_udp( + &udp_repl.to_immutable(), + &ip_req.get_destination(), + &ip_req.get_source(), + )); + /* Compute answer length */ + let udp_len = udp_repl.packet().len(); + let ip_len = Ipv6Packet::minimum_packet_size() + udp_len; + /* Create answer packet */ + ip_repl = MutableIpv6Packet::owned(vec![0; ip_len]) + .expect("error constructing an IPv6 packet"); + /* Set next header protocol and payload */ + ip_repl.set_next_header(IpNextHeaderProtocols::Udp); + ip_repl.set_payload_length(udp_len as u16); + ip_repl.set_payload(&udp_repl.packet()); + } else { + return None; + } + } + /* Other protocols are not handled (yet) - dropping */ + _ => { + info!( + "IPv6 upper layer not handled: {:?}", + ip_req.get_next_header() + ); + return None; + } + }; + /* If not already set, we set the hlim value */ + if ip_repl.get_hop_limit() == 0 { + ip_repl.set_hop_limit(64); + } + /* Set IP version */ + ip_repl.set_version(6); + /* Set packet source and dest. */ + ip_repl.set_source(dst); + ip_repl.set_destination(src); + debug!("sending IPv6 packet: {:?}", ip_repl); + Some(ip_repl) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::net::Ipv6Addr; + use std::str::FromStr; + + use pnet::util::MacAddr; + + #[test] + fn test_ipv6_reply() { + /* test payload is scapy> IPv6(src="7777:6666:5555:4444:3333:2222:1111:0000", + * dst="0000:1111:2222:3333:4444:5555:6666:7777")/TCP(sport=12345, dport=54321, + * flags="S"))[TCP] */ + let payload = b"09\xd41\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xcf\xbc\x00\x00"; + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv6Addr::new( + 0x7777, 0x6666, 0x5555, 0x4444, 0x3333, 0x2222, 0x1111, 0x0000, + ); + let masscanned_ip_addr = Ipv6Addr::new( + 0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, + ); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V6(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + let mut ip_req = + MutableIpv6Packet::owned(vec![0; Ipv6Packet::minimum_packet_size() + payload.len()]) + .expect("error constructing IPv6 packet"); + ip_req.set_version(6); + ip_req.set_source(test_ip_addr); + /* Set test payload for layer 4 */ + ip_req.set_payload_length(payload.len() as u16); + ip_req.set_payload(payload); + /* Set next protocol */ + ip_req.set_next_header(IpNextHeaderProtocols::Tcp); + /* Send to a legitimate IP address */ + ip_req.set_destination(masscanned_ip_addr); + if let Some(ip_repl) = repl(&ip_req.to_immutable(), &masscanned, &mut client_info) { + assert!(ip_repl.get_destination() == test_ip_addr); + assert!(ip_repl.get_source() == masscanned_ip_addr); + assert!(ip_repl.get_next_header() == IpNextHeaderProtocols::Tcp); + assert!(ip_repl.get_payload_length() == ip_repl.payload().len() as u16); + } else { + panic!("expected an IP answer, got None"); + } + /* Send to a non-legitimate IP address */ + ip_req.set_destination(Ipv6Addr::new( + 0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7778, + )); + assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None); + } +} diff --git a/src/layer_3/mod.rs b/src/layer_3/mod.rs new file mode 100644 index 0000000..68918eb --- /dev/null +++ b/src/layer_3/mod.rs @@ -0,0 +1,18 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +pub mod ipv4; +pub mod ipv6; diff --git a/src/layer_4/icmpv4.rs b/src/layer_4/icmpv4.rs new file mode 100644 index 0000000..95cdf7c --- /dev/null +++ b/src/layer_4/icmpv4.rs @@ -0,0 +1,119 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; + +use pnet::packet::{ + icmp::{IcmpCode, IcmpPacket, IcmpTypes, MutableIcmpPacket}, + Packet, +}; + +use crate::client::ClientInfo; +use crate::Masscanned; + +pub fn repl<'a, 'b>( + icmp_req: &'a IcmpPacket, + _masscanned: &Masscanned, + mut _client_info: &ClientInfo, +) -> Option> { + debug!("receiving ICMPv4 packet: {:?}", icmp_req); + let mut icmp_repl; + match icmp_req.get_icmp_type() { + IcmpTypes::EchoRequest => { + /* Check code of ICMP packet */ + if icmp_req.get_icmp_code() != IcmpCode(0) { + info!("ICMP code not handled: {:?}", icmp_req.get_icmp_code()); + return None; + } + /* Compute answer length */ + let payload_len = icmp_req.payload().len(); + let icmp_len = MutableIcmpPacket::minimum_packet_size() + payload_len; + /* Construct answer packet */ + icmp_repl = MutableIcmpPacket::owned(vec![0; icmp_len]) + .expect("error constructing an ICMP packet"); + /* Set ICMP type and code */ + icmp_repl.set_icmp_type(IcmpTypes::EchoReply); + icmp_repl.set_icmp_code(IcmpCode(0)); + /* Set payload identical to incoming packet + * See RFC 792 - https://datatracker.ietf.org/doc/html/rfc792 p15 + * "The data received in the echo message must be returned in the echo + * reply message." + **/ + icmp_repl.set_payload(icmp_req.payload()); + warn!("ICMP-Echo-Reply to ICMP-Echo-Request"); + } + _ => { + return None; + } + }; + debug!("sending ICMPv4 packet: {:?}", icmp_repl); + Some(icmp_repl) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + use pnet::util::MacAddr; + + #[test] + fn test_icmpv4_reply() { + /* test payload is scapy> ICMP() */ + let payload = b"testpayload"; + let mut client_info = ClientInfo::new(); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: None, + }; + let mut icmp_req = + MutableIcmpPacket::owned(vec![0; IcmpPacket::minimum_packet_size() + payload.len()]) + .expect("error constructing ICMPv4 packet"); + /* Set ICMP payload */ + icmp_req.set_payload(payload); + /* Set legitimate ICMP type and code */ + icmp_req.set_icmp_type(IcmpTypes::EchoRequest); + icmp_req.set_icmp_code(IcmpCode(0)); + if let Some(icmp_repl) = repl(&icmp_req.to_immutable(), &masscanned, &mut client_info) { + assert!(icmp_repl.get_icmp_type() == IcmpTypes::EchoReply); + assert!(icmp_repl.get_icmp_code() == IcmpCode(0)); + assert!(icmp_repl.payload() == payload); + } else { + panic!("expected an IP answer, got None"); + } + /* Set wrong code */ + icmp_req.set_icmp_code(IcmpCode(1)); + assert!(repl(&icmp_req.to_immutable(), &masscanned, &mut client_info) == None); + /* Set wrong type */ + icmp_req.set_icmp_code(IcmpCode(0)); + icmp_req.set_icmp_type(IcmpTypes::EchoReply); + assert!(repl(&icmp_req.to_immutable(), &masscanned, &mut client_info) == None); + /* Try with another payload */ + icmp_req.set_icmp_type(IcmpTypes::EchoRequest); + let payload = b"newpayload!"; + icmp_req.set_payload(payload); + if let Some(icmp_repl) = repl(&icmp_req.to_immutable(), &masscanned, &mut client_info) { + assert!(icmp_repl.get_icmp_type() == IcmpTypes::EchoReply); + assert!(icmp_repl.get_icmp_code() == IcmpCode(0)); + assert!(icmp_repl.payload() == payload); + } else { + panic!("expected an IP answer, got None"); + } + } +} diff --git a/src/layer_4/icmpv6.rs b/src/layer_4/icmpv6.rs new file mode 100644 index 0000000..6db03d2 --- /dev/null +++ b/src/layer_4/icmpv6.rs @@ -0,0 +1,266 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; +use std::convert::From; +use std::net::{IpAddr, Ipv6Addr}; + +use pnet::packet::{ + icmpv6::ndp::{ + Icmpv6Codes, MutableNeighborAdvertPacket, NdpOption, NdpOptionPacket, NdpOptionTypes, + NeighborAdvert, NeighborAdvertFlags, NeighborSolicitPacket, + }, + icmpv6::{Icmpv6, Icmpv6Packet, Icmpv6Types, MutableIcmpv6Packet}, + Packet, +}; + +use crate::client::ClientInfo; +use crate::Masscanned; + +pub fn nd_ns_repl<'a, 'b>( + nd_ns_req: &'a NeighborSolicitPacket, + masscanned: &Masscanned, + _client_info: &ClientInfo, +) -> Option> { + debug!("receiving ND-NS packet: {:?}", nd_ns_req); + /* If masscanned is configured with IP addresses, then + * check that the dest. IP address of the packet is one of + * those handled by masscanned - otherwise, drop the packet. + **/ + if let Some(addresses) = masscanned.ip_addresses { + if !addresses.contains(&IpAddr::V6(nd_ns_req.get_target_addr())) { + return None; + } + } + /* Set answer option to TargetLLAddr(2) */ + let ndp_opt = NdpOption { + option_type: NdpOptionTypes::TargetLLAddr, + /* From RFC 4861, section 4.6: + * Length 8-bit unsigned integer. The length of the option + * (including the type and length fields) in units of + * 8 octets. The value 0 is invalid. Nodes MUST + * silently discard an ND packet that contains an + * option with length zero. + **/ + length: 1, + /* From RFC 4861, section 4.6: + * Options should be padded when necessary to ensure that they end on + * their natural 64-bit boundaries. + * In this case, no need as 6 bytes (mac addr) + 2 bytes (option type + * and length) = 8 bytes + **/ + data: Vec::from(<[u8; 6]>::from(masscanned.mac)), + }; + /* Compute site of options to construct ndp packet */ + let ndp_opt_size = NdpOptionPacket::packet_size(&ndp_opt); + /* Neighbor advertisement response content */ + let ndp_na = NeighborAdvert { + icmpv6_type: Icmpv6Types::NeighborAdvert, + icmpv6_code: Icmpv6Codes::NoCode, + checksum: 0, + flags: NeighborAdvertFlags::Override | NeighborAdvertFlags::Solicited, + reserved: 0, + target_addr: nd_ns_req.get_target_addr(), + options: vec![], + payload: vec![], + }; + /* Construct ND-NA response packet */ + let mut nd_na_repl = MutableNeighborAdvertPacket::owned(vec![ + 0; + /* Size includes the options */ + MutableNeighborAdvertPacket::packet_size(&ndp_na) + + ndp_opt_size + ]) + .expect("error constructing a ND-NA packet"); + /* Set content of response */ + nd_na_repl.populate(&ndp_na); + /* Set content of options */ + nd_na_repl.set_options(&[ndp_opt]); + warn!("ND-NA to ND-NS for {}", nd_ns_req.get_target_addr()); + debug!("sending ND-NA packet: {:?}", nd_na_repl); + Some(nd_na_repl) +} + +/* Because L3 may not know the dest. IPv6 address of the packet in the case + * of a ND-NS packet, this function returns the reply *plus* the dest. IPv6 + * address in the case of a ND-NS, so that L3 knows to which masscanned IP + * address the packet was targetting */ +pub fn repl<'a, 'b>( + icmp_req: &'a Icmpv6Packet, + masscanned: &Masscanned, + client_info: &ClientInfo, +) -> (Option>, Option) { + debug!("receiving ICMPv6 packet: {:?}", icmp_req); + let mut dst_ip = None; + if icmp_req.get_icmpv6_code() != Icmpv6Codes::NoCode { + return (None, None); + } + let mut icmp_repl; + match icmp_req.get_icmpv6_type() { + /* Answer to a neighbor solicitation packet (aka ARP for IPv6) */ + Icmpv6Types::NeighborSolicit => { + let nd_ns_req = + NeighborSolicitPacket::new(icmp_req.packet()).expect("error parsing ND-NS packet"); + /* Construct the answer to the NS - should be a ND-NA */ + if let Some(nd_na_repl) = nd_ns_repl(&nd_ns_req, masscanned, &client_info) { + dst_ip = Some(nd_ns_req.get_target_addr()); + icmp_repl = MutableIcmpv6Packet::owned(nd_na_repl.packet().to_vec()) + .expect("error constructing an ICMPv6 packet"); + } else { + return (None, None); + } + } + /* Answer to an echo request packet */ + Icmpv6Types::EchoRequest => { + /* Construct the echo reply packet */ + let echo_repl = Icmpv6 { + icmpv6_type: Icmpv6Types::EchoReply, + icmpv6_code: Icmpv6Codes::NoCode, + checksum: 0, + /* Same payload as the echo request */ + payload: icmp_req.payload().to_vec(), + }; + icmp_repl = MutableIcmpv6Packet::owned(vec![0; Icmpv6Packet::packet_size(&echo_repl)]) + .expect("error constructing an ICMPv6 packet"); + icmp_repl.populate(&echo_repl); + warn!("ICMPv6-Echo-Reply to ICMPv6-Echo-Request"); + } + _ => { + info!( + "ICMPv6 packet not handled: {:?}", + icmp_req.get_icmpv6_type() + ); + return (None, None); + } + }; + debug!("sending ICMPv6 packet: {:?}", icmp_repl); + (Some(icmp_repl), dst_ip) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::net::Ipv6Addr; + use std::str::FromStr; + + use pnet::packet::icmpv6::ndp::{MutableNeighborSolicitPacket, NeighborSolicit}; + use pnet::util::MacAddr; + + #[test] + fn test_nd_na_reply() { + let client_info = ClientInfo::new(); + let masscanned_ip_addr = Ipv6Addr::new( + 0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, + ); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V6(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + /* Legitimate solicitation */ + let ndp_ns = NeighborSolicit { + icmpv6_type: Icmpv6Types::NeighborSolicit, + icmpv6_code: Icmpv6Codes::NoCode, + checksum: 0, + reserved: 0, + target_addr: masscanned_ip_addr, + options: vec![], + payload: vec![], + }; + let mut nd_ns = MutableNeighborSolicitPacket::owned(vec![ + 0; + /* Size includes the options */ + MutableNeighborSolicitPacket::packet_size(&ndp_ns) + //+ ndp_opt_size + ]) + .expect("error constructing ND-NS packet"); + nd_ns.populate(&ndp_ns); + if let Some(nd_na) = nd_ns_repl(&nd_ns.to_immutable(), &masscanned, &client_info) { + assert!(nd_na.get_icmpv6_code() == Icmpv6Codes::NoCode); + assert!(nd_na.get_icmpv6_type() == Icmpv6Types::NeighborAdvert); + assert!(nd_na.get_target_addr() == masscanned_ip_addr); + assert!(nd_na.get_options().len() == 1); + let nd_na_opt = &nd_na.get_options()[0]; + assert!(nd_na_opt.option_type == NdpOptionTypes::TargetLLAddr); + assert!(nd_na_opt.data.len() == 6); + assert!(nd_na_opt.length == 1); + assert!( + MacAddr::new( + nd_na_opt.data[0], + nd_na_opt.data[1], + nd_na_opt.data[2], + nd_na_opt.data[3], + nd_na_opt.data[4], + nd_na_opt.data[5] + ) == masscanned.mac + ); + } else { + panic!("expected a ND NA answer, got None"); + } + /* Solicitation for another IPv6 address */ + let ndp_ns = NeighborSolicit { + icmpv6_type: Icmpv6Types::NeighborSolicit, + icmpv6_code: Icmpv6Codes::NoCode, + checksum: 0, + reserved: 0, + target_addr: Ipv6Addr::new( + 0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x8888, + ), + options: vec![], + payload: vec![], + }; + nd_ns.populate(&ndp_ns); + assert!(nd_ns_repl(&nd_ns.to_immutable(), &masscanned, &client_info) == None); + } + + #[test] + fn test_icmpv6_reply() { + let payload = b"testpayload"; + let client_info = ClientInfo::new(); + let masscanned_ip_addr = Ipv6Addr::new( + 0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, + ); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V6(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + let mut icmpv6_echo_req = MutableIcmpv6Packet::owned(vec![ + 0; + MutableIcmpv6Packet::minimum_packet_size() + + payload.len() + ]) + .expect("error constructing Icmpv6 packet"); + icmpv6_echo_req.set_icmpv6_code(Icmpv6Codes::NoCode); + icmpv6_echo_req.set_icmpv6_type(Icmpv6Types::EchoRequest); + icmpv6_echo_req.set_payload(payload); + if let (Some(_icmpv6_echo_repl), _) = + repl(&icmpv6_echo_req.to_immutable(), &masscanned, &client_info) + { + } else { + panic!("expected ICMPv6 echo repy - got None"); + } + } +} diff --git a/src/layer_4/mod.rs b/src/layer_4/mod.rs new file mode 100644 index 0000000..2decc18 --- /dev/null +++ b/src/layer_4/mod.rs @@ -0,0 +1,20 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +pub mod icmpv4; +pub mod icmpv6; +pub mod tcp; +pub mod udp; diff --git a/src/layer_4/tcp.rs b/src/layer_4/tcp.rs new file mode 100644 index 0000000..d93585f --- /dev/null +++ b/src/layer_4/tcp.rs @@ -0,0 +1,218 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; + +use pnet::packet::{ + tcp::{MutableTcpPacket, TcpFlags, TcpPacket}, + Packet, +}; + +use crate::client::ClientInfo; +use crate::proto; +use crate::synackcookie; +use crate::Masscanned; + +pub fn repl<'a, 'b>( + tcp_req: &'a TcpPacket, + masscanned: &Masscanned, + mut client_info: &mut ClientInfo, +) -> Option> { + debug!("receiving TCP packet: {:?}", tcp_req); + /* Fill client info with source and dest. TCP port */ + client_info.port.src = Some(tcp_req.get_source()); + client_info.port.dst = Some(tcp_req.get_destination()); + /* Construct response TCP packet */ + let mut tcp_repl; + match tcp_req.get_flags() { + /* Answer to data */ + flags if flags & (TcpFlags::PSH | TcpFlags::ACK) == (TcpFlags::PSH | TcpFlags::ACK) => { + /* First check the synack cookie */ + let ackno = if tcp_req.get_acknowledgement() > 0 { + tcp_req.get_acknowledgement() - 1 + } else { + /* underflow hack */ + 0xFFFFFFFF + }; + /* Compute syncookie */ + if let Ok(cookie) = synackcookie::generate(&client_info, &masscanned.synack_key) { + if cookie != ackno { + info!("PSH-ACK ignored: synackcookie not valid"); + return None; + } + client_info.cookie = Some(cookie); + } + warn!("ACK to PSH-ACK on port {}", tcp_req.get_destination()); + let payload = tcp_req.payload(); + /* Any answer to upper-layer protocol? */ + if let Some(repl) = proto::repl(&payload, masscanned, &mut client_info) { + tcp_repl = MutableTcpPacket::owned( + [vec![0; MutableTcpPacket::minimum_packet_size()], repl].concat(), + ) + .expect("error constructing a TCP packet"); + tcp_repl.set_flags(TcpFlags::ACK | TcpFlags::PSH); + } else { + tcp_repl = + MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]) + .expect("error constructing a TCP packet"); + tcp_repl.set_flags(TcpFlags::ACK); + } + tcp_repl.set_acknowledgement(tcp_req.get_sequence() + (tcp_req.payload().len() as u32)); + tcp_repl.set_sequence(tcp_req.get_acknowledgement()); + } + /* Answer to ACK: nothing */ + flags if flags == TcpFlags::ACK => { + /* answer here when server needs to speak first after handshake */ + return None; + } + /* Answer to RST and FIN: nothing */ + flags if (flags == TcpFlags::RST || flags == (TcpFlags::FIN | TcpFlags::ACK)) => { + return None; + } + /* Answer to SYN */ + flags if flags & TcpFlags::SYN == TcpFlags::SYN => { + tcp_repl = MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]) + .expect("error constructing a TCP packet"); + tcp_repl.set_flags(TcpFlags::ACK); + tcp_repl.set_flags(TcpFlags::SYN | TcpFlags::ACK); + tcp_repl.set_acknowledgement(tcp_req.get_sequence() + 1); + /* generate a SYNACK-cookie (same as masscan) */ + tcp_repl.set_sequence( + synackcookie::generate(&client_info, &masscanned.synack_key).unwrap(), + ); + warn!("SYN-ACK to ACK on port {}", tcp_req.get_destination()); + } + _ => { + info!("TCP flag not handled: {}", tcp_req.get_flags()); + return None; + } + } + /* Set source and dest. port for response packet from client info */ + /* Note: client info could have been modified by upper layers (e.g., STUN) */ + tcp_repl.set_source(client_info.port.dst.unwrap()); + tcp_repl.set_destination(client_info.port.src.unwrap()); + /* Set TCP headers */ + tcp_repl.set_data_offset(5); + tcp_repl.set_window(65535); + debug!("sending TCP packet: {:?}", tcp_repl); + Some(tcp_repl) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::client::ClientInfoSrcDst; + use pnet::util::MacAddr; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + #[test] + fn test_synack_cookie_ipv4() { + let masscanned = Masscanned { + mac: MacAddr(0, 0, 0, 0, 0, 0), + ip_addresses: None, + synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f], + iface: None, + }; + /* reference */ + let ip_src = IpAddr::V4(Ipv4Addr::new(27, 198, 143, 1)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(90, 64, 122, 203)); + let tcp_sport = 65000; + let tcp_dport = 80; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let cookie = synackcookie::generate(&client_info, &masscanned.synack_key).unwrap(); + let mut tcp_req = + MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap(); + tcp_req.set_source(tcp_sport); + tcp_req.set_destination(tcp_dport); + tcp_req.set_flags(TcpFlags::SYN); + let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info); + if some_tcp_repl == None { + assert!(false); + return; + } + let tcp_repl = some_tcp_repl.unwrap(); + assert!(synackcookie::_check( + &client_info, + tcp_repl.get_sequence(), + &masscanned.synack_key + )); + assert!(cookie == tcp_repl.get_sequence()); + } + + #[test] + fn test_synack_cookie_ipv6() { + let masscanned = Masscanned { + mac: MacAddr(0, 0, 0, 0, 0, 0), + ip_addresses: None, + synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f], + iface: None, + }; + /* reference */ + let ip_src = IpAddr::V6(Ipv6Addr::new(234, 52, 183, 47, 184, 172, 64, 141)); + let ip_dst = IpAddr::V6(Ipv6Addr::new(25, 179, 227, 231, 53, 216, 45, 144)); + let tcp_sport = 65000; + let tcp_dport = 80; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let cookie = synackcookie::generate(&client_info, &masscanned.synack_key).unwrap(); + let mut tcp_req = + MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap(); + tcp_req.set_source(tcp_sport); + tcp_req.set_destination(tcp_dport); + tcp_req.set_flags(TcpFlags::SYN); + let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info); + if some_tcp_repl == None { + assert!(false); + return; + } + let tcp_repl = some_tcp_repl.unwrap(); + assert!(synackcookie::_check( + &client_info, + tcp_repl.get_sequence(), + &masscanned.synack_key + )); + assert!(cookie == tcp_repl.get_sequence()); + } +} diff --git a/src/layer_4/udp.rs b/src/layer_4/udp.rs new file mode 100644 index 0000000..cdc1d47 --- /dev/null +++ b/src/layer_4/udp.rs @@ -0,0 +1,54 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; + +use pnet::packet::{ + udp::{MutableUdpPacket, UdpPacket}, + Packet, +}; + +use crate::client::ClientInfo; +use crate::proto; +use crate::Masscanned; + +pub fn repl<'a, 'b>( + udp_req: &'a UdpPacket, + masscanned: &Masscanned, + mut client_info: &mut ClientInfo, +) -> Option> { + debug!("receiving UDP packet: {:?}", udp_req); + /* Fill client info with source and dest. UDP port */ + client_info.port.src = Some(udp_req.get_source()); + client_info.port.dst = Some(udp_req.get_destination()); + let payload = udp_req.payload(); + let mut udp_repl; + if let Some(repl) = proto::repl(&payload, masscanned, &mut client_info) { + udp_repl = MutableUdpPacket::owned( + [vec![0; MutableUdpPacket::minimum_packet_size()], repl].concat(), + ) + .expect("error constructing a UDP packet"); + udp_repl.set_length(udp_repl.packet().len() as u16); + } else { + return None; + } + /* Set source and dest. port for response packet from client info */ + /* Note: client info could have been modified by upper layers (e.g., STUN) */ + udp_repl.set_source(client_info.port.dst.unwrap()); + udp_repl.set_destination(client_info.port.src.unwrap()); + debug!("sending UDP packet: {:?}", udp_repl); + Some(udp_repl) +} diff --git a/src/masscanned.rs b/src/masscanned.rs new file mode 100644 index 0000000..5794357 --- /dev/null +++ b/src/masscanned.rs @@ -0,0 +1,218 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +#[macro_use] +extern crate bitflags; +extern crate lazy_static; + +use std::boxed::Box; +use std::collections::HashSet; +use std::fs::File; +use std::net::IpAddr; +use std::str::FromStr; + +use clap::{App, Arg}; +use log::*; +use pnet::{ + datalink::{self, Channel::Ethernet, DataLinkReceiver, DataLinkSender, NetworkInterface}, + packet::{ + ethernet::{EthernetPacket, MutableEthernetPacket}, + Packet, + }, + util::MacAddr, +}; + +use crate::utils::IpAddrParser; + +mod client; +mod layer_2; +mod layer_3; +mod layer_4; +mod proto; +mod smack; +mod synackcookie; +mod utils; + +const VERSION: &str = "0.2.0"; +const DEFAULT_MAC_ADDR: &str = "c0:ff:ee:c0:ff:ee"; + +pub struct Masscanned<'a> { + pub synack_key: [u64; 2], + pub mac: MacAddr, + /* iface is an Option to make tests easier */ + pub iface: Option<&'a NetworkInterface>, + pub ip_addresses: Option<&'a HashSet>, +} + +/* Get the L2 network interface from its name */ +// TODO testme +// TODO handle errors +fn get_interface(iface_name: &str) -> Option { + let interface_names_match = |iface: &NetworkInterface| iface.name == iface_name; + // Find the network interface with the provided name + let interfaces = datalink::interfaces(); + interfaces.into_iter().find(interface_names_match) +} + +/* Get two L2 channels: + * - one to send data + * - one to receive data + */ +// TODO testme +// TODO handle errors +fn get_channel( + interface: &NetworkInterface, +) -> ( + Box<(dyn DataLinkSender + 'static)>, + Box<(dyn DataLinkReceiver + 'static)>, +) { + // Create a new channel, dealing with layer 2 packets + match datalink::channel(&interface, Default::default()) { + Ok(Ethernet(tx, rx)) => (tx, rx), + Ok(_) => panic!("Unhandled channel type"), + Err(e) => panic!( + "An error occurred when creating the datalink channel: {}", + e + ), + } +} + +fn reply<'a, 'b>(packet: &'a [u8], masscanned: &Masscanned) -> Option> { + let mut client_info = client::ClientInfo::new(); + let eth_req = EthernetPacket::new(packet).expect("impossible to parse Ethernet packet"); + layer_2::reply(ð_req, masscanned, &mut client_info) +} + +fn main() { + /* parse arguments from CLI */ + let args = App::new("Network responder - answer them all") + .version(VERSION) + .about("Network answering machine for various network protocols (L2-L3-L4 + applications)") + .arg( + Arg::with_name("interface") + .short("i") + .long("iface") + .value_name("iface") + .help("the interface to use for receiving/sending packets") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("mac") + .short("a") + .long("mac-addr") + .help("MAC address to use in the response packets") + .takes_value(true), + ) + .arg( + Arg::with_name("ip") + .short("f") + .long("ip-addr-file") + .help("File with the list of IP addresses to impersonate") + .takes_value(true), + ) + .arg( + Arg::with_name("verbosity") + .short("v") + .multiple(true) + .help("Increase message verbosity"), + ) + .get_matches(); + let verbose = args.occurrences_of("verbosity") as usize; + /* initialise logger */ + stderrlog::new() + .module(module_path!()) + .verbosity(verbose) + .init() + .expect("error while initializing logging module"); + warn!("warn messages enabled"); + info!("info messages enabled"); + debug!("debug messages enabled"); + trace!("trace messages enabled"); + info!("Command line arguments:"); + for arg in &args.args { + info!("....{:?}", arg); + } + let iface = if let Some(i) = get_interface( + args.value_of("interface") + .expect("error parsing iface argument"), + ) { + i + } else { + error!( + "Cannot open interface \"{}\" - are you sure it exists?", + args.value_of("interface") + .expect("error parsing iface argument") + ); + return; + }; + if iface.flags & (netdevice::IFF_UP.bits() as u32) == 0 { + error!("specified interface is DOWN"); + return; + } + let mac = if let Some(m) = args.value_of("mac") { + MacAddr::from_str(m).expect("error parsing provided MAC address") + } else if let Some(m) = iface.mac { + m + } else { + MacAddr::from_str(DEFAULT_MAC_ADDR).expect("error parsing default MAC address") + }; + /* Parse ip address file specified */ + /* FIXME: .and_then(|path| File::open(path).map(|file| )).unwrap_or_default() ? */ + let ip_list = if let Some(ref path) = args.value_of("ip") { + if let Ok(file) = File::open(path) { + info!("parsing ip address file: {}", &path); + file.extract_ip_addresses_only(None) + } else { + HashSet::new() + } + } else { + HashSet::new() + }; + let ip_addresses = if !ip_list.is_empty() { + Some(&ip_list) + } else { + None + }; + let masscanned = Masscanned { + synack_key: [0, 0], + mac, + iface: Some(&iface), + ip_addresses, + }; + info!("interface......{}", masscanned.iface.unwrap().name); + info!("mac address....{}", masscanned.mac); + let (mut tx, mut rx) = get_channel(masscanned.iface.unwrap()); + loop { + /* check if network interface is still up */ + if masscanned.iface.unwrap().flags & (netdevice::IFF_UP.bits() as u32) == 0 { + error!("interface is DOWN - aborting"); + break; + } + match rx.next() { + Ok(packet) => { + if let Some(pkt_rep) = reply(packet, &masscanned) { + tx.send_to(pkt_rep.packet(), None); + } else { + info!("packet not handled: {:?}", packet); + } + } + Err(e) => { + error!("An error occurred while reading: {}", e); + } + } + } +} diff --git a/src/proto/http.rs b/src/proto/http.rs new file mode 100644 index 0000000..8a3da01 --- /dev/null +++ b/src/proto/http.rs @@ -0,0 +1,388 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; + +use chrono::Utc; +use lazy_static::lazy_static; +use std::str; + +use crate::client::ClientInfo; +use crate::smack::{ + Smack, SmackFlags, BASE_STATE, NO_MATCH, SMACK_CASE_INSENSITIVE, UNANCHORED_STATE, +}; +use crate::Masscanned; + +pub const HTTP_VERBS: [&str; 9] = [ + "GET", "PUT", "POST", "HEAD", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH", +]; + +#[derive(Copy, Clone)] +enum HttpField { + Verb, + // Incomplete, + // Server, + ContentLength, + ContentType, + // Via, + // Location, + Unknown, + NewLine, +} + +const HTTP_STATE_START: usize = 0; +const HTTP_STATE_VERB: usize = 1; +const HTTP_STATE_SPACE: usize = 2; +const HTTP_STATE_URI: usize = 3; +const HTTP_STATE_H: usize = 4; +const HTTP_STATE_T1: usize = 5; +const HTTP_STATE_T2: usize = 6; +const HTTP_STATE_P: usize = 7; +const HTTP_STATE_SLASH: usize = 8; +const HTTP_STATE_VERSION_MAJ: usize = 9; +const HTTP_STATE_VERSION_MIN: usize = 10; + +const HTTP_STATE_FIELD_START: usize = 32; +const HTTP_STATE_FIELD_NAME: usize = 33; +const HTTP_STATE_FIELD_VALUE: usize = 34; +const HTTP_STATE_CONTENT: usize = 64; + +const HTTP_STATE_FAIL: usize = 0xFFFF; + +struct ProtocolState { + state: usize, + state_bis: usize, + smack_state: usize, + smack_id: usize, + http_verb: Vec, + http_uri: Vec, +} + +impl ProtocolState { + fn new() -> Self { + ProtocolState { + state: HTTP_STATE_START, + state_bis: 0, + smack_state: BASE_STATE, + smack_id: NO_MATCH, + http_verb: Vec::::new(), + http_uri: Vec::::new(), + } + } +} + +const HTTP_PATTERN: [(&str, HttpField, SmackFlags); 4] = [ + ( + "Content-Length", + HttpField::ContentLength, + SmackFlags::ANCHOR_BEGIN, + ), + ( + "Content-Type", + HttpField::ContentType, + SmackFlags::ANCHOR_BEGIN, + ), + (":", HttpField::Unknown, SmackFlags::EMPTY), + ("\n", HttpField::NewLine, SmackFlags::EMPTY), +]; + +lazy_static! { + static ref HTTP_SMACK: Smack = http_init(); +} + +fn http_init() -> Smack { + let mut smack = Smack::new("http".to_string(), SMACK_CASE_INSENSITIVE); + for verb in HTTP_VERBS.iter() { + smack.add_pattern( + verb.as_bytes(), + HttpField::Verb as usize, + SmackFlags::ANCHOR_BEGIN, + ); + } + for p in HTTP_PATTERN.iter() { + smack.add_pattern(p.0.as_bytes(), p.1 as usize, p.2); + } + smack.compile(); + smack +} + +fn http_parse(pstate: &mut ProtocolState, data: &[u8]) { + /* RFC 2616: + * The Request-Line begins with a method token, followed by the + * Request-URI and the protocol version, and ending with CRLF. The + * elements are separated by SP characters. No CR or LF is allowed + * except in the final CRLF sequence. + */ + let mut i = 0; + while i < data.len() { + match pstate.state { + HTTP_STATE_START => { + pstate.state += 1; + continue; + } + HTTP_STATE_VERB => { + let i_save = i; + pstate.smack_id = HTTP_SMACK.search_next(&mut pstate.smack_state, data, &mut i); + pstate.http_verb.extend_from_slice(&data[i_save..i]); + i -= 1; + if pstate.smack_id == HttpField::Verb as usize { + pstate.state += 1; + } else if pstate.smack_id == NO_MATCH { + /* if in UNANCHORED_STATE, it means we'll never get a match from now on */ + if pstate.smack_state == UNANCHORED_STATE { + pstate.state = HTTP_STATE_FAIL; + } else { + /* continue getting input */ + } + } + } + HTTP_STATE_SPACE => { + if data[i] == b' ' { + pstate.state += 1; + } else { + pstate.state = HTTP_STATE_FAIL; + } + } + HTTP_STATE_URI => { + if data[i] != b' ' { + pstate.http_uri.push(data[i]); + } else { + pstate.state += 1; + } + } + HTTP_STATE_H | HTTP_STATE_T1 | HTTP_STATE_T2 | HTTP_STATE_P | HTTP_STATE_SLASH => { + if data[i] != b"HTTP/"[pstate.state - HTTP_STATE_H] { + pstate.state = HTTP_STATE_FAIL; + } else { + pstate.state += 1; + } + } + HTTP_STATE_VERSION_MAJ => { + if data[i] == b'.' { + pstate.state += 1; + } else if !data[i].is_ascii_digit() { + pstate.state = HTTP_STATE_FAIL; + } + } + HTTP_STATE_VERSION_MIN => { + /* ignore \r to be compliant with relaxed implementations of the protocole */ + if data[i] == b'\r' { + } else if data[i] == b'\n' { + pstate.state = HTTP_STATE_FIELD_START; + } else if !data[i].is_ascii_digit() { + pstate.state = HTTP_STATE_FAIL; + } + } + HTTP_STATE_FIELD_START => { + if data[i] == b'\r' { + } else if data[i] == b'\n' { + pstate.state_bis = 0; + pstate.state = HTTP_STATE_CONTENT; + } else { + pstate.state_bis = 0; + pstate.state = HTTP_STATE_FIELD_NAME; + } + } + HTTP_STATE_FIELD_NAME => { + if data[i] == b'\r' || data[i] == b'\n' { + pstate.state = HTTP_STATE_FAIL; + } else if data[i] == b':' { + pstate.state = HTTP_STATE_FIELD_VALUE; + } + } + HTTP_STATE_FIELD_VALUE => { + if data[i] == b'\r' { + } else if data[i] == b'\n' { + pstate.state = HTTP_STATE_FIELD_START; + } + } + HTTP_STATE_FAIL => { + return; + } + HTTP_STATE_CONTENT => { /* so far, do not parse content */ } + _ => {} + }; + i += 1; + } +} + +pub fn repl<'a>( + data: &'a [u8], + _masscanned: &Masscanned, + _client_info: &ClientInfo, +) -> Option> { + debug!("receiving HTTP data"); + let mut pstate = ProtocolState::new(); + http_parse(&mut pstate, data); + if pstate.state == HTTP_STATE_FAIL { + debug!("data in not correctly formatted - not responding"); + debug!("pstate: {}", pstate.state); + return None; + } + let content = "\ + +401 Authorization Required + +

401 Authorization Required

+
nginx/1.14.2
+ + +"; + let repl_data = format!( + "\ +HTTP/1.1 401 Unauthorized +Server: nginx/1.14.2 +Date: {} +Content-Type: text/html +Content-Length: {} +Connection: keep-alive +WWW-Authenticate: Basic realm=\"Access to admin page\" + +{} +", + Utc::now().to_rfc2822(), + content.len(), + content + ) + .into_bytes(); + debug!("sending HTTP data"); + warn!( + "HTTP/1.1 401 to {} {}", + str::from_utf8(&pstate.http_verb).unwrap(), + str::from_utf8(&pstate.http_uri).unwrap() + ); + Some(repl_data) +} + +#[test] +fn test_http_verb() { + /* all at once */ + for verb in HTTP_VERBS.iter() { + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, &verb.as_bytes()); + assert!(pstate.state == HTTP_STATE_SPACE); + assert!(pstate.smack_id == (HttpField::Verb as usize)); + assert!(pstate.http_verb == verb.as_bytes()); + } + /* byte by byte */ + for verb in HTTP_VERBS.iter() { + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + for i in 0..verb.len() { + if i > 0 { + assert!(pstate.state == HTTP_STATE_VERB); + assert!(pstate.smack_id == NO_MATCH); + } + http_parse(&mut pstate, &verb.as_bytes()[i..i + 1]); + } + assert!(pstate.state == HTTP_STATE_SPACE); + assert!(pstate.smack_id == (HttpField::Verb as usize)); + assert!(pstate.http_verb == verb.as_bytes()); + } + /* KO test: XXX */ + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, "XXX".as_bytes()); + assert!(pstate.state == HTTP_STATE_FAIL); + assert!(pstate.smack_state == UNANCHORED_STATE); + assert!(pstate.smack_id == NO_MATCH); + /* KO test: XGET */ + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, "XGET".as_bytes()); + assert!(pstate.state == HTTP_STATE_FAIL); + assert!(pstate.smack_state == UNANCHORED_STATE); + assert!(pstate.smack_id == NO_MATCH); + /* KO test: GEX */ + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, "GEX".as_bytes()); + assert!(pstate.state == HTTP_STATE_FAIL); + assert!(pstate.smack_state == UNANCHORED_STATE); + assert!(pstate.smack_id == NO_MATCH); + /* KO test: GE T */ + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, "GE T".as_bytes()); + assert!(pstate.state == HTTP_STATE_FAIL); + assert!(pstate.smack_state == UNANCHORED_STATE); + assert!(pstate.smack_id == NO_MATCH); +} + +#[test] +fn test_http_request_line() { + let mut pstate = ProtocolState::new(); + let data = "GET /index.php HTTP/1.1\r\n".as_bytes(); + for i in 0..data.len() { + http_parse(&mut pstate, &data[i..i + 1]); + if i < 2 { + assert!(pstate.state == HTTP_STATE_VERB); + } else if i == 2 { + assert!(pstate.state == HTTP_STATE_SPACE); + } else if 3 <= i && i <= 13 { + assert!(pstate.state == HTTP_STATE_URI); + } else if 14 <= i && i <= 19 { + assert!(pstate.state == HTTP_STATE_H + (i - 14)); + } else if i == 20 { + assert!(pstate.state == HTTP_STATE_VERSION_MAJ); + } else if 21 <= i && i <= 23 { + assert!(pstate.state == HTTP_STATE_VERSION_MIN); + } else if i == 24 { + assert!(pstate.state == HTTP_STATE_FIELD_START); + } + } +} + +#[test] +fn test_http_request_field() { + let mut pstate = ProtocolState::new(); + let req = "POST /index.php HTTP/2.0\r\n".as_bytes(); + http_parse(&mut pstate, req); + assert!(pstate.state == HTTP_STATE_FIELD_START); + let field = b"Content-Length"; + http_parse(&mut pstate, field); + assert!(pstate.state == HTTP_STATE_FIELD_NAME); + let dot = b": "; + http_parse(&mut pstate, dot); + assert!(pstate.state == HTTP_STATE_FIELD_VALUE); + let value = b": 0\r\n"; + http_parse(&mut pstate, value); + assert!(pstate.state == HTTP_STATE_FIELD_START); +} + +#[test] +fn test_http_request_no_field() { + let mut pstate = ProtocolState::new(); + let req = "POST /index.php HTTP/2.0\r\n".as_bytes(); + http_parse(&mut pstate, req); + assert!(pstate.state == HTTP_STATE_FIELD_START); + let crlf = "\r\n".as_bytes(); + http_parse(&mut pstate, crlf); + assert!(pstate.state == HTTP_STATE_CONTENT); +} diff --git a/src/proto/mod.rs b/src/proto/mod.rs new file mode 100644 index 0000000..5094c75 --- /dev/null +++ b/src/proto/mod.rs @@ -0,0 +1,239 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use lazy_static::lazy_static; +use log::*; +use pnet::packet::ip::IpNextHeaderProtocols; +use std::collections::HashMap; +use std::sync::Mutex; + +use crate::client::ClientInfo; +use crate::smack::{Smack, SmackFlags, BASE_STATE, NO_MATCH, SMACK_CASE_SENSITIVE}; +use crate::Masscanned; + +mod http; +use http::HTTP_VERBS; + +mod stun; +use stun::{STUN_PATTERN_CHANGE_REQUEST, STUN_PATTERN_EMPTY, STUN_PATTERN_MAGIC}; + +mod ssh; +use ssh::SSH_PATTERN_CLIENT_PROTOCOL; + +const PROTO_HTTP: usize = 1; +const PROTO_STUN: usize = 2; +const PROTO_SSH: usize = 3; + +struct TCPControlBlock { + proto_state: usize, +} + +lazy_static! { + static ref PROTO_SMACK: Smack = proto_init(); + static ref CONTABLE: Mutex> = Mutex::new(HashMap::new()); +} + +fn proto_init() -> Smack { + let mut smack = Smack::new("proto".to_string(), SMACK_CASE_SENSITIVE); + /* HTTP markers */ + for (_, v) in HTTP_VERBS.iter().enumerate() { + smack.add_pattern( + format!("{} /", v).as_bytes(), + PROTO_HTTP, + SmackFlags::ANCHOR_BEGIN, + ); + } + smack.add_pattern( + STUN_PATTERN_MAGIC, + PROTO_STUN, + SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, + ); + smack.add_pattern( + STUN_PATTERN_EMPTY, + PROTO_STUN, + SmackFlags::ANCHOR_BEGIN | SmackFlags::ANCHOR_END | SmackFlags::WILDCARDS, + ); + smack.add_pattern( + STUN_PATTERN_CHANGE_REQUEST, + PROTO_STUN, + SmackFlags::ANCHOR_BEGIN | SmackFlags::ANCHOR_END | SmackFlags::WILDCARDS, + ); + smack.add_pattern( + SSH_PATTERN_CLIENT_PROTOCOL, + PROTO_SSH, + SmackFlags::ANCHOR_BEGIN, + ); + smack.compile(); + smack +} + +pub fn repl<'a>( + data: &'a [u8], + masscanned: &Masscanned, + mut client_info: &mut ClientInfo, +) -> Option> { + debug!("packet payload: {:?}", data); + let mut id; + if client_info.transport == Some(IpNextHeaderProtocols::Tcp) && client_info.cookie == None { + error!("Unexpected empty cookie"); + return None; + } else if client_info.cookie != None { + /* proto over TCP */ + let cookie = client_info.cookie.unwrap(); + let mut ct = CONTABLE.lock().unwrap(); + if !ct.contains_key(&cookie) { + ct.insert( + cookie, + TCPControlBlock { + proto_state: BASE_STATE, + }, + ); + } + let mut i = 0; + let mut tcb = ct.get_mut(&cookie).unwrap(); + let mut state = tcb.proto_state; + id = PROTO_SMACK.search_next(&mut state, &data.to_vec(), &mut i); + tcb.proto_state = state; + } else { + /* proto over else (e.g., UDP) */ + let mut i = 0; + let mut state = BASE_STATE; + id = PROTO_SMACK.search_next(&mut state, &data.to_vec(), &mut i); + /* because we are not over TCP, we can afford to assume end of pattern */ + if id == NO_MATCH { + id = PROTO_SMACK.search_next_end(&mut state); + } + } + /* proto over else (e.g., UDP) */ + if id == PROTO_HTTP { + return http::repl(data, masscanned, client_info); + } else if id == PROTO_STUN { + return stun::repl(data, masscanned, &mut client_info); + } else if id == PROTO_SSH { + return ssh::repl(data, masscanned, &mut client_info); + } else { + debug!("id: {}", id); + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::net::{IpAddr, Ipv4Addr}; + use std::str::FromStr; + + use pnet::util::MacAddr; + + #[test] + fn test_proto_dispatch_stun() { + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.port.src = Some(65000); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + /***** TEST STUN - MAGIC *****/ + /* test payload is: + * - bind request: 0x0001 + * - length: 0x0000 + * - magic cookie: 0x2112a442 + * - message: empty + */ + let payload = + b"\x00\x01\x00\x00\x21\x12\xa4\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + r + } else { + panic!("expected an answer, got nothing"); + }; + /***** TEST STUN - EMPTY *****/ + /* test payload is: + * - bind request: 0x0001 + * - length: 0x0000 + * - magic cookie: 0xaabbccdd + * - message: empty + */ + let payload = + b"\x00\x01\x00\x00\xaa\xbb\xcc\xdd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + r + } else { + panic!("expected an answer, got nothing"); + }; + /***** TEST STUN - CHANGE_REQUEST *****/ + /* test payload is: + * - bind request: 0x0001 + * - length: 0x0008 + * - message: change request + */ + let payload = + b"\x00\x01\x00\x08\x01\xdb\xd4]4\x9f\xe2RQ\x19\x05,\x93\x14f4\x00\x03\x00\x04\x00\x00\x00\x00"; + let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + r + } else { + panic!("expected an answer, got nothing"); + }; + } + + #[test] + fn test_proto_dispatch_ssh() { + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.port.src = Some(65000); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + /***** TEST SSH *****/ + let payloads = [ + "SSH-2.0-PUTTY", + "SSH-2.0-Go", + "SSH-2.0-libssh2_1.4.3", + "SSH-2.0-PuTTY", + "SSH-2.0-AsyncSSH_2.1.0", + "SSH-2.0-libssh2_1.9.0", + "SSH-2.0-libssh2_1.7.0", + "SSH-2.0-8.35 FlowSsh: FlowSshNet_SftpStress54.38.116.473", + "SSH-2.0-libssh_0.9.5", + "SSH-2.0-OpenSSH_6.7p1 Raspbian-5+deb8u3", + ]; + for payload in payloads.iter() { + let _ssh_resp = if let Some(r) = repl(payload.as_bytes(), &masscanned, &mut client_info) + { + r + } else { + panic!("expected an answer, got nothing"); + }; + } + } +} diff --git a/src/proto/ssh.rs b/src/proto/ssh.rs new file mode 100644 index 0000000..a14c6d9 --- /dev/null +++ b/src/proto/ssh.rs @@ -0,0 +1,36 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; + +use std::str; + +use crate::client::ClientInfo; +use crate::Masscanned; + +pub const SSH_PATTERN_CLIENT_PROTOCOL: &[u8; 7] = b"SSH-2.0"; + +pub fn repl<'a>( + data: &'a [u8], + _masscanned: &Masscanned, + mut _client_info: &mut ClientInfo, +) -> Option> { + debug!("receiving SSH data"); + let repl_data = b"SSH-2.0-1\r\n".to_vec(); + debug!("sending SSH answer"); + warn!("SSH server banner to {}", str::from_utf8(&data).unwrap().trim_end()); + return Some(repl_data); +} diff --git a/src/proto/stun.rs b/src/proto/stun.rs new file mode 100644 index 0000000..a5c7ac4 --- /dev/null +++ b/src/proto/stun.rs @@ -0,0 +1,793 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned 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. +// +// Masscanned 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 Masscanned. If not, see . + +use log::*; + +use std::convert::TryInto; + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use byteorder::{BigEndian, ByteOrder}; +use std::io; + +use crate::client::ClientInfo; +use crate::Masscanned; + +/* RFC 5389: The magic cookie field MUST contain the fixed value 0x2112A442 in +network byte order. */ +/* Note: disabled for now due to a « bug » in smack */ +pub const STUN_PATTERN_MAGIC: &[u8; 8] = b"\x00\x01**\x21\x12\xa4\x42"; +pub const STUN_PATTERN_EMPTY: &[u8; 20] = b"\x00\x01\x00\x00****************"; +/* RFC 3489: support without cookie */ +pub const STUN_PATTERN_CHANGE_REQUEST: &[u8; 28] = + b"\x00\x01\x00\x08****************\x00\x03\x00\x04\x00\x00\x00*"; +pub const _STUN_MAGIC: u32 = 0x2112a442; + +pub const STUN_CLASS_REQUEST: u8 = 0b00; +#[allow(dead_code)] +pub const STUN_CLASS_INDICATE: u8 = 0b01; +pub const STUN_CLASS_SUCCESS_RESPONSE: u8 = 0b10; +#[allow(dead_code)] +pub const STUN_CLASS_FAILURE_RESPONSE: u8 = 0b11; + +pub const STUN_ATTR_MAPPED_ADDRESS: u16 = 0x0001; +pub const STUN_ATTR_CHANGE_REQUEST: u16 = 0x0003; + +pub const STUN_METHOD_BINDING: u16 = 0x001; + +pub const STUN_PROTOCOL_FAMILY_IPV4: u8 = 0x01; +pub const STUN_PROTOCOL_FAMILY_IPV6: u8 = 0x02; + +pub const STUN_CHANGE_REQUEST_MASK_IP: u32 = 0x00000004; +pub const STUN_CHANGE_REQUEST_MASK_PORT: u32 = 0x00000002; + +struct StunGenericAttribute { + type_: u16, + length: u16, + data: Vec, +} + +impl StunGenericAttribute { + #[allow(dead_code)] + fn new(data: &[u8]) -> Result { + if data.len() < 4 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "not enough data", + )); + } + let type_ = BigEndian::read_u16(&data[0..2]); + let length = BigEndian::read_u16(&data[2..4]); + if data.len() < 4 + length as usize { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "not enough data", + )); + } + let data = data[4..4 + length as usize].to_vec(); + Ok(StunGenericAttribute { + type_, + length, + data, + }) + } +} + +impl Into> for &StunGenericAttribute { + fn into(self) -> Vec { + let mut v = Vec::::new(); + v.append(&mut self.type_.to_be_bytes().to_vec()); + v.append(&mut self.length.to_be_bytes().to_vec()); + v.append(&mut self.data.clone()); + v + } +} + +struct StunChangeRequestAttribute { + type_: u16, + length: u16, + change_ip: bool, + change_port: bool, +} + +impl Into> for &StunChangeRequestAttribute { + fn into(self) -> Vec { + let mut v = Vec::::new(); + v.append(&mut self.type_.to_be_bytes().to_vec()); + v.append(&mut self.length.to_be_bytes().to_vec()); + let mut flags: u32 = 0; + if self.change_ip { + flags |= STUN_CHANGE_REQUEST_MASK_IP; + } + if self.change_port { + flags |= STUN_CHANGE_REQUEST_MASK_PORT; + } + v.append(&mut flags.to_be_bytes().to_vec()); + v + } +} +struct StunMappedAddressAttribute { + type_: u16, + length: u16, + reserved: u8, + protocol_family: u8, + port: u16, + ip: IpAddr, +} + +impl StunMappedAddressAttribute { + fn new(ip: IpAddr, port: u16) -> Self { + StunMappedAddressAttribute { + type_: STUN_ATTR_MAPPED_ADDRESS, + length: 4 + if let IpAddr::V4(_) = ip { 4 } else { 16 }, + reserved: 0, + protocol_family: if let IpAddr::V4(_) = ip { + STUN_PROTOCOL_FAMILY_IPV4 + } else { + STUN_PROTOCOL_FAMILY_IPV6 + }, + port: port, + ip: ip, + } + } +} + +impl Into> for &StunMappedAddressAttribute { + fn into(self) -> Vec { + let mut v = Vec::::new(); + v.append(&mut self.type_.to_be_bytes().to_vec()); + v.append(&mut self.length.to_be_bytes().to_vec()); + v.push(self.reserved); + v.push(self.protocol_family); + v.push(((self.port & 0xFF00) >> 8).try_into().unwrap()); + v.push((self.port & 0x00FF).try_into().unwrap()); + let mut ip = if let IpAddr::V4(ip) = self.ip { + ip.octets().to_vec() + } else if let IpAddr::V6(ip) = self.ip { + ip.octets().to_vec() + } else { + Vec::new() + }; + v.append(&mut ip); + v + } +} + +enum StunAttribute { + MappedAddress(StunMappedAddressAttribute), + ChangeRequest(StunChangeRequestAttribute), + Generic(StunGenericAttribute), +} + +impl StunAttribute { + fn len(&self) -> u16 { + match self { + StunAttribute::MappedAddress(s) => s.length, + StunAttribute::ChangeRequest(s) => s.length, + StunAttribute::Generic(s) => s.length, + } + } + + #[allow(dead_code)] + fn type_(&self) -> u16 { + match self { + StunAttribute::MappedAddress(s) => s.type_, + StunAttribute::ChangeRequest(s) => s.type_, + StunAttribute::Generic(s) => s.type_, + } + } +} + +impl From> for StunAttribute { + fn from(v: Vec) -> Self { + if v.len() < 4 { + panic!("not enough data"); + } + let type_ = BigEndian::read_u16(&v[0..2]); + let length = BigEndian::read_u16(&v[2..4]); + if v.len() < 4 + length as usize { + panic!("not enough data"); + } + match type_ { + STUN_ATTR_MAPPED_ADDRESS => { + let reserved = v[4]; + let protocol_family = v[5]; + let port = BigEndian::read_u16(&v[6..8]); + StunAttribute::MappedAddress(StunMappedAddressAttribute { + type_, + length, + reserved, + protocol_family, + port, + ip: if protocol_family == STUN_PROTOCOL_FAMILY_IPV4 { + IpAddr::V4(Ipv4Addr::new(v[8], v[9], v[10], v[11])) + } else if protocol_family == STUN_PROTOCOL_FAMILY_IPV6 { + IpAddr::V6(Ipv6Addr::new( + BigEndian::read_u16(&v[8..10]), + BigEndian::read_u16(&v[10..12]), + BigEndian::read_u16(&v[12..14]), + BigEndian::read_u16(&v[14..16]), + BigEndian::read_u16(&v[16..18]), + BigEndian::read_u16(&v[18..20]), + BigEndian::read_u16(&v[20..22]), + BigEndian::read_u16(&v[22..24]), + )) + } else { + panic!("unexpected protocol family"); + }, + }) + } + STUN_ATTR_CHANGE_REQUEST => StunAttribute::ChangeRequest(StunChangeRequestAttribute { + type_, + length, + change_ip: (BigEndian::read_u32(&v[4..8]) & STUN_CHANGE_REQUEST_MASK_IP) + == STUN_CHANGE_REQUEST_MASK_IP, + change_port: (BigEndian::read_u32(&v[4..8]) & STUN_CHANGE_REQUEST_MASK_PORT) + == STUN_CHANGE_REQUEST_MASK_PORT, + }), + _ => StunAttribute::Generic(StunGenericAttribute { + type_, + length, + data: v[4..].to_vec(), + }), + } + } +} + +impl Into> for &StunAttribute { + fn into(self) -> Vec { + match self { + StunAttribute::Generic(s) => s.into(), + StunAttribute::MappedAddress(s) => s.into(), + StunAttribute::ChangeRequest(s) => s.into(), + } + } +} + +/* +struct StunPacket { + class: u8, + method: u16, + length: u16, + magic: u32, + id: u128, + data: Vec, + attributes: Vec, +} + +impl StunPacket { + fn new(data: &[u8]) -> Result { + if data.len() < 20 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "not enough data", + )); + } + let class: u8 = ((data[0] & 0x01) << 1) | ((data[1] & 0x10) >> 4); + let method: u16 = (((data[0] & 0b00111110) << 7) as u16) | ((data[1] & 0b11101111) as u16); + let length: u16 = BigEndian::read_u16(&data[2..4]); + let magic: u32 = BigEndian::read_u32(&data[4..8]); + let id: u128 = ((BigEndian::read_u64(&data[8..16]) as u128) << 32) + | (BigEndian::read_u32(&data[16..20]) as u128); + if data.len() < 20 + length as usize { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "not enough data", + )); + } + let data: Vec = data[20..(20 + length) as usize].to_vec(); + let mut stun = StunPacket { + class, + method, + length, + magic, + id, + data, + attributes: Vec::::new(), + }; + stun.attributes = stun.get_attributes(); + Ok(stun) + } + + fn empty() -> Self { + StunPacket { + class: 0, + method: 0, + length: 0, + magic: 0, + id: 0, + data: Vec::new(), + attributes: Vec::new(), + } + } + + fn get_attributes(&self) -> Vec { + let mut i = 0; + let mut attributes = Vec::::new(); + while i + 4 < self.data.len() { + let attr = StunAttribute::from(self.data[i..].to_vec()); + i += 4 + attr.len() as usize; + attributes.push(attr); + } + attributes + } + + fn set_length(&mut self) { + self.length = 0; + for attr in &self.attributes { + self.length += 4 + attr.len(); + } + } +} + +impl Into> for StunPacket { + fn into(self) -> Vec { + let mut v = Vec::::new(); + // first cocktail with class and method bits + v.push( + TryInto::::try_into((self.method >> 7) & 0b00111110).unwrap() + | TryInto::::try_into((self.class & 0b10) >> 1).unwrap(), + ); + // second cocktail with class and method bits + v.push( + TryInto::::try_into((self.method & 0b01110000) << 1).unwrap() + | TryInto::::try_into((self.class & 0b01) << 4).unwrap() + | TryInto::::try_into(self.method & 0b00001111).unwrap(), + ); + v.append(&mut self.length.to_be_bytes().to_vec()); + v.append(&mut self.magic.to_be_bytes().to_vec()); + v.append(&mut self.id.to_be_bytes()[4..].to_vec()); + for attr in &self.attributes { + v.append(&mut attr.into()); + } + v + } +} +*/ + +struct StunPacket { + class: u8, + method: u16, + length: u16, + id: u128, + data: Vec, + attributes: Vec, +} + +impl StunPacket { + fn new(data: &[u8]) -> Result { + if data.len() < 20 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "not enough data", + )); + } + let class: u8 = ((data[0] & 0x01) << 1) | ((data[1] & 0x10) >> 4); + let method: u16 = (((data[0] & 0b00111110) << 7) as u16) | ((data[1] & 0b11101111) as u16); + let length: u16 = BigEndian::read_u16(&data[2..4]); + let id: u128 = BigEndian::read_u128(&data[4..20]); + if data.len() < 20 + length as usize { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "not enough data", + )); + } + let data: Vec = data[20..(20 + length) as usize].to_vec(); + let mut stun = StunPacket { + class, + method, + length, + id, + data, + attributes: Vec::::new(), + }; + stun.attributes = stun.get_attributes(); + Ok(stun) + } + + fn empty() -> Self { + StunPacket { + class: 0, + method: 0, + length: 0, + id: 0, + data: Vec::new(), + attributes: Vec::new(), + } + } + + fn get_attributes(&self) -> Vec { + let mut i = 0; + let mut attributes = Vec::::new(); + while i + 4 < self.data.len() { + let attr = StunAttribute::from(self.data[i..].to_vec()); + i += 4 + attr.len() as usize; + attributes.push(attr); + } + attributes + } + + fn set_length(&mut self) { + self.length = 0; + for attr in &self.attributes { + self.length += 4 + attr.len(); + } + } +} + +impl Into> for StunPacket { + fn into(self) -> Vec { + let mut v = Vec::::new(); + // first cocktail with class and method bits + v.push( + TryInto::::try_into((self.method >> 7) & 0b00111110).unwrap() + | TryInto::::try_into((self.class & 0b10) >> 1).unwrap(), + ); + // second cocktail with class and method bits + v.push( + TryInto::::try_into((self.method & 0b01110000) << 1).unwrap() + | TryInto::::try_into((self.class & 0b01) << 4).unwrap() + | TryInto::::try_into(self.method & 0b00001111).unwrap(), + ); + v.append(&mut self.length.to_be_bytes().to_vec()); + v.append(&mut self.id.to_be_bytes().to_vec()); + for attr in &self.attributes { + v.append(&mut attr.into()); + } + v + } +} + +/* +pub fn repl<'a>( + data: &'a [u8], + _masscanned: &Masscanned, + client_info: ClientInfo, +) -> Option> { + debug!("receiving STUN data"); + let stun_req: StunPacket = if let Ok(s) = StunPacket::new(&data) { + s + } else { + return None; + }; + if stun_req.class != STUN_CLASS_REQUEST { + info!( + "STUN packet not handled (class unknown: 0b{:b})", + stun_req.class + ); + return None; + } + if stun_req.method != STUN_METHOD_BINDING { + info!( + "STUN packet not handled (method unknown: 0x{:03x})", + stun_req.method + ); + return None; + } + /* + * To be compatible with RFC3489: ignore magic + if stun_req.magic != STUN_MAGIC { + info!( + "STUN packet not handled (magic unknown: 0x{:04x})", + stun_req.magic + ); + return None; + } + */ + if client_info.ip.src == None { + error!("STUN packet not handled (expected client ip address not found)"); + return None; + } + if client_info.port.src == None { + error!("STUN packet not handled (expected client port address not found)"); + return None; + } + let mut stun_resp: StunPacket = StunPacket::empty(); + stun_resp.class = STUN_CLASS_SUCCESS_RESPONSE; + stun_resp.method = STUN_METHOD_BINDING; + stun_resp.id = stun_req.id; + stun_resp.attributes = Vec::::new(); + stun_resp.attributes.push(StunAttribute::MappedAddress( + StunMappedAddressAttribute::new(client_info.ip.src.unwrap(), client_info.port.src.unwrap()), + )); + stun_resp.set_length(); + return Some(stun_resp.into()); +} +*/ + +pub fn repl<'a>( + data: &'a [u8], + _masscanned: &Masscanned, + mut client_info: &mut ClientInfo, +) -> Option> { + debug!("receiving STUN data"); + let stun_req: StunPacket = if let Ok(s) = StunPacket::new(&data) { + s + } else { + return None; + }; + if stun_req.class != STUN_CLASS_REQUEST { + info!( + "STUN packet not handled (class unknown: 0b{:b})", + stun_req.class + ); + return None; + } + if stun_req.method != STUN_METHOD_BINDING { + info!( + "STUN packet not handled (method unknown: 0x{:03x})", + stun_req.method + ); + return None; + } + if client_info.ip.src == None { + error!("STUN packet not handled (expected client ip address not found)"); + return None; + } + if client_info.port.src == None { + error!("STUN packet not handled (expected client port address not found)"); + return None; + } + /* Change client_info if CHANGE_REQUEST was set by client */ + for attr in &stun_req.attributes { + if let StunAttribute::ChangeRequest(a) = attr { + if a.change_ip {} + if a.change_port { + client_info.port.dst = Some(client_info.port.dst.unwrap().wrapping_add(1)); + } + } + } + let mut stun_resp: StunPacket = StunPacket::empty(); + stun_resp.class = STUN_CLASS_SUCCESS_RESPONSE; + stun_resp.method = STUN_METHOD_BINDING; + stun_resp.id = stun_req.id; + stun_resp.attributes = Vec::::new(); + stun_resp.attributes.push(StunAttribute::MappedAddress( + StunMappedAddressAttribute::new(client_info.ip.src.unwrap(), client_info.port.src.unwrap()), + )); + stun_resp.set_length(); + debug!("sending STUN answer"); + return Some(stun_resp.into()); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::str::FromStr; + + use pnet::util::MacAddr; + + #[test] + fn test_proto_stun_ipv4() { + /* test payload is: + * - bind request: 0x0001 + * - length: 0x0000 + * - magic cookie: 0x2112a442 + * - id: 0xaabbccddeeffffeeddccbbaa + * - message: empty + */ + let payload = + b"\x00\x01\x00\x00\x21\x12\xa4\x42\xaa\xbb\xcc\xdd\xee\xff\xff\xee\xdd\xcc\xbb\xaa"; + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr)); + client_info.port.src = Some(55000); + client_info.port.dst = Some(65000); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + r + } else { + panic!("expected an answer, got None"); + }; + let stun_resp = StunPacket::new(&payload_resp).unwrap(); + assert!(stun_resp.class == STUN_CLASS_SUCCESS_RESPONSE); + assert!(stun_resp.method == STUN_METHOD_BINDING); + assert!( + stun_resp.id + == BigEndian::read_u128( + b"\x21\x12\xa4\x42\xaa\xbb\xcc\xdd\xee\xff\xff\xee\xdd\xcc\xbb\xaa" + ) + ); + assert!(stun_resp.attributes.len() == 1); + if let StunAttribute::MappedAddress(attr) = &stun_resp.attributes[0] { + assert!(attr.type_ == STUN_ATTR_MAPPED_ADDRESS); + assert!(attr.length == 8); + assert!(attr.reserved == 0); + assert!(attr.protocol_family == STUN_PROTOCOL_FAMILY_IPV4); + assert!(attr.port == client_info.port.src.unwrap()); + assert!(attr.ip == client_info.ip.src.unwrap()); + } else { + panic!("expected MappedAddress attribute"); + } + /* Check that client_info was not modified */ + assert!(client_info.ip.src == Some(IpAddr::V4(test_ip_addr))); + assert!(client_info.ip.dst == Some(IpAddr::V4(masscanned_ip_addr))); + assert!(client_info.port.src == Some(55000)); + assert!(client_info.port.dst == Some(65000)); + } + + #[test] + fn test_proto_stun_ipv6() { + /* test payload is: + * - bind request: 0x0001 + * - length: 0x0000 + * - magic cookie: 0x2112a442 + * - id: 0xaabbccddeeffffeeddccbbaa + * - message: empty + */ + let payload = + b"\x00\x01\x00\x00\x21\x12\xa4\x42\xaa\xbb\xcc\xdd\xee\xff\xff\xee\xdd\xcc\xbb\xaa"; + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv6Addr::new( + 0x7777, 0x6666, 0x5555, 0x4444, 0x3333, 0x2222, 0x1111, 0x0000, + ); + let masscanned_ip_addr = Ipv6Addr::new( + 0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, + ); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V6(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + client_info.ip.src = Some(IpAddr::V6(test_ip_addr)); + client_info.ip.dst = Some(IpAddr::V6(masscanned_ip_addr)); + client_info.port.src = Some(55000); + client_info.port.dst = Some(65000); + let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + r + } else { + panic!("expected an answer, got None"); + }; + let stun_resp = StunPacket::new(&payload_resp).unwrap(); + assert!(stun_resp.class == STUN_CLASS_SUCCESS_RESPONSE); + assert!(stun_resp.method == STUN_METHOD_BINDING); + assert!( + stun_resp.id + == BigEndian::read_u128( + b"\x21\x12\xa4\x42\xaa\xbb\xcc\xdd\xee\xff\xff\xee\xdd\xcc\xbb\xaa" + ) + ); + assert!(stun_resp.attributes.len() == 1); + if let StunAttribute::MappedAddress(attr) = &stun_resp.attributes[0] { + assert!(attr.type_ == STUN_ATTR_MAPPED_ADDRESS); + assert!(attr.length == 20); + assert!(attr.reserved == 0); + assert!(attr.protocol_family == STUN_PROTOCOL_FAMILY_IPV6); + assert!(attr.port == client_info.port.src.unwrap()); + assert!(attr.ip == client_info.ip.src.unwrap()); + } else { + panic!("expected MappedAddress attribute"); + } + /* Check that client_info was not modified */ + assert!(client_info.ip.src == Some(IpAddr::V6(test_ip_addr))); + assert!(client_info.ip.dst == Some(IpAddr::V6(masscanned_ip_addr))); + assert!(client_info.port.src == Some(55000)); + assert!(client_info.port.dst == Some(65000)); + } + + #[test] + fn test_change_request_port() { + let payload = b"\x00\x01\x00\x08\x03\xa3\xb9FM\xd8\xebu\xe1\x94\x81GB\x93\x84\\\x00\x03\x00\x04\x00\x00\x00\x02"; + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr)); + client_info.port.src = Some(55000); + client_info.port.dst = Some(65000); + let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + r + } else { + panic!("expected an answer, got None"); + }; + let stun_resp = StunPacket::new(&payload_resp).unwrap(); + assert!(stun_resp.class == STUN_CLASS_SUCCESS_RESPONSE); + assert!(stun_resp.method == STUN_METHOD_BINDING); + assert!( + stun_resp.id + == BigEndian::read_u128(b"\x03\xa3\xb9FM\xd8\xebu\xe1\x94\x81GB\x93\x84\\") + ); + assert!(stun_resp.attributes.len() == 1); + if let StunAttribute::MappedAddress(attr) = &stun_resp.attributes[0] { + assert!(attr.type_ == STUN_ATTR_MAPPED_ADDRESS); + assert!(attr.length == 8); + assert!(attr.reserved == 0); + assert!(attr.protocol_family == STUN_PROTOCOL_FAMILY_IPV4); + assert!(attr.port == client_info.port.src.unwrap()); + assert!(attr.ip == client_info.ip.src.unwrap()); + } else { + panic!("expected MappedAddress attribute"); + } + /* Check that client_info was not modified */ + assert!(client_info.ip.src == Some(IpAddr::V4(test_ip_addr))); + assert!(client_info.ip.dst == Some(IpAddr::V4(masscanned_ip_addr))); + assert!(client_info.port.src == Some(55000)); + assert!(client_info.port.dst == Some(65001)); + } + + #[test] + fn test_change_request_port_overflow() { + let payload = b"\x00\x01\x00\x08\x03\xa3\xb9FM\xd8\xebu\xe1\x94\x81GB\x93\x84\\\x00\x03\x00\x04\x00\x00\x00\x02"; + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr)); + client_info.port.src = Some(55000); + client_info.port.dst = Some(65535); + let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + r + } else { + panic!("expected an answer, got None"); + }; + let stun_resp = StunPacket::new(&payload_resp).unwrap(); + assert!(stun_resp.class == STUN_CLASS_SUCCESS_RESPONSE); + assert!(stun_resp.method == STUN_METHOD_BINDING); + assert!( + stun_resp.id + == BigEndian::read_u128(b"\x03\xa3\xb9FM\xd8\xebu\xe1\x94\x81GB\x93\x84\\") + ); + assert!(stun_resp.attributes.len() == 1); + if let StunAttribute::MappedAddress(attr) = &stun_resp.attributes[0] { + assert!(attr.type_ == STUN_ATTR_MAPPED_ADDRESS); + assert!(attr.length == 8); + assert!(attr.reserved == 0); + assert!(attr.protocol_family == STUN_PROTOCOL_FAMILY_IPV4); + assert!(attr.port == client_info.port.src.unwrap()); + assert!(attr.ip == client_info.ip.src.unwrap()); + } else { + panic!("expected MappedAddress attribute"); + } + /* Check that client_info was not modified */ + assert!(client_info.ip.src == Some(IpAddr::V4(test_ip_addr))); + assert!(client_info.ip.dst == Some(IpAddr::V4(masscanned_ip_addr))); + assert!(client_info.port.src == Some(55000)); + assert!(client_info.port.dst == Some(0)); + } +} diff --git a/src/smack/mod.rs b/src/smack/mod.rs new file mode 100644 index 0000000..0d9b0a7 --- /dev/null +++ b/src/smack/mod.rs @@ -0,0 +1,9 @@ +mod smack; +mod smack_constants; +mod smack_pattern; +mod smack_queue; +mod smack_utils; + +pub use smack::Smack; +pub use smack_constants::*; +pub use smack_utils::SmackFlags; diff --git a/src/smack/smack.rs b/src/smack/smack.rs new file mode 100644 index 0000000..bb37358 --- /dev/null +++ b/src/smack/smack.rs @@ -0,0 +1,758 @@ +use std::mem; + +use crate::smack::smack_constants::*; +use crate::smack::smack_pattern::SmackPattern; +use crate::smack::smack_queue::SmackQueue; +use crate::smack::smack_utils::{row_shift_from_symbol_count, SmackFlags}; + +struct SmackRow { + next_state: Vec, + fail: usize, +} + +impl SmackRow { + fn new() -> Self { + SmackRow { + next_state: vec![BASE_STATE; ALPHABET_SIZE], + fail: 0, + } + } +} + +struct SmackMatches { + m_ids: Vec, + m_count: usize, +} + +impl SmackMatches { + fn new() -> Self { + SmackMatches { + m_ids: Vec::new(), + m_count: 0, + } + } + fn copy_matches(&mut self, new_ids: Vec) { + for id in &new_ids { + if !self.m_ids.contains(id) { + self.m_count += 1; + self.m_ids.push(*id) + } + } + } +} + +pub struct Smack { + _name: String, + is_nocase: bool, + is_anchor_begin: bool, + is_anchor_end: bool, + m_pattern_list: Vec, + m_pattern_count: usize, + m_state_table: Vec, + m_state_count: usize, + m_state_max: usize, + m_match: Vec, + m_match_limit: usize, + symbol_to_char: Vec, + char_to_symbol: Vec, + symbol_count: usize, + row_shift: usize, + transitions: Vec, +} + +fn make_copy_of_pattern(pattern: &[u8], is_nocase: bool) -> Vec { + let mut p = pattern.clone().to_vec(); + for i in 0..p.len() { + if is_nocase { + p[i] = p[i].to_ascii_lowercase(); + } + } + p +} + +impl Smack { + pub fn new(name: String, nocase: bool) -> Self { + Smack { + _name: name, + is_nocase: nocase, + is_anchor_begin: false, + is_anchor_end: false, + m_pattern_list: Vec::new(), + m_pattern_count: 0, + m_state_table: Vec::new(), + m_state_count: 0, + m_state_max: 0, + m_match: Vec::new(), + m_match_limit: 0, + symbol_to_char: vec![0; ALPHABET_SIZE], + char_to_symbol: vec![0; ALPHABET_SIZE], + symbol_count: 0, + row_shift: 0, + transitions: Vec::new(), + } + } + fn create_intermediate_table(&mut self, size: usize) { + for _ in 0..size { + self.m_state_table.push(SmackRow::new()); + } + } + fn create_matches_table(&mut self, size: usize) { + for _ in 0..size { + self.m_match.push(SmackMatches::new()); + } + } + fn add_symbol(&mut self, c: usize) -> usize { + for i in 1..self.symbol_count + 1 { + if self.symbol_to_char[i] == c { + return i; + } + } + self.symbol_count += 1; + let symbol = self.symbol_count; + self.symbol_to_char[symbol] = c; + self.char_to_symbol[c] = symbol.to_le_bytes()[0]; + symbol + } + fn add_symbols(&mut self, pattern: &[u8]) { + for c in pattern { + if self.is_nocase { + self.add_symbol(c.to_ascii_lowercase().into()); + } else { + self.add_symbol((*c).into()); + } + } + } + pub fn add_pattern(&mut self, pattern: &[u8], id: usize, flags: SmackFlags) { + let p = SmackPattern::new(make_copy_of_pattern(pattern, self.is_nocase), id, flags); + if p.is_anchor_begin { + self.is_anchor_begin = true; + } + if p.is_anchor_end { + self.is_anchor_end = true; + } + self.add_symbols(&p.pattern); + self.m_pattern_list.push(p); + self.m_pattern_count += 1; + } + fn set_goto(&mut self, r: usize, a: usize, h: usize) { + self.m_state_table[r].next_state[a] = h; + } + fn goto(&self, r: usize, a: usize) -> usize { + self.m_state_table[r].next_state[a] + } + fn set_goto_fail(&mut self, r: usize, h: usize) { + self.m_state_table[r].fail = h; + } + fn goto_fail(&self, r: usize) -> usize { + self.m_state_table[r].fail + } + fn new_state(&mut self) -> usize { + self.m_state_count += 1; + self.m_state_count - 1 + } + fn add_prefixes(&mut self, p: &SmackPattern) { + let mut state = BASE_STATE; + let pattern = &p.pattern; + if p.is_anchor_begin { + state = self.goto(state, CHAR_ANCHOR_START); + } + let mut i = 0; + while i < pattern.len() && self.goto(state, pattern[i].into()) != FAIL_STATE { + state = self.goto(state, pattern[i].into()); + i += 1; + } + while i < pattern.len() { + let new_state = self.new_state(); + self.set_goto(state, pattern[i].into(), new_state); + state = new_state; + i += 1; + } + if p.is_anchor_end { + let new_state = self.new_state(); + self.set_goto(state, CHAR_ANCHOR_END, new_state); + state = new_state; + } + self.m_match[state].copy_matches(vec![p.id]); + } + fn stage0_compile_prefixes(&mut self) { + self.m_state_count = 1; + for s in 0..self.m_state_max { + for a in 0..ALPHABET_SIZE { + self.set_goto(s, a, FAIL_STATE); + } + } + if self.is_anchor_begin { + let anchor_begin = self.new_state(); + self.set_goto(BASE_STATE, CHAR_ANCHOR_START, anchor_begin); + } + let plist = mem::replace(&mut self.m_pattern_list, Vec::new()); + for p in plist.iter() { + self.add_prefixes(&p); + } + self.m_pattern_list = plist; + for a in 0..ALPHABET_SIZE { + if self.goto(BASE_STATE, a) == FAIL_STATE { + self.set_goto(BASE_STATE, a, BASE_STATE); + } + } + } + fn stage1_generate_fails(&mut self) { + let mut queue: SmackQueue = SmackQueue::new(); + for a in 0..ALPHABET_SIZE { + let s = self.goto(BASE_STATE, a); + if s != BASE_STATE { + queue.enqueue(s); + self.set_goto_fail(s, BASE_STATE); + } + } + while queue.has_more_items() { + let r = queue.dequeue(); + for a in 0..ALPHABET_SIZE { + let s = self.goto(r, a); + if s == FAIL_STATE { + continue; + } + if s == r { + continue; + } + queue.enqueue(s); + let mut f = self.goto_fail(r); + while self.goto(f, a) == FAIL_STATE { + f = self.goto_fail(f); + } + self.set_goto_fail(s, self.goto(f, a)); + if self.m_match[self.goto(f, a)].m_count > 0 { + let gt = self.goto(f, a); + let m = mem::take(&mut self.m_match[gt].m_ids); + self.m_match[s].copy_matches(m.clone()); + self.m_match[gt].m_ids = m; + } + } + } + } + fn stage2_link_fails(&mut self) { + let mut queue = SmackQueue::new(); + for a in 0..ALPHABET_SIZE { + if self.goto(BASE_STATE, a) != BASE_STATE { + queue.enqueue(self.goto(BASE_STATE, a)); + } + } + loop { + if !queue.has_more_items() { + break; + } + let r = queue.dequeue(); + for a in 0..ALPHABET_SIZE { + if self.goto(r, a) == FAIL_STATE { + self.set_goto(r, a, self.goto(self.goto_fail(r), a)); + } else if self.goto(r, a) == r { + } else { + queue.enqueue(self.goto(r, a)); + } + } + } + } + fn swap_rows(&mut self, row1: usize, row2: usize) { + let tmp = mem::replace(&mut self.m_state_table[row1], SmackRow::new()); + self.m_state_table[row1] = mem::replace(&mut self.m_state_table[row2], tmp); + let tmp = mem::replace(&mut self.m_match[row1], SmackMatches::new()); + self.m_match[row1] = mem::replace(&mut self.m_match[row2], tmp); + for s in 0..self.m_state_count { + for a in 0..ALPHABET_SIZE { + if self.goto(s, a) == row1 { + self.set_goto(s, a, row2); + } else if self.goto(s, a) == row2 { + self.set_goto(s, a, row1); + } + } + } + } + fn stage3_sort(&mut self) { + let mut start = 0; + let mut end = self.m_state_count; + loop { + while start < end && self.m_match[start].m_count == 0 { + start += 1; + } + while start < end && self.m_match[end - 1].m_count != 0 { + end -= 1; + } + if start >= end { + break; + } + self.swap_rows(start, end - 1); + } + self.m_match_limit = start; + } + fn stage4_make_final_table(&mut self) { + let row_count = self.m_state_count; + self.row_shift = row_shift_from_symbol_count(self.symbol_count); + let column_count = 1 << self.row_shift; + self.transitions = vec![0; row_count * column_count]; + for row in 0..row_count { + for c in 0..ALPHABET_SIZE { + let symbol = usize::from(self.char_to_symbol[c]); + let transition = self.goto(row, c); + self.transitions[row * column_count + symbol] = transition; + } + } + } + fn fixup_wildcards(&mut self) { + for i in 0..self.m_pattern_count { + let p = &self.m_pattern_list[i]; + if !p.is_wildcards { + continue; + } + for j in 0..p.pattern.len() { + let mut row = 0; + let mut offset = 0; + let row_size = 1 << self.row_shift; + let base_state = if self.is_anchor_begin { + UNANCHORED_STATE + } else { + BASE_STATE + }; + if p.pattern[j] != b'*' { + continue; + } + while offset < j { + self.search_next(&mut row, &p.pattern[..j], &mut offset); + } + row &= 0xFFFFFF; + let next_pattern = self.transitions + [(row << self.row_shift) + usize::from(self.char_to_symbol[usize::from(b'*')])]; + for k in 0..row_size { + if self.transitions[(row << self.row_shift) + k] == base_state { + self.transitions[(row << self.row_shift) + k] = next_pattern; + } + } + } + } + } + fn inner_match(&self, px: Vec, length: usize, state: usize) -> (usize, usize) { + let px_start = 0; + let px_end = length; + let mut row = state; + let mut idx = px_start; + while idx < px_end { + let column: usize = self.char_to_symbol[usize::from(px[idx])].into(); + row = self.transitions[(row << self.row_shift) + column]; + if row >= self.m_match_limit { + break; + } + idx += 1; + } + (idx - px_start, row) + } + fn inner_match_shift7(&self, px: Vec, length: usize, state: usize) -> (usize, usize) { + let px_start = 0; + let px_end = length; + let mut row = state; + let mut idx = px_start; + while idx < px_end { + let column: usize = self.char_to_symbol[usize::from(px[idx])].into(); + row = self.transitions[(row << 7) + column]; + if row >= self.m_match_limit { + break; + } + idx += 1; + } + (idx - px_start, row) + } + pub fn search_next(&self, current_state: &mut usize, v_px: &[u8], offset: &mut usize) -> usize { + let px = v_px; + let length = px.len(); + let mut i = *offset; + let mut id = NO_MATCH; + let mut row = *current_state & 0xFFFFFF; + let mut current_matches = *current_state >> 24; + if current_matches == 0 { + if self.row_shift == 7 { + let (ii, new_row) = self.inner_match_shift7(px[i..].to_vec(), length - i, row); + i += ii; + row = new_row; + } else { + let (ii, new_row) = self.inner_match(px[i..].to_vec(), length - i, row); + i += ii; + row = new_row; + } + if self.m_match[row].m_count != 0 { + i += 1; + current_matches = self.m_match[row].m_count; + } + } + if current_matches != 0 { + id = self.m_match[row].m_ids[current_matches - 1]; + current_matches -= 1; + } + let new_state = row | (current_matches << 24); + *current_state = new_state; + *offset = i; + id + } + pub fn search_next_end(&self, current_state: &mut usize) -> usize { + let id; + let mut row = *current_state & 0xFFFFFF; + let mut current_matches = *current_state >> 24; + let column = self.char_to_symbol[CHAR_ANCHOR_END]; + /* + * We can enumerate more than one matching end patterns. When we + * reach the end of that list, return NOT FOUND. + */ + if current_matches == 0xFF { + return NO_MATCH; + } + /* + * If we've already returned the first result in our list, + * then return the next result. + */ + if current_matches != 0 { + id = self.m_match[row].m_ids[current_matches - 1]; + current_matches -= 1; + } else { + /* + * This is the same logic as for "smack_search()", except there is + * only one byte of input -- the virtual character ($) that represents + * the anchor at the end of some patterns. + */ + row = self.transitions[(row << self.row_shift) + column as usize]; + /* There was no match, so therefore return NOT FOUND */ + if self.m_match[row].m_count == 0 { + return NO_MATCH; + } + /* + * If we reach this point, we have found matches, but + * haven't started returning them. So start returning + * them. This returns the first one in the list. + */ + current_matches = self.m_match[row].m_count; + id = self.m_match[row].m_ids[current_matches - 1]; + if current_matches > 0 { + current_matches -= 1; + } else { + current_matches = 0xFF; + } + } + let new_state = row | (current_matches << 24); + *current_state = new_state; + id + } + pub fn _next_match(&self, current_state: &mut usize) -> usize { + let mut id = NO_MATCH; + let row = *current_state & 0xFFFFFF; + let mut current_matches = *current_state >> 24; + if current_matches != 0 { + id = self.m_match[row].m_ids[current_matches - 1]; + current_matches -= 1; + } + *current_state = row | (current_matches << 24); + return id; + } + pub fn compile(&mut self) { + if self.is_anchor_begin { + self.add_symbol(CHAR_ANCHOR_START); + } + if self.is_anchor_end { + self.add_symbol(CHAR_ANCHOR_END); + } + if self.is_nocase { + for i in b'A'..b'Z' + 1 { + self.char_to_symbol[usize::from(i)] = + self.char_to_symbol[usize::from(i.to_ascii_lowercase())]; + } + } + self.m_state_max = 1; + for p in self.m_pattern_list.iter() { + if p.is_anchor_begin { + self.m_state_max += 1; + } + if p.is_anchor_end { + self.m_state_max += 1; + } + self.m_state_max += p.pattern.len(); + } + self.create_intermediate_table(self.m_state_max); + self.create_matches_table(self.m_state_max); + self.stage0_compile_prefixes(); + self.stage1_generate_fails(); + self.stage2_link_fails(); + if self.is_anchor_begin { + self.swap_rows(BASE_STATE, UNANCHORED_STATE); + } + self.stage3_sort(); + self.stage4_make_final_table(); + // self.dump(); + // self.dump_transitions(); + self.fixup_wildcards(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pattern() { + let mut smack = Smack::new("test".to_string(), SMACK_CASE_INSENSITIVE); + let patterns = vec![ + "GET", + "PUT", + "POST", + "OPTIONS", + "HEAD", + "DELETE", + "TRACE", + "CONNECT", + "PROPFIND", + "PROPPATCH", + "MKCOL", + "MKWORKSPACE", + "MOVE", + "LOCK", + "UNLOCK", + "VERSION-CONTROL", + "REPORT", + "CHECKOUT", + "CHECKIN", + "UNCHECKOUT", + "COPY", + "UPDATE", + "LABEL", + "BASELINE-CONTROL", + "MERGE", + "SEARCH", + "ACL", + "ORDERPATCH", + "PATCH", + "MKACTIVITY", + ]; + let text = "ahpropfinddf;orderpatchposearchmoversion-controlockasldhf"; + let mut state = 0; + for (i, p) in patterns.iter().enumerate() { + smack.add_pattern(p.as_bytes(), i, SmackFlags::EMPTY); + } + smack.compile(); + let mut i = 0; + let test = |pat: usize, offset: usize, id: usize, i: usize| (pat == id) && (offset == i); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(test(8, 10, id, i)); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(test(28, 23, id, i)); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(test(27, 23, id, i)); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(test(25, 31, id, i)); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(test(12, 35, id, i)); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(test(15, 48, id, i)); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(test(13, 51, id, i)); + } + + #[test] + fn test_anchor_begin() { + /* test without anchor */ + let mut smack = Smack::new("test anchor begin".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern(b"abc", 0, SmackFlags::EMPTY); + smack.add_pattern(b"def", 1, SmackFlags::EMPTY); + smack.compile(); + let mut i = 0; + let mut state = BASE_STATE; + let text = "abc_def"; + /* should find abc and then def */ + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == 0); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == 1); + /* test with anchor - OK */ + let mut smack = Smack::new("test anchor begin".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern(b"abc", 0, SmackFlags::ANCHOR_BEGIN); + smack.add_pattern(b"def", 1, SmackFlags::EMPTY); + smack.compile(); + let mut i = 0; + let mut state = BASE_STATE; + let text = "abc_def"; + /* should find abc and then def */ + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == 0); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == 1); + /* test with anchor - KO */ + let mut smack = Smack::new("test anchor begin".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern(b"abc", 0, SmackFlags::ANCHOR_BEGIN); + smack.add_pattern(b"def", 1, SmackFlags::ANCHOR_BEGIN); + smack.compile(); + let mut i = 0; + let mut state = BASE_STATE; + let text = "abc_def"; + /* should find abc and then nothing */ + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == 0); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == NO_MATCH); + } + + #[test] + fn test_wildcard() { + /* test wildcard without wildcard */ + let mut smack = Smack::new("test".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern(b"abc", 0, SmackFlags::EMPTY); + smack.add_pattern(b"egjkfhd", 1, SmackFlags::EMPTY); + /* here we do not specify the WILDCARD flag */ + smack.add_pattern(b"c*ap", 2, SmackFlags::EMPTY); + smack.compile(); + let mut i = 0; + let mut state = BASE_STATE; + let text = "abc_clap"; + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == 0); + assert!(i == 3); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id != 2); + + /* test wildcard */ + let mut smack = Smack::new("test".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern(b"abc", 0, SmackFlags::EMPTY); + smack.add_pattern(b"egjkfhd", 1, SmackFlags::EMPTY); + smack.add_pattern(b"c*ap", 2, SmackFlags::WILDCARDS); + smack.compile(); + let mut i = 0; + let mut state = BASE_STATE; + let text = "abc_clap"; + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == 0); + assert!(i == 3); + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == 2); + + /* test wildcard + anchor beg */ + let mut smack = Smack::new("test".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern( + b"abc*ef", + 0, + SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, + ); + smack.compile(); + let mut i = 0; + let mut state = BASE_STATE; + let text = "abc_ef"; + let id = smack.search_next(&mut state, &text.as_bytes().to_vec(), &mut i); + assert!(id == 0); + } + + #[test] + fn test_http_banner() { + let mut smack = Smack::new("test".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern(b"Server:", 0, SmackFlags::ANCHOR_BEGIN); + smack.add_pattern(b"Via:", 1, SmackFlags::ANCHOR_BEGIN); + smack.add_pattern(b"Location:", 2, SmackFlags::ANCHOR_BEGIN); + smack.add_pattern(b":", 3, SmackFlags::EMPTY); + smack.compile(); + let mut state = BASE_STATE; + let mut offset = 0; + let id = smack.search_next(&mut state, &b"server: lol\n".to_vec(), &mut offset); + assert!(id == 3); + let id = smack._next_match(&mut state); + assert!(id == 0); + let id = smack._next_match(&mut state); + assert!(id == NO_MATCH); + } + + #[test] + fn test_anchor_end() { + let mut smack = Smack::new("test".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern(b"def", 0, SmackFlags::ANCHOR_END); + smack.compile(); + let mut state = BASE_STATE; + let mut offset = 0; + let mut id = smack.search_next(&mut state, &b"defabcabb".to_vec(), &mut offset); + assert!(id == NO_MATCH); + id = smack.search_next_end(&mut state); + assert!(id == NO_MATCH); + let mut state = BASE_STATE; + let mut offset = 0; + let mut id = smack.search_next(&mut state, &b"def".to_vec(), &mut offset); + assert!(id == NO_MATCH); + id = smack.search_next_end(&mut state); + assert!(id == 0); + let mut state = BASE_STATE; + let mut offset = 0; + let mut id = smack.search_next(&mut state, &b"abcdef".to_vec(), &mut offset); + assert!(id == NO_MATCH); + id = smack.search_next_end(&mut state); + assert!(id == 0); + } + + #[test] + fn test_multiple_matches() { + let mut smack = Smack::new("test".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern(b"aabb", 0, SmackFlags::ANCHOR_BEGIN); + smack.add_pattern(b"abb", 1, SmackFlags::EMPTY); + smack.add_pattern(b"bb", 2, SmackFlags::EMPTY); + smack.compile(); + let mut state = BASE_STATE; + let mut offset = 0; + let id = smack.search_next(&mut state, &b"aabb".to_vec(), &mut offset); + assert!(id <= 2); + let id = smack._next_match(&mut state); + assert!(id <= 2); + let id = smack._next_match(&mut state); + assert!(id <= 2); + let id = smack._next_match(&mut state); + assert!(id == NO_MATCH); + } + + #[test] + fn test_multiple_matches_wildcard() { + let mut smack = Smack::new("test".to_string(), SMACK_CASE_INSENSITIVE); + smack.add_pattern(b"aab", 0, SmackFlags::ANCHOR_BEGIN); + smack.add_pattern(b"*ac", 1, SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS); + smack.compile(); + let mut state = BASE_STATE; + let mut offset = 0; + let id = smack.search_next(&mut state, &b"aab".to_vec(), &mut offset); + assert!(id == 0); + let mut state = BASE_STATE; + let mut offset = 0; + let id = smack.search_next(&mut state, &b"bac".to_vec(), &mut offset); + assert!(id == 1); + } + + #[test] + fn test_proto() { + const PROTO_HTTP: usize = 0; + const PROTO_SMB: usize = 1; + let mut smack = Smack::new("proto".to_string(), SMACK_CASE_SENSITIVE); + /* HTTP markers */ + let http_verbs = [ + "GET /", + "PUT /", + "POST /", + "HEAD /", + "DELETE /", + "CONNECT /", + "OPTIONS /", + "TRACE /", + "PATCH /", + ]; + for (_, v) in http_verbs.iter().enumerate() { + smack.add_pattern(v.as_bytes(), PROTO_HTTP, SmackFlags::ANCHOR_BEGIN); + } + /* SMB markers */ + smack.add_pattern( + b"\x00\x00**\xffSMB", + PROTO_SMB, + SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, + ); + smack.compile(); + let mut state = BASE_STATE; + let mut offset = 0; + let id = smack.search_next(&mut state, &b"HEAD /".to_vec(), &mut offset); + assert!(id == PROTO_HTTP); + let mut state = BASE_STATE; + let mut offset = 0; + let id = smack.search_next(&mut state, &b"\x00\x00aa\xffSMB".to_vec(), &mut offset); + assert!(id == PROTO_SMB); + } +} diff --git a/src/smack/smack_constants.rs b/src/smack/smack_constants.rs new file mode 100644 index 0000000..e7e7a02 --- /dev/null +++ b/src/smack/smack_constants.rs @@ -0,0 +1,12 @@ +pub const ALPHABET_SIZE: usize = 256 + 2; +pub const BASE_STATE: usize = 0; +pub const UNANCHORED_STATE: usize = 1; +pub const FAIL_STATE: usize = 0xFFFFFFFF; + +pub const CHAR_ANCHOR_START: usize = 256; +pub const CHAR_ANCHOR_END: usize = 257; + +pub const NO_MATCH: usize = 0xFFFFFFFFFFFFFFFF; + +pub const SMACK_CASE_INSENSITIVE: bool = true; +pub const SMACK_CASE_SENSITIVE: bool = false; diff --git a/src/smack/smack_pattern.rs b/src/smack/smack_pattern.rs new file mode 100644 index 0000000..bcd3c95 --- /dev/null +++ b/src/smack/smack_pattern.rs @@ -0,0 +1,21 @@ +use crate::smack::smack_utils::SmackFlags; + +pub struct SmackPattern { + pub id: usize, + pub pattern: Vec, + pub is_anchor_begin: bool, + pub is_anchor_end: bool, + pub is_wildcards: bool, +} + +impl SmackPattern { + pub fn new(pattern: Vec, id: usize, flags: SmackFlags) -> Self { + SmackPattern { + id, + is_anchor_begin: flags.contains(SmackFlags::ANCHOR_BEGIN), + is_anchor_end: flags.contains(SmackFlags::ANCHOR_END), + is_wildcards: flags.contains(SmackFlags::WILDCARDS), + pattern, + } + } +} diff --git a/src/smack/smack_queue.rs b/src/smack/smack_queue.rs new file mode 100644 index 0000000..a71428d --- /dev/null +++ b/src/smack/smack_queue.rs @@ -0,0 +1,18 @@ +pub struct SmackQueue { + queue: Vec, +} + +impl SmackQueue { + pub fn new() -> Self { + SmackQueue { queue: Vec::new() } + } + pub fn enqueue(&mut self, data: T) { + self.queue.push(data); + } + pub fn dequeue(&mut self) -> T { + self.queue.remove(0) + } + pub fn has_more_items(&self) -> bool { + !self.queue.is_empty() + } +} diff --git a/src/smack/smack_utils.rs b/src/smack/smack_utils.rs new file mode 100644 index 0000000..79bb459 --- /dev/null +++ b/src/smack/smack_utils.rs @@ -0,0 +1,17 @@ +bitflags! { + pub struct SmackFlags: usize { + const EMPTY = 0x00; + const ANCHOR_BEGIN = 0x01; + const ANCHOR_END = 0x02; + const WILDCARDS = 0x04; + } +} + +pub fn row_shift_from_symbol_count(symbol_count: usize) -> usize { + let mut row_shift = 1; + let symbol_count = symbol_count + 1; + while (1 << row_shift) < symbol_count { + row_shift += 1; + } + row_shift +} diff --git a/src/synackcookie/mod.rs b/src/synackcookie/mod.rs new file mode 100644 index 0000000..a672ee6 --- /dev/null +++ b/src/synackcookie/mod.rs @@ -0,0 +1,357 @@ +use crate::client::ClientInfo; +use siphasher::sip::SipHasher24; +use std::convert::TryInto; +use std::hash::Hasher; +use std::io; +use std::net::IpAddr; + +pub fn generate(client_info: &ClientInfo, key: &[u64; 2]) -> Result { + /* check parameters */ + /* ip fields must not be None */ + if client_info.ip.src == None || client_info.ip.dst == None { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "IP addresses must not be None", + )); + } + /* port fields must not be None */ + if client_info.port.src == None || client_info.port.dst == None { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Ports must not be None", + )); + } + let mut sip = SipHasher24::new_with_keys(key[0], key[1]); + /* check IPAddr type */ + if let Some(IpAddr::V6(s)) = client_info.ip.src { + if let Some(IpAddr::V6(d)) = client_info.ip.dst { + sip.write_u128(s.into()); + sip.write_u128(d.into()); + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "The two IP addresses (src and dst) must be of same type", + )); + } + } else if let Some(IpAddr::V4(s)) = client_info.ip.src { + if let Some(IpAddr::V4(d)) = client_info.ip.dst { + sip.write_u32(s.into()); + sip.write_u32(d.into()); + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "The two IP addresses (src and dst) must be of same type", + )); + } + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Unknown data type", + )); + } + sip.write_u16(client_info.port.src.unwrap()); + sip.write_u16(client_info.port.dst.unwrap()); + Ok((sip.finish() & 0xFFFFFFFF).try_into().unwrap()) +} + +pub fn _check(client_info: &ClientInfo, val: u32, key: &[u64; 2]) -> bool { + if let Ok(cookie) = generate(client_info, &key) { + cookie == val + } else { + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::client::ClientInfoSrcDst; + use std::net::{Ipv4Addr, Ipv6Addr}; + + #[test] + fn test_ip4() { + let key = [0xfb3818fcf501729d, 0xeb3b3e8720618e69]; + let ip_src = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let tcp_sport = 65000; + let tcp_dport = 80; + let client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let res = generate(&client_info, &key); + if let Ok(_) = res { + assert!(true); + } else { + assert!(false); + } + } + + #[test] + fn test_ip6() { + let key = [0x6b794087697b9180, 0x0c149aa303534b02]; + let ip_src = IpAddr::V6(Ipv6Addr::new( + 0xe50f, 0xe521, 0x70a2, 0xa3b3, 0x2135, 0x52d9, 0x6a0d, 0xe215, + )); + let ip_dst = IpAddr::V6(Ipv6Addr::new( + 0xc2eb, 0x33cf, 0x2c15, 0x4f7a, 0x7085, 0x492c, 0x2dbc, 0xf35b, + )); + let tcp_sport = 65000; + let tcp_dport = 80; + let client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let res = generate(&client_info, &key); + if let Ok(_) = res { + assert!(true); + } else { + assert!(false); + } + } + + #[test] + fn test_clientinfo() { + let key = [0x0b1a8621b0caf88d, 0x677cc071dab41639]; + let err = Err(io::ErrorKind::InvalidInput); + /* all ok */ + let ip_src = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let tcp_sport = 65000; + let tcp_dport = 80; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let res = generate(&client_info, &key); + if let Ok(_) = res { + assert!(true); + } else { + assert!(false); + } + /* ip src is None */ + client_info.ip.src = None; + let res = generate(&client_info, &key); + assert_eq!(res.map_err(|e| e.kind()), err); + client_info.ip.src = Some(ip_src); + /* ip dst is None */ + client_info.ip.dst = None; + let res = generate(&client_info, &key); + assert_eq!(res.map_err(|e| e.kind()), err); + client_info.ip.dst = Some(ip_dst); + /* port src is None */ + client_info.port.src = None; + let res = generate(&client_info, &key); + assert_eq!(res.map_err(|e| e.kind()), err); + client_info.port.src = Some(tcp_sport); + /* port dst is None */ + client_info.port.dst = None; + let res = generate(&client_info, &key); + assert_eq!(res.map_err(|e| e.kind()), err); + client_info.port.dst = Some(tcp_dport); + } + + #[test] + fn test_key() { + /* reference */ + let ref_key = [0x1e9219e0b0e0b44c, 0x9e460bcddf4eaac9]; + let ip_src = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let tcp_sport = 65000; + let tcp_dport = 80; + let client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let ref_cookie = generate(&client_info, &ref_key).unwrap(); + assert!(_check(&client_info, ref_cookie, &ref_key)); + /* change key */ + let key = [0xc98a8cb8579004d4, 0x8b53a2735381ded4]; + let cookie = generate(&client_info, &key).unwrap(); + assert_ne!(ref_key, key); + assert_ne!(cookie, ref_cookie); + assert!(_check(&client_info, cookie, &key)); + assert!(!_check(&client_info, ref_cookie, &key)); + assert!(!_check(&client_info, cookie, &ref_key)); + } + + #[test] + fn test_ip4_src() { + let key = [0x77b781aaeca4f0d1, 0x7481d7251789d247]; + /* reference */ + let ip_src = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let tcp_sport = 65000; + let tcp_dport = 80; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let ref_cookie = generate(&client_info, &key).unwrap(); + assert!(_check(&client_info, ref_cookie, &key)); + client_info.ip.src = Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1))); + let cookie = generate(&client_info, &key).unwrap(); + assert!(_check(&client_info, cookie, &key)); + assert!(!_check(&client_info, ref_cookie, &key)); + assert_ne!(cookie, ref_cookie); + } + + #[test] + fn test_ip4_dst() { + let key = [0xe2ada0ff90978791, 0xb18586de261db429]; + /* reference */ + let ip_src = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(3, 3, 3, 3)); + let tcp_sport = 65000; + let tcp_dport = 80; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let ref_cookie = generate(&client_info, &key).unwrap(); + assert!(_check(&client_info, ref_cookie, &key)); + client_info.ip.dst = Some(IpAddr::V4(Ipv4Addr::new(4, 4, 3, 3))); + let cookie = generate(&client_info, &key).unwrap(); + assert!(_check(&client_info, cookie, &key)); + assert!(!_check(&client_info, ref_cookie, &key)); + assert_ne!(cookie, ref_cookie); + } + + #[test] + fn test_tcp_src() { + let key = [0xda0e06f5916b0a24, 0x754a8c2f23106b5f]; + /* reference */ + let ip_src = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(3, 4, 3, 4)); + let tcp_sport = 65000; + let tcp_dport = 443; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let ref_cookie = generate(&client_info, &key).unwrap(); + assert!(_check(&client_info, ref_cookie, &key)); + client_info.port.src = Some(12345); + let cookie = generate(&client_info, &key).unwrap(); + assert!(_check(&client_info, cookie, &key)); + assert!(!_check(&client_info, ref_cookie, &key)); + assert_ne!(cookie, ref_cookie); + } + + #[test] + fn test_tcp_dst() { + let key = [0x85fa7e3f1cd254b7, 0xcfce5e92a7bb7595]; + /* reference */ + let ip_src = IpAddr::V4(Ipv4Addr::new(200, 210, 220, 230)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(172, 48, 14, 103)); + let tcp_sport = 65000; + let tcp_dport = 443; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let ref_cookie = generate(&client_info, &key).unwrap(); + assert!(_check(&client_info, ref_cookie, &key)); + client_info.port.dst = Some(80); + let cookie = generate(&client_info, &key).unwrap(); + assert!(_check(&client_info, cookie, &key)); + assert!(!_check(&client_info, ref_cookie, &key)); + assert_ne!(cookie, ref_cookie); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..3c788a6 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +mod parsers; + +pub use parsers::IpAddrParser; diff --git a/src/utils/parsers.rs b/src/utils/parsers.rs new file mode 100644 index 0000000..a17210d --- /dev/null +++ b/src/utils/parsers.rs @@ -0,0 +1,249 @@ +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io::BufRead; +use std::io::BufReader; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use log::*; +use pcap_file::pcap::{Packet, PcapReader}; +use pnet::packet::{ + ethernet::{EtherTypes, EthernetPacket}, + ipv4::Ipv4Packet, + ipv6::Ipv6Packet, + Packet as Pkt, +}; + +/* Generic IP packet (either IPv4 or IPv6) */ +pub enum IpPacket<'a> { + V4(Ipv4Packet<'a>), + V6(Ipv6Packet<'a>), +} + +/* Get source or dest. IP address from a packet (IPv4 or IPv6) */ +impl<'a> IpPacket<'a> { + // Macro ? + pub fn src(&self) -> IpAddr { + match self { + IpPacket::V4(p) => IpAddr::V4(p.get_source()), + IpPacket::V6(p) => IpAddr::V6(p.get_source()), + } + } + pub fn dst(&self) -> IpAddr { + match self { + IpPacket::V4(p) => IpAddr::V4(p.get_destination()), + IpPacket::V6(p) => IpAddr::V6(p.get_destination()), + } + } +} + +pub trait IpAddrParser { + fn extract_ip_addresses_with_count( + self, + blacklist: Option>, + ) -> HashMap; + fn extract_ip_addresses_only(self, blacklist: Option>) -> HashSet; +} + +/* Parse IP addresses from a text file */ +impl IpAddrParser for File { + fn extract_ip_addresses_with_count( + self, + blacklist: Option>, + ) -> HashMap { + let mut ip_addresses = HashMap::new(); + let buf = BufReader::new(self); + for (i, line) in buf.lines().enumerate() { + let entry: Vec<&str> = match &line { + Ok(l) => l.split('\t').collect(), + Err(e) => { + warn!("cannot read line {} - {}", i, e); + continue; + } + }; + /* Should never occur */ + if entry.is_empty() { + warn!("cannot parse line: {}", line.expect("error reading line")); + continue; + } + let ip: IpAddr; + if let Ok(val) = entry[0].parse::() { + ip = IpAddr::V4(val); + } else if let Ok(val) = entry[0].parse::() { + ip = IpAddr::V6(val); + } else { + warn!( + "cannot parse IP address from line: {}", + line.expect("error reading line") + ); + continue; + } + if let Some(ref b) = blacklist { + if b.contains(&ip) { + info!("[blacklist] ignoring {}", &ip); + continue; + } + } + let ip_entry = ip_addresses.entry(ip).or_insert(0); + if entry.len() < 2 { + continue; + } + if let Ok(count) = entry[1].parse::() { + *ip_entry += count; + } + } + ip_addresses + } + + fn extract_ip_addresses_only(self, blacklist: Option>) -> HashSet { + let mut ip_addresses = HashSet::new(); + let buf = BufReader::new(self); + for (i, line) in buf.lines().enumerate() { + let entry: Vec<&str> = match &line { + Ok(l) => l.split('\t').collect(), + Err(e) => { + warn!("cannot read line {} - {}", i, e); + continue; + } + }; + /* Should never occur */ + if entry.is_empty() { + warn!("cannot parse line: {}", line.expect("error reading line")); + continue; + } + let ip: IpAddr; + if let Ok(val) = entry[0].parse::() { + ip = IpAddr::V4(val); + } else if let Ok(val) = entry[0].parse::() { + ip = IpAddr::V6(val); + } else { + warn!( + "cannot parse IP address from line: {}", + line.expect("error reading line") + ); + continue; + } + if let Some(ref b) = blacklist { + if b.contains(&ip) { + info!("[blacklist] ignoring {}", &ip); + continue; + } + } + ip_addresses.insert(ip); + } + ip_addresses + } +} + +/* Get the IP address of source and dest. from an IP packet. + * works with both IPv4 and IPv6 packets/addresses */ +fn extract_ip(pkt: Packet) -> Option<(IpAddr, IpAddr)> { + let eth = EthernetPacket::new(&pkt.data).expect("error parsing Ethernet packet"); + let payload = eth.payload(); + let ip = match eth.get_ethertype() { + EtherTypes::Ipv4 => match Ipv4Packet::new(payload) { + Some(p) => IpPacket::V4(p), + None => { + warn!("error parsing IPv4 packet - {:?}", pkt); + return None; + } + }, + EtherTypes::Ipv6 => match Ipv6Packet::new(payload) { + Some(p) => IpPacket::V6(p), + None => { + warn!("error parsing IPv6 packet - {:?}", pkt); + return None; + } + }, + EtherTypes::Arp => { + return None; + } + t => { + warn!("unknown layer 2: {}", t); + return None; + } + }; + Some((ip.src(), ip.dst())) +} + +impl IpAddrParser for PcapReader { + /* Extract IP addresses (v4 and v6) from a capture and count occurrences of + * each. */ + fn extract_ip_addresses_with_count( + self: PcapReader, + blacklist: Option>, + ) -> HashMap { + let mut ip_addresses = HashMap::new(); + // pcap.map(fn) , map_Ok + // .iter, into_iter + for pkt in self { + match pkt { + Ok(pkt) => { + // map_Some map_None + if let Some((s, d)) = extract_ip(pkt) { + match blacklist { + Some(ref b) if b.contains(&s) => { + info!("[blacklist] ignoring {}", &s); + } + _ => { + let ip = ip_addresses.entry(s).or_insert(0); + *ip += 1; + } + } + match blacklist { + Some(ref b) if b.contains(&d) => { + info!("[blacklist] ignoring {}", &d); + } + _ => { + let ip = ip_addresses.entry(d).or_insert(0); + *ip += 1; + } + } + }; + } + Err(e) => { + warn!("error reading packet - {}", e); + continue; + } + } + } + ip_addresses + } + fn extract_ip_addresses_only( + self: PcapReader, + blacklist: Option>, + ) -> HashSet { + let mut ip_addresses = HashSet::new(); + // pcap.map(fn) , map_Ok + // .iter, into_iter + for pkt in self { + match pkt { + Ok(pkt) => { + // map_Some map_None + if let Some((s, d)) = extract_ip(pkt) { + match blacklist { + Some(ref b) if b.contains(&s) => { + info!("[blacklist] ignoring {}", &s); + } + _ => { + ip_addresses.insert(s); + } + } + match blacklist { + Some(ref b) if b.contains(&d) => { + info!("[blacklist] ignoring {}", &d); + } + _ => { + ip_addresses.insert(d); + } + } + }; + } + Err(e) => { + warn!("error reading packet - {}", e); + continue; + } + } + } + ip_addresses + } +} diff --git a/test/res/.gitignore b/test/res/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/test/src/__init__.py b/test/src/__init__.py new file mode 100644 index 0000000..831f613 --- /dev/null +++ b/test/src/__init__.py @@ -0,0 +1,15 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned 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. +# +# Masscanned 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 Masscanned. If not, see . diff --git a/test/src/all.py b/test/src/all.py new file mode 100644 index 0000000..e031228 --- /dev/null +++ b/test/src/all.py @@ -0,0 +1,593 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned 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. +# +# Masscanned 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 Masscanned. If not, see . + +from scapy.all import * +import requests +import requests.packages.urllib3.util.connection as urllib3_cn +import logging + +from .conf import * + +fmt = logging.Formatter("%(levelname)s\t%(message)s") +ch = logging.StreamHandler() +ch.setFormatter(fmt) +ch.setLevel(logging.DEBUG) +LOG = logging.getLogger(__name__) +LOG.setLevel(logging.DEBUG) +LOG.addHandler(ch) + +tests = list() + +# decorator to automatically add a function to tests +def test(f): + OK = "\033[1mOK\033[0m" + KO = "\033[1m\033[1;%dmKO\033[0m" % 31 + global tests + fname = f.__name__.ljust(50, '.') + def w(iface): + try: + f(iface) + LOG.info("{}{}".format(fname, OK)) + except AssertionError as e: + LOG.info("{}{}: {}".format(fname, KO, e)) + tests.append(w) + return w + +def multicast(ip6): + a, b = ip6.split(":")[-2:] + mac = ["33", "33", "ff"] + if len(a) == 4: + mac.append(a[2:]) + else: + mac.append("00") + if len(b) >= 2: + mac.append(b[:2]) + else: + mac.append("00") + if len(b) >= 4: + mac.append(b[2:]) + else: + mac.append("00") + return ":".join(mac) + +def check_ip_checksum(pkt): + assert(IP in pkt), "no IP layer found" + ip_pkt = pkt[IP] + chksum = ip_pkt.chksum + del ip_pkt.chksum + assert(IP(raw(ip_pkt)).chksum == chksum), "bad IPv4 checksum" + +def check_ipv6_checksum(pkt): + assert(IPv6 in pkt), "no IP layer found" + ip_pkt = pkt[IPv6] + chksum = ip_pkt.chksum + del ip_pkt.chksum + assert(IPv6(raw(ip_pkt)).chksum == chksum), "bad IPv6 checksum" + +@test +def test_arp_req(iface): + ##### ARP ##### + arp_req = Ether()/ARP(psrc='192.0.0.2', pdst=IPV4_ADDR) + arp_repl = iface.sr1(arp_req, timeout=1) + assert(arp_repl is not None), "expecting answer, got nothing" + assert(ARP in arp_repl), "no ARP layer found" + arp_repl = arp_repl[ARP] + # check answer + ## op is "is-at" + assert(arp_repl.op == 2), "unexpected ARP op: {}".format(arp_repl.op) + ## answer for the requested IP + assert(arp_repl.psrc == arp_req.pdst), "unexpected ARP psrc: {}".format(arp_repl.psrc) + assert(arp_repl.pdst == arp_req.psrc), "unexpected ARP pdst: {}".format(arp_repl.pdst) + ## answer is expected MAC address + assert(arp_repl.hwsrc == MAC_ADDR), "unexpected ARP hwsrc: {}".format(arp_repl.hwsrc) + +@test +def test_arp_req_other_ip(iface): + ##### ARP ##### + arp_req = Ether()/ARP(psrc='192.0.0.2', pdst='1.2.3.4') + arp_repl = iface.sr1(arp_req, timeout=1) + assert(arp_repl is None), "responding to ARP requests for other IP addresses" + +@test +def test_ipv4_req(iface): + ##### IP ##### + ip_req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR, id=0x1337)/ICMP(type=8, code=0) + ip_repl = iface.sr1(ip_req, timeout=1) + assert(ip_repl is not None), "expecting answer, got nothing" + check_ip_checksum(ip_repl) + assert(IP in ip_repl), "no IP layer in response" + ip_repl = ip_repl[IP] + assert(ip_repl.id == 0), "IP identification unexpected" + +@test +def test_eth_req_other_mac(iface): + #### ETH #### + ip_req = Ether(dst="00:00:00:11:11:11")/IP(dst=IPV4_ADDR)/ICMP(type=8, code=0) + ip_repl = iface.sr1(ip_req, timeout=1) + assert(ip_repl is None), "responding to other MAC addresses" + +@test +def test_ipv4_req_other_ip(iface): + ##### IP ##### + ip_req = Ether(dst=MAC_ADDR)/IP(dst="1.2.3.4")/ICMP(type=8, code=0) + ip_repl = iface.sr1(ip_req, timeout=1) + assert(ip_repl is None), "responding to other IP addresses" + +@test +def test_icmpv4_echo_req(iface): + ##### ICMPv4 ##### + icmp_req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/ICMP(type=8, code=0)/Raw("idrinkwaytoomuchcoffee") + icmp_repl = iface.sr1(icmp_req, timeout=1) + assert(icmp_repl is not None), "expecting answer, got nothing" + check_ip_checksum(icmp_repl) + assert(ICMP in icmp_repl) + icmp_repl = icmp_repl[ICMP] + # check answer + ## type is "echo-reply" + assert(icmp_repl.type == 0) + assert(icmp_repl.code == 0) + ## data is the same as sent + assert(icmp_repl.load == icmp_req.load) + +@test +def test_icmpv6_neighbor_solicitation(iface): + ##### IPv6 Neighbor Solicitation ##### + for mac in ["ff:ff:ff:ff:ff:ff", "33:33:00:00:00:01", MAC_ADDR, multicast(IPV6_ADDR)]: + nd_ns = Ether(dst=mac)/IPv6()/ICMPv6ND_NS(tgt=IPV6_ADDR) + nd_na = iface.sr1(nd_ns, timeout=1) + assert(nd_na is not None), "expecting answer, got nothing" + assert(ICMPv6ND_NA in nd_na) + nd_na = nd_na[ICMPv6ND_NA] + # check answer content + assert(nd_na.code == 0) + assert(nd_na.R == 0) + assert(nd_na.S == 1) + assert(nd_na.O == 1) + assert(nd_na.tgt == IPV6_ADDR) + # check ND Option + assert(nd_na.haslayer(ICMPv6NDOptDstLLAddr)) + assert(nd_na.getlayer(ICMPv6NDOptDstLLAddr).lladdr == MAC_ADDR) + for mac in ["00:00:00:00:00:00", "33:33:33:00:00:01"]: + nd_ns = Ether(dst="ff:ff:ff:ff:ff:ff")/IPv6()/ICMPv6ND_NS(tgt=IPV6_ADDR) + nd_na = iface.sr1(nd_ns, timeout=1) + assert(nd_na is not None), "expecting no answer, got one" + +@test +def test_icmpv6_neighbor_solicitation_other_ip(iface): + ##### IPv6 Neighbor Solicitation ##### + nd_ns = Ether(dst="ff:ff:ff:ff:ff:ff")/IPv6()/ICMPv6ND_NS(tgt="2020:4141:3030:2020::bdbd") + nd_na = iface.sr1(nd_ns, timeout=1) + assert(nd_na is None), "responding to ND_NS for other IP addresses" + +@test +def test_icmpv6_echo_req(iface): + ##### IPv6 Ping ##### + echo_req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/ICMPv6EchoRequest(data="waytoomanynapkins") + echo_repl = iface.sr1(echo_req, timeout=1) + assert(echo_repl is not None), "expecting answer, got nothing" + assert(ICMPv6EchoReply in echo_repl) + echo_repl = echo_repl[ICMPv6EchoReply] + # check answer content + assert(echo_repl.code == 0) + assert(echo_repl.data == echo_req.data) + +@test +def test_tcp_syn(iface): + ##### SYN-ACK ##### + # test a list of ports, randomly generated once + ports_to_test = [ + 1152, 2003, 2193, 3709, 4054, 6605, 6737, 6875, 7320, 8898, 9513, 9738, 10623, 10723, + 11253, 12125, 12189, 12873, 14648, 14659, 16242, 16243, 17209, 17492, 17667, 17838, + 18081, 18682, 18790, 19124, 19288, 19558, 19628, 19789, 20093, 21014, 21459, 21740, + 24070, 24312, 24576, 26939, 27136, 27165, 27361, 29971, 31088, 33011, 33068, 34990, + 35093, 35958, 36626, 36789, 37130, 37238, 37256, 37697, 37890, 38958, 42131, 43864, + 44420, 44655, 44868, 45157, 46213, 46497, 46955, 49049, 49067, 49452, 49480, 50498, + 50945, 51181, 52890, 53301, 53407, 53417, 53980, 55827, 56483, 58552, 58713, 58836, + 59362, 59560, 60534, 60555, 60660, 61615, 62402, 62533, 62941, 63240, 63339, 63616, + 64380, 65438, + ] + for p in ports_to_test: + syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", dport=p) + syn_ack = iface.sr1(syn, timeout=1) + assert(syn_ack is not None), "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert(TCP in syn_ack) + syn_ack = syn_ack[TCP] + assert(syn_ack.flags == "SA") + +@test +def test_ipv4_tcp_psh_ack(iface): + ##### PSH-ACK ##### + sport = 26695 + port = 445 + # send PSH-ACK first + psh_ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", dport=port)/Raw("payload") + syn_ack = iface.sr1(psh_ack, timeout=1) + assert(syn_ack is None), "no answer expected, got one" + # test the anti-injection mechanism + syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", dport=port) + syn_ack = iface.sr1(syn, timeout=1) + assert(syn_ack is not None), "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert(TCP in syn_ack) + syn_ack = syn_ack[TCP] + assert(syn_ack.flags == "SA") + ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", dport=port) + # should fail because no ack given + psh_ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", dport=port) + ack = iface.sr1(psh_ack, timeout=1) + assert(ack is None), "no answer expected, got one" + # should get an answer this time + psh_ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", dport=port, ack=syn_ack.seq + 1) + ack = iface.sr1(psh_ack, timeout=1) + assert(ack is not None), "expecting answer, got nothing" + check_ip_checksum(ack) + assert(TCP in ack) + ack = ack[TCP] + assert(ack.flags == "A") + +@test +def test_ipv6_tcp_psh_ack(iface): + ##### PSH-ACK ##### + sport = 26695 + port = 445 + # send PSH-ACK first + psh_ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", dport=port)/Raw("payload") + syn_ack = iface.sr1(psh_ack, timeout=1) + assert(syn_ack is None), "no answer expected, got one" + # test the anti-injection mechanism + syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", dport=port) + syn_ack = iface.sr1(syn, timeout=1) + assert(syn_ack is not None), "expecting answer, got nothing" + check_ipv6_checksum(syn_ack) + assert(TCP in syn_ack) + syn_ack = syn_ack[TCP] + assert(syn_ack.flags == "SA") + ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", dport=port) + # should fail because no ack given + psh_ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", dport=port) + ack = iface.sr1(psh_ack, timeout=1) + assert(ack is None), "no answer expected, got one" + # should get an answer this time + psh_ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", dport=port, ack=syn_ack.seq + 1) + ack = iface.sr1(psh_ack, timeout=1) + assert(ack is not None), "expecting answer, got nothing" + check_ipv6_checksum(ack) + assert(TCP in ack) + ack = ack[TCP] + assert(ack.flags == "A") + +@test +def test_ipv4_tcp_http(iface): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", sport=sport, dport=dport) + syn_ack = iface.sr1(syn, timeout=1) + assert(syn_ack is not None), "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert(TCP in syn_ack) + syn_ack = syn_ack[TCP] + assert(syn_ack.flags == "SA") + ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) + _ = iface.sr1(ack, timeout=1) + req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n") + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ip_checksum(resp) + assert(TCP in resp) + tcp = resp[TCP] + assert(tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")) + +@test +def test_ipv6_tcp_http(iface): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", sport=sport, dport=dport) + syn_ack = iface.sr1(syn, timeout=1) + assert(syn_ack is not None), "expecting answer, got nothing" + check_ipv6_checksum(syn_ack) + assert(TCP in syn_ack) + syn_ack = syn_ack[TCP] + assert(syn_ack.flags == "SA") + ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) + _ = iface.sr1(ack, timeout=1) + req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n") + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert(TCP in resp) + tcp = resp[TCP] + assert(tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")) + +@test +def test_ipv4_udp_http(iface): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n") + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ip_checksum(resp) + assert(UDP in resp) + udp = resp[UDP] + assert(udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")) + +@test +def test_ipv6_udp_http(iface): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n") + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert(UDP in resp) + udp = resp[UDP] + assert(udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")) + +@test +def test_ipv4_tcp_http_ko(iface): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", sport=sport, dport=dport) + syn_ack = iface.sr1(syn, timeout=1) + assert(syn_ack is not None), "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert(TCP in syn_ack) + syn_ack = syn_ack[TCP] + assert(syn_ack.flags == "SA") + ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) + _ = iface.sr1(ack, timeout=1) + req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53")) + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ip_checksum(resp) + assert(TCP in resp) + assert("P" not in resp[TCP].flags) + assert(len(resp[TCP].payload) == 0) + +@test +def test_ipv4_udp_http_ko(iface): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53")) + resp = iface.sr1(req, timeout=1) + assert(resp is None), "expecting no answer, got one" + +@test +def test_ipv6_tcp_http_ko(iface): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", sport=sport, dport=dport) + syn_ack = iface.sr1(syn, timeout=1) + assert(syn_ack is not None), "expecting answer, got nothing" + check_ipv6_checksum(syn_ack) + assert(TCP in syn_ack) + syn_ack = syn_ack[TCP] + assert(syn_ack.flags == "SA") + ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) + _ = iface.sr1(ack, timeout=1) + req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53")) + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert(TCP in resp) + assert("P" not in resp[TCP].flags) + assert(len(resp[TCP].payload) == 0) + +@test +def test_ipv6_udp_http_ko(iface): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53")) + resp = iface.sr1(req, timeout=1) + assert(resp is None), "expecting no answer, got one" + +@test +def test_ipv4_udp_stun(iface): + sports = [12345, 55555, 80, 43273] + dports = [80, 800, 8000, 3478] + payload = bytes.fromhex("000100002112a442000000000000000000000000") + for sport in sports: + for dport in dports: + req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload) + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ip_checksum(resp) + assert(UDP in resp), "no UDP layer found" + udp = resp[UDP] + assert(udp.sport == dport), "unexpected UDP sport: {}".format(udp.sport) + assert(udp.dport == sport), "unexpected UDP dport: {}".format(udp.dport) + resp_payload = udp.payload.load + type_, length, magic = struct.unpack(">HHI", resp_payload[:8]) + tid = resp_payload[8:20] + data = resp_payload[20:] + assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_) + assert(length == 12), "expected length 12, got {}".format(length) + assert(magic == 0x2112a442), "expected magic 0x2112a442, got 0x{:08x}".format(magic) + assert(tid == b'\x00' * 12), "expected tid 0x000000000000000000000000, got {:x}".format(tid) + assert(data == bytes.fromhex("000100080001") + struct.pack(">H", sport) + bytes.fromhex("00000000")), "unexpected data" + +@test +def test_ipv6_udp_stun(iface): + sports = [12345, 55555, 80, 43273] + dports = [80, 800, 8000, 3478] + payload = bytes.fromhex("000100002112a442000000000000000000000000") + for sport in sports: + for dport in dports: + req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload) + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert(UDP in resp) + udp = resp[UDP] + assert(udp.sport == dport) + assert(udp.dport == sport) + resp_payload = udp.payload.load + type_, length, magic = struct.unpack(">HHI", resp_payload[:8]) + tid = resp_payload[8:20] + data = resp_payload[20:] + assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_) + assert(length == 24), "expected length 24, got {}".format(length) + assert(magic == 0x2112a442), "expected magic 0x2112a442, got 0x{:08x}".format(magic) + assert(tid == b'\x00' * 12), "expected tid 0x000000000000000000000000, got {:x}".format(tid) + assert(data == bytes.fromhex("000100140002") + struct.pack(">H", sport) + bytes.fromhex("00000000" * 4)), "unexpected data: {}".format(data) + +@test +def test_ipv4_udp_stun_change_port(iface): + sports = [12345, 55555, 80, 43273] + dports = [80, 800, 8000, 3478, 65535] + payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002") + for sport in sports: + for dport in dports: + req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload) + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ip_checksum(resp) + assert(UDP in resp), "no UDP layer found" + udp = resp[UDP] + assert(udp.sport == (dport + 1) % 2**16), "expected answer from UDP/{}, got it from UDP/{}".format((dport + 1) % 2**16, udp.sport) + assert(udp.dport == sport), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport) + resp_payload = udp.payload.load + type_, length = struct.unpack(">HH", resp_payload[:4]) + tid = resp_payload[4:20] + data = resp_payload[20:] + assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_) + assert(length == 12), "expected length 12, got {}".format(length) + assert(tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c")), "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid + assert(data == bytes.fromhex("000100080001") + struct.pack(">H", sport) + bytes.fromhex("00000000")), "unexpected data" + +@test +def test_ipv6_udp_stun_change_port(iface): + sports = [12345, 55555, 80, 43273] + dports = [80, 800, 8000, 3478, 65535] + payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002") + for sport in sports: + for dport in dports: + req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload) + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert(UDP in resp), "expecting UDP layer in answer, got nothing" + udp = resp[UDP] + assert(udp.sport == (dport + 1) % 2**16), "expected answer from UDP/{}, got it from UDP/{}".format((dport + 1) % 2**16, udp.sport) + assert(udp.dport == sport), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport) + resp_payload = udp.payload.load + type_, length = struct.unpack(">HH", resp_payload[:4]) + tid = resp_payload[4:20] + data = resp_payload[20:] + assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_) + assert(length == 24), "expected length 12, got {}".format(length) + assert(tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c")), "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid + assert(data == bytes.fromhex("000100140002") + struct.pack(">H", sport) + bytes.fromhex("00000000" * 4)) + +@test +def test_ipv4_tcp_ssh(iface): + sport = 37183 + dports = [22, 80, 2222, 2022, 23874, 50000] + for i, dport in enumerate(dports): + banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5] + syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", sport=sport, dport=dport) + syn_ack = iface.sr1(syn, timeout=1) + assert(syn_ack is not None), "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert(TCP in syn_ack) + syn_ack = syn_ack[TCP] + assert(syn_ack.flags == "SA") + ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) + _ = iface.sr1(ack, timeout=1) + req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(banner + b"\r\n") + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ip_checksum(resp) + assert(TCP in resp) + tcp = resp[TCP] + assert("A" in tcp.flags), "expecting ACK flag, not set (%r)" % tcp.flags + assert("P" in tcp.flags), "expecting PSH flag, not set (%r)" % tcp.flags + assert(len(tcp.payload) > 0), "expecting payload, got none" + assert(tcp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % tcp.payload.load + assert(tcp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % tcp.payload.load + +@test +def test_ipv4_udp_ssh(iface): + sport = 37183 + dports = [22, 80, 2222, 2022, 23874, 50000] + for i, dport in enumerate(dports): + banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5] + req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(banner + b"\r\n") + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ip_checksum(resp) + assert(UDP in resp) + udp = resp[UDP] + assert(len(udp.payload) > 0), "expecting payload, got none" + assert(udp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % udp.payload.load + assert(udp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % udp.payload.load + +@test +def test_ipv6_tcp_ssh(iface): + sport = 37183 + dports = [22, 80, 2222, 2022, 23874, 50000] + for i, dport in enumerate(dports): + banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5] + syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", sport=sport, dport=dport) + syn_ack = iface.sr1(syn, timeout=1) + assert(syn_ack is not None), "expecting answer, got nothing" + check_ipv6_checksum(syn_ack) + assert(TCP in syn_ack) + syn_ack = syn_ack[TCP] + assert(syn_ack.flags == "SA") + ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) + _ = iface.sr1(ack, timeout=1) + req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(banner + b"\r\n") + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert(TCP in resp) + tcp = resp[TCP] + assert("A" in tcp.flags), "expecting ACK flag, not set (%r)" % tcp.flags + assert("P" in tcp.flags), "expecting PSH flag, not set (%r)" % tcp.flags + assert(len(tcp.payload) > 0), "expecting payload, got none" + assert(tcp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % tcp.payload.load + assert(tcp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % tcp.payload.load + +@test +def test_ipv6_udp_ssh(iface): + sport = 37183 + dports = [22, 80, 2222, 2022, 23874, 50000] + for i, dport in enumerate(dports): + banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5] + req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(banner + b"\r\n") + resp = iface.sr1(req, timeout=1) + assert(resp is not None), "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert(UDP in resp) + udp = resp[UDP] + assert(len(udp.payload) > 0), "expecting payload, got none" + assert(udp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % udp.payload.load + assert(udp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % udp.payload.load + +def test_all(iface): + global tests + # execute tests + for t in tests: + t(iface) diff --git a/test/src/conf.py b/test/src/conf.py new file mode 100644 index 0000000..f7fbd2b --- /dev/null +++ b/test/src/conf.py @@ -0,0 +1,20 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned 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. +# +# Masscanned 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 Masscanned. If not, see . + +IPV4_ADDR = "192.0.0.1" +IPV6_ADDR = "2001:41d0::ab32:bdb8" +MAC_ADDR = "52:1c:4e:c2:a4:1f" +OUTDIR = "test/res/" diff --git a/test/test_masscanned.py b/test/test_masscanned.py new file mode 100755 index 0000000..7384005 --- /dev/null +++ b/test/test_masscanned.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned 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. +# +# Masscanned 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 Masscanned. If not, see . + +from scapy.all import * +from time import sleep +from tempfile import _get_candidate_names as gen_tmp_filename +from tempfile import gettempdir +import subprocess +import logging +import sys +import os + +from src.all import test_all +from src.conf import * + +# if args in CLI, they are passed to masscanned +if len(sys.argv) > 1: + args = " ".join(sys.argv[1:]) +else: + args = "" + +fmt = logging.Formatter("%(levelname)s\t%(message)s") +ch = logging.StreamHandler() +ch.setFormatter(fmt) +ch.setLevel(logging.INFO) +LOG = logging.getLogger(__name__) +LOG.setLevel(logging.INFO) +LOG.addHandler(ch) + +conf.iface = 'tap0' +conf.verb = 0 + +# prepare configuration file for masscanned +ipfile = os.path.join(gettempdir(), next(gen_tmp_filename())) +with open(ipfile, "w") as f: + f.write("{}\n".format(IPV4_ADDR)) + f.write("{}\n".format(IPV6_ADDR)) + +# create test interface +tap = TunTapInterface(resolve_iface(conf.iface)) + +# set interface +subprocess.run("ip a a dev {} 192.0.0.2".format(conf.iface), shell=True) +subprocess.run("ip link set {} up".format(conf.iface), shell=True) + +# start capture +tcpdump = subprocess.Popen("tcpdump -enli {} -w {}".format(conf.iface, os.path.join(OUTDIR, "test_capture.pcap")), shell=True, + stdin=None, stdout=None, stderr=None, close_fds=True) +# run masscanned +masscanned = subprocess.Popen("RUST_BACKTRACE=1 ./target/debug/masscanned -vvvvv -i {} -f {} -a {} {}".format(conf.iface, ipfile, MAC_ADDR, args), shell=True, + stdin=None, stdout=open("test/res/masscanned.stdout", "w"), stderr=open("test/res/masscanned.stderr", "w"), close_fds=True) +sleep(1) + +try: + test_all(tap) +except AssertionError: + pass + +# terminate masscanned +masscanned.kill() +# terminate capture +sleep(2) +tcpdump.kill()