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.
+
+
+
+**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()