diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8d02d92
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/bin/
+.vs/
+*.user
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..2a99aee
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,675 @@
+### GNU GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+### Preamble
+
+The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom
+to share and change all versions of a program--to make sure it remains
+free software for all its users. We, the Free Software Foundation, use
+the GNU General Public License for most of our software; it applies
+also to any other work released this way by its authors. You can apply
+it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you
+have certain responsibilities if you distribute copies of the
+software, or if you modify it: responsibilities to respect the freedom
+of others.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the
+manufacturer can do so. This is fundamentally incompatible with the
+aim of protecting users' freedom to change the software. The
+systematic pattern of such abuse occurs in the area of products for
+individuals to use, which is precisely where it is most unacceptable.
+Therefore, we have designed this version of the GPL to prohibit the
+practice for those products. If such problems arise substantially in
+other domains, we stand ready to extend this provision to those
+domains in future versions of the GPL, as needed to protect the
+freedom of users.
+
+Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish
+to avoid the special danger that patents applied to a free program
+could make it effectively proprietary. To prevent this, the GPL
+assures that patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS
+
+#### 0. Definitions.
+
+"This License" refers to version 3 of the GNU General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+#### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+#### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+#### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+#### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+#### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+#### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+#### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+#### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+#### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+#### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+#### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+#### 13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in
+detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU General Public
+License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that numbered version or
+of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of the GNU General Public
+License, you may choose any version ever published by the Free
+Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU General Public License can be used, that proxy's public
+statement of acceptance of a version permanently authorizes you to
+choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+#### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+#### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+#### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+### How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+To do so, attach the following notices to the program. It is safest to
+attach them to the start of each source file to most effectively state
+the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper
+mail.
+
+If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands \`show w' and \`show c' should show the
+appropriate parts of the General Public License. Of course, your
+program's commands might be different; for a GUI interface, you would
+use an "about box".
+
+You should also get your employer (if you work as a programmer) or
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. For more information on this, and how to apply and follow
+the GNU GPL, see .
+
+The GNU General Public License does not permit incorporating your
+program into proprietary programs. If your program is a subroutine
+library, you may consider it more useful to permit linking proprietary
+applications with the library. If this is what you want to do, use the
+GNU Lesser General Public License instead of this License. But first,
+please read .
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1b03ae6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,39 @@
+# Mullvad split tunnel driver for Windows
+
+This is a non-PnP KMDF driver suitable for implementing split tunneling in VPN client software. The driver works on Windows 7 through 10.
+
+Main features:
+
+- Exclude network traffic from VPN tunnel based on process paths.
+- Tracking of arriving and departing processes.
+- Atomic process classifications remove any races that could enable traffic leaks.
+- Propagation of exclusion flag to child processes.
+- Dynamic reconfiguration.
+- Blocking of pre-existing unwanted connections.
+- Blocking of IPv6 in cases where it would otherwise leak inside the tunnel.
+
+# Development environment
+
+Visual Studio 2019, any edition.
+
+WDK, recent version.
+
+# Architecture
+
+The features mentioned above are wholly implemented in the driver. However, the driver needs a user mode agent to initially and continuously provide it with configuration data.
+
+Specifically, the agent provides a set of application paths that should be excluded from the tunnel. It also communicates the tunnel IPs (IPv4/IPv6) as well as IPs of the primary network interface.
+
+The agent is required to monitor network interfaces and update the driver with new IPs, as they change.
+
+The code in `./testing` gives an example of building blocks needed in the agent. This code is mostly useful for manual testing. For an implementation that is more suited for production use, refer to relevant sections of the [Mullvad VPN app](https://github.com/mullvad/mullvadvpn-app)
+
+# License
+
+Copyright (C) 2021 Mullvad VPN AB
+
+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.
+
+For the full license agreement, see the LICENSE.md file
diff --git a/build.bat b/build.bat
new file mode 100644
index 0000000..3d06fc9
--- /dev/null
+++ b/build.bat
@@ -0,0 +1,108 @@
+@echo off
+
+if [%1]==[] goto USAGE
+
+set CERT_THUMBPRINT=%1
+set CROSSCERT=digicert-high-assurance-ev.crt
+set TIMESTAMP_SERVER=http://timestamp.digicert.com
+
+set ROOT=%~dp0
+
+:: Register "x64 Native Tools" environment
+
+call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"
+
+:: Build driver but do not sign it
+:: It's not possible to control all arguments to signtool through msbuild
+
+msbuild.exe %ROOT%src\mullvad-split-tunnel.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SignMode=Off
+
+IF %ERRORLEVEL% NEQ 0 goto ERROR
+
+:: Sign driver
+
+signtool sign /tr %TIMESTAMP_SERVER% /td sha256 /fd sha256 /sha1 "%1" /v /ac %ROOT%resources\%CROSSCERT% %ROOT%bin\x64-Release\mullvad-split-tunnel\mullvad-split-tunnel.sys
+
+IF %ERRORLEVEL% NEQ 0 goto ERROR
+
+:: Re-generate catalog file now that driver binary has changed
+
+del %ROOT%bin\x64-Release\mullvad-split-tunnel\mullvad-split-tunnel.cat
+"%WindowsSdkVerBinPath%x86\inf2cat.exe" /driver:%ROOT%bin\x64-Release\mullvad-split-tunnel /os:"7_x64" /verbose
+
+IF %ERRORLEVEL% NEQ 0 goto ERROR
+
+:: Sign catalog
+
+signtool sign /tr %TIMESTAMP_SERVER% /td sha256 /fd sha256 /sha1 "%1" /v /ac %ROOT%resources\%CROSSCERT% %ROOT%bin\x64-Release\mullvad-split-tunnel\mullvad-split-tunnel.cat
+
+IF %ERRORLEVEL% NEQ 0 goto ERROR
+
+:: Copy artifacts
+
+rmdir /s /q %ROOT%bin\dist
+
+mkdir %ROOT%bin\dist\legacy
+copy /b %ROOT%bin\x64-Release\mullvad-split-tunnel\* %ROOT%bin\dist\legacy\
+
+::
+:: Build a CAB file for submission to the MS Hardware Dev Center
+::
+
+mkdir %ROOT%bin\dist\win10
+
+>"%ROOT%bin\dist\win10\mullvad-split-tunnel-amd64.ddf" (
+ echo .OPTION EXPLICIT ; Generate errors
+ echo .Set CabinetFileCountThreshold=0
+ echo .Set FolderFileCountThreshold=0
+ echo .Set FolderSizeThreshold=0
+ echo .Set MaxCabinetSize=0
+ echo .Set MaxDiskFileCount=0
+ echo .Set MaxDiskSize=0
+ echo .Set CompressionType=MSZIP
+ echo .Set Cabinet=on
+ echo .Set Compress=on
+ echo .Set CabinetNameTemplate=mullvad-split-tunnel-amd64.cab
+ echo .Set DestinationDir=Package
+ echo .Set DiskDirectoryTemplate=%ROOT%bin\dist\win10
+ echo %ROOT%bin\dist\legacy\mullvad-split-tunnel.cat
+ echo %ROOT%bin\dist\legacy\mullvad-split-tunnel.inf
+ echo %ROOT%bin\dist\legacy\mullvad-split-tunnel.sys
+ echo %ROOT%bin\dist\legacy\WdfCoinstaller01011.dll
+)
+
+::
+:: makecab produces several garbage files
+:: force current working directory to prevent spreading them out
+::
+
+pushd %ROOT%bin\dist\win10
+
+makecab /f "%ROOT%bin\dist\win10\mullvad-split-tunnel-amd64.ddf"
+
+popd
+
+IF %ERRORLEVEL% NEQ 0 goto ERROR
+
+signtool sign /tr %TIMESTAMP_SERVER% /td sha256 /fd sha256 /sha1 "%1" /v /ac %ROOT%resources\%CROSSCERT% %ROOT%bin\dist\win10\mullvad-split-tunnel-amd64.cab
+
+IF %ERRORLEVEL% NEQ 0 goto ERROR
+
+echo;
+echo BUILD COMPLETED SUCCESSFULLY
+echo;
+
+exit /b 0
+
+:USAGE
+
+echo Usage: %0 ^
+exit /b 1
+
+:ERROR
+
+echo;
+echo !!! BUILD FAILED !!!
+echo;
+
+exit /b 1
diff --git a/resources/digicert-high-assurance-ev.crt b/resources/digicert-high-assurance-ev.crt
new file mode 100644
index 0000000..c42e0fc
--- /dev/null
+++ b/resources/digicert-high-assurance-ev.crt
@@ -0,0 +1,30 @@
+-----BEGIN CERTIFICATE-----
+MIIFOzCCAyOgAwIBAgIKYSBNtAAAAAAAJzANBgkqhkiG9w0BAQUFADB/MQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe
+MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQDEyBNaWNyb3Nv
+ZnQgQ29kZSBWZXJpZmljYXRpb24gUm9vdDAeFw0xMTA0MTUxOTQ1MzNaFw0yMTA0
+MTUxOTU1MzNaMGwxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
+GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xKzApBgNVBAMTIkRpZ2lDZXJ0IEhp
+Z2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDGzOVz5vvUu+UtLTKm3+WBP8nNJUm2cSrD1ZQ0Z6IKHLBfaaZAscS3
+so/QmKSpQVk609yU1jzbdDikSsxNJYL3SqVTEjju80ltcZF+Y7arpl/DpIT4T2JR
+vvjF7Ns4kuMG5QiRDMQoQVX7y1qJFX5x6DW/TXIJPb46OFBbdzEbjbPHJEWap6xt
+ABRaBLe6E+tRCphBQSJOZWGHgUFQpnlcid4ZSlfVLuZdHFMsfpjNGgYWpGhz0DQE
+E1yhcdNafFXbXmThN4cwVgTlEbQpgBLxeTmIogIRfCdmt4i3ePLKCqg4qwpkwr9m
+XZWEwaElHoddGlALIBLMQbtuC1E4uEvLAgMBAAGjgcswgcgwEQYDVR0gBAowCDAG
+BgRVHSAAMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSx
+PsNpA/i/RwHUmCYaCALvY2QrwzAfBgNVHSMEGDAWgBRi+wohW39DbhHaCVRQa/XS
+lnHxnjBVBgNVHR8ETjBMMEqgSKBGhkRodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
+cGtpL2NybC9wcm9kdWN0cy9NaWNyb3NvZnRDb2RlVmVyaWZSb290LmNybDANBgkq
+hkiG9w0BAQUFAAOCAgEAIIzBWe1vnGstwUo+dR1FTEFQHL2A6tmwkosGKhM/Uxae
+VjlqimO2eCR59X24uUehCpbC9su9omafBuGs0nkJDv083KwCDHCvPxvseH7U60sF
+YCbZc2GRIe2waGPglxKrb6AS7dmf0tonPLPkVvnR1IEPcb1CfKaJ3M3VvZWiq/GT
+EX3orDEpqF1mcEGd/HXJ1bMaOSrQhQVQi6yRysSTy3GlnaSUb1gM+m4gxAgxtYWd
+foH50j3KWxiFbAqG7CIJG6V0NE9/KLyVSqsdtpiwXQmkd3Z+76eOXYT2GCTL0W2m
+w6GcwhB1gP+dMv3mz0M6gvfOj+FyKptit1/tlRo5XC+UbUi3AV8zL7vcLXM0iQRC
+ChyLefmj+hfv+qEaEN/gssGV61wMBZc7NT4YiE3bbL8kiY3Ivdifezk6JKDV39Hz
+ShqX9qZveh+wkKmzrAE5kdNht2TxPlc4A6/OetK1kPWu3DmZ1bY8l+2myxbHfWsq
+TJCU5kxU/R7NIOzOaJyHWOlhYL7rDsnVGX2f6Xi9DqwhdQePqW7gjGoqa5zj52W8
+vC08bdwE3GdFNjKvBIG8qABuYUyVxVzUjo6fL8EydL29EWUDB83vt14CV9qG1Boo
+NK+ISbLPpd2CVm9oqhTiWVT+/+ru7+qScCJggeMlI8CfzA9JsjWqWMM6w9kWlBA=
+-----END CERTIFICATE-----
diff --git a/src/containers.h b/src/containers.h
new file mode 100644
index 0000000..a8fc1a2
--- /dev/null
+++ b/src/containers.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include
+#include
+#include "containers/procregistry.h"
+#include "containers/registeredimage.h"
+
+//
+// The single instance of this struct lives in the device context.
+// But it has to be defined here so it can be shared with other components
+// in the system that should not be concerned with the full context.
+//
+struct PROCESS_REGISTRY_MGMT
+{
+ WDFSPINLOCK Lock;
+ procregistry::CONTEXT *Instance;
+};
+
+//
+// Same deal as above.
+//
+// This instance is replaced from time to time hence wrapping it makes
+// for a better interface when sharing it.
+//
+struct REGISTERED_IMAGE_MGMT
+{
+ registeredimage::CONTEXT * volatile Instance;
+};
diff --git a/src/containers/procregistry.cpp b/src/containers/procregistry.cpp
new file mode 100644
index 0000000..fb02198
--- /dev/null
+++ b/src/containers/procregistry.cpp
@@ -0,0 +1,370 @@
+#include
+#include "procregistry.h"
+#include "../util.h"
+
+namespace procregistry
+{
+
+struct CONTEXT
+{
+ RTL_AVL_TABLE Tree;
+ ST_PAGEABLE Pageable;
+};
+
+namespace
+{
+
+RTL_GENERIC_COMPARE_RESULTS
+TreeCompareRoutine
+(
+ __in struct _RTL_AVL_TABLE *Table,
+ __in PVOID FirstStruct,
+ __in PVOID SecondStruct
+)
+{
+ UNREFERENCED_PARAMETER(Table);
+
+ auto first = ((PROCESS_REGISTRY_ENTRY*)FirstStruct)->ProcessId;
+ auto second = ((PROCESS_REGISTRY_ENTRY*)SecondStruct)->ProcessId;
+
+ if (first < second)
+ {
+ return GenericLessThan;
+ }
+
+ if (first > second)
+ {
+ return GenericGreaterThan;
+ }
+
+ return GenericEqual;
+}
+
+PVOID
+TreeAllocateRoutineNonPaged
+(
+ __in struct _RTL_AVL_TABLE *Table,
+ __in CLONG ByteSize
+)
+{
+ UNREFERENCED_PARAMETER(Table);
+
+ return ExAllocatePoolWithTag(NonPagedPool, ByteSize, ST_POOL_TAG);
+}
+
+PVOID
+TreeAllocateRoutinePaged
+(
+ __in struct _RTL_AVL_TABLE *Table,
+ __in CLONG ByteSize
+)
+{
+ UNREFERENCED_PARAMETER(Table);
+
+ return ExAllocatePoolWithTag(PagedPool, ByteSize, ST_POOL_TAG);
+}
+
+VOID
+TreeFreeRoutine
+(
+ __in struct _RTL_AVL_TABLE *Table,
+ __in PVOID Buffer
+)
+{
+ UNREFERENCED_PARAMETER(Table);
+
+ ExFreePoolWithTag(Buffer, ST_POOL_TAG);
+}
+
+//
+// ClearDepartingParentLink()
+//
+// `Entry` is an enumerated entry in the tree.
+// `Context` is an entry that's being removed from the tree.
+//
+// If `Entry` is a child of `Context` it needs to be updated to indicate that the parent process
+// is no longer available.
+//
+bool
+NTAPI
+ClearDepartingParentLink
+(
+ PROCESS_REGISTRY_ENTRY *Entry,
+ void *Context
+)
+{
+ auto parentEntry = (PROCESS_REGISTRY_ENTRY*)Context;
+
+ if (Entry->ParentEntry == parentEntry)
+ {
+ NT_ASSERT(Entry->ParentProcessId == parentEntry->ProcessId);
+
+ Entry->ParentProcessId = 0;
+ Entry->ParentEntry = NULL;
+ }
+
+ return true;
+}
+
+void
+InnerDeleteEntry
+(
+ CONTEXT *Context,
+ PROCESS_REGISTRY_ENTRY *Entry
+)
+{
+ if (Entry->ImageName.Buffer != NULL)
+ {
+ util::FreeStringBuffer(&Entry->ImageName);
+ }
+
+ RtlDeleteElementGenericTableAvl(&Context->Tree, Entry);
+}
+
+} // anonymous namespace
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ ST_PAGEABLE Pageable
+)
+{
+ const auto poolType = (Pageable == ST_PAGEABLE::YES) ? PagedPool : NonPagedPool;
+
+ *Context = (CONTEXT*)
+ ExAllocatePoolWithTag(poolType, sizeof(CONTEXT), ST_POOL_TAG);
+
+ if (*Context == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ (*Context)->Pageable = Pageable;
+
+ const auto allocRoutine = (Pageable == ST_PAGEABLE::YES)
+ ? TreeAllocateRoutinePaged : TreeAllocateRoutineNonPaged;
+
+ RtlInitializeGenericTableAvl(&(*Context)->Tree, TreeCompareRoutine,
+ allocRoutine, TreeFreeRoutine, NULL);
+
+ return STATUS_SUCCESS;
+}
+
+void
+TearDown
+(
+ CONTEXT **Context
+)
+{
+ Reset(*Context);
+
+ ExFreePoolWithTag(*Context, ST_POOL_TAG);
+
+ *Context = NULL;
+}
+
+void
+Reset
+(
+ CONTEXT *Context
+)
+{
+ for (;;)
+ {
+ auto entry = (PROCESS_REGISTRY_ENTRY*)RtlGetElementGenericTableAvl(&Context->Tree, 0);
+
+ if (NULL == entry)
+ {
+ break;
+ }
+
+ InnerDeleteEntry(Context, entry);
+ }
+}
+
+NTSTATUS
+InitializeEntry
+(
+ CONTEXT *Context,
+ HANDLE ParentProcessId,
+ HANDLE ProcessId,
+ ST_PROCESS_SPLIT_STATUS Split,
+ UNICODE_STRING *ImageName,
+ PROCESS_REGISTRY_ENTRY *Entry
+)
+{
+ RtlZeroMemory(Entry, sizeof(*Entry));
+
+ if (ImageName != NULL
+ && ImageName->Length != 0)
+ {
+ LOWER_UNICODE_STRING lowerImageName;
+
+ auto status = util::AllocateCopyDowncaseString(ImageName, &lowerImageName, Context->Pageable);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ Entry->ImageName = lowerImageName;
+ }
+
+ Entry->ParentProcessId = ParentProcessId;
+ Entry->ProcessId = ProcessId;
+
+ static const PROCESS_REGISTRY_ENTRY_SETTINGS settings =
+ {
+ .Split = ST_PROCESS_SPLIT_STATUS_OFF,
+ .HasFirewallState = false
+ };
+
+ Entry->Settings = { Split, false };
+ Entry->TargetSettings = settings;
+ Entry->PreviousSettings = settings;
+
+ Entry->ParentEntry = NULL;
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+AddEntry
+(
+ CONTEXT *Context,
+ PROCESS_REGISTRY_ENTRY *Entry
+)
+{
+ //
+ // Insert entry into tree.
+ // This makes a copy of the entry.
+ //
+
+ BOOLEAN newElement;
+
+ auto record = RtlInsertElementGenericTableAvl(&Context->Tree, Entry, (CLONG)sizeof(*Entry), &newElement);
+
+ if (record != NULL && newElement != FALSE)
+ {
+ return STATUS_SUCCESS;
+ }
+
+ //
+ // Handle failure cases.
+ //
+
+ if (record == NULL)
+ {
+ // Allocation of record failed.
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ // There's already a record for this PID.
+ return STATUS_DUPLICATE_OBJECTID;
+}
+
+void
+ReleaseEntry
+(
+ PROCESS_REGISTRY_ENTRY *Entry
+)
+{
+ if (Entry->ImageName.Buffer != NULL)
+ {
+ util::FreeStringBuffer(&Entry->ImageName);
+ }
+}
+
+PROCESS_REGISTRY_ENTRY*
+FindEntry
+(
+ CONTEXT *Context,
+ HANDLE ProcessId
+)
+{
+ PROCESS_REGISTRY_ENTRY record = { 0 };
+
+ record.ProcessId = ProcessId;
+
+ return (PROCESS_REGISTRY_ENTRY*)RtlLookupElementGenericTableAvl(&Context->Tree, &record);
+}
+
+void
+DeleteEntry
+(
+ CONTEXT *Context,
+ PROCESS_REGISTRY_ENTRY *Entry
+)
+{
+ ForEach(Context, ClearDepartingParentLink, Entry);
+
+ InnerDeleteEntry(Context, Entry);
+}
+
+void
+DeleteEntryById
+(
+ CONTEXT *Context,
+ HANDLE ProcessId
+)
+{
+ auto entry = FindEntry(Context, ProcessId);
+
+ if (entry != NULL)
+ {
+ DeleteEntry(Context, entry);
+ }
+}
+
+bool
+ForEach
+(
+ CONTEXT *Context,
+ ST_PR_FOREACH Callback,
+ void *ClientContext
+)
+{
+ for (auto entry = RtlEnumerateGenericTableAvl(&Context->Tree, TRUE);
+ entry != NULL;
+ entry = RtlEnumerateGenericTableAvl(&Context->Tree, FALSE))
+ {
+ if (!Callback((PROCESS_REGISTRY_ENTRY*)entry, ClientContext))
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+PROCESS_REGISTRY_ENTRY*
+GetParentEntry
+(
+ CONTEXT *Context,
+ PROCESS_REGISTRY_ENTRY *Entry
+)
+{
+ if (NULL != Entry->ParentEntry)
+ {
+ return Entry->ParentEntry;
+ }
+
+ if (0 == Entry->ParentProcessId)
+ {
+ return NULL;
+ }
+
+ return (Entry->ParentEntry = FindEntry(Context, Entry->ParentProcessId));
+}
+
+bool
+IsEmpty
+(
+ CONTEXT *Context
+)
+{
+ return NULL == RtlEnumerateGenericTableAvl(&Context->Tree, TRUE);
+}
+
+} // namespace procregistry
diff --git a/src/containers/procregistry.h b/src/containers/procregistry.h
new file mode 100644
index 0000000..cfa44c9
--- /dev/null
+++ b/src/containers/procregistry.h
@@ -0,0 +1,162 @@
+#pragma once
+
+#include
+#include "../defs/types.h"
+
+namespace procregistry
+{
+
+struct PROCESS_REGISTRY_ENTRY_SETTINGS
+{
+ // Whether traffic should be split.
+ ST_PROCESS_SPLIT_STATUS Split;
+
+ // Whether the process is associated with any firewall filters.
+ bool HasFirewallState;
+};
+
+struct PROCESS_REGISTRY_ENTRY
+{
+ HANDLE ParentProcessId;
+ HANDLE ProcessId;
+
+ PROCESS_REGISTRY_ENTRY_SETTINGS Settings;
+
+ PROCESS_REGISTRY_ENTRY_SETTINGS TargetSettings;
+
+ PROCESS_REGISTRY_ENTRY_SETTINGS PreviousSettings;
+
+ // Device path using all lower-case characters.
+ LOWER_UNICODE_STRING ImageName;
+
+ //
+ // This is management data initialized and updated
+ // by the implementation.
+ //
+ // It would be inconvenient to store it anywhere else.
+ //
+ PROCESS_REGISTRY_ENTRY *ParentEntry;
+};
+
+struct CONTEXT;
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ ST_PAGEABLE Pageable
+);
+
+void
+TearDown
+(
+ CONTEXT **Context
+);
+
+void
+Reset
+(
+ CONTEXT *Context
+);
+
+//
+// InitializeEntry()
+//
+// IRQL <= APC.
+//
+// Initializes `Entry` with provided values and initializes a buffer of
+// the correct backing and format for `Entry->ImageName.Buffer`.
+//
+// The provided `Entry` argument is typically allocated on the stack.
+//
+NTSTATUS
+InitializeEntry
+(
+ CONTEXT *Context,
+ HANDLE ParentProcessId,
+ HANDLE ProcessId,
+ ST_PROCESS_SPLIT_STATUS Split,
+ UNICODE_STRING *ImageName,
+ PROCESS_REGISTRY_ENTRY *Entry
+);
+
+//
+// AddEntry()
+//
+// IRQL <= DISPATCH.
+//
+// On Success:
+//
+// The `Entry` argument will be copied and `Entry->ImageName.Buffer`
+// is taken ownership of.
+//
+// On failure:
+//
+// `Entry->ImageName.Buffer` is not taken ownership of.
+//
+NTSTATUS
+AddEntry
+(
+ CONTEXT *Context,
+ PROCESS_REGISTRY_ENTRY *Entry
+);
+
+//
+// ReleaseEntry()
+//
+// Memory backing the imagename string buffer is allocated by InitializeEntry().
+//
+// Use this function to release an entry that could not be added, in order to
+// keep details abstracted.
+//
+void
+ReleaseEntry
+(
+ PROCESS_REGISTRY_ENTRY *Entry
+);
+
+PROCESS_REGISTRY_ENTRY*
+FindEntry
+(
+ CONTEXT *Context,
+ HANDLE ProcessId
+);
+
+void
+DeleteEntry
+(
+ CONTEXT *Context,
+ PROCESS_REGISTRY_ENTRY *Entry
+);
+
+void
+DeleteEntryById
+(
+ CONTEXT *Context,
+ HANDLE ProcessId
+);
+
+typedef bool (NTAPI *ST_PR_FOREACH)(PROCESS_REGISTRY_ENTRY *Entry, void *Context);
+
+bool
+ForEach
+(
+ CONTEXT *Context,
+ ST_PR_FOREACH Callback,
+ void *ClientContext
+);
+
+PROCESS_REGISTRY_ENTRY*
+GetParentEntry
+(
+ CONTEXT *Context,
+ PROCESS_REGISTRY_ENTRY *Entry
+);
+
+bool
+IsEmpty
+(
+ CONTEXT *Context
+);
+
+} // namespace procregistry
diff --git a/src/containers/registeredimage.cpp b/src/containers/registeredimage.cpp
new file mode 100644
index 0000000..5443c48
--- /dev/null
+++ b/src/containers/registeredimage.cpp
@@ -0,0 +1,327 @@
+#include
+#include "registeredimage.h"
+#include "../util.h"
+
+namespace registeredimage
+{
+
+struct CONTEXT
+{
+ LIST_ENTRY ListEntry;
+ ST_PAGEABLE Pageable;
+};
+
+namespace
+{
+
+//
+// FindEntry()
+//
+// Use at PASSIVE only (APC is OK unless in paging file IO path).
+// Presumably because character tables are stored in pageable memory.
+//
+// Implements case-insensitive comparison.
+//
+REGISTERED_IMAGE_ENTRY*
+FindEntry
+(
+ CONTEXT *Context,
+ UNICODE_STRING *ImageName
+)
+{
+ for (auto entry = Context->ListEntry.Flink;
+ entry != &Context->ListEntry;
+ entry = entry->Flink)
+ {
+ auto candidate = (REGISTERED_IMAGE_ENTRY*)entry;
+
+ if (0 == RtlCompareUnicodeString((UNICODE_STRING*)&candidate->ImageName, ImageName, TRUE))
+ {
+ return candidate;
+ }
+ }
+
+ return NULL;
+}
+
+//
+// FindEntryExact()
+//
+// Use at DISPATCH.
+// Implements case-sensitive comparison.
+//
+REGISTERED_IMAGE_ENTRY*
+FindEntryExact
+(
+ CONTEXT *Context,
+ LOWER_UNICODE_STRING *ImageName
+)
+{
+ for (auto entry = Context->ListEntry.Flink;
+ entry != &Context->ListEntry;
+ entry = entry->Flink)
+ {
+ auto candidate = (REGISTERED_IMAGE_ENTRY*)entry;
+
+ if (candidate->ImageName.Length != ImageName->Length)
+ {
+ continue;
+ }
+
+ const auto equalBytes = RtlCompareMemory
+ (
+ candidate->ImageName.Buffer,
+ ImageName->Buffer,
+ ImageName->Length
+ );
+
+ if (equalBytes == ImageName->Length)
+ {
+ return candidate;
+ }
+ }
+
+ return NULL;
+}
+
+NTSTATUS
+AddEntryInner
+(
+ CONTEXT *Context,
+ LOWER_UNICODE_STRING *ImageName
+)
+{
+ //
+ // Make a single allocation for the struct and string buffer.
+ //
+
+ auto offsetStringBuffer = util::RoundToMultiple(sizeof(REGISTERED_IMAGE_ENTRY), 8);
+
+ auto allocationSize = offsetStringBuffer + ImageName->Length;
+
+ const auto poolType = (Context->Pageable == ST_PAGEABLE::YES) ? PagedPool : NonPagedPool;
+
+ auto record = (REGISTERED_IMAGE_ENTRY*)
+ ExAllocatePoolWithTag(poolType, allocationSize, ST_POOL_TAG);
+
+ if (record == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ auto stringBuffer = (WCHAR*)(((CHAR*)record) + offsetStringBuffer);
+
+ InitializeListHead(&record->ListEntry);
+
+ record->ImageName.Length = ImageName->Length;
+ record->ImageName.MaximumLength = ImageName->Length;
+ record->ImageName.Buffer = stringBuffer;
+
+ RtlCopyMemory(stringBuffer, ImageName->Buffer, ImageName->Length);
+
+ InsertTailList(&Context->ListEntry, &record->ListEntry);
+
+ return STATUS_SUCCESS;
+}
+
+bool
+RemoveEntryInner
+(
+ REGISTERED_IMAGE_ENTRY *Entry
+)
+{
+ if (Entry == NULL)
+ {
+ return false;
+ }
+
+ RemoveEntryList(&Entry->ListEntry);
+
+ ExFreePoolWithTag(Entry, ST_POOL_TAG);
+
+ return true;
+}
+
+} // anonymous namespace
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ ST_PAGEABLE Pageable
+)
+{
+ const auto poolType = (Pageable == ST_PAGEABLE::YES) ? PagedPool : NonPagedPool;
+
+ *Context = (CONTEXT*)ExAllocatePoolWithTag(poolType, sizeof(CONTEXT), ST_POOL_TAG);
+
+ if (*Context == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ InitializeListHead(&(*Context)->ListEntry);
+ (*Context)->Pageable = Pageable;
+
+ return STATUS_SUCCESS;
+}
+
+_IRQL_requires_(PASSIVE_LEVEL)
+NTSTATUS
+AddEntry
+(
+ CONTEXT *Context,
+ UNICODE_STRING *ImageName
+)
+{
+ NT_ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);
+
+ //
+ // Avoid storing duplicates.
+ // FindEntry doesn't care about character casing.
+ //
+
+ if (NULL != FindEntry(Context, ImageName))
+ {
+ return STATUS_SUCCESS;
+ }
+
+ //
+ // Make a lower case string copy.
+ //
+
+ UNICODE_STRING lowerImageName;
+
+ auto status = RtlDowncaseUnicodeString(&lowerImageName, ImageName, TRUE);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = AddEntryInner(Context, (LOWER_UNICODE_STRING*)&lowerImageName);
+
+ RtlFreeUnicodeString(&lowerImageName);
+
+ return status;
+}
+
+NTSTATUS
+AddEntryExact
+(
+ CONTEXT *Context,
+ LOWER_UNICODE_STRING *ImageName
+)
+{
+ if (NULL != FindEntryExact(Context, ImageName))
+ {
+ return STATUS_SUCCESS;
+ }
+
+ return AddEntryInner(Context, ImageName);
+}
+
+bool
+HasEntry
+(
+ CONTEXT *Context,
+ UNICODE_STRING *ImageName
+)
+{
+ auto record = FindEntry(Context, ImageName);
+
+ return record != NULL;
+}
+
+bool
+HasEntryExact
+(
+ CONTEXT *Context,
+ LOWER_UNICODE_STRING *ImageName
+)
+{
+ auto record = FindEntryExact(Context, ImageName);
+
+ return record != NULL;
+}
+
+bool
+RemoveEntry
+(
+ CONTEXT *Context,
+ UNICODE_STRING *ImageName
+)
+{
+ return RemoveEntryInner(FindEntry(Context, ImageName));
+}
+
+bool
+RemoveEntryExact
+(
+ CONTEXT *Context,
+ LOWER_UNICODE_STRING *ImageName
+)
+{
+ return RemoveEntryInner(FindEntryExact(Context, ImageName));
+}
+
+bool
+ForEach
+(
+ CONTEXT *Context,
+ ST_RI_FOREACH Callback,
+ void *ClientContext
+)
+{
+ for (auto entry = Context->ListEntry.Flink;
+ entry != &Context->ListEntry;
+ entry = entry->Flink)
+ {
+ auto typedEntry = (REGISTERED_IMAGE_ENTRY *)entry;
+
+ if (!Callback(&typedEntry->ImageName, ClientContext))
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void
+Reset
+(
+ CONTEXT *Context
+)
+{
+ while (FALSE == IsListEmpty(&Context->ListEntry))
+ {
+ auto entry = RemoveHeadList(&Context->ListEntry);
+
+ ExFreePoolWithTag(entry, ST_POOL_TAG);
+ }
+}
+
+void
+TearDown
+(
+ CONTEXT **Context
+)
+{
+ Reset(*Context);
+
+ ExFreePoolWithTag(*Context, ST_POOL_TAG);
+
+ *Context = NULL;
+}
+
+bool
+IsEmpty
+(
+ CONTEXT *Context
+)
+{
+ return bool_cast(IsListEmpty(&Context->ListEntry));
+}
+
+} // namespace registeredimage
diff --git a/src/containers/registeredimage.h b/src/containers/registeredimage.h
new file mode 100644
index 0000000..d9301e3
--- /dev/null
+++ b/src/containers/registeredimage.h
@@ -0,0 +1,139 @@
+#pragma once
+
+#include
+#include "../defs/types.h"
+
+namespace registeredimage
+{
+
+struct REGISTERED_IMAGE_ENTRY
+{
+ LIST_ENTRY ListEntry;
+
+ // Device path using all lower-case characters.
+ LOWER_UNICODE_STRING ImageName;
+};
+
+struct CONTEXT;
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ ST_PAGEABLE Pageable
+);
+
+//
+// AddEntry()
+//
+// IRQL == PASSIVE_LEVEL
+//
+// Converts imagename to lower case before creating an entry.
+//
+_IRQL_requires_(PASSIVE_LEVEL)
+NTSTATUS
+AddEntry
+(
+ CONTEXT *Context,
+ UNICODE_STRING *ImageName
+);
+
+//
+// AddEntryExact()
+//
+// IRQL <= DISPATCH
+//
+// Creates a new entry with the `ImageName` argument exactly as passed.
+//
+NTSTATUS
+AddEntryExact
+(
+ CONTEXT *Context,
+ LOWER_UNICODE_STRING *ImageName
+);
+
+//
+// HasEntry()
+//
+// IRQL <= APC
+//
+// Compares existing entries against `ImageName` without regard to character casing.
+//
+bool
+HasEntry
+(
+ CONTEXT *Context,
+ UNICODE_STRING *ImageName
+);
+
+//
+// HasEntryExact()
+//
+// IRQL <= DISPATCH
+//
+// Compares existing entries against case-sensitive `ImageName` argument.
+//
+bool
+HasEntryExact
+(
+ CONTEXT *Context,
+ LOWER_UNICODE_STRING *ImageName
+);
+
+//
+// RemoveEntry()
+//
+// IRQL <= APC
+//
+// Searches for and removes entry matching `ImageName` without regard to character casing.
+//
+bool
+RemoveEntry
+(
+ CONTEXT *Context,
+ UNICODE_STRING *ImageName
+);
+
+//
+// RemoveEntryExact()
+//
+// IRQL <= DISPATCH
+//
+// Searches for and removes entry using case-sensitive matching of `ImageName`.
+//
+bool
+RemoveEntryExact
+(
+ CONTEXT *Context,
+ LOWER_UNICODE_STRING *ImageName
+);
+
+typedef bool (NTAPI *ST_RI_FOREACH)(LOWER_UNICODE_STRING *ImageName, void *Context);
+
+bool
+ForEach
+(
+ CONTEXT *Context,
+ ST_RI_FOREACH Callback,
+ void *ClientContext
+);
+
+void
+Reset
+(
+ CONTEXT *Context
+);
+
+void
+TearDown
+(
+ CONTEXT **Context
+);
+
+bool
+IsEmpty
+(
+ CONTEXT *Context
+);
+
+} // namespace registeredimage
diff --git a/src/custom-stampinf.bat b/src/custom-stampinf.bat
new file mode 100644
index 0000000..f68b3ed
--- /dev/null
+++ b/src/custom-stampinf.bat
@@ -0,0 +1,59 @@
+@echo off
+
+:: Visual Studio invokes the pre-build event only after it executes stampinf.
+:: Therefore, any scheme to attempt to set STAMPINF_VERSION (using e.g. setx) during a pre-build event, is doomed to fail.
+:: You also cannot disable stampinf being called by Visual Studio.
+:: So what we'll do is run stampinf again, with the correct version data.
+::
+
+if [%1]==[] goto ABORT_ARGUMENTS
+if [%2]==[] goto ABORT_ARGUMENTS
+if [%3]==[] goto ABORT_ARGUMENTS
+if [%4]==[] goto ABORT_ARGUMENTS
+if [%5]==[] goto ABORT_ARGUMENTS
+
+:: Arguments 1, 4, 5 are quoted strings containing absolute paths.
+:: This avoids any issues with spaces in paths, quotes, concatenation.
+
+set INF_BINARY=%1
+set INF_ARCH=%2
+set DRIVER_KMDF_VERSION=%3
+set INTERMEDIATE_DIR_TARGET=%4
+set OUTPUT_DIR_TARGET=%5
+
+setlocal enabledelayedexpansion
+
+:: Import version defines into environment
+
+for /f "tokens=1-3 delims= " %%i in (%~dp0\version.h) do (
+ if /i "%%i"=="#define" (
+ set %%j=%%k
+ )
+)
+
+set DRIVER_VERSION=%DRIVER_VERSION_MAJOR%.%DRIVER_VERSION_MINOR%.%DRIVER_VERSION_PATCH%.%DRIVER_VERSION_BUILD%
+
+:: Broken actions such as the DriverPackageTarget references the intermediate INF
+:: So we have to re-stamp the intermediate file and copy it to the output directory, so they're both up-to-date
+
+echo Stamping INF again with correct version data
+
+%INF_BINARY% -d "*" -a "%INF_ARCH%" -v "%DRIVER_VERSION%" -k "%DRIVER_KMDF_VERSION%" -f %INTERMEDIATE_DIR_TARGET%
+
+if %ERRORLEVEL% neq 0 goto FAILED_STAMP
+
+copy /y /b %INTERMEDIATE_DIR_TARGET% %OUTPUT_DIR_TARGET%
+
+exit /b 0
+
+:ABORT_ARGUMENTS
+
+echo ERROR: %0 invoked without enough arguments
+
+exit /b 1
+
+:FAILED_STAMP
+
+echo ERROR: %0 has failed
+
+exit /b 1
diff --git a/src/defs/config.h b/src/defs/config.h
new file mode 100644
index 0000000..70ea92e
--- /dev/null
+++ b/src/defs/config.h
@@ -0,0 +1,26 @@
+#pragma once
+
+//
+// Structures related to configuration.
+//
+
+typedef struct tag_ST_CONFIGURATION_ENTRY
+{
+ // Offset into buffer region that follows all entries.
+ // The image name uses the device path.
+ SIZE_T ImageNameOffset;
+
+ // Byte length for non-null terminated wide char string.
+ USHORT ImageNameLength;
+}
+ST_CONFIGURATION_ENTRY;
+
+typedef struct tag_ST_CONFIGURATION_HEADER
+{
+ // Number of entries immediately following the header.
+ SIZE_T NumEntries;
+
+ // Total byte length: header + entries + string buffer.
+ SIZE_T TotalLength;
+}
+ST_CONFIGURATION_HEADER;
diff --git a/src/defs/events.h b/src/defs/events.h
new file mode 100644
index 0000000..7359103
--- /dev/null
+++ b/src/defs/events.h
@@ -0,0 +1,56 @@
+#pragma once
+
+enum ST_EVENT_ID
+{
+ ST_EVENT_START_SPLITTING_PROCESS = 0, // ST_SPLITTING_EVENT
+ ST_EVENT_STOP_SPLITTING_PROCESS, // ST_SPLITTING_EVENT
+
+ ST_EVENT_ERROR_FLAG = 0x80000000,
+
+ ST_EVENT_ERROR_START_SPLITTING_PROCESS, // ST_SPLITTING_ERROR_EVENT
+ ST_EVENT_ERROR_STOP_SPLITTING_PROCESS // ST_SPLITTING_ERROR_EVENT
+};
+
+typedef struct tag_ST_EVENT_HEADER
+{
+ ST_EVENT_ID EventId;
+
+ // Size of payload.
+ SIZE_T EventSize;
+
+ // Message defined payload.
+ UCHAR EventData[ANYSIZE_ARRAY];
+}
+ST_EVENT_HEADER;
+
+enum ST_SPLITTING_STATUS_CHANGE_REASON
+{
+ ST_SPLITTING_REASON_BY_INHERITANCE = 1,
+ ST_SPLITTING_REASON_BY_CONFIG = 2,
+ ST_SPLITTING_REASON_PROCESS_ARRIVING = 4,
+ ST_SPLITTING_REASON_PROCESS_DEPARTING = 8
+};
+
+typedef struct tag_ST_SPLITTING_EVENT
+{
+ HANDLE ProcessId;
+
+ ST_SPLITTING_STATUS_CHANGE_REASON Reason;
+
+ // Byte length for non-null terminated wide char string.
+ USHORT ImageNameLength;
+
+ WCHAR ImageName[ANYSIZE_ARRAY];
+}
+ST_SPLITTING_EVENT;
+
+typedef struct tag_ST_SPLITTING_ERROR_EVENT
+{
+ HANDLE ProcessId;
+
+ // Byte length for non-null terminated wide char string.
+ USHORT ImageNameLength;
+
+ WCHAR ImageName[ANYSIZE_ARRAY];
+}
+ST_SPLITTING_ERROR_EVENT;
diff --git a/src/defs/ioctl.h b/src/defs/ioctl.h
new file mode 100644
index 0000000..7615288
--- /dev/null
+++ b/src/defs/ioctl.h
@@ -0,0 +1,49 @@
+#pragma once
+
+//
+// IOCTLs for controlling the driver.
+//
+
+#define ST_DEVICE_TYPE 0x8000
+
+#define IOCTL_ST_INITIALIZE \
+ CTL_CODE(ST_DEVICE_TYPE, 1, METHOD_NEITHER, FILE_ANY_ACCESS)
+
+#define IOCTL_ST_DEQUEUE_EVENT \
+ CTL_CODE(ST_DEVICE_TYPE, 2, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+#define IOCTL_ST_REGISTER_PROCESSES \
+ CTL_CODE(ST_DEVICE_TYPE, 3, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+#define IOCTL_ST_REGISTER_IP_ADDRESSES \
+ CTL_CODE(ST_DEVICE_TYPE, 4, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+#define IOCTL_ST_GET_IP_ADDRESSES \
+ CTL_CODE(ST_DEVICE_TYPE, 5, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+#define IOCTL_ST_SET_CONFIGURATION \
+ CTL_CODE(ST_DEVICE_TYPE, 6, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+#define IOCTL_ST_GET_CONFIGURATION \
+ CTL_CODE(ST_DEVICE_TYPE, 7, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+#define IOCTL_ST_CLEAR_CONFIGURATION \
+ CTL_CODE(ST_DEVICE_TYPE, 8, METHOD_NEITHER, FILE_ANY_ACCESS)
+
+#define IOCTL_ST_GET_STATE \
+ CTL_CODE(ST_DEVICE_TYPE, 9, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+#define IOCTL_ST_QUERY_PROCESS \
+ CTL_CODE(ST_DEVICE_TYPE, 10, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+//
+// IOCTL_ST_RESET:
+//
+// Use before attempting to unload the driver.
+// Subsystems will be torn down, resources released etc.
+//
+// On success, the new state will be ST_DRIVER_STATE_STARTED.
+// On error, the new state will be ST_DRIVER_STATE_ZOMBIE.
+//
+#define IOCTL_ST_RESET \
+ CTL_CODE(ST_DEVICE_TYPE, 11, METHOD_NEITHER, FILE_ANY_ACCESS)
diff --git a/src/defs/process.h b/src/defs/process.h
new file mode 100644
index 0000000..c28f08a
--- /dev/null
+++ b/src/defs/process.h
@@ -0,0 +1,29 @@
+#pragma once
+
+//
+// Structures related to initial process registration.
+//
+
+typedef struct tag_ST_PROCESS_DISCOVERY_ENTRY
+{
+ HANDLE ProcessId;
+ HANDLE ParentProcessId;
+
+ // Offset into buffer region that follows all entries.
+ // The image name uses the device path.
+ SIZE_T ImageNameOffset;
+
+ // Byte length for non-null terminated wide char string.
+ USHORT ImageNameLength;
+}
+ST_PROCESS_DISCOVERY_ENTRY;
+
+typedef struct tag_ST_PROCESS_DISCOVERY_HEADER
+{
+ // Number of entries immediately following the header.
+ SIZE_T NumEntries;
+
+ // Total byte length: header + entries + string buffer.
+ SIZE_T TotalLength;
+}
+ST_PROCESS_DISCOVERY_HEADER;
diff --git a/src/defs/queryprocess.h b/src/defs/queryprocess.h
new file mode 100644
index 0000000..ffb91ad
--- /dev/null
+++ b/src/defs/queryprocess.h
@@ -0,0 +1,25 @@
+#pragma once
+
+//
+// Structures related to querying process information.
+//
+
+typedef struct tag_ST_QUERY_PROCESS
+{
+ HANDLE ProcessId;
+}
+ST_QUERY_PROCESS;
+
+typedef struct tag_ST_QUERY_PROCESS_RESPONSE
+{
+ HANDLE ProcessId;
+ HANDLE ParentProcessId;
+
+ BOOLEAN Split;
+
+ // Byte length for non-null terminated wide char string.
+ USHORT ImageNameLength;
+
+ WCHAR ImageName[ANYSIZE_ARRAY];
+}
+ST_QUERY_PROCESS_RESPONSE;
diff --git a/src/defs/state.h b/src/defs/state.h
new file mode 100644
index 0000000..ccc1a0a
--- /dev/null
+++ b/src/defs/state.h
@@ -0,0 +1,28 @@
+#pragma once
+
+//
+// All possible states in the driver.
+//
+
+enum ST_DRIVER_STATE
+{
+ // Default state after being loaded.
+ ST_DRIVER_STATE_NONE = 0,
+
+ // DriverEntry has completed successfully.
+ // Basically only driver and device objects are created at this point.
+ ST_DRIVER_STATE_STARTED = 1,
+
+ // All subsystems are initialized.
+ ST_DRIVER_STATE_INITIALIZED = 2,
+
+ // User mode has registered all processes in the system.
+ ST_DRIVER_STATE_READY = 3,
+
+ // IP addresses are registered.
+ // A valid configuration is registered.
+ ST_DRIVER_STATE_ENGAGED = 4,
+
+ // Driver could not tear down subsystems.
+ ST_DRIVER_STATE_ZOMBIE = 5,
+};
diff --git a/src/defs/types.h b/src/defs/types.h
new file mode 100644
index 0000000..ac1f01b
--- /dev/null
+++ b/src/defs/types.h
@@ -0,0 +1,39 @@
+#pragma once
+
+//
+// types.h
+//
+// Miscellaneous types and defines used internally.
+//
+
+#define ST_POOL_TAG 'UTPS'
+
+enum class ST_PAGEABLE
+{
+ YES = 0,
+ NO
+};
+
+//
+// Type-safety when passing around lower case device paths.
+// Same definition as UNICODE_STRING so they can be cast between.
+//
+typedef struct tag_LOWER_UNICODE_STRING
+{
+ USHORT Length;
+ USHORT MaximumLength;
+ PWCH Buffer;
+}
+LOWER_UNICODE_STRING;
+
+enum ST_PROCESS_SPLIT_STATUS
+{
+ // Traffic should be split.
+ ST_PROCESS_SPLIT_STATUS_ON_BY_CONFIG = 0,
+
+ // Traffic should be split.
+ ST_PROCESS_SPLIT_STATUS_ON_BY_INHERITANCE,
+
+ // Traffic should not be split.
+ ST_PROCESS_SPLIT_STATUS_OFF
+};
diff --git a/src/devicecontext.h b/src/devicecontext.h
new file mode 100644
index 0000000..42773b5
--- /dev/null
+++ b/src/devicecontext.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include
+#include "ipaddr.h"
+#include "containers.h"
+#include "defs/state.h"
+#include "firewall/firewall.h"
+#include "procmgmt/procmgmt.h"
+#include "eventing/eventing.h"
+#include "procbroker/procbroker.h"
+
+struct DRIVER_STATE_MGMT
+{
+ WDFWAITLOCK Lock;
+ ST_DRIVER_STATE State;
+};
+
+typedef struct tag_ST_DEVICE_CONTEXT
+{
+ DRIVER_STATE_MGMT DriverState;
+
+ // Serialized queue for processing of most IOCTLs.
+ WDFQUEUE IoCtlQueue;
+
+ ST_IP_ADDRESSES IpAddresses;
+
+ PROCESS_REGISTRY_MGMT ProcessRegistry;
+
+ // Protected by state lock.
+ REGISTERED_IMAGE_MGMT RegisteredImage;
+
+ firewall::CONTEXT *Firewall;
+
+ procmgmt::CONTEXT *ProcessMgmt;
+
+ eventing::CONTEXT *Eventing;
+
+ procbroker::CONTEXT *ProcessEventBroker;
+}
+ST_DEVICE_CONTEXT;
+
+WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(ST_DEVICE_CONTEXT, DeviceGetSplitTunnelContext)
diff --git a/src/driverentry.cpp b/src/driverentry.cpp
new file mode 100644
index 0000000..b68c614
--- /dev/null
+++ b/src/driverentry.cpp
@@ -0,0 +1,558 @@
+#include "x64guard.h"
+
+#include
+#include
+#include
+#include
+
+#include "devicecontext.h"
+#include "util.h"
+#include "ioctl.h"
+#include "firewall/firewall.h"
+#include "defs/ioctl.h"
+#include "eventing/eventing.h"
+
+extern "C"
+DRIVER_INITIALIZE DriverEntry;
+
+extern "C" // Because alloc_text requires this.
+NTSTATUS
+StCreateDevice
+(
+ IN WDFDRIVER WdfDriver
+);
+
+EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL StEvtIoDeviceControl;
+
+EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL StEvtIoDeviceControlSerial;
+
+EVT_WDF_DRIVER_UNLOAD StEvtDriverUnload;
+
+#pragma alloc_text (INIT, DriverEntry)
+#pragma alloc_text (INIT, StCreateDevice)
+
+#if DBG
+#define ST_DEVICE_SECURITY_DESCRIPTOR SDDL_DEVOBJ_SYS_ALL_ADM_RWX_WORLD_RWX_RES_RWX
+#else
+#define ST_DEVICE_SECURITY_DESCRIPTOR SDDL_DEVOBJ_SYS_ALL
+#endif
+
+#define ST_DEVICE_NAME_STRING L"\\Device\\MULLVADSPLITTUNNEL"
+#define ST_SYMBOLIC_NAME_STRING L"\\Global??\\MULLVADSPLITTUNNEL"
+
+//
+// DriverEntry
+//
+// Creates a single device with associated symbolic link.
+// Does minimal initialization.
+//
+extern "C"
+NTSTATUS
+DriverEntry
+(
+ _In_ PDRIVER_OBJECT DriverObject,
+ _In_ PUNICODE_STRING RegistryPath
+)
+{
+ DbgPrint("Loading Mullvad split tunnel driver\n");
+
+ ExInitializeDriverRuntime(DrvRtPoolNxOptIn);
+
+ //
+ // Create WDF driver object.
+ //
+
+ WDF_DRIVER_CONFIG config;
+
+ WDF_DRIVER_CONFIG_INIT(&config, WDF_NO_EVENT_CALLBACK);
+
+ config.DriverInitFlags |= WdfDriverInitNonPnpDriver;
+ config.EvtDriverUnload = StEvtDriverUnload;
+ config.DriverPoolTag = ST_POOL_TAG;
+
+ WDFDRIVER wdfDriver;
+
+ auto status = WdfDriverCreate
+ (
+ DriverObject,
+ RegistryPath,
+ WDF_NO_OBJECT_ATTRIBUTES,
+ &config,
+ &wdfDriver
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfDriverCreate() failed 0x%X\n", status);
+ return status;
+ }
+
+ //
+ // Create WDF device object.
+ //
+
+ status = StCreateDevice(wdfDriver);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("StCreateDevice() failed 0x%X\n", status);
+ return status;
+ }
+
+ //
+ // All set.
+ //
+
+ DbgPrint("Successfully loaded Mullvad split tunnel driver\n");
+
+ return STATUS_SUCCESS;
+}
+
+extern "C"
+NTSTATUS
+StCreateDevice
+(
+ IN WDFDRIVER WdfDriver
+)
+{
+ DECLARE_CONST_UNICODE_STRING(deviceName, ST_DEVICE_NAME_STRING);
+ DECLARE_CONST_UNICODE_STRING(symbolicLinkName, ST_SYMBOLIC_NAME_STRING);
+
+ auto deviceInit = WdfControlDeviceInitAllocate
+ (
+ WdfDriver,
+ &ST_DEVICE_SECURITY_DESCRIPTOR
+ );
+
+ if (deviceInit == NULL)
+ {
+ DbgPrint("WdfControlDeviceInitAllocate() failed\n");
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ WdfDeviceInitSetExclusive(deviceInit, TRUE);
+
+ auto status = WdfDeviceInitAssignName(deviceInit, &deviceName);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfDeviceInitAssignName() failed 0x%X\n", status);
+ goto Cleanup;
+ }
+
+ //
+ // No need to call WdfDeviceInitSetIoType() that configures the I/O type for
+ // read and write requests.
+ //
+ // We're using IOCTL for everything, which have the I/O type encoded.
+ //
+ // ---
+ //
+ // No need to call WdfControlDeviceInitSetShutdownNotification() because
+ // we don't care about the system being shut down.
+ //
+ // ---
+ //
+ // No need to call WdfDeviceInitSetFileObjectConfig() because we're not
+ // interested in receiving events when device handles are created/destroyed.
+ //
+ // --
+ //
+ // No need to call WdfDeviceInitSetIoInCallerContextCallback() because
+ // we're not using METHOD_NEITHER for any buffers.
+ //
+
+ WDF_OBJECT_ATTRIBUTES attributes;
+
+ WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE
+ (
+ &attributes,
+ ST_DEVICE_CONTEXT
+ );
+
+ WDFDEVICE wdfDevice;
+
+ status = WdfDeviceCreate
+ (
+ &deviceInit,
+ &attributes,
+ &wdfDevice
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfDeviceCreate() failed 0x%X\n", status);
+ goto Cleanup;
+ }
+
+ status = WdfDeviceCreateSymbolicLink
+ (
+ wdfDevice,
+ &symbolicLinkName
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfDeviceCreateSymbolicLink() failed 0x%X\n", status);
+ goto Cleanup;
+ }
+
+ //
+ // Create a default request queue.
+ // Only register to handle IOCTL requests.
+ // Use WdfIoQueueDispatchParallel to enable inverted call.
+ //
+
+ WDF_IO_QUEUE_CONFIG queueConfig;
+
+ WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE
+ (
+ &queueConfig,
+ WdfIoQueueDispatchParallel
+ );
+
+ queueConfig.EvtIoDeviceControl = StEvtIoDeviceControl;
+ queueConfig.PowerManaged = WdfFalse;
+
+ WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
+ attributes.ExecutionLevel = WdfExecutionLevelPassive;
+
+ status = WdfIoQueueCreate
+ (
+ wdfDevice,
+ &queueConfig,
+ &attributes,
+ WDF_NO_HANDLE
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfIoQueueCreate() for default queue failed 0x%X\n", status);
+ goto Cleanup;
+ }
+
+ //
+ // Create a secondary queue that is serialized.
+ // Commands that need to be serialized can then be forwarded to this queue.
+ //
+
+ WDF_IO_QUEUE_CONFIG_INIT
+ (
+ &queueConfig,
+ WdfIoQueueDispatchSequential
+ );
+
+ queueConfig.EvtIoDeviceControl = StEvtIoDeviceControlSerial;
+ queueConfig.PowerManaged = WdfFalse;
+
+ WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
+ attributes.ExecutionLevel = WdfExecutionLevelPassive;
+
+ WDFQUEUE serialQueue;
+
+ status = WdfIoQueueCreate
+ (
+ wdfDevice,
+ &queueConfig,
+ &attributes,
+ &serialQueue
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfIoQueueCreate() for secondary queue failed 0x%X\n", status);
+ goto Cleanup;
+ }
+
+ //
+ // Initialize context.
+ //
+
+ auto context = DeviceGetSplitTunnelContext(wdfDevice);
+
+ RtlZeroMemory(context, sizeof(*context));
+
+ status = WdfWaitLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &context->DriverState.Lock);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfWaitLockCreate() failed 0x%X\n", status);
+ goto Cleanup;
+ }
+
+ context->DriverState.State = ST_DRIVER_STATE_STARTED;
+
+ context->IoCtlQueue = serialQueue;
+
+ WdfControlFinishInitializing(wdfDevice);
+
+ status = STATUS_SUCCESS;
+
+Cleanup:
+
+ if (deviceInit != NULL)
+ {
+ WdfDeviceInitFree(deviceInit);
+ }
+
+ return status;
+}
+
+VOID
+StEvtIoDeviceControl
+(
+ WDFQUEUE Queue,
+ WDFREQUEST Request,
+ size_t OutputBufferLength,
+ size_t InputBufferLength,
+ ULONG IoControlCode
+)
+{
+ UNREFERENCED_PARAMETER(OutputBufferLength);
+ UNREFERENCED_PARAMETER(InputBufferLength);
+
+ auto device = WdfIoQueueGetDevice(Queue);
+ auto context = DeviceGetSplitTunnelContext(device);
+
+ //
+ // Querying the current driver state is always a valid operation.
+ //
+
+ if (IoControlCode == IOCTL_ST_GET_STATE)
+ {
+ ioctl::GetStateComplete(device, Request);
+
+ return;
+ }
+
+ //
+ // Once the basic initialization is out of the way
+ // it's always valid for the client to attempt to dequeue an event.
+ //
+ // TODO: This approach is slightly broken.
+ //
+ // CollectOne() may enqueue the request in anticipation of an event arriving.
+ // That means the request completion may come at a later time when the asserted
+ // driver state has changed.
+ //
+ // But this probably doesn't matter.
+ //
+
+ if (IoControlCode == IOCTL_ST_DEQUEUE_EVENT)
+ {
+ WdfWaitLockAcquire(context->DriverState.Lock, NULL);
+
+ if (context->DriverState.State >= ST_DRIVER_STATE_INITIALIZED
+ && context->DriverState.State < ST_DRIVER_STATE_ZOMBIE)
+ {
+ eventing::CollectOne(context->Eventing, Request);
+ }
+ else
+ {
+ DbgPrint("Cannot dequeue event at current driver state\n");
+
+ WdfRequestComplete(Request, STATUS_INVALID_DEVICE_REQUEST);
+ }
+
+ WdfWaitLockRelease(context->DriverState.Lock);
+
+ return;
+ }
+
+ //
+ // Forward to serialized queue.
+ //
+
+ auto status = WdfRequestForwardToIoQueue(Request, context->IoCtlQueue);
+
+ if (NT_SUCCESS(status))
+ {
+ return;
+ }
+
+ DbgPrint("Failed to forward request to serialized queue\n");
+
+ WdfRequestComplete(Request, status);
+}
+
+VOID
+StEvtIoDeviceControlSerial
+(
+ WDFQUEUE Queue,
+ WDFREQUEST Request,
+ size_t OutputBufferLength,
+ size_t InputBufferLength,
+ ULONG IoControlCode
+)
+{
+ UNREFERENCED_PARAMETER(Queue);
+ UNREFERENCED_PARAMETER(OutputBufferLength);
+ UNREFERENCED_PARAMETER(InputBufferLength);
+
+ auto device = WdfIoQueueGetDevice(Queue);
+
+ if (IoControlCode == IOCTL_ST_RESET)
+ {
+ //
+ // Potential state transition here.
+ //
+ ioctl::ResetComplete(device, Request);
+
+ return;
+ }
+
+ auto context = DeviceGetSplitTunnelContext(device);
+
+ switch (context->DriverState.State)
+ {
+ case ST_DRIVER_STATE_STARTED:
+ {
+ //
+ // Valid controls:
+ //
+ // IOCTL_ST_INITIALIZE
+ //
+
+ if (IoControlCode == IOCTL_ST_INITIALIZE)
+ {
+ //
+ // Definitive state transition here.
+ // No locking needed this early.
+ //
+ WdfRequestComplete(Request, ioctl::Initialize(device));
+
+ return;
+ }
+
+ break;
+ }
+ case ST_DRIVER_STATE_INITIALIZED:
+ {
+ //
+ // Valid controls:
+ //
+ // IOCTL_ST_REGISTER_PROCESSES
+ //
+
+ if (IoControlCode == IOCTL_ST_REGISTER_PROCESSES)
+ {
+ //
+ // Definitive state transition here.
+ // No locking needed this early.
+ //
+ WdfRequestComplete(Request, ioctl::RegisterProcesses(device, Request));
+
+ return;
+ }
+
+ break;
+ }
+ case ST_DRIVER_STATE_READY:
+ case ST_DRIVER_STATE_ENGAGED:
+ {
+ //
+ // Valid controls:
+ //
+ // IOCTL_ST_REGISTER_IP_ADDRESSES
+ // IOCTL_ST_GET_IP_ADDRESSES
+ // IOCTL_ST_SET_CONFIGURATION
+ // IOCTL_ST_GET_CONFIGURATION
+ // IOCTL_ST_CLEAR_CONFIGURATION
+ // IOCTL_ST_QUERY_PROCESS
+ //
+
+ if (IoControlCode == IOCTL_ST_REGISTER_IP_ADDRESSES)
+ {
+ //
+ // Potential state transition here.
+ //
+ auto status = ioctl::RegisterIpAddresses(device, Request);
+
+ WdfRequestComplete(Request, status);
+
+ return;
+ }
+
+ if (IoControlCode == IOCTL_ST_GET_IP_ADDRESSES)
+ {
+ ioctl::GetIpAddressesComplete(device, Request);
+
+ return;
+ }
+
+ if (IoControlCode == IOCTL_ST_SET_CONFIGURATION)
+ {
+ registeredimage::CONTEXT *imageset;
+
+ auto status = ioctl::SetConfigurationPrepare(Request, &imageset);
+
+ if (!NT_SUCCESS(status))
+ {
+ WdfRequestComplete(Request, status);
+
+ return;
+ }
+
+ //
+ // Potential state transition here.
+ //
+ status = ioctl::SetConfiguration(device, imageset);
+
+ WdfRequestComplete(Request, status);
+
+ return;
+ }
+
+ if (IoControlCode == IOCTL_ST_GET_CONFIGURATION)
+ {
+ ioctl::GetConfigurationComplete(device, Request);
+
+ return;
+ }
+
+ if (IoControlCode == IOCTL_ST_CLEAR_CONFIGURATION)
+ {
+ //
+ // Potential state transition here.
+ //
+ auto status = ioctl::ClearConfiguration(device);
+
+ WdfRequestComplete(Request, status);
+
+ return;
+ }
+
+ if (IoControlCode == IOCTL_ST_QUERY_PROCESS)
+ {
+ ioctl::QueryProcessComplete(device, Request);
+
+ return;
+ }
+
+ break;
+ }
+ case ST_DRIVER_STATE_ZOMBIE:
+ {
+ DbgPrint("Zombie state: Rejecting all requests\n");
+
+ WdfRequestComplete(Request, STATUS_CANCELLED);
+
+ return;
+ }
+ }
+
+ DbgPrint("Invalid IOCTL or not valid for current driver state\n");
+
+ WdfRequestComplete(Request, STATUS_INVALID_DEVICE_REQUEST);
+}
+
+VOID
+StEvtDriverUnload
+(
+ IN WDFDRIVER WdfDriver
+)
+{
+ UNREFERENCED_PARAMETER(WdfDriver);
+
+ DbgPrint("Unloading Mullvad split tunnel driver\n");
+}
diff --git a/src/eventing/builder.cpp b/src/eventing/builder.cpp
new file mode 100644
index 0000000..55564c5
--- /dev/null
+++ b/src/eventing/builder.cpp
@@ -0,0 +1,213 @@
+#include "builder.h"
+
+namespace eventing
+{
+
+namespace
+{
+
+bool
+BuildSplittingEvent
+(
+ HANDLE ProcessId,
+ ST_SPLITTING_STATUS_CHANGE_REASON Reason,
+ LOWER_UNICODE_STRING *ImageName,
+ bool Start,
+ void **Buffer,
+ size_t *BufferSize
+)
+{
+ auto headerSize = FIELD_OFFSET(ST_EVENT_HEADER, EventData);
+ auto eventSize = FIELD_OFFSET(ST_SPLITTING_EVENT, ImageName) + ImageName->Length;
+ auto allocationSize = headerSize + eventSize;
+
+ auto buffer = ExAllocatePoolWithTag(NonPagedPool, allocationSize, ST_POOL_TAG);
+
+ if (buffer == NULL)
+ {
+ return false;
+ }
+
+ auto header = (ST_EVENT_HEADER*)buffer;
+ auto evt = (ST_SPLITTING_EVENT*)(((UCHAR*)buffer) + FIELD_OFFSET(ST_EVENT_HEADER, EventData));
+
+ header->EventId = (Start ? ST_EVENT_START_SPLITTING_PROCESS : ST_EVENT_STOP_SPLITTING_PROCESS);
+ header->EventSize = eventSize;
+
+ evt->ProcessId = ProcessId;
+ evt->Reason = Reason;
+ evt->ImageNameLength = ImageName->Length;
+
+ RtlCopyMemory(evt->ImageName, ImageName->Buffer, ImageName->Length);
+
+ *Buffer = buffer;
+ *BufferSize = allocationSize;
+
+ return true;
+}
+
+bool
+BuildSplittingErrorEvent
+(
+ HANDLE ProcessId,
+ LOWER_UNICODE_STRING *ImageName,
+ bool Start,
+ void **Buffer,
+ size_t *BufferSize
+)
+{
+ auto headerSize = FIELD_OFFSET(ST_EVENT_HEADER, EventData);
+ auto eventSize = FIELD_OFFSET(ST_SPLITTING_ERROR_EVENT, ImageName) + ImageName->Length;
+ auto allocationSize = headerSize + eventSize;
+
+ auto buffer = ExAllocatePoolWithTag(NonPagedPool, allocationSize, ST_POOL_TAG);
+
+ if (buffer == NULL)
+ {
+ return false;
+ }
+
+ auto header = (ST_EVENT_HEADER*)buffer;
+ auto evt = (ST_SPLITTING_ERROR_EVENT*)(((UCHAR*)buffer) + FIELD_OFFSET(ST_EVENT_HEADER, EventData));
+
+ header->EventId = (Start ? ST_EVENT_ERROR_START_SPLITTING_PROCESS : ST_EVENT_ERROR_STOP_SPLITTING_PROCESS);
+ header->EventSize = eventSize;
+
+ evt->ProcessId = ProcessId;
+ evt->ImageNameLength = ImageName->Length;
+
+ RtlCopyMemory(evt->ImageName, ImageName->Buffer, ImageName->Length);
+
+ *Buffer = buffer;
+ *BufferSize = allocationSize;
+
+ return true;
+}
+
+RAW_EVENT*
+WrapEvent
+(
+ void *Buffer,
+ size_t BufferSize
+)
+{
+ auto evt = (RAW_EVENT*)ExAllocatePoolWithTag(NonPagedPool, sizeof(RAW_EVENT), ST_POOL_TAG);
+
+ if (evt == NULL)
+ {
+ ExFreePoolWithTag(Buffer, ST_POOL_TAG);
+
+ return NULL;
+ }
+
+ evt->SListEntry.Next = NULL;
+ evt->Buffer = Buffer;
+ evt->BufferSize = BufferSize;
+
+ return evt;
+}
+
+} // anonymous namespace
+
+RAW_EVENT*
+BuildStartSplittingEvent
+(
+ HANDLE ProcessId,
+ ST_SPLITTING_STATUS_CHANGE_REASON Reason,
+ LOWER_UNICODE_STRING *ImageName
+)
+{
+ void *buffer;
+ size_t bufferSize;
+
+ auto status = BuildSplittingEvent(ProcessId, Reason, ImageName, true, &buffer, &bufferSize);
+
+ if (!status)
+ {
+ return NULL;
+ }
+
+ return WrapEvent(buffer, bufferSize);
+}
+
+RAW_EVENT*
+BuildStopSplittingEvent
+(
+ HANDLE ProcessId,
+ ST_SPLITTING_STATUS_CHANGE_REASON Reason,
+ LOWER_UNICODE_STRING *ImageName
+)
+{
+ void *buffer;
+ size_t bufferSize;
+
+ auto status = BuildSplittingEvent(ProcessId, Reason, ImageName, false, &buffer, &bufferSize);
+
+ if (!status)
+ {
+ return NULL;
+ }
+
+ return WrapEvent(buffer, bufferSize);
+}
+
+RAW_EVENT*
+BuildStartSplittingErrorEvent
+(
+ HANDLE ProcessId,
+ LOWER_UNICODE_STRING *ImageName
+)
+{
+ void *buffer;
+ size_t bufferSize;
+
+ auto status = BuildSplittingErrorEvent(ProcessId, ImageName, false, &buffer, &bufferSize);
+
+ if (!status)
+ {
+ return NULL;
+ }
+
+ return WrapEvent(buffer, bufferSize);
+}
+
+RAW_EVENT*
+BuildStopSplittingErrorEvent
+(
+ HANDLE ProcessId,
+ LOWER_UNICODE_STRING *ImageName
+)
+{
+ void *buffer;
+ size_t bufferSize;
+
+ auto status = BuildSplittingErrorEvent(ProcessId, ImageName, false, &buffer, &bufferSize);
+
+ if (!status)
+ {
+ return NULL;
+ }
+
+ return WrapEvent(buffer, bufferSize);
+}
+
+void
+ReleaseEvent
+(
+ RAW_EVENT **Event
+)
+{
+ auto evt = *Event;
+
+ if (evt == NULL)
+ {
+ return;
+ }
+
+ *Event = NULL;
+
+ ExFreePoolWithTag(evt->Buffer, ST_POOL_TAG);
+ ExFreePoolWithTag(evt, ST_POOL_TAG);
+}
+
+} // namespace eventing
diff --git a/src/eventing/builder.h b/src/eventing/builder.h
new file mode 100644
index 0000000..a3110e6
--- /dev/null
+++ b/src/eventing/builder.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include
+#include "../defs/events.h"
+#include "../defs/types.h"
+#include "eventing.h"
+
+namespace eventing
+{
+
+RAW_EVENT*
+BuildStartSplittingEvent
+(
+ HANDLE ProcessId,
+ ST_SPLITTING_STATUS_CHANGE_REASON Reason,
+ LOWER_UNICODE_STRING *ImageName
+);
+
+RAW_EVENT*
+BuildStopSplittingEvent
+(
+ HANDLE ProcessId,
+ ST_SPLITTING_STATUS_CHANGE_REASON Reason,
+ LOWER_UNICODE_STRING *ImageName
+);
+
+RAW_EVENT*
+BuildStartSplittingErrorEvent
+(
+ HANDLE ProcessId,
+ LOWER_UNICODE_STRING *ImageName
+);
+
+RAW_EVENT*
+BuildStopSplittingErrorEvent
+(
+ HANDLE ProcessId,
+ LOWER_UNICODE_STRING *ImageName
+);
+
+void
+ReleaseEvent
+(
+ RAW_EVENT **Event
+);
+
+} // namespace eventing
diff --git a/src/eventing/context.h b/src/eventing/context.h
new file mode 100644
index 0000000..a9ccb3c
--- /dev/null
+++ b/src/eventing/context.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include
+#include
+
+namespace eventing
+{
+
+struct CONTEXT
+{
+ // Pended IOCTL requests for inverted call.
+ WDFQUEUE RequestQueue;
+
+ KSPIN_LOCK EventQueueLock;
+
+ SLIST_HEADER EventQueue;
+};
+
+} // namespace eventing
diff --git a/src/eventing/eventing.cpp b/src/eventing/eventing.cpp
new file mode 100644
index 0000000..6935642
--- /dev/null
+++ b/src/eventing/eventing.cpp
@@ -0,0 +1,239 @@
+#include "eventing.h"
+#include "context.h"
+#include "builder.h"
+#include "../defs/types.h"
+
+namespace eventing
+{
+
+namespace
+{
+
+void CompleteRequestReleaseEvent
+(
+ WDFREQUEST Request,
+ void *RequestBuffer,
+ RAW_EVENT *Event
+)
+{
+ RtlCopyMemory(RequestBuffer, Event->Buffer, Event->BufferSize);
+
+ WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, Event->BufferSize);
+
+ ReleaseEvent(&Event);
+}
+
+} // anonymous namespace
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ WDFDEVICE Device
+)
+{
+ *Context = NULL;
+
+ auto context = (CONTEXT*)ExAllocatePoolWithTag(NonPagedPool, sizeof(CONTEXT), ST_POOL_TAG);
+
+ if (NULL == context)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ RtlZeroMemory(context, sizeof(*context));
+
+ InitializeSListHead(&context->EventQueue);
+ KeInitializeSpinLock(&context->EventQueueLock);
+
+ WDF_IO_QUEUE_CONFIG queueConfig;
+
+ WDF_IO_QUEUE_CONFIG_INIT
+ (
+ &queueConfig,
+ WdfIoQueueDispatchManual
+ );
+
+ queueConfig.PowerManaged = WdfFalse;
+
+ auto status = WdfIoQueueCreate
+ (
+ Device,
+ &queueConfig,
+ WDF_NO_OBJECT_ATTRIBUTES,
+ &context->RequestQueue
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfIoQueueCreate() failed 0x%X\n", status);
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+
+ return status;
+ }
+
+ *Context = context;
+
+ return STATUS_SUCCESS;
+}
+
+void
+TearDown
+(
+ CONTEXT **Context
+)
+{
+ auto context = *Context;
+
+ RAW_EVENT *evt = NULL;
+
+ //
+ // Discard and release all queued events.
+ //
+
+ while (NULL != (evt = (RAW_EVENT*)ExInterlockedPopEntrySList(&context->EventQueue, &context->EventQueueLock)))
+ {
+ ReleaseEvent(&evt);
+ }
+
+ //
+ // Cancel all queued requests.
+ //
+
+ WDFREQUEST pendedRequest;
+
+ for (;;)
+ {
+ auto status = WdfIoQueueRetrieveNextRequest(context->RequestQueue, &pendedRequest);
+
+ if (!NT_SUCCESS(status) || pendedRequest == NULL)
+ {
+ break;
+ }
+
+ WdfRequestComplete(pendedRequest, STATUS_CANCELLED);
+ }
+
+ WdfObjectDelete(context->RequestQueue);
+
+ //
+ // Release context.
+ //
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+
+ *Context = NULL;
+}
+
+void
+Emit
+(
+ CONTEXT *Context,
+ RAW_EVENT **Event
+)
+{
+ auto *evt = *Event;
+
+ if (evt == NULL)
+ {
+ return;
+ }
+
+ *Event = NULL;
+
+ WDFREQUEST pendedRequest;
+
+ void *buffer;
+
+ //
+ // Look for a pended request with a correctly sized buffer.
+ //
+ // Fail all requests we encounter that have tiny buffers.
+ // User mode should know better.
+ //
+
+ for (;;)
+ {
+ auto status = WdfIoQueueRetrieveNextRequest(Context->RequestQueue, &pendedRequest);
+
+ if (!NT_SUCCESS(status) || pendedRequest == NULL)
+ {
+ ExInterlockedPushEntrySList(&Context->EventQueue, &evt->SListEntry, &Context->EventQueueLock);
+
+ return;
+ }
+
+ status = WdfRequestRetrieveOutputBuffer
+ (
+ pendedRequest,
+ evt->BufferSize,
+ &buffer,
+ NULL
+ );
+
+ if (NT_SUCCESS(status))
+ {
+ break;
+ }
+
+ WdfRequestComplete(pendedRequest, status);
+ }
+
+ CompleteRequestReleaseEvent(pendedRequest, buffer, evt);
+}
+
+void
+CollectOne
+(
+ CONTEXT *Context,
+ WDFREQUEST Request
+)
+{
+ auto evt = (RAW_EVENT*)ExInterlockedPopEntrySList(&Context->EventQueue, &Context->EventQueueLock);
+
+ if (evt == NULL)
+ {
+ auto status = WdfRequestForwardToIoQueue(Request, Context->RequestQueue);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Failed to pend event request\n");
+
+ WdfRequestComplete(Request, STATUS_INTERNAL_ERROR);
+ }
+
+ return;
+ }
+
+ //
+ // Acquire and validate request buffer.
+ //
+
+ void *buffer;
+
+ auto status = WdfRequestRetrieveOutputBuffer
+ (
+ Request,
+ evt->BufferSize,
+ &buffer,
+ NULL
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ WdfRequestComplete(Request, status);
+
+ //
+ // Put the event back.
+ //
+
+ ExInterlockedPushEntrySList(&Context->EventQueue, &evt->SListEntry, &Context->EventQueueLock);
+
+ return;
+ }
+
+ CompleteRequestReleaseEvent(Request, buffer, evt);
+}
+
+} // eventing
diff --git a/src/eventing/eventing.h b/src/eventing/eventing.h
new file mode 100644
index 0000000..3a72321
--- /dev/null
+++ b/src/eventing/eventing.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include
+#include
+
+namespace eventing
+{
+
+struct CONTEXT;
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ WDFDEVICE Device
+);
+
+void
+TearDown
+(
+ CONTEXT **Context
+);
+
+struct RAW_EVENT
+{
+ SLIST_ENTRY SListEntry;
+
+ size_t BufferSize;
+
+ void *Buffer;
+};
+
+//
+// Emit()
+//
+// Takes ownership of passed event.
+//
+// If possible, sends the event to user mode immediately.
+// Otherwise queues the event for later dispatching.
+//
+void
+Emit
+(
+ CONTEXT *Context,
+ RAW_EVENT **Evt
+);
+
+//
+// CollectOne()
+//
+// Collects a single event and completes the request.
+// Or pends the request if there are no queued events.
+//
+void
+CollectOne
+(
+ CONTEXT *Context,
+ WDFREQUEST Request
+);
+
+} // namespace eventing
diff --git a/src/firewall/asyncbind.cpp b/src/firewall/asyncbind.cpp
new file mode 100644
index 0000000..b1a0acd
--- /dev/null
+++ b/src/firewall/asyncbind.cpp
@@ -0,0 +1,310 @@
+#include "asyncbind.h"
+#include "../util.h"
+
+namespace firewall
+{
+
+namespace
+{
+
+const ULONGLONG RECORD_MAX_LIFETIME_MS = 10000;
+
+bool
+FailBindRequest
+(
+ HANDLE ProcessId,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut,
+ UINT64 ClassifyHandle,
+ UINT64 FilterId,
+ bool Ipv4
+)
+{
+ DbgPrint("Failing bind request from process %p\n", ProcessId);
+
+ //
+ // There doesn't seem to be any support in WFP for blocking a bind request.
+ // Specifying `FWP_ACTION_BLOCK` will just resume request processing.
+ // So the best we can do is rewrite the bind to do as little harm as possible.
+ //
+
+ FWPS_BIND_REQUEST0 *bindRequest = NULL;
+
+ auto status = FwpsAcquireWritableLayerDataPointer0
+ (
+ ClassifyHandle,
+ FilterId,
+ 0,
+ (PVOID*)&bindRequest,
+ ClassifyOut
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("FwpsAcquireWritableLayerDataPointer0() failed 0x%X\n", status);
+
+ return false;
+ }
+
+ ClassifyOut->actionType = FWP_ACTION_PERMIT;
+ ClassifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE;
+
+ if (Ipv4)
+ {
+ auto bindTarget = (SOCKADDR_IN*)&(bindRequest->localAddressAndPort);
+
+ IN_ADDR localhost;
+
+ localhost.S_un.S_un_b.s_b1 = 127;
+ localhost.S_un.S_un_b.s_b2 = 0;
+ localhost.S_un.S_un_b.s_b3 = 0;
+ localhost.S_un.S_un_b.s_b4 = 1;
+
+ bindTarget->sin_addr = localhost;
+ }
+ else
+ {
+ auto bindTarget = (SOCKADDR_IN6*)&(bindRequest->localAddressAndPort);
+
+ IN6_ADDR localhost;
+
+ localhost.u.Word[0] = 0;
+ localhost.u.Word[1] = 0;
+ localhost.u.Word[2] = 0;
+ localhost.u.Word[3] = 0;
+ localhost.u.Word[4] = 0;
+ localhost.u.Word[5] = 0;
+ localhost.u.Word[6] = 0;
+ localhost.u.Word[7] = htons(USHORT(1));
+
+ bindTarget->sin6_addr = localhost;
+ }
+
+ FwpsApplyModifiedLayerData0(ClassifyHandle, (PVOID*)&bindRequest, 0);
+
+ return true;
+}
+
+void
+ReauthPendedBindRequest
+(
+ PENDED_BIND *Record
+)
+{
+ DbgPrint("Requesting re-auth for bind request from process %p\n", Record->ProcessId);
+
+ FwpsCompleteClassify0(Record->ClassifyHandle, 0, NULL);
+ FwpsReleaseClassifyHandle0(Record->ClassifyHandle);
+
+ ExFreePoolWithTag(Record, ST_POOL_TAG);
+}
+
+void
+FailPendedBindRequest
+(
+ PENDED_BIND *Record
+)
+{
+ const auto status = FailBindRequest(Record->ProcessId, &Record->ClassifyOut,
+ Record->ClassifyHandle, Record->FilterId, Record->Ipv4);
+
+ if (!status)
+ {
+ //
+ // At this point there are basically two options:
+ //
+ // #1 Leak the bind request to prevent it from successfully binding to the tunnel interface.
+ // #2 Request a re-auth of the bind request.
+ //
+ // We choose to implement #2 in order to retry the processing.
+ //
+
+ ReauthPendedBindRequest(Record);
+
+ return;
+ }
+
+ FwpsCompleteClassify0(Record->ClassifyHandle, 0, &Record->ClassifyOut);
+ FwpsReleaseClassifyHandle0(Record->ClassifyHandle);
+
+ ExFreePoolWithTag(Record, ST_POOL_TAG);
+}
+
+} // anonymous namespace
+
+NTSTATUS
+PendBindRequest
+(
+ CONTEXT *Context,
+ HANDLE ProcessId,
+ void *ClassifyContext,
+ UINT64 FilterId,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut,
+ bool Ipv4
+)
+{
+ DbgPrint("Pending bind request from process %p\n", ProcessId);
+
+ auto record = (PENDED_BIND*)
+ ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDED_BIND), ST_POOL_TAG);
+
+ if (record == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ UINT64 classifyHandle;
+
+ auto status = FwpsAcquireClassifyHandle0(ClassifyContext, 0, &classifyHandle);
+
+ if (!NT_SUCCESS(status))
+ {
+ ExFreePoolWithTag(record, ST_POOL_TAG);
+
+ return status;
+ }
+
+ status = FwpsPendClassify0(classifyHandle, FilterId, 0, ClassifyOut);
+
+ if (!NT_SUCCESS(status))
+ {
+ FwpsReleaseClassifyHandle0(classifyHandle);
+
+ ExFreePoolWithTag(record, ST_POOL_TAG);
+
+ return status;
+ }
+
+ record->ProcessId = ProcessId;
+ record->Timestamp = KeQueryInterruptTime();
+ record->ClassifyHandle = classifyHandle;
+ record->ClassifyOut = *ClassifyOut;
+ record->FilterId = FilterId;
+ record->Ipv4 = Ipv4;
+
+ WdfWaitLockAcquire(Context->PendedBinds.Lock, NULL);
+
+ InsertTailList(&Context->PendedBinds.Records, &record->ListEntry);
+
+ WdfWaitLockRelease(Context->PendedBinds.Lock);
+
+ return STATUS_SUCCESS;
+}
+
+void
+FailBindRequest
+(
+ HANDLE ProcessId,
+ void *ClassifyContext,
+ UINT64 FilterId,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut,
+ bool Ipv4
+)
+{
+ UINT64 classifyHandle = 0;
+
+ auto status = FwpsAcquireClassifyHandle0
+ (
+ ClassifyContext,
+ 0,
+ &classifyHandle
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("FwpsAcquireClassifyHandle0() failed 0x%X\n", status);
+
+ return;
+ }
+
+ FailBindRequest(ProcessId, ClassifyOut, classifyHandle, FilterId, Ipv4);
+
+ FwpsReleaseClassifyHandle0(classifyHandle);
+}
+
+void
+HandleProcessEvent
+(
+ HANDLE ProcessId,
+ bool Arriving,
+ void *Context
+)
+{
+ auto context = (CONTEXT*)Context;
+
+ auto timeNow = KeQueryInterruptTime();
+
+ static const ULONGLONG MS_TO_100NS_FACTOR = 10000;
+
+ auto maxAge = RECORD_MAX_LIFETIME_MS * MS_TO_100NS_FACTOR;
+
+ //
+ // Iterate over all pended bind requests.
+ //
+ // Fail all requests that are too old.
+ // Re-auth all requests that belong to the arriving process.
+ //
+
+ WdfWaitLockAcquire(context->PendedBinds.Lock, NULL);
+
+ for (auto rawRecord = context->PendedBinds.Records.Flink;
+ rawRecord != &context->PendedBinds.Records;
+ /* no post-condition */)
+ {
+ auto record = (PENDED_BIND*)rawRecord;
+
+ rawRecord = rawRecord->Flink;
+
+ auto timeDelta = timeNow - record->Timestamp;
+
+ if (timeDelta > maxAge)
+ {
+ RemoveEntryList(&record->ListEntry);
+
+ FailPendedBindRequest(record);
+
+ continue;
+ }
+
+ if (record->ProcessId != ProcessId)
+ {
+ continue;
+ }
+
+ RemoveEntryList(&record->ListEntry);
+
+ if (Arriving)
+ {
+ ReauthPendedBindRequest(record);
+ }
+ else
+ {
+ FailPendedBindRequest(record);
+ }
+ }
+
+ WdfWaitLockRelease(context->PendedBinds.Lock);
+}
+
+void
+FailPendedBinds
+(
+ CONTEXT *Context
+)
+{
+ auto context = (CONTEXT*)Context;
+
+ for (auto rawRecord = context->PendedBinds.Records.Flink;
+ rawRecord != &context->PendedBinds.Records;
+ /* no post-condition */)
+ {
+ auto record = (PENDED_BIND*)rawRecord;
+
+ rawRecord = rawRecord->Flink;
+
+ RemoveEntryList(&record->ListEntry);
+
+ FailPendedBindRequest(record);
+ }
+}
+
+} // namespace firewall
diff --git a/src/firewall/asyncbind.h b/src/firewall/asyncbind.h
new file mode 100644
index 0000000..a6ce3e3
--- /dev/null
+++ b/src/firewall/asyncbind.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "wfp.h"
+#include
+#include "context.h"
+
+namespace firewall
+{
+
+NTSTATUS
+PendBindRequest
+(
+ CONTEXT *Context,
+ HANDLE ProcessId,
+ void *ClassifyContext,
+ UINT64 FilterId,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut,
+ bool Ipv4
+);
+
+void
+FailBindRequest
+(
+ HANDLE ProcessId,
+ void *ClassifyContext,
+ UINT64 FilterId,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut,
+ bool Ipv4
+);
+
+void
+HandleProcessEvent
+(
+ HANDLE ProcessId,
+ bool Arriving,
+ void *Context
+);
+
+void
+FailPendedBinds
+(
+ CONTEXT *Context
+);
+
+} // namespace firewall
diff --git a/src/firewall/blocking.cpp b/src/firewall/blocking.cpp
new file mode 100644
index 0000000..c805f34
--- /dev/null
+++ b/src/firewall/blocking.cpp
@@ -0,0 +1,1254 @@
+#include "wfp.h"
+#include "identifiers.h"
+#include "constants.h"
+#include "../defs/types.h"
+#include "../util.h"
+#include "blocking.h"
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// This module register filters that block tunnel traffic. This is done to
+// ensure an application's existing connections are blocked when they
+// start being split.
+//
+// When filters are added, a re-auth occurs, and matching existing connections
+// are presented to the linked callout, to approve or block.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+namespace firewall::blocking
+{
+
+namespace
+{
+
+typedef struct tag_BLOCK_CONNECTIONS_ENTRY
+{
+ LIST_ENTRY ListEntry;
+
+ //
+ // Device path using all lower-case characters.
+ //
+ LOWER_UNICODE_STRING ImageName;
+
+ //
+ // Number of process instances that use this entry.
+ //
+ SIZE_T RefCount;
+
+ //
+ // WFP filter IDs.
+ //
+ UINT64 OutboundFilterIdV4;
+ UINT64 InboundFilterIdV4;
+ UINT64 OutboundFilterIdV6;
+ UINT64 InboundFilterIdV6;
+}
+BLOCK_CONNECTIONS_ENTRY;
+
+typedef struct tag_STATE_DATA
+{
+ HANDLE WfpSession;
+
+ LIST_ENTRY BlockedTunnelConnections;
+
+ LIST_ENTRY TransactionEvents;
+}
+STATE_DATA;
+
+//
+// Transaction events represent logically atomic operations on the list of
+// block-connection entries.
+//
+// The most recent event is represented by the transaction event record at the head of
+// the transaction record list.
+//
+// An individual event record states what action needs to be taken
+// to undo a change to the block-connection list.
+//
+enum class TRANSACTION_EVENT_TYPE
+{
+ INCREMENT_REF_COUNT,
+ DECREMENT_REF_COUNT,
+ ADD_ENTRY,
+ REMOVE_ENTRY,
+ SWAP_LISTS
+};
+
+typedef struct tag_TRANSACTION_EVENT
+{
+ LIST_ENTRY ListEntry;
+ TRANSACTION_EVENT_TYPE EventType;
+ BLOCK_CONNECTIONS_ENTRY *Target;
+}
+TRANSACTION_EVENT;
+
+typedef struct tag_TRANSACTION_EVENT_ADD_ENTRY
+{
+ LIST_ENTRY ListEntry;
+ TRANSACTION_EVENT_TYPE EventType;
+ BLOCK_CONNECTIONS_ENTRY *Target;
+
+ //
+ // This may or may not be the real list head.
+ // We insert to the right of it.
+ //
+ LIST_ENTRY *MockHead;
+}
+TRANSACTION_EVENT_ADD_ENTRY;
+
+typedef struct tag_TRANSACTION_EVENT_SWAP_LISTS
+{
+ LIST_ENTRY ListEntry;
+ TRANSACTION_EVENT_TYPE EventType;
+
+ //
+ // This is the list head of the previous list.
+ //
+ LIST_ENTRY BlockedTunnelConnections;
+}
+TRANSACTION_EVENT_SWAP_LISTS;
+
+NTSTATUS
+PushTransactionEvent
+(
+ LIST_ENTRY *TransactionEvents,
+ TRANSACTION_EVENT_TYPE EventType,
+ BLOCK_CONNECTIONS_ENTRY *Target
+)
+{
+ auto evt = (TRANSACTION_EVENT*)ExAllocatePoolWithTag(NonPagedPool, sizeof(TRANSACTION_EVENT), ST_POOL_TAG);
+
+ if (evt == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ InitializeListHead((LIST_ENTRY*)evt);
+
+ evt->EventType = EventType;
+ evt->Target = Target;
+
+ InsertHeadList(TransactionEvents, (LIST_ENTRY*)evt);
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+TransactionIncrementedRefCount
+(
+ LIST_ENTRY *TransactionEvents,
+ BLOCK_CONNECTIONS_ENTRY *Target
+)
+{
+ return PushTransactionEvent
+ (
+ TransactionEvents,
+ TRANSACTION_EVENT_TYPE::DECREMENT_REF_COUNT,
+ Target
+ );
+}
+
+NTSTATUS
+TransactionDecrementedRefCount
+(
+ LIST_ENTRY *TransactionEvents,
+ BLOCK_CONNECTIONS_ENTRY *Target
+)
+{
+ return PushTransactionEvent
+ (
+ TransactionEvents,
+ TRANSACTION_EVENT_TYPE::INCREMENT_REF_COUNT,
+ Target
+ );
+}
+
+NTSTATUS
+TransactionAddedEntry
+(
+ LIST_ENTRY *TransactionEvents,
+ BLOCK_CONNECTIONS_ENTRY *Target
+)
+{
+ return PushTransactionEvent
+ (
+ TransactionEvents,
+ TRANSACTION_EVENT_TYPE::REMOVE_ENTRY,
+ Target
+ );
+}
+
+NTSTATUS
+TransactionRemovedEntry
+(
+ LIST_ENTRY *TransactionEvents,
+ BLOCK_CONNECTIONS_ENTRY *Target,
+ LIST_ENTRY *MockHead
+)
+{
+ auto evt = (TRANSACTION_EVENT_ADD_ENTRY*)
+ ExAllocatePoolWithTag(NonPagedPool, sizeof(TRANSACTION_EVENT_ADD_ENTRY), ST_POOL_TAG);
+
+ if (evt == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ InitializeListHead((LIST_ENTRY*)evt);
+
+ evt->EventType = TRANSACTION_EVENT_TYPE::ADD_ENTRY;
+ evt->Target = Target;
+ evt->MockHead = MockHead;
+
+ InsertHeadList(TransactionEvents, (LIST_ENTRY*)evt);
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+TransactionSwappedLists
+(
+ LIST_ENTRY *TransactionEvents,
+ LIST_ENTRY *BlockedTunnelConnections
+)
+{
+ auto evt = (TRANSACTION_EVENT_SWAP_LISTS*)
+ ExAllocatePoolWithTag(NonPagedPool, sizeof(TRANSACTION_EVENT_SWAP_LISTS), ST_POOL_TAG);
+
+ if (evt == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ InitializeListHead((LIST_ENTRY*)evt);
+
+ evt->EventType = TRANSACTION_EVENT_TYPE::SWAP_LISTS;
+
+ //
+ // Ownership of list is moved to transaction entry.
+ //
+
+ util::ReparentList(&evt->BlockedTunnelConnections, BlockedTunnelConnections);
+
+ InsertHeadList(TransactionEvents, (LIST_ENTRY*)evt);
+
+ return STATUS_SUCCESS;
+}
+
+//
+// CustomGetAppIdFromFileName()
+//
+// The API FwpmGetAppIdFromFileName() is not exposed in kernel mode, but we
+// don't need it. All it does is look up the device path which we already have.
+//
+// However, for some reason the string also has to be null-terminated.
+//
+NTSTATUS
+CustomGetAppIdFromFileName
+(
+ const LOWER_UNICODE_STRING *ImageName,
+ FWP_BYTE_BLOB **AppId
+)
+{
+ auto offsetStringBuffer =
+ util::RoundToMultiple(sizeof(FWP_BYTE_BLOB), TYPE_ALIGNMENT(WCHAR));
+
+ UINT32 copiedStringLength = ImageName->Length + sizeof(WCHAR);
+
+ auto allocationSize = offsetStringBuffer + copiedStringLength;
+
+ auto blob = (FWP_BYTE_BLOB*)
+ ExAllocatePoolWithTag(PagedPool, allocationSize, ST_POOL_TAG);
+
+ if (blob == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ auto stringBuffer = ((UINT8*)blob) + offsetStringBuffer;
+
+ RtlCopyMemory(stringBuffer, ImageName->Buffer, ImageName->Length);
+
+ stringBuffer[copiedStringLength - 2] = 0;
+ stringBuffer[copiedStringLength - 1] = 0;
+
+ blob->size = copiedStringLength;
+ blob->data = stringBuffer;
+
+ *AppId = blob;
+
+ return STATUS_SUCCESS;
+}
+
+//
+// FindBlockConnectionsEntry()
+//
+// Returns pointer to matching entry or NULL.
+//
+BLOCK_CONNECTIONS_ENTRY*
+FindBlockConnectionsEntry
+(
+ LIST_ENTRY *List,
+ const LOWER_UNICODE_STRING *ImageName
+)
+{
+ for (auto entry = List->Flink;
+ entry != List;
+ entry = entry->Flink)
+ {
+ auto candidate = (BLOCK_CONNECTIONS_ENTRY*)entry;
+
+ if (candidate->ImageName.Length != ImageName->Length)
+ {
+ continue;
+ }
+
+ const auto equalBytes = RtlCompareMemory
+ (
+ candidate->ImageName.Buffer,
+ ImageName->Buffer,
+ ImageName->Length
+ );
+
+ if (equalBytes == ImageName->Length)
+ {
+ return candidate;
+ }
+ }
+
+ return NULL;
+}
+
+NTSTATUS
+AddTunnelBlockFiltersTx
+(
+ HANDLE WfpSession,
+ const LOWER_UNICODE_STRING *ImageName,
+ const IN_ADDR *TunnelIpv4,
+ const IN6_ADDR *TunnelIpv6,
+ UINT64 *OutboundFilterIdV4,
+ UINT64 *InboundFilterIdV4,
+ UINT64 *OutboundFilterIdV6,
+ UINT64 *InboundFilterIdV6
+)
+{
+ //
+ // Format APP_ID payload that will be used with all filters.
+ //
+
+ FWP_BYTE_BLOB *appIdPayload;
+
+ auto status = CustomGetAppIdFromFileName(ImageName, &appIdPayload);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ //
+ // Register outbound IPv4 filter.
+ //
+
+ FWPM_FILTER0 filter = { 0 };
+
+ const auto FilterNameOutboundIpv4 = L"Mullvad Split Tunnel In-Tunnel Blocking Filter (Outbound IPv4)";
+ const auto FilterDescription = L"Blocks existing connections in the tunnel";
+
+ filter.displayData.name = const_cast(FilterNameOutboundIpv4);
+ filter.displayData.description = const_cast(FilterDescription);
+ filter.flags = FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT | FWPM_FILTER_FLAG_HAS_PROVIDER_CONTEXT;
+ filter.providerKey = const_cast(&ST_FW_PROVIDER_KEY);
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
+ filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
+ filter.weight.type = FWP_UINT64;
+ filter.weight.uint64 = const_cast(&ST_MAX_FILTER_WEIGHT);
+ filter.action.type = FWP_ACTION_CALLOUT_UNKNOWN;
+ filter.action.calloutKey = ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV4_CONN_KEY;
+ filter.providerContextKey = ST_FW_PROVIDER_CONTEXT_KEY;
+
+ //
+ // Conditions are:
+ //
+ // APP_ID == ImageName
+ // LOCAL_ADDRESS == TunnelIp
+ //
+
+ FWPM_FILTER_CONDITION0 cond[2];
+
+ cond[0].fieldKey = FWPM_CONDITION_ALE_APP_ID;
+ cond[0].matchType = FWP_MATCH_EQUAL;
+ cond[0].conditionValue.type = FWP_BYTE_BLOB_TYPE;
+ cond[0].conditionValue.byteBlob = appIdPayload;
+
+ cond[1].fieldKey = FWPM_CONDITION_IP_LOCAL_ADDRESS;
+ cond[1].matchType = FWP_MATCH_EQUAL;
+ cond[1].conditionValue.type = FWP_UINT32;
+ cond[1].conditionValue.uint32 = RtlUlongByteSwap(TunnelIpv4->s_addr);
+
+ filter.filterCondition = cond;
+ filter.numFilterConditions = ARRAYSIZE(cond);
+
+ status = FwpmFilterAdd0(WfpSession, &filter, NULL, OutboundFilterIdV4);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Cleanup;
+ }
+
+ //
+ // Register inbound IPv4 filter.
+ //
+
+ const auto FilterNameInboundIpv4 = L"Mullvad Split Tunnel In-Tunnel Blocking Filter (Inbound IPv4)";
+
+ RtlZeroMemory(&filter.filterKey, sizeof(filter.filterKey));
+ filter.displayData.name = const_cast(FilterNameInboundIpv4);
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
+ filter.action.calloutKey = ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV4_RECV_KEY;
+
+ status = FwpmFilterAdd0(WfpSession, &filter, NULL, InboundFilterIdV4);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Cleanup;
+ }
+
+ //
+ // Skip IPv6 filters if IPv6 is not available.
+ //
+
+ if (TunnelIpv6 == NULL)
+ {
+ *OutboundFilterIdV6 = 0;
+ *InboundFilterIdV6 = 0;
+
+ status = STATUS_SUCCESS;
+
+ goto Cleanup;
+ }
+
+ //
+ // Register outbound IPv6 filter.
+ //
+
+ const auto FilterNameOutboundIpv6 = L"Mullvad Split Tunnel In-Tunnel Blocking Filter (Outbound IPv6)";
+
+ RtlZeroMemory(&filter.filterKey, sizeof(filter.filterKey));
+ filter.displayData.name = const_cast(FilterNameOutboundIpv6);
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
+ filter.action.calloutKey = ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_CONN_KEY;
+
+ cond[1].conditionValue.type = FWP_BYTE_ARRAY16_TYPE;
+ cond[1].conditionValue.byteArray16 = (FWP_BYTE_ARRAY16*)TunnelIpv6->u.Byte;
+
+ status = FwpmFilterAdd0(WfpSession, &filter, NULL, OutboundFilterIdV6);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Cleanup;
+ }
+
+ //
+ // Register inbound IPv6 filter.
+ //
+
+ const auto FilterNameInboundIpv6 = L"Mullvad Split Tunnel In-Tunnel Blocking Filter (Inbound IPv6)";
+
+ RtlZeroMemory(&filter.filterKey, sizeof(filter.filterKey));
+ filter.displayData.name = const_cast(FilterNameInboundIpv6);
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
+ filter.action.calloutKey = ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_RECV_KEY;
+
+ status = FwpmFilterAdd0(WfpSession, &filter, NULL, InboundFilterIdV6);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Cleanup;
+ }
+
+ status = STATUS_SUCCESS;
+
+Cleanup:
+
+ ExFreePoolWithTag(appIdPayload, ST_POOL_TAG);
+
+ return status;
+}
+
+typedef NTSTATUS (*AddBlockFiltersFunc)
+(
+ HANDLE WfpSession,
+ const LOWER_UNICODE_STRING *ImageName,
+ const IN_ADDR *TunnelIpv4,
+ const IN6_ADDR *TunnelIpv6,
+ UINT64 *OutboundFilterIdV4,
+ UINT64 *InboundFilterIdV4,
+ UINT64 *OutboundFilterIdV6,
+ UINT64 *InboundFilterIdV6
+);
+
+NTSTATUS
+AddBlockFiltersCreateEntryTx
+(
+ HANDLE WfpSession,
+ const LOWER_UNICODE_STRING *ImageName,
+ const IN_ADDR *TunnelIpv4,
+ const IN6_ADDR *TunnelIpv6,
+ AddBlockFiltersFunc Blocker,
+ BLOCK_CONNECTIONS_ENTRY **Entry
+)
+{
+ auto offsetStringBuffer = util::RoundToMultiple(sizeof(BLOCK_CONNECTIONS_ENTRY),
+ TYPE_ALIGNMENT(WCHAR));
+
+ auto allocationSize = offsetStringBuffer + ImageName->Length;
+
+ auto entry = (BLOCK_CONNECTIONS_ENTRY*)
+ ExAllocatePoolWithTag(PagedPool, allocationSize, ST_POOL_TAG);
+
+ if (entry == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ auto status = Blocker
+ (
+ WfpSession,
+ ImageName,
+ TunnelIpv4,
+ TunnelIpv6,
+ &entry->OutboundFilterIdV4,
+ &entry->InboundFilterIdV4,
+ &entry->OutboundFilterIdV6,
+ &entry->InboundFilterIdV6
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Failed to add block filters: 0x%X\n", status);
+
+ goto Cleanup;
+ }
+
+ auto stringBuffer = (WCHAR*)(((UINT8*)entry) + offsetStringBuffer);
+
+ InitializeListHead(&entry->ListEntry);
+
+ entry->RefCount = 1;
+
+ entry->ImageName.Length = ImageName->Length;
+ entry->ImageName.MaximumLength = ImageName->Length;
+ entry->ImageName.Buffer = stringBuffer;
+
+ RtlCopyMemory(stringBuffer, ImageName->Buffer, ImageName->Length);
+
+ *Entry = entry;
+
+ return STATUS_SUCCESS;
+
+Cleanup:
+
+ ExFreePoolWithTag(entry, ST_POOL_TAG);
+
+ return status;
+}
+
+NTSTATUS
+RemoveBlockFiltersTx
+(
+ HANDLE WfpSession,
+ UINT64 OutboundFilterIdV4,
+ UINT64 InboundFilterIdV4,
+ UINT64 OutboundFilterIdV6,
+ UINT64 InboundFilterIdV6
+)
+{
+ auto status = FwpmFilterDeleteById0(WfpSession, OutboundFilterIdV4);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = FwpmFilterDeleteById0(WfpSession, InboundFilterIdV4);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ if (0 != OutboundFilterIdV6)
+ {
+ status = FwpmFilterDeleteById0(WfpSession, OutboundFilterIdV6);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+ }
+
+ if (0 != InboundFilterIdV6)
+ {
+ status = FwpmFilterDeleteById0(WfpSession, InboundFilterIdV6);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+ }
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+RemoveBlockFiltersAndEntryTx
+(
+ HANDLE WfpSession,
+ LIST_ENTRY *TransactionEvents,
+ BLOCK_CONNECTIONS_ENTRY *Entry
+)
+{
+ auto status = RemoveBlockFiltersTx
+ (
+ WfpSession,
+ Entry->OutboundFilterIdV4,
+ Entry->InboundFilterIdV4,
+ Entry->OutboundFilterIdV6,
+ Entry->InboundFilterIdV6
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not remove block filters: 0x%X\n", status);
+
+ return status;
+ }
+
+ //
+ // Record in transaction history before unlinking, because the former is a fallible operation.
+ //
+
+ status = TransactionRemovedEntry(TransactionEvents, Entry, Entry->ListEntry.Blink);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not update local transaction: 0x%X\n", status);
+
+ return status;
+ }
+
+ RemoveEntryList((LIST_ENTRY*)Entry);
+
+ return STATUS_SUCCESS;
+}
+
+void
+FreeList
+(
+ LIST_ENTRY *List
+)
+{
+ LIST_ENTRY *entry;
+
+ while ((entry = RemoveHeadList(List)) != List)
+ {
+ ExFreePoolWithTag(entry, ST_POOL_TAG);
+ }
+}
+
+//
+// This is a little crude, but avoids having to maintain state around these filters
+// and managing registration/removal in a double transaction.
+//
+bool
+GenericIpv6BlockFiltersRegistered
+(
+ HANDLE WfpSession
+)
+{
+ FWPM_FILTER0 *filter;
+
+ auto status = FwpmFilterGetByKey0(WfpSession, &ST_FW_FILTER_BLOCK_ALL_SPLIT_APPS_IPV6_CONN_KEY, &filter);
+
+ if (!NT_SUCCESS(status))
+ {
+ return false;
+ }
+
+ FwpmFreeMemory0((void**)&filter);
+
+ return true;
+}
+
+} // anonymous namespace
+
+NTSTATUS
+Initialize
+(
+ HANDLE WfpSession,
+ void **Context
+)
+{
+ auto stateData = (STATE_DATA*)
+ ExAllocatePoolWithTag(PagedPool, sizeof(STATE_DATA), ST_POOL_TAG);
+
+ if (stateData == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ stateData->WfpSession = WfpSession;
+
+ InitializeListHead(&stateData->BlockedTunnelConnections);
+
+ InitializeListHead(&stateData->TransactionEvents);
+
+ *Context = stateData;
+
+ return STATUS_SUCCESS;
+}
+
+void
+TearDown
+(
+ void **Context
+)
+{
+ auto stateData = (STATE_DATA*)*Context;
+
+ //
+ // This is a best effort venture so just keep going.
+ //
+ // Undo Ipv6 blocking filters.
+ //
+
+ RemoveFilterBlockSplitAppsIpv6Tx(*Context);
+
+ //
+ // Remove all app specific filters.
+ //
+
+ for (auto rawEntry = stateData->BlockedTunnelConnections.Flink;
+ rawEntry != &stateData->BlockedTunnelConnections;
+ /* no post-condition */)
+ {
+ auto entry = (BLOCK_CONNECTIONS_ENTRY*)rawEntry;
+
+ RemoveBlockFiltersTx
+ (
+ stateData->WfpSession,
+ entry->OutboundFilterIdV4,
+ entry->InboundFilterIdV4,
+ entry->OutboundFilterIdV6,
+ entry->InboundFilterIdV6
+ );
+
+ auto next = rawEntry->Flink;
+
+ ExFreePoolWithTag(rawEntry, ST_POOL_TAG);
+
+ rawEntry = next;
+ }
+
+ InitializeListHead(&stateData->BlockedTunnelConnections);
+
+ //
+ // This works because a commit discards all transaction events.
+ // (Also, there shouldn't be any events at this time.)
+ //
+
+ if (!IsListEmpty(&stateData->TransactionEvents))
+ {
+ DbgPrint("ERROR: Active transaction while tearing down blocking subsystem\n");
+ }
+
+ TransactionCommit(*Context);
+
+ //
+ // Release context.
+ //
+
+ ExFreePoolWithTag(stateData, ST_POOL_TAG);
+
+ *Context = NULL;
+}
+
+NTSTATUS
+ResetTx2
+(
+ void *Context
+)
+{
+ auto stateData = (STATE_DATA*)Context;
+
+ if (GenericIpv6BlockFiltersRegistered(stateData->WfpSession))
+ {
+ auto status = RemoveFilterBlockSplitAppsIpv6Tx(Context);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+ }
+
+ if (IsListEmpty(&stateData->BlockedTunnelConnections))
+ {
+ return STATUS_SUCCESS;
+ }
+
+ for (auto rawEntry = stateData->BlockedTunnelConnections.Flink;
+ rawEntry != &stateData->BlockedTunnelConnections;
+ rawEntry = rawEntry->Flink)
+ {
+ auto entry = (BLOCK_CONNECTIONS_ENTRY*)rawEntry;
+
+ auto status = RemoveBlockFiltersTx
+ (
+ stateData->WfpSession,
+ entry->OutboundFilterIdV4,
+ entry->InboundFilterIdV4,
+ entry->OutboundFilterIdV6,
+ entry->InboundFilterIdV6
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+ }
+
+ //
+ // Create transaction event and pass ownership of list to it.
+ //
+ auto status = TransactionSwappedLists(&stateData->TransactionEvents, &stateData->BlockedTunnelConnections);
+
+ if (!NT_SUCCESS(status))
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ //
+ // Clear list to reflect new state.
+ //
+ InitializeListHead(&stateData->BlockedTunnelConnections);
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+TransactionBegin
+(
+ void *Context
+)
+{
+ auto stateData = (STATE_DATA*)Context;
+
+ if (IsListEmpty(&stateData->TransactionEvents))
+ {
+ return STATUS_SUCCESS;
+ }
+
+ return STATUS_TRANSACTION_REQUEST_NOT_VALID;
+}
+
+void
+TransactionCommit
+(
+ void *Context
+)
+{
+ //
+ // All changes are already applied, discard transaction events.
+ //
+ // Each event has to be released, and some of them point to
+ // a target entry which must also be released.
+ //
+
+ auto stateData = (STATE_DATA*)Context;
+
+ auto list = &stateData->TransactionEvents;
+ LIST_ENTRY *rawEvent;
+
+ while ((rawEvent = RemoveHeadList(list)) != list)
+ {
+ switch (((TRANSACTION_EVENT*)rawEvent)->EventType)
+ {
+ case TRANSACTION_EVENT_TYPE::ADD_ENTRY:
+ {
+ auto addEvent = (TRANSACTION_EVENT_ADD_ENTRY*)rawEvent;
+
+ ExFreePoolWithTag(addEvent->Target, ST_POOL_TAG);
+
+ break;
+ }
+ case TRANSACTION_EVENT_TYPE::SWAP_LISTS:
+ {
+ auto swapEvent = (TRANSACTION_EVENT_SWAP_LISTS*)rawEvent;
+
+ FreeList(&swapEvent->BlockedTunnelConnections);
+
+ break;
+ }
+ }
+
+ ExFreePoolWithTag(rawEvent, ST_POOL_TAG);
+ }
+}
+
+void
+TransactionAbort
+(
+ void *Context
+)
+{
+ //
+ // Step back through event records and undo all changes.
+ //
+
+ auto stateData = (STATE_DATA*)Context;
+
+ auto list = &stateData->TransactionEvents;
+ LIST_ENTRY *rawEvent;
+
+ while ((rawEvent = RemoveHeadList(list)) != list)
+ {
+ auto evt = (TRANSACTION_EVENT*)rawEvent;
+
+ switch (evt->EventType)
+ {
+ case TRANSACTION_EVENT_TYPE::INCREMENT_REF_COUNT:
+ {
+ ++evt->Target->RefCount;
+
+ break;
+ }
+ case TRANSACTION_EVENT_TYPE::DECREMENT_REF_COUNT:
+ {
+ --evt->Target->RefCount;
+
+ break;
+ }
+ case TRANSACTION_EVENT_TYPE::ADD_ENTRY:
+ {
+ auto addEvent = (TRANSACTION_EVENT_ADD_ENTRY*)rawEvent;
+
+ InsertHeadList(addEvent->MockHead, (LIST_ENTRY*)addEvent->Target);
+
+ break;
+ }
+ case TRANSACTION_EVENT_TYPE::REMOVE_ENTRY:
+ {
+ RemoveEntryList((LIST_ENTRY*)evt->Target);
+
+ ExFreePoolWithTag(evt->Target, ST_POOL_TAG);
+
+ break;
+ }
+ case TRANSACTION_EVENT_TYPE::SWAP_LISTS:
+ {
+ auto liveList = &stateData->BlockedTunnelConnections;
+
+ FreeList(liveList);
+
+ auto swapEvent = (TRANSACTION_EVENT_SWAP_LISTS*)rawEvent;
+
+ util::ReparentList(liveList, &swapEvent->BlockedTunnelConnections);
+
+ break;
+ }
+ };
+
+ ExFreePoolWithTag(rawEvent, ST_POOL_TAG);
+ }
+}
+
+NTSTATUS
+RegisterFilterBlockSplitAppTx2
+(
+ void *Context,
+ const LOWER_UNICODE_STRING *ImageName,
+ const IN_ADDR *TunnelIpv4,
+ const IN6_ADDR *TunnelIpv6
+)
+{
+ auto stateData = (STATE_DATA*)Context;
+
+ auto existingEntry = FindBlockConnectionsEntry(&stateData->BlockedTunnelConnections, ImageName);
+
+ if (existingEntry != NULL)
+ {
+ auto status = TransactionIncrementedRefCount(&stateData->TransactionEvents, existingEntry);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not update local transaction: 0x%X\n", status);
+
+ return status;
+ }
+
+ ++existingEntry->RefCount;
+
+ return STATUS_SUCCESS;
+ }
+
+ BLOCK_CONNECTIONS_ENTRY *entry;
+
+ auto status = AddBlockFiltersCreateEntryTx
+ (
+ stateData->WfpSession,
+ ImageName,
+ TunnelIpv4,
+ TunnelIpv6,
+ AddTunnelBlockFiltersTx,
+ &entry
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = TransactionAddedEntry(&stateData->TransactionEvents, entry);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not update local transaction: 0x%X\n", status);
+
+ ExFreePoolWithTag(entry, ST_POOL_TAG);
+
+ return status;
+ }
+
+ InsertTailList(&stateData->BlockedTunnelConnections, &entry->ListEntry);
+
+ DbgPrint("Added tunnel block filters for %wZ\n", ImageName);
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+RemoveFilterBlockSplitAppTx2
+(
+ void *Context,
+ const LOWER_UNICODE_STRING *ImageName
+)
+{
+ auto stateData = (STATE_DATA*)Context;
+
+ auto entry = FindBlockConnectionsEntry(&stateData->BlockedTunnelConnections, ImageName);
+
+ if (entry == NULL)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (entry->RefCount > 1)
+ {
+ auto status = TransactionDecrementedRefCount(&stateData->TransactionEvents, entry);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not update local transaction: 0x%X\n", status);
+
+ return status;
+ }
+
+ --entry->RefCount;
+
+ //
+ // TODO: Indicate to layer above that it might want to force an ALE reauthorization in WFP.
+ //
+ // https://docs.microsoft.com/en-us/windows/win32/fwp/ale-re-authorization
+ //
+ // Forcing a reauthorization is only necessary in very specific cases. Usually the transaction
+ // will include the addition/removal of at least one filter, and this triggers a reauthorization.
+ //
+ // Also, the issue only comes into play if a process stops being split but keeps running.
+ //
+ // Rationale:
+ //
+ // There could be existing connections which have been blocked for some duration of time
+ // and now need to be reauthorized in WFP so they are no longer blocked.
+ //
+ // Similarly, there could be non-tunnel connections that were previously approved
+ // and should now be reauthorized so they can be blocked.
+ //
+
+ return STATUS_SUCCESS;
+ }
+
+ auto status = RemoveBlockFiltersAndEntryTx
+ (
+ stateData->WfpSession,
+ &stateData->TransactionEvents,
+ entry
+ );
+
+ if (NT_SUCCESS(status))
+ {
+ DbgPrint("Removed tunnel block filters for %wZ\n", ImageName);
+ }
+
+ return status;
+}
+
+NTSTATUS
+RegisterFilterBlockSplitAppsIpv6Tx
+(
+ void *Context
+)
+{
+ auto stateData = (STATE_DATA*)Context;
+
+ //
+ // Create filters that match all traffic.
+ // The linked callout will then block all attempted connections
+ // that can be associated with apps that are being split.
+ //
+
+ FWPM_FILTER0 filter = { 0 };
+
+ const auto filterNameOutbound = L"Mullvad Split Tunnel IPv6 Blocking Filter (Outbound)";
+ const auto filterDescription = L"Blocks IPv6 traffic for connections being split";
+
+ filter.filterKey = ST_FW_FILTER_BLOCK_ALL_SPLIT_APPS_IPV6_CONN_KEY;
+ filter.displayData.name = const_cast(filterNameOutbound);
+ filter.displayData.description = const_cast(filterDescription);
+ filter.flags = FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT | FWPM_FILTER_FLAG_HAS_PROVIDER_CONTEXT;
+ filter.providerKey = const_cast(&ST_FW_PROVIDER_KEY);
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
+ filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
+ filter.weight.type = FWP_UINT64;
+ filter.weight.uint64 = const_cast(&ST_MAX_FILTER_WEIGHT);
+ filter.action.type = FWP_ACTION_CALLOUT_UNKNOWN;
+ filter.action.calloutKey = ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_CONN_KEY;
+ filter.providerContextKey = ST_FW_PROVIDER_CONTEXT_KEY;
+
+ auto status = FwpmFilterAdd0(stateData->WfpSession, &filter, NULL, NULL);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ const auto filterNameInbound = L"Mullvad Split Tunnel IPv6 Blocking Filter (Inbound)";
+
+ filter.filterKey = ST_FW_FILTER_BLOCK_ALL_SPLIT_APPS_IPV6_RECV_KEY;
+ filter.displayData.name = const_cast(filterNameInbound);
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
+ filter.action.calloutKey = ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_RECV_KEY;
+
+ return FwpmFilterAdd0(stateData->WfpSession, &filter, NULL, NULL);
+}
+
+NTSTATUS
+RemoveFilterBlockSplitAppsIpv6Tx
+(
+ void *Context
+)
+{
+ auto stateData = (STATE_DATA*)Context;
+
+ auto status = FwpmFilterDeleteByKey0(stateData->WfpSession, &ST_FW_FILTER_BLOCK_ALL_SPLIT_APPS_IPV6_CONN_KEY);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ return FwpmFilterDeleteByKey0(stateData->WfpSession, &ST_FW_FILTER_BLOCK_ALL_SPLIT_APPS_IPV6_RECV_KEY);
+}
+
+NTSTATUS
+UpdateBlockingFiltersTx2
+(
+ void *Context,
+ const IN_ADDR *TunnelIpv4,
+ const IN6_ADDR *TunnelIpv6
+)
+{
+ auto stateData = (STATE_DATA*)Context;
+
+ if (IsListEmpty(&stateData->BlockedTunnelConnections))
+ {
+ return STATUS_SUCCESS;
+ }
+
+ LIST_ENTRY newList;
+
+ InitializeListHead(&newList);
+
+ for (auto rawEntry = stateData->BlockedTunnelConnections.Flink;
+ rawEntry != &stateData->BlockedTunnelConnections;
+ rawEntry = rawEntry->Flink)
+ {
+ auto entry = (BLOCK_CONNECTIONS_ENTRY*)rawEntry;
+
+ auto status = RemoveBlockFiltersTx
+ (
+ stateData->WfpSession,
+ entry->OutboundFilterIdV4,
+ entry->InboundFilterIdV4,
+ entry->OutboundFilterIdV6,
+ entry->InboundFilterIdV6
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ FreeList(&newList);
+
+ return status;
+ }
+
+ BLOCK_CONNECTIONS_ENTRY *newEntry;
+
+ status = AddBlockFiltersCreateEntryTx
+ (
+ stateData->WfpSession,
+ &entry->ImageName,
+ TunnelIpv4,
+ TunnelIpv6,
+ AddTunnelBlockFiltersTx,
+ &newEntry
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ FreeList(&newList);
+
+ return status;
+ }
+
+ newEntry->RefCount = entry->RefCount;
+
+ InsertTailList(&newList, (LIST_ENTRY*)newEntry);
+ }
+
+ //
+ // stateData->BlockedTunnelConnections is now completely obsolete.
+ // newList has all the updated entries.
+ //
+
+ auto status = TransactionSwappedLists(&stateData->TransactionEvents, &stateData->BlockedTunnelConnections);
+
+ if (!NT_SUCCESS(status))
+ {
+ FreeList(&newList);
+
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ //
+ // Ownership of the list formerly rooted at stateData->BlockedTunnelConnections
+ // has been moved to the recently queued transaction event.
+ //
+ // Perform actual state update.
+ //
+
+ util::ReparentList(&stateData->BlockedTunnelConnections, &newList);
+
+ return STATUS_SUCCESS;
+}
+
+} // namespace firewall::blocking
diff --git a/src/firewall/blocking.h b/src/firewall/blocking.h
new file mode 100644
index 0000000..f4ce680
--- /dev/null
+++ b/src/firewall/blocking.h
@@ -0,0 +1,119 @@
+#pragma once
+
+#include
+#include
+#include
+#include "../defs/types.h"
+
+namespace firewall::blocking
+{
+
+NTSTATUS
+Initialize
+(
+ HANDLE WfpSession,
+ void **Context
+);
+
+void
+TearDown
+(
+ void **Context
+);
+
+//
+// ResetTx2()
+//
+// Remove all app specific blocking filters.
+// Remove generic IPv6 blocking if active.
+//
+// IMPORTANT: This function needs to be running inside a WFP transaction as well as a
+// local transaction managed by this module.
+//
+NTSTATUS
+ResetTx2
+(
+ void *Context
+);
+
+NTSTATUS
+TransactionBegin
+(
+ void *Context
+);
+
+void
+TransactionCommit
+(
+ void *Context
+);
+
+void
+TransactionAbort
+(
+ void *Context
+);
+
+//
+// RegisterFilterBlockSplitAppTx2()
+//
+// Register WFP filters, with linked callout, that will block connections in the tunnel
+// from applications being split.
+//
+// This is used to block existing connections inside the tunnel for applications that are
+// just now being split.
+//
+// IMPORTANT: These functions need to be running inside a WFP transaction as well as a
+// local transaction managed by this module.
+//
+NTSTATUS
+RegisterFilterBlockSplitAppTx2
+(
+ void *Context,
+ const LOWER_UNICODE_STRING *ImageName,
+ const IN_ADDR *TunnelIpv4,
+ const IN6_ADDR *TunnelIpv6
+);
+
+NTSTATUS
+RemoveFilterBlockSplitAppTx2
+(
+ void *Context,
+ const LOWER_UNICODE_STRING *ImageName
+);
+
+//
+// RegisterFilterBlockSplitAppsTunnelIpv6Tx()
+//
+// Block all tunnel IPv6 traffic for applications being split.
+// To be used when the physical adapter doesn't have an IPv6 interface.
+//
+NTSTATUS
+RegisterFilterBlockSplitAppsIpv6Tx
+(
+ void *Context
+);
+
+NTSTATUS
+RemoveFilterBlockSplitAppsIpv6Tx
+(
+ void *Context
+);
+
+//
+// UpdateBlockingFiltersTx2()
+//
+// Rewrite filters with updated IP addresses.
+//
+// IMPORTANT: This function needs to be running inside a WFP transaction as well as a
+// local transaction managed by this module.
+//
+NTSTATUS
+UpdateBlockingFiltersTx2
+(
+ void *Context,
+ const IN_ADDR *TunnelIpv4,
+ const IN6_ADDR *TunnelIpv6
+);
+
+} // namespace firewall::blocking
diff --git a/src/firewall/callouts.cpp b/src/firewall/callouts.cpp
new file mode 100644
index 0000000..83db8d7
--- /dev/null
+++ b/src/firewall/callouts.cpp
@@ -0,0 +1,740 @@
+#include "wfp.h"
+#include "firewall.h"
+#include "context.h"
+#include "identifiers.h"
+#include "splitting.h"
+#include "asyncbind.h"
+#include "callouts.h"
+
+namespace firewall
+{
+
+namespace
+{
+
+//
+// NotifyFilterAttach()
+//
+// Receive notifications about filters attaching/detaching the callout.
+//
+NTSTATUS
+NotifyFilterAttach
+(
+ FWPS_CALLOUT_NOTIFY_TYPE notifyType,
+ const GUID *filterKey,
+ FWPS_FILTER1 *filter
+)
+{
+ UNREFERENCED_PARAMETER(notifyType);
+ UNREFERENCED_PARAMETER(filterKey);
+ UNREFERENCED_PARAMETER(filter);
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+RegisterCalloutTx
+(
+ PDEVICE_OBJECT DeviceObject,
+ HANDLE WfpSession,
+ FWPS_CALLOUT_CLASSIFY_FN1 Callout,
+ const GUID *CalloutKey,
+ const GUID *LayerKey,
+ const wchar_t *CalloutName,
+ const wchar_t* CalloutDescription
+)
+{
+ //
+ // Logically, this is the wrong order, but it results in cleaner code.
+ // You're encouraged to first register the callout and then add it.
+ //
+ // However, what's currently here is fully supported:
+ //
+ // `By default filters that reference callouts that have been added
+ // but have not yet registered with the filter engine are treated as Block filters.`
+ //
+
+ FWPM_CALLOUT0 callout;
+
+ RtlZeroMemory(&callout, sizeof(callout));
+
+ callout.calloutKey = *CalloutKey;
+ callout.displayData.name = const_cast(CalloutName);
+ callout.displayData.description = const_cast(CalloutDescription);
+ callout.flags = FWPM_CALLOUT_FLAG_USES_PROVIDER_CONTEXT;
+ callout.providerKey = const_cast(&ST_FW_PROVIDER_KEY);
+ callout.applicableLayer = *LayerKey;
+
+ auto status = FwpmCalloutAdd0(WfpSession, &callout, NULL, NULL);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ FWPS_CALLOUT1 aCallout = { 0 };
+
+ aCallout.calloutKey = *CalloutKey;
+ aCallout.classifyFn = Callout;
+ aCallout.notifyFn = NotifyFilterAttach;
+ aCallout.flowDeleteFn = NULL;
+
+ return FwpsCalloutRegister1(DeviceObject, &aCallout, NULL);
+}
+
+void
+ClassifyUnknownBind
+(
+ CONTEXT *Context,
+ HANDLE ProcessId,
+ UINT64 FilterId,
+ const void *ClassifyContext,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut,
+ bool Ipv4
+)
+{
+ //
+ // Pend the bind and wait for process to become known and classified.
+ //
+
+ auto status = PendBindRequest
+ (
+ Context,
+ ProcessId,
+ const_cast(ClassifyContext),
+ FilterId,
+ ClassifyOut,
+ Ipv4
+ );
+
+ if (NT_SUCCESS(status))
+ {
+ return;
+ }
+
+ DbgPrint("Could not pend bind request from process %p, blocking instead\n", ProcessId);
+
+ FailBindRequest
+ (
+ ProcessId,
+ const_cast(ClassifyContext),
+ FilterId,
+ ClassifyOut,
+ Ipv4
+ );
+}
+
+//
+// CalloutClassifyBind()
+//
+// Entry point for splitting traffic.
+// Check whether the binding process is marked for having its traffic split.
+//
+// FWPS_LAYER_ALE_BIND_REDIRECT_V4
+// FWPS_LAYER_ALE_BIND_REDIRECT_V6
+//
+void
+CalloutClassifyBind
+(
+ const FWPS_INCOMING_VALUES0 *FixedValues,
+ const FWPS_INCOMING_METADATA_VALUES0 *MetaValues,
+ void *LayerData,
+ const void *ClassifyContext,
+ const FWPS_FILTER1 *Filter,
+ UINT64 FlowContext,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut
+)
+{
+ UNREFERENCED_PARAMETER(LayerData);
+ UNREFERENCED_PARAMETER(FlowContext);
+
+ NT_ASSERT
+ (
+ FixedValues->layerId == FWPS_LAYER_ALE_BIND_REDIRECT_V4
+ || FixedValues->layerId == FWPS_LAYER_ALE_BIND_REDIRECT_V6
+ );
+
+ NT_ASSERT
+ (
+ Filter->providerContext != NULL
+ && Filter->providerContext->type == FWPM_GENERAL_CONTEXT
+ && Filter->providerContext->dataBuffer->size == sizeof(CONTEXT*)
+ );
+
+ auto context = *(CONTEXT**)Filter->providerContext->dataBuffer->data;
+
+ if (0 == (ClassifyOut->rights & FWPS_RIGHT_ACTION_WRITE))
+ {
+ DbgPrint("Aborting bind processing because hard permit/block already applied\n");
+
+ return;
+ }
+
+ if (ClassifyOut->actionType == FWP_ACTION_NONE)
+ {
+ ClassifyOut->actionType = FWP_ACTION_CONTINUE;
+ }
+
+ if (!FWPS_IS_METADATA_FIELD_PRESENT(MetaValues, FWPS_METADATA_FIELD_PROCESS_ID))
+ {
+ DbgPrint("Failed to classify bind because PID was not provided\n");
+
+ return;
+ }
+
+ const CALLBACKS &callbacks = context->Callbacks;
+
+ const auto verdict = callbacks.QueryProcess(HANDLE(MetaValues->processId), callbacks.Context);
+
+ switch (verdict)
+ {
+ case PROCESS_SPLIT_VERDICT::DO_SPLIT:
+ {
+ RewriteBind
+ (
+ context,
+ FixedValues,
+ MetaValues,
+ Filter->filterId,
+ ClassifyContext,
+ ClassifyOut
+ );
+
+ break;
+ }
+ case PROCESS_SPLIT_VERDICT::UNKNOWN:
+ {
+ ClassifyUnknownBind
+ (
+ context,
+ HANDLE(MetaValues->processId),
+ Filter->filterId,
+ ClassifyContext,
+ ClassifyOut,
+ FixedValues->layerId == FWPS_LAYER_ALE_BIND_REDIRECT_V4
+ );
+
+ break;
+ }
+ };
+}
+
+bool IsAleReauthorize
+(
+ const FWPS_INCOMING_VALUES *FixedValues
+)
+{
+ size_t index;
+
+ switch (FixedValues->layerId)
+ {
+ case FWPS_LAYER_ALE_AUTH_CONNECT_V4:
+ {
+ index = FWPS_FIELD_ALE_AUTH_CONNECT_V4_FLAGS;
+ break;
+ }
+ case FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V4:
+ {
+ index = FWPS_FIELD_ALE_AUTH_RECV_ACCEPT_V4_FLAGS;
+ break;
+ }
+ case FWPS_LAYER_ALE_AUTH_CONNECT_V6:
+ {
+ index = FWPS_FIELD_ALE_AUTH_CONNECT_V6_FLAGS;
+ break;
+ }
+ case FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V6:
+ {
+ index = FWPS_FIELD_ALE_AUTH_RECV_ACCEPT_V6_FLAGS;
+ break;
+ }
+ default:
+ {
+ return false;
+ }
+ };
+
+ const auto flags = FixedValues->incomingValue[index].value.uint32;
+
+ return ((flags & FWP_CONDITION_FLAG_IS_REAUTHORIZE) != 0);
+}
+
+//
+// CalloutPermitSplitApps()
+//
+// For processes being split, the bind will have already been moved off the
+// tunnel interface.
+//
+// So now it's only a matter of approving the connection.
+//
+// FWPS_LAYER_ALE_AUTH_CONNECT_V4
+// FWPS_LAYER_ALE_AUTH_CONNECT_V6
+// FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V4
+// FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V6
+//
+void
+CalloutPermitSplitApps
+(
+ const FWPS_INCOMING_VALUES0 *FixedValues,
+ const FWPS_INCOMING_METADATA_VALUES0 *MetaValues,
+ void *LayerData,
+ const void *ClassifyContext,
+ const FWPS_FILTER1 *Filter,
+ UINT64 FlowContext,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut
+)
+{
+#if !DBG
+ UNREFERENCED_PARAMETER(FixedValues);
+#endif
+ UNREFERENCED_PARAMETER(LayerData);
+ UNREFERENCED_PARAMETER(ClassifyContext);
+ UNREFERENCED_PARAMETER(Filter);
+ UNREFERENCED_PARAMETER(FlowContext);
+
+ NT_ASSERT
+ (
+ FixedValues->layerId == FWPS_LAYER_ALE_AUTH_CONNECT_V4
+ || FixedValues->layerId == FWPS_LAYER_ALE_AUTH_CONNECT_V6
+ || FixedValues->layerId == FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V4
+ || FixedValues->layerId == FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V6
+ );
+
+ NT_ASSERT
+ (
+ Filter->providerContext != NULL
+ && Filter->providerContext->type == FWPM_GENERAL_CONTEXT
+ && Filter->providerContext->dataBuffer->size == sizeof(CONTEXT*)
+ );
+
+ auto context = *(CONTEXT**)Filter->providerContext->dataBuffer->data;
+
+ if (0 == (ClassifyOut->rights & FWPS_RIGHT_ACTION_WRITE))
+ {
+ DbgPrint("Aborting connection processing because hard permit/block already applied\n");
+
+ return;
+ }
+
+ if (ClassifyOut->actionType == FWP_ACTION_NONE)
+ {
+ ClassifyOut->actionType = FWP_ACTION_CONTINUE;
+ }
+
+ if (!FWPS_IS_METADATA_FIELD_PRESENT(MetaValues, FWPS_METADATA_FIELD_PROCESS_ID))
+ {
+ DbgPrint("Failed to classify connection because PID was not provided\n");
+
+ return;
+ }
+
+ const CALLBACKS &callbacks = context->Callbacks;
+
+ const auto verdict = callbacks.QueryProcess(HANDLE(MetaValues->processId), callbacks.Context);
+
+ if (verdict == PROCESS_SPLIT_VERDICT::DO_SPLIT)
+ {
+ DbgPrint("APPROVING CONNECTION\n");
+
+ ClassifyOut->actionType = FWP_ACTION_PERMIT;
+ ClassifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE;
+ }
+ else
+ {
+#if DBG
+ if (IsAleReauthorize(FixedValues))
+ {
+ DbgPrint("[CalloutPermitSplitApps] Reauthorized connection (PID: %p) is not explicitly "\
+ "approved by callout\n", HANDLE(MetaValues->processId));
+ }
+#endif
+ }
+}
+
+//
+// CalloutBlockSplitApps()
+//
+// For processes just now being split, it could be the case that they have existing
+// long-lived connections inside the tunnel.
+//
+// These connections need to be blocked to ensure the process exists on
+// only one side of the tunnel.
+//
+// FWPS_LAYER_ALE_AUTH_CONNECT_V4
+// FWPS_LAYER_ALE_AUTH_CONNECT_V6
+// FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V4
+// FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V6
+//
+void
+CalloutBlockSplitApps
+(
+ const FWPS_INCOMING_VALUES0 *FixedValues,
+ const FWPS_INCOMING_METADATA_VALUES0 *MetaValues,
+ void *LayerData,
+ const void *ClassifyContext,
+ const FWPS_FILTER1 *Filter,
+ UINT64 FlowContext,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut
+)
+{
+#if !DBG
+ UNREFERENCED_PARAMETER(FixedValues);
+#endif
+ UNREFERENCED_PARAMETER(LayerData);
+ UNREFERENCED_PARAMETER(ClassifyContext);
+ UNREFERENCED_PARAMETER(Filter);
+ UNREFERENCED_PARAMETER(FlowContext);
+
+ NT_ASSERT
+ (
+ FixedValues->layerId == FWPS_LAYER_ALE_AUTH_CONNECT_V4
+ || FixedValues->layerId == FWPS_LAYER_ALE_AUTH_CONNECT_V6
+ || FixedValues->layerId == FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V4
+ || FixedValues->layerId == FWPS_LAYER_ALE_AUTH_RECV_ACCEPT_V6
+ );
+
+ NT_ASSERT
+ (
+ Filter->providerContext != NULL
+ && Filter->providerContext->type == FWPM_GENERAL_CONTEXT
+ && Filter->providerContext->dataBuffer->size == sizeof(CONTEXT*)
+ );
+
+ auto context = *(CONTEXT**)Filter->providerContext->dataBuffer->data;
+
+ if (0 == (ClassifyOut->rights & FWPS_RIGHT_ACTION_WRITE))
+ {
+ DbgPrint("Aborting connection processing because hard permit/block already applied\n");
+
+ return;
+ }
+
+ if (ClassifyOut->actionType == FWP_ACTION_NONE)
+ {
+ ClassifyOut->actionType = FWP_ACTION_CONTINUE;
+ }
+
+ if (!FWPS_IS_METADATA_FIELD_PRESENT(MetaValues, FWPS_METADATA_FIELD_PROCESS_ID))
+ {
+ DbgPrint("Failed to classify connection because PID was not provided\n");
+
+ return;
+ }
+
+ const CALLBACKS &callbacks = context->Callbacks;
+
+ const auto verdict = callbacks.QueryProcess(HANDLE(MetaValues->processId), callbacks.Context);
+
+ if (verdict == PROCESS_SPLIT_VERDICT::DO_SPLIT)
+ {
+ DbgPrint("BLOCKING CONNECTION\n");
+
+ ClassifyOut->actionType = FWP_ACTION_BLOCK;
+ ClassifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE;
+ }
+ else
+ {
+#if DBG
+ if (IsAleReauthorize(FixedValues))
+ {
+ DbgPrint("[CalloutBlockSplitApps] Reauthorized connection (PID: %p) is not explicitly "\
+ "blocked by callout\n", HANDLE(MetaValues->processId));
+ }
+#endif
+ }
+}
+
+} // anonymous namespace
+
+//
+// RegisterCalloutClassifyBindTx()
+//
+// Register callout with WFP. In all applicable layers.
+//
+// "Tx" (in transaction) suffix means there is no clean-up in failure paths.
+//
+NTSTATUS
+RegisterCalloutClassifyBindTx
+(
+ PDEVICE_OBJECT DeviceObject,
+ HANDLE WfpSession
+)
+{
+ auto status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutClassifyBind,
+ &ST_FW_CALLOUT_CLASSIFY_BIND_IPV4_KEY,
+ &FWPM_LAYER_ALE_BIND_REDIRECT_V4,
+ L"Mullvad Split Tunnel Bind Redirect Callout (IPv4)",
+ L"Redirects certain binds away from tunnel interface"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutClassifyBind,
+ &ST_FW_CALLOUT_CLASSIFY_BIND_IPV6_KEY,
+ &FWPM_LAYER_ALE_BIND_REDIRECT_V6,
+ L"Mullvad Split Tunnel Bind Redirect Callout (IPv6)",
+ L"Redirects certain binds away from tunnel interface"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ UnregisterCalloutClassifyBind();
+ }
+
+ return status;
+}
+
+NTSTATUS
+UnregisterCalloutClassifyBind
+(
+)
+{
+#define RETURN_IF_FAILED(status) \
+ if (!NT_SUCCESS(status) && status != STATUS_FWP_CALLOUT_NOT_FOUND) \
+ { \
+ return status; \
+ }
+
+ auto s1 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_CLASSIFY_BIND_IPV4_KEY);
+ auto s2 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_CLASSIFY_BIND_IPV6_KEY);
+
+ RETURN_IF_FAILED(s1)
+ RETURN_IF_FAILED(s2)
+
+ return STATUS_SUCCESS;
+}
+
+//
+// RegisterCalloutPermitSplitAppsTx()
+//
+// Register callout with WFP. In all applicable layers.
+//
+// "Tx" (in transaction) suffix means there is no clean-up in failure paths.
+//
+NTSTATUS
+RegisterCalloutPermitSplitAppsTx
+(
+ PDEVICE_OBJECT DeviceObject,
+ HANDLE WfpSession
+)
+{
+ auto status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutPermitSplitApps,
+ &ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV4_CONN_KEY,
+ &FWPM_LAYER_ALE_AUTH_CONNECT_V4,
+ L"Mullvad Split Tunnel Permitting Callout (IPv4)",
+ L"Permits selected connections outside the tunnel"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutPermitSplitApps,
+ &ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV4_RECV_KEY,
+ &FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4,
+ L"Mullvad Split Tunnel Permitting Callout (IPv4)",
+ L"Permits selected connections outside the tunnel"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ UnregisterCalloutPermitSplitApps();
+
+ return status;
+ }
+
+ status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutPermitSplitApps,
+ &ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV6_CONN_KEY,
+ &FWPM_LAYER_ALE_AUTH_CONNECT_V6,
+ L"Mullvad Split Tunnel Permitting Callout (IPv6)",
+ L"Permits selected connections outside the tunnel"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ UnregisterCalloutPermitSplitApps();
+
+ return status;
+ }
+
+ status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutPermitSplitApps,
+ &ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV6_RECV_KEY,
+ &FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6,
+ L"Mullvad Split Tunnel Permitting Callout (IPv6)",
+ L"Permits selected connections outside the tunnel"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ UnregisterCalloutPermitSplitApps();
+
+ return status;
+ }
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+UnregisterCalloutPermitSplitApps
+(
+)
+{
+#define RETURN_IF_FAILED(status) \
+ if (!NT_SUCCESS(status) && status != STATUS_FWP_CALLOUT_NOT_FOUND) \
+ { \
+ return status; \
+ }
+
+ auto s1 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV4_CONN_KEY);
+ auto s2 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV4_RECV_KEY);
+ auto s3 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV6_CONN_KEY);
+ auto s4 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV6_RECV_KEY);
+
+ RETURN_IF_FAILED(s1);
+ RETURN_IF_FAILED(s2);
+ RETURN_IF_FAILED(s3);
+ RETURN_IF_FAILED(s4);
+
+ return STATUS_SUCCESS;
+}
+
+//
+// RegisterCalloutBlockSplitAppsTx()
+//
+// Register callout with WFP. In all applicable layers.
+//
+// "Tx" (in transaction) suffix means there is no clean-up in failure paths.
+//
+NTSTATUS
+RegisterCalloutBlockSplitAppsTx
+(
+ PDEVICE_OBJECT DeviceObject,
+ HANDLE WfpSession
+)
+{
+ auto status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutBlockSplitApps,
+ &ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV4_CONN_KEY,
+ &FWPM_LAYER_ALE_AUTH_CONNECT_V4,
+ L"Mullvad Split Tunnel Blocking Callout (IPv4)",
+ L"Blocks unwanted connections in relation to splitting"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutBlockSplitApps,
+ &ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV4_RECV_KEY,
+ &FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4,
+ L"Mullvad Split Tunnel Blocking Callout (IPv4)",
+ L"Blocks unwanted connections in relation to splitting"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ UnregisterCalloutBlockSplitApps();
+
+ return status;
+ }
+
+ status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutBlockSplitApps,
+ &ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_CONN_KEY,
+ &FWPM_LAYER_ALE_AUTH_CONNECT_V6,
+ L"Mullvad Split Tunnel Blocking Callout (IPv6)",
+ L"Blocks unwanted connections in relation to splitting"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ UnregisterCalloutBlockSplitApps();
+
+ return status;
+ }
+
+ status = RegisterCalloutTx
+ (
+ DeviceObject,
+ WfpSession,
+ CalloutBlockSplitApps,
+ &ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_RECV_KEY,
+ &FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6,
+ L"Mullvad Split Tunnel Blocking Callout (IPv6)",
+ L"Blocks unwanted connections in relation to splitting"
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ UnregisterCalloutBlockSplitApps();
+
+ return status;
+ }
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+UnregisterCalloutBlockSplitApps
+(
+)
+{
+#define RETURN_IF_FAILED(status) \
+ if (!NT_SUCCESS(status) && status != STATUS_FWP_CALLOUT_NOT_FOUND) \
+ { \
+ return status; \
+ }
+
+ auto s1 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV4_CONN_KEY);
+ auto s2 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV4_RECV_KEY);
+ auto s3 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_CONN_KEY);
+ auto s4 = FwpsCalloutUnregisterByKey0(&ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_RECV_KEY);
+
+ RETURN_IF_FAILED(s1);
+ RETURN_IF_FAILED(s2);
+ RETURN_IF_FAILED(s3);
+ RETURN_IF_FAILED(s4);
+
+ return STATUS_SUCCESS;
+}
+
+} // namespace firewall
diff --git a/src/firewall/callouts.h b/src/firewall/callouts.h
new file mode 100644
index 0000000..fbf6cda
--- /dev/null
+++ b/src/firewall/callouts.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include
+
+namespace firewall
+{
+
+NTSTATUS
+RegisterCalloutClassifyBindTx
+(
+ PDEVICE_OBJECT DeviceObject,
+ HANDLE WfpSession
+);
+
+NTSTATUS
+UnregisterCalloutClassifyBind
+(
+);
+
+NTSTATUS
+RegisterCalloutPermitSplitAppsTx
+(
+ PDEVICE_OBJECT DeviceObject,
+ HANDLE WfpSession
+);
+
+NTSTATUS
+UnregisterCalloutPermitSplitApps
+(
+);
+
+NTSTATUS
+RegisterCalloutBlockSplitAppsTx
+(
+ PDEVICE_OBJECT DeviceObject,
+ HANDLE WfpSession
+);
+
+NTSTATUS
+UnregisterCalloutBlockSplitApps
+(
+);
+
+} // namespace firewall
diff --git a/src/firewall/constants.h b/src/firewall/constants.h
new file mode 100644
index 0000000..1d24348
--- /dev/null
+++ b/src/firewall/constants.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include
+
+namespace
+{
+
+static const UINT64 ST_MAX_FILTER_WEIGHT = MAXUINT64;
+static const UINT64 ST_HIGH_FILTER_WEIGHT = MAXUINT64 - 10;
+
+} // anonymous namespace
diff --git a/src/firewall/context.h b/src/firewall/context.h
new file mode 100644
index 0000000..65de998
--- /dev/null
+++ b/src/firewall/context.h
@@ -0,0 +1,106 @@
+#pragma once
+
+#include
+#include
+#include "firewall.h"
+#include "../ipaddr.h"
+#include "../procbroker/procbroker.h"
+
+namespace firewall
+{
+
+enum class IPV6_ACTION
+{
+ //
+ // There's an IPv6 address on both of the adapters we're working with.
+ // Split all IPV6 traffic.
+ //
+ SPLIT,
+
+ //
+ // Only the tunnel adapter has an IPV6 address.
+ // Block all IPv6 traffic to avoid it leaking inside the tunnel.
+ //
+ BLOCK,
+
+ //
+ // Only the internet connected adapter has an IPv6 address, or none
+ // of the adapters have one.
+ //
+ // Take no action.
+ //
+ NONE
+};
+
+struct IP_ADDRESSES_MGMT
+{
+ WDFWAITLOCK Lock;
+ ST_IP_ADDRESSES Addresses;
+ IPV6_ACTION Ipv6Action;
+};
+
+struct PENDED_BIND
+{
+ LIST_ENTRY ListEntry;
+
+ // Process that is trying to bind.
+ HANDLE ProcessId;
+
+ // Timestamp when record was created.
+ ULONGLONG Timestamp;
+
+ // Handle used to trigger re-auth or resume request processing.
+ UINT64 ClassifyHandle;
+
+ // Classification data for when we don't want a re-auth
+ // but instead wish to break and deny the bind.
+ FWPS_CLASSIFY_OUT0 ClassifyOut;
+
+ // The filter that triggered the classification.
+ UINT64 FilterId;
+
+ // Whether this is an IPv4 or IPv6 bind.
+ bool Ipv4;
+};
+
+struct PENDED_BIND_MGMT
+{
+ WDFWAITLOCK Lock;
+ LIST_ENTRY Records;
+};
+
+struct TRANSACTION_MGMT
+{
+ // Lock that is held for the duration of a transaction.
+ WDFWAITLOCK Lock;
+
+ // Indicator of active transaction.
+ bool Active;
+
+ // Thread ID of transaction owner.
+ HANDLE OwnerId;
+};
+
+struct CONTEXT
+{
+ bool SplittingEnabled;
+
+ CALLBACKS Callbacks;
+
+ HANDLE WfpSession;
+
+ IP_ADDRESSES_MGMT IpAddresses;
+
+ PENDED_BIND_MGMT PendedBinds;
+
+ procbroker::CONTEXT *ProcessEventBroker;
+
+ TRANSACTION_MGMT Transaction;
+
+ //
+ // Context used with the blocking subsystem.
+ //
+ void *BlockingContext;
+};
+
+} // namespace firewall
diff --git a/src/firewall/firewall.cpp b/src/firewall/firewall.cpp
new file mode 100644
index 0000000..1225965
--- /dev/null
+++ b/src/firewall/firewall.cpp
@@ -0,0 +1,1005 @@
+#include "wfp.h"
+#include "context.h"
+#include "identifiers.h"
+#include "blocking.h"
+#include "splitting.h"
+#include "callouts.h"
+#include "constants.h"
+#include "asyncbind.h"
+#include "../util.h"
+#include "firewall.h"
+
+namespace firewall
+{
+
+namespace
+{
+
+//
+// CreateWfpSession()
+//
+// Create dynamic WFP session that will be used for all filters etc.
+//
+NTSTATUS
+CreateWfpSession
+(
+ HANDLE *WfpSession
+)
+{
+ FWPM_SESSION0 sessionInfo = { 0 };
+
+ sessionInfo.flags = FWPM_SESSION_FLAG_DYNAMIC;
+
+ const auto status = FwpmEngineOpen0(NULL, RPC_C_AUTHN_DEFAULT, NULL, &sessionInfo, WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ *WfpSession = 0;
+ }
+
+ return status;
+}
+
+NTSTATUS
+DestroyWfpSession
+(
+ HANDLE WfpSession
+)
+{
+ return FwpmEngineClose0(WfpSession);
+}
+
+//
+// ConfigureWfp()
+//
+// Register structural objects with WFP.
+// Essentially making everything ready for installing callouts and filters.
+//
+// "Tx" (in transaction) suffix means there is no clean-up in failure paths.
+//
+NTSTATUS
+ConfigureWfpTx
+(
+ HANDLE WfpSession,
+ CONTEXT *Context
+)
+{
+ FWPM_PROVIDER0 provider = { 0 };
+
+ const auto ProviderName = L"Mullvad Split Tunnel";
+ const auto ProviderDescription = L"Manages filters and callouts that aid in implementing split tunneling";
+
+ provider.providerKey = ST_FW_PROVIDER_KEY;
+ provider.displayData.name = const_cast(ProviderName);
+ provider.displayData.description = const_cast(ProviderDescription);
+
+ auto status = FwpmProviderAdd0(WfpSession, &provider, NULL);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ FWPM_PROVIDER_CONTEXT1 pc = { 0 };
+
+ const auto ProviderContextName = L"Mullvad Split Tunnel Provider Context";
+ const auto ProviderContextDescription = L"Exposes context data to callouts";
+
+ FWP_BYTE_BLOB blob = { .size = sizeof(CONTEXT*), .data = (UINT8*)&Context };
+
+ pc.providerContextKey = ST_FW_PROVIDER_CONTEXT_KEY;
+ pc.displayData.name = const_cast(ProviderContextName);
+ pc.displayData.description = const_cast(ProviderContextDescription);
+ pc.providerKey = const_cast(&ST_FW_PROVIDER_KEY);
+ pc.type = FWPM_GENERAL_CONTEXT;
+ pc.dataBuffer = &blob;
+
+ status = FwpmProviderContextAdd1(WfpSession, &pc, NULL, NULL);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ //
+ // Adding a specific sublayer for split tunneling is futile unless a hard permit
+ // applied by the connect callout overrides filters registered by winfw
+ // - which it won't.
+ //
+ // A hard permit applied by a callout doesn't seem to be respected at all.
+ //
+ // Using a plain filter with no callout, it's possible to sometimes make
+ // a hard permit override a lower-weighted block, but it's not entirely consistent.
+ //
+ // And even then, it's not applicable to what we're doing since the logic
+ // applied here cannot be expressed using a plain filter.
+ //
+
+ return STATUS_SUCCESS;
+}
+
+void
+UpdateIpv6Action
+(
+ IP_ADDRESSES_MGMT *IpAddresses
+)
+{
+ if (ip::ValidTunnelIpv6Address(&IpAddresses->Addresses))
+ {
+ IpAddresses->Ipv6Action =
+ (ip::ValidInternetIpv6Address(&IpAddresses->Addresses)
+ ? IPV6_ACTION::SPLIT
+ : IPV6_ACTION::BLOCK);
+ }
+ else
+ {
+ IpAddresses->Ipv6Action = IPV6_ACTION::NONE;
+ }
+}
+
+NTSTATUS
+UnregisterCallouts
+(
+)
+{
+#define RETURN_IF_FAILED(status) \
+ if (!NT_SUCCESS(status)) \
+ { \
+ DbgPrint("Could not unregister callout\n"); \
+ return status; \
+ }
+
+ auto s1 = UnregisterCalloutBlockSplitApps();
+ auto s2 = UnregisterCalloutPermitSplitApps();
+ auto s3 = UnregisterCalloutClassifyBind();
+
+ RETURN_IF_FAILED(s1);
+ RETURN_IF_FAILED(s2);
+ RETURN_IF_FAILED(s3);
+
+ return STATUS_SUCCESS;
+}
+
+//
+// RegisterCallouts()
+//
+// The reason we need this function is because the called functions are individually
+// safe if called inside a transaction.
+//
+// But a successful call is not undone by destroying the transaction.
+//
+NTSTATUS
+RegisterCallouts
+(
+ PDEVICE_OBJECT DeviceObject,
+ HANDLE WfpSession
+)
+{
+ auto status = RegisterCalloutClassifyBindTx(DeviceObject, WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not register callout\n");
+
+ return status;
+ }
+
+ status = RegisterCalloutPermitSplitAppsTx(DeviceObject, WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not register callout\n");
+
+ UnregisterCallouts();
+
+ return status;
+ }
+
+ status = RegisterCalloutBlockSplitAppsTx(DeviceObject, WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not register callout\n");
+
+ UnregisterCallouts();
+
+ return status;
+ }
+
+ return STATUS_SUCCESS;
+}
+
+PROCESS_SPLIT_VERDICT
+NTAPI
+DummyQueryProcessFunc
+(
+ HANDLE ProcessId,
+ void *Context
+)
+{
+ UNREFERENCED_PARAMETER(ProcessId);
+ UNREFERENCED_PARAMETER(Context);
+
+ return PROCESS_SPLIT_VERDICT::DONT_SPLIT;
+}
+
+//
+// ResetClientCallbacks()
+//
+// This function is used if callouts/filters can't be unregistered.
+//
+// It's assumed that the driver was tearing down all subsystems in preparation
+// for unloading.
+//
+// Other parts of the driver may no longer be available so we have to prevent
+// callouts from accessing these parts through previously registered callbacks.
+//
+void
+ResetClientCallbacks
+(
+ CONTEXT *Context
+)
+{
+ Context->Callbacks.QueryProcess = DummyQueryProcessFunc;
+ Context->Callbacks.Context = NULL;
+}
+
+} // anonymous namespace
+
+//
+// Initialize()
+//
+// Initialize data structures and locks etc.
+//
+// Configure WFP.
+//
+// We don't actually need a transaction here, if there are any failures
+// we destroy the entire WFP session, which resets everything.
+//
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ PDEVICE_OBJECT DeviceObject,
+ const CALLBACKS *Callbacks,
+ procbroker::CONTEXT *ProcessEventBroker
+)
+{
+ auto context = (CONTEXT*)ExAllocatePoolWithTag(NonPagedPool, sizeof(CONTEXT), ST_POOL_TAG);
+
+ if (context == NULL)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ RtlZeroMemory(context, sizeof(*context));
+
+ InitializeListHead(&context->PendedBinds.Records);
+
+ context->IpAddresses.Ipv6Action = IPV6_ACTION::NONE;
+ context->Callbacks = *Callbacks;
+ context->ProcessEventBroker = ProcessEventBroker;
+
+ auto status = WdfWaitLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &context->IpAddresses.Lock);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfWaitLockCreate() failed 0x%X\n", status);
+
+ context->IpAddresses.Lock = NULL;
+
+ goto Abort;
+ }
+
+ status = WdfWaitLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &context->PendedBinds.Lock);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfWaitLockCreate() failed 0x%X\n", status);
+
+ context->PendedBinds.Lock = NULL;
+
+ goto Abort_delete_ip_lock;
+ }
+
+ status = WdfWaitLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &context->Transaction.Lock);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfWaitLockCreate() failed 0x%X\n", status);
+
+ context->Transaction.Lock = NULL;
+
+ goto Abort_delete_bind_lock;
+ }
+
+ status = CreateWfpSession(&context->WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_delete_transaction_lock;
+ }
+
+ status = ConfigureWfpTx(context->WfpSession, context);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_destroy_session;
+ }
+
+ status = RegisterCallouts(DeviceObject, context->WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_destroy_session;
+ }
+
+ status = blocking::Initialize(context->WfpSession, &context->BlockingContext);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_unregister_callouts;
+ }
+
+ status = procbroker::Subscribe(ProcessEventBroker, HandleProcessEvent, context);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_teardown_blocking;
+ }
+
+ *Context = context;
+
+ return STATUS_SUCCESS;
+
+Abort_teardown_blocking:
+
+ blocking::TearDown(&context->BlockingContext);
+
+Abort_unregister_callouts:
+
+ UnregisterCallouts();
+
+Abort_destroy_session:
+
+ DestroyWfpSession(context->WfpSession);
+
+Abort_delete_transaction_lock:
+
+ WdfObjectDelete(context->Transaction.Lock);
+
+Abort_delete_bind_lock:
+
+ WdfObjectDelete(context->PendedBinds.Lock);
+
+Abort_delete_ip_lock:
+
+ WdfObjectDelete(context->IpAddresses.Lock);
+
+Abort:
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+
+ *Context = NULL;
+
+ return status;
+}
+
+//
+// TearDown()
+//
+// Destroy WFP session along with all filters.
+// Release resources.
+//
+// If the return value is not successful, it means the following:
+//
+// The context used by callouts has been updated to make callouts return early,
+// thereby avoiding crashes.
+//
+// The callouts are still registered with the system so the driver cannot be unloaded.
+//
+NTSTATUS
+TearDown
+(
+ CONTEXT **Context
+)
+{
+ auto context = *Context;
+
+ *Context = NULL;
+
+ //
+ // Clean up adjacent systems.
+ //
+
+ procbroker::CancelSubscription(context->ProcessEventBroker, HandleProcessEvent);
+
+ FailPendedBinds(context);
+
+ WdfObjectDelete(context->PendedBinds.Lock);
+
+ blocking::TearDown(&context->BlockingContext);
+
+ //
+ // Since we're using a dynamic session we don't actually
+ // have to remove all WFP objects one by one.
+ //
+ // Everything will be cleaned up when the session is ended.
+ //
+ // (Except for callout registrations.)
+ //
+
+ auto status = DestroyWfpSession(context->WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WFP session could not be cleaned up: 0x%X\n", status);
+
+ ResetClientCallbacks(context);
+
+ // Leak context structure.
+ return status;
+ }
+
+ status = UnregisterCallouts();
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("One or more callouts could not be unregistered: 0x%X\n", status);
+
+ ResetClientCallbacks(context);
+
+ // Leak context structure.
+ return status;
+ }
+
+ WdfObjectDelete(context->IpAddresses.Lock);
+
+ WdfObjectDelete(context->Transaction.Lock);
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+
+ return STATUS_SUCCESS;
+}
+
+//
+// EnableSplitting()
+//
+// Register all filters required for splitting.
+//
+NTSTATUS
+EnableSplitting
+(
+ CONTEXT *Context,
+ const ST_IP_ADDRESSES *IpAddresses
+)
+{
+ NT_ASSERT(!Context->SplittingEnabled);
+ NT_ASSERT(!Context->Transaction.Active);
+
+ if (Context->SplittingEnabled || Context->Transaction.Active)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ //
+ // There are no readers at this time so we can update at leasure and without
+ // taking the lock.
+ //
+
+ Context->IpAddresses.Addresses = *IpAddresses;
+
+ UpdateIpv6Action(&Context->IpAddresses);
+
+ const auto registerIpv6 = (Context->IpAddresses.Ipv6Action == IPV6_ACTION::SPLIT);
+ const auto blockIpv6 = (Context->IpAddresses.Ipv6Action == IPV6_ACTION::BLOCK);
+
+ //
+ // Update WFP inside a transaction.
+ //
+
+ auto status = FwpmTransactionBegin0(Context->WfpSession, 0);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = RegisterFilterBindRedirectTx(Context->WfpSession, registerIpv6);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ status = RegisterFilterPermitSplitAppsTx
+ (
+ Context->WfpSession,
+ &Context->IpAddresses.Addresses.TunnelIpv4,
+ registerIpv6 ? &Context->IpAddresses.Addresses.TunnelIpv6 : NULL
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ //
+ // If we are not splitting IPv6, we may need to block it.
+ //
+
+ if (blockIpv6)
+ {
+ status = blocking::RegisterFilterBlockSplitAppsIpv6Tx(Context->BlockingContext);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+ }
+
+ //
+ // Commit filters.
+ //
+
+ status = FwpmTransactionCommit0(Context->WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Failed to commit transaction\n");
+
+ goto Abort;
+ }
+
+ Context->SplittingEnabled = true;
+
+ return STATUS_SUCCESS;
+
+Abort:
+
+ //
+ // Do not overwrite error code in status variable.
+ //
+
+ if (!NT_SUCCESS(FwpmTransactionAbort0(Context->WfpSession)))
+ {
+ DbgPrint("Failed to abort transaction\n");
+ }
+
+ return status;
+}
+
+//
+// DisableSplitting()
+//
+// Remove all filters associated with splitting.
+//
+NTSTATUS
+DisableSplitting
+(
+ CONTEXT *Context
+)
+{
+ NT_ASSERT(Context->SplittingEnabled);
+ NT_ASSERT(!Context->Transaction.Active);
+
+ if (!Context->SplittingEnabled || Context->Transaction.Active)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ //
+ // Use double transaction because resetting blocking subsystem requires this.
+ //
+
+ auto status = TransactionBegin(Context);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ const auto removeIpv6 = (Context->IpAddresses.Ipv6Action == IPV6_ACTION::SPLIT);
+
+ status = RemoveFilterBindRedirectTx(Context->WfpSession, removeIpv6);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ status = RemoveFilterPermitSplitAppsTx(Context->WfpSession, removeIpv6);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ status = blocking::ResetTx2(Context->BlockingContext);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ status = TransactionCommit(Context);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Failed to commit transaction\n");
+
+ goto Abort;
+ }
+
+ Context->SplittingEnabled = false;
+
+ return STATUS_SUCCESS;
+
+Abort:
+
+ //
+ // Do not overwrite error code in status variable.
+ //
+
+ if (!NT_SUCCESS(TransactionAbort(Context)))
+ {
+ DbgPrint("Failed to abort transaction\n");
+ }
+
+ return status;
+}
+
+NTSTATUS
+RegisterUpdatedIpAddresses
+(
+ CONTEXT *Context,
+ const ST_IP_ADDRESSES *IpAddresses
+)
+{
+ if (!Context->SplittingEnabled)
+ {
+ return STATUS_SUCCESS;
+ }
+
+ //
+ // Create temporary management structure for IP addresses.
+ //
+
+ IP_ADDRESSES_MGMT IpMgmt;
+
+ IpMgmt.Addresses = *IpAddresses;
+
+ UpdateIpv6Action(&IpMgmt);
+
+ const auto registerIpv6 = (IpMgmt.Ipv6Action == IPV6_ACTION::SPLIT);
+ const auto blockIpv6 = (IpMgmt.Ipv6Action == IPV6_ACTION::BLOCK);
+
+ //
+ // Using a transaction, remove and add back relevant filters.
+ //
+ // Relevant filters in this case are all those that directly reference an IP address
+ // or are registered conditionally depending on which IP addresses are present.
+ //
+
+ auto status = FwpmTransactionBegin0(Context->WfpSession, 0);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ const auto removeIpv6 = (Context->IpAddresses.Ipv6Action == IPV6_ACTION::SPLIT);
+ const auto removeBlockIpv6 = (Context->IpAddresses.Ipv6Action == IPV6_ACTION::BLOCK);
+
+ if (registerIpv6 != removeIpv6)
+ {
+ status = RemoveFilterBindRedirectTx(Context->WfpSession, removeIpv6);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ status = RegisterFilterBindRedirectTx(Context->WfpSession, registerIpv6);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+ }
+
+ status = RemoveFilterPermitSplitAppsTx(Context->WfpSession, removeIpv6);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ status = RegisterFilterPermitSplitAppsTx
+ (
+ Context->WfpSession,
+ &IpMgmt.Addresses.TunnelIpv4,
+ registerIpv6 ? &IpMgmt.Addresses.TunnelIpv6 : NULL
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ if (blockIpv6 != removeBlockIpv6)
+ {
+ status = blocking::RemoveFilterBlockSplitAppsIpv6Tx(Context->BlockingContext);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ status = blocking::RegisterFilterBlockSplitAppsIpv6Tx(Context->BlockingContext);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+ }
+
+ //
+ // Update blocking subsystem.
+ //
+
+ status = blocking::TransactionBegin(Context->BlockingContext);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort;
+ }
+
+ status = blocking::UpdateBlockingFiltersTx2(Context->BlockingContext,
+ &IpMgmt.Addresses.TunnelIpv4, &IpMgmt.Addresses.TunnelIpv6);
+
+ if (!NT_SUCCESS(status))
+ {
+ blocking::TransactionAbort(Context->BlockingContext);
+
+ goto Abort;
+ }
+
+ //
+ // Finalize.
+ //
+
+ status = FwpmTransactionCommit0(Context->WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ blocking::TransactionAbort(Context->BlockingContext);
+
+ DbgPrint("Failed to commit transaction\n");
+
+ goto Abort;
+ }
+
+ blocking::TransactionCommit(Context->BlockingContext);
+
+ WdfWaitLockAcquire(Context->IpAddresses.Lock, NULL);
+
+ Context->IpAddresses.Addresses = IpMgmt.Addresses;
+ Context->IpAddresses.Ipv6Action = IpMgmt.Ipv6Action;
+
+ WdfWaitLockRelease(Context->IpAddresses.Lock);
+
+ return STATUS_SUCCESS;
+
+Abort:
+
+ //
+ // Do not overwrite error code in status variable.
+ //
+
+ if (!NT_SUCCESS(FwpmTransactionAbort0(Context->WfpSession)))
+ {
+ DbgPrint("Failed to abort transaction\n");
+ }
+
+ return status;
+}
+
+NTSTATUS
+TransactionBegin
+(
+ CONTEXT *Context
+)
+{
+ NT_ASSERT(Context->SplittingEnabled);
+
+ if (!Context->SplittingEnabled)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ WdfWaitLockAcquire(Context->Transaction.Lock, NULL);
+
+ auto status = FwpmTransactionBegin0(Context->WfpSession, 0);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not create WFP transaction: 0x%X", status);
+
+ goto Abort;
+ }
+
+ status = blocking::TransactionBegin(Context->BlockingContext);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not create transaction in blocking subsystem: 0x%X", status);
+
+ goto Abort_cancel_wfp;
+ }
+
+ Context->Transaction.OwnerId = PsGetCurrentThreadId();
+ Context->Transaction.Active = true;
+
+ return STATUS_SUCCESS;
+
+Abort_cancel_wfp:
+
+ auto s2 = FwpmTransactionAbort0(Context->WfpSession);
+
+ if (!NT_SUCCESS(s2))
+ {
+ DbgPrint("Could not abort WFP transaction: 0x%X", s2);
+ }
+
+Abort:
+
+ WdfWaitLockRelease(Context->Transaction.Lock);
+
+ return status;
+}
+
+NTSTATUS
+TransactionCommit
+(
+ CONTEXT *Context
+)
+{
+ NT_ASSERT(Context->SplittingEnabled);
+ NT_ASSERT(Context->Transaction.Active);
+
+ if (!Context->SplittingEnabled || !Context->Transaction.Active)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ if (Context->Transaction.OwnerId != PsGetCurrentThreadId())
+ {
+ DbgPrint("TransactionCommit() called by other than transaction owner");
+
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ auto status = FwpmTransactionCommit0(Context->WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not commit WFP transaction: 0x%X", status);
+
+ return status;
+ }
+
+ blocking::TransactionCommit(Context->BlockingContext);
+
+ Context->Transaction.OwnerId = NULL;
+ Context->Transaction.Active = false;
+
+ WdfWaitLockRelease(Context->Transaction.Lock);
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+TransactionAbort
+(
+ CONTEXT *Context
+)
+{
+ NT_ASSERT(Context->SplittingEnabled);
+ NT_ASSERT(Context->Transaction.Active);
+
+ if (!Context->SplittingEnabled || !Context->Transaction.Active)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ if (Context->Transaction.OwnerId != PsGetCurrentThreadId())
+ {
+ DbgPrint("TransactionAbort() called by other than transaction owner");
+
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ auto status = FwpmTransactionAbort0(Context->WfpSession);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not abort WFP transaction: 0x%X", status);
+
+ return status;
+ }
+
+ blocking::TransactionAbort(Context->BlockingContext);
+
+ Context->Transaction.OwnerId = NULL;
+ Context->Transaction.Active = false;
+
+ WdfWaitLockRelease(Context->Transaction.Lock);
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+RegisterAppBecomingSplitTx2
+(
+ CONTEXT *Context,
+ const LOWER_UNICODE_STRING *ImageName
+)
+{
+ NT_ASSERT(Context->SplittingEnabled);
+ NT_ASSERT(Context->Transaction.Active);
+
+ if (!Context->SplittingEnabled || !Context->Transaction.Active)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ if (Context->Transaction.OwnerId != PsGetCurrentThreadId())
+ {
+ DbgPrint("RegisterAppBecomingSplitTx2() called by other than transaction owner");
+
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ WdfWaitLockAcquire(Context->IpAddresses.Lock, NULL);
+
+ auto ipv4 = Context->IpAddresses.Addresses.TunnelIpv4;
+ auto ipv6 = Context->IpAddresses.Addresses.TunnelIpv6;
+
+ WdfWaitLockRelease(Context->IpAddresses.Lock);
+
+ return blocking::RegisterFilterBlockSplitAppTx2
+ (
+ Context->BlockingContext,
+ ImageName,
+ &ipv4,
+ &ipv6
+ );
+}
+
+NTSTATUS
+RegisterAppBecomingUnsplitTx2
+(
+ CONTEXT *Context,
+ const LOWER_UNICODE_STRING *ImageName
+)
+{
+ NT_ASSERT(Context->SplittingEnabled);
+ NT_ASSERT(Context->Transaction.Active);
+
+ if (!Context->SplittingEnabled || !Context->Transaction.Active)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ if (Context->Transaction.OwnerId != PsGetCurrentThreadId())
+ {
+ DbgPrint("RegisterAppBecomingUnsplitTx2() called by other than transaction owner");
+
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ return blocking::RemoveFilterBlockSplitAppTx2(Context->BlockingContext, ImageName);
+}
+
+} // namespace firewall
diff --git a/src/firewall/firewall.h b/src/firewall/firewall.h
new file mode 100644
index 0000000..dfbc1b4
--- /dev/null
+++ b/src/firewall/firewall.h
@@ -0,0 +1,117 @@
+#pragma once
+
+#include
+#include "../ipaddr.h"
+#include "../defs/types.h"
+#include "../procbroker/procbroker.h"
+
+namespace firewall
+{
+
+struct CONTEXT;
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// Callback definitions.
+// Client(s) of the firewall subsystem provide the implementations.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+enum class PROCESS_SPLIT_VERDICT
+{
+ DO_SPLIT,
+ DONT_SPLIT,
+
+ // PID is unknown
+ UNKNOWN
+};
+
+typedef
+PROCESS_SPLIT_VERDICT
+(NTAPI *QUERY_PROCESS_FUNC)
+(
+ HANDLE ProcessId,
+ void *Context
+);
+
+typedef struct tag_CALLBACKS
+{
+ QUERY_PROCESS_FUNC QueryProcess;
+ void *Context;
+}
+CALLBACKS;
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// Public functions.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ PDEVICE_OBJECT DeviceObject,
+ const CALLBACKS *Callbacks,
+ procbroker::CONTEXT *ProcessEventBroker
+);
+
+NTSTATUS
+TearDown
+(
+ CONTEXT **Context
+);
+
+NTSTATUS
+EnableSplitting
+(
+ CONTEXT *Context,
+ const ST_IP_ADDRESSES *IpAddresses
+);
+
+NTSTATUS
+DisableSplitting
+(
+ CONTEXT *Context
+);
+
+NTSTATUS
+RegisterUpdatedIpAddresses
+(
+ CONTEXT *Context,
+ const ST_IP_ADDRESSES *IpAddresses
+);
+
+NTSTATUS
+TransactionBegin
+(
+ CONTEXT *Context
+);
+
+NTSTATUS
+TransactionCommit
+(
+ CONTEXT *Context
+);
+
+NTSTATUS
+TransactionAbort
+(
+ CONTEXT *Context
+);
+
+NTSTATUS
+RegisterAppBecomingSplitTx2
+(
+ CONTEXT *Context,
+ const LOWER_UNICODE_STRING *ImageName
+);
+
+NTSTATUS
+RegisterAppBecomingUnsplitTx2
+(
+ CONTEXT *Context,
+ const LOWER_UNICODE_STRING *ImageName
+);
+
+} // namespace firewall
diff --git a/src/firewall/identifiers.h b/src/firewall/identifiers.h
new file mode 100644
index 0000000..8fba46a
--- /dev/null
+++ b/src/firewall/identifiers.h
@@ -0,0 +1,97 @@
+#pragma once
+
+#include
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// Identifiers used with WFP.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+// {E2C114EE-F32A-4264-A6CB-3FA7996356D9}
+DEFINE_GUID(ST_FW_PROVIDER_KEY,
+ 0xe2c114ee, 0xf32a, 0x4264, 0xa6, 0xcb, 0x3f, 0xa7, 0x99, 0x63, 0x56, 0xd9);
+
+// {76653805-1972-45D1-B47C-3140AEBABC49}
+DEFINE_GUID(ST_FW_CALLOUT_CLASSIFY_BIND_IPV4_KEY,
+ 0x76653805, 0x1972, 0x45d1, 0xb4, 0x7c, 0x31, 0x40, 0xae, 0xba, 0xbc, 0x49);
+
+// {53FB3120-B6A4-462B-BFFC-6978AADA1DA2}
+DEFINE_GUID(ST_FW_CALLOUT_CLASSIFY_BIND_IPV6_KEY,
+ 0x53fb3120, 0xb6a4, 0x462b, 0xbf, 0xfc, 0x69, 0x78, 0xaa, 0xda, 0x1d, 0xa2);
+
+// {33F3EDCC-EB5E-41CF-9250-702C94A28E39}
+DEFINE_GUID(ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV4_CONN_KEY,
+ 0x33f3edcc, 0xeb5e, 0x41cf, 0x92, 0x50, 0x70, 0x2c, 0x94, 0xa2, 0x8e, 0x39);
+
+// {A7A13809-0DE6-48AB-9BB8-20A8BCEC37AB}
+DEFINE_GUID(ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV4_RECV_KEY,
+ 0xa7a13809, 0xde6, 0x48ab, 0x9b, 0xb8, 0x20, 0xa8, 0xbc, 0xec, 0x37, 0xab);
+
+// {7B7E0055-89F5-4760-8928-CCD57C8830AB}
+DEFINE_GUID(ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV6_CONN_KEY,
+ 0x7b7e0055, 0x89f5, 0x4760, 0x89, 0x28, 0xcc, 0xd5, 0x7c, 0x88, 0x30, 0xab);
+
+// {B40B78EF-5642-40EF-AC4D-F9651261F9E7}
+DEFINE_GUID(ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV6_RECV_KEY,
+ 0xb40b78ef, 0x5642, 0x40ef, 0xac, 0x4d, 0xf9, 0x65, 0x12, 0x61, 0xf9, 0xe7);
+
+// {974AA588-397A-483E-AC29-88F4F4112AC2}
+DEFINE_GUID(ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV4_CONN_KEY,
+ 0x974aa588, 0x397a, 0x483e, 0xac, 0x29, 0x88, 0xf4, 0xf4, 0x11, 0x2a, 0xc2);
+
+// {8E314FD7-BDD3-45A4-A712-46036B25B3E1}
+DEFINE_GUID(ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV4_RECV_KEY,
+ 0x8e314fd7, 0xbdd3, 0x45a4, 0xa7, 0x12, 0x46, 0x3, 0x6b, 0x25, 0xb3, 0xe1);
+
+// {466B7800-5EF4-4772-AA79-E0A834328214}
+DEFINE_GUID(ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_CONN_KEY,
+ 0x466b7800, 0x5ef4, 0x4772, 0xaa, 0x79, 0xe0, 0xa8, 0x34, 0x32, 0x82, 0x14);
+
+// {D25AFB1B-4645-43CB-B0BE-3794FE487BAC}
+DEFINE_GUID(ST_FW_CALLOUT_BLOCK_SPLIT_APPS_IPV6_RECV_KEY,
+ 0xd25afb1b, 0x4645, 0x43cb, 0xb0, 0xbe, 0x37, 0x94, 0xfe, 0x48, 0x7b, 0xac);
+
+// {B47D14A7-AEED-48B9-AD4E-5529619F1337}
+DEFINE_GUID(ST_FW_FILTER_CLASSIFY_BIND_IPV4_KEY,
+ 0xb47d14a7, 0xaeed, 0x48b9, 0xad, 0x4e, 0x55, 0x29, 0x61, 0x9f, 0x13, 0x37);
+
+// {2F607222-B2EB-443C-B6E0-641067375478}
+DEFINE_GUID(ST_FW_FILTER_CLASSIFY_BIND_IPV6_KEY,
+ 0x2f607222, 0xb2eb, 0x443c, 0xb6, 0xe0, 0x64, 0x10, 0x67, 0x37, 0x54, 0x78);
+
+// {66CED079-C270-4B4D-A45C-D11711C0D600}
+DEFINE_GUID(ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV4_CONN_KEY,
+ 0x66ced079, 0xc270, 0x4b4d, 0xa4, 0x5c, 0xd1, 0x17, 0x11, 0xc0, 0xd6, 0x0);
+
+// {37972155-EBDB-49FC-9A37-3A0B3B0AA100}
+DEFINE_GUID(ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV4_RECV_KEY,
+ 0x37972155, 0xebdb, 0x49fc, 0x9a, 0x37, 0x3a, 0xb, 0x3b, 0xa, 0xa1, 0x0);
+
+// {0AFA08E3-B010-4082-9E03-1CC4BE1C6CF8}
+DEFINE_GUID(ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV6_CONN_KEY,
+ 0xafa08e3, 0xb010, 0x4082, 0x9e, 0x3, 0x1c, 0xc4, 0xbe, 0x1c, 0x6c, 0xf8);
+
+// {7835DFD7-24AE-44F4-8A8A-5E9C766AAE63}
+DEFINE_GUID(ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV6_RECV_KEY,
+ 0x7835dfd7, 0x24ae, 0x44f4, 0x8a, 0x8a, 0x5e, 0x9c, 0x76, 0x6a, 0xae, 0x63);
+
+// {05CB3C5E-6F64-44F7-81B1-C890563FA280}
+DEFINE_GUID(ST_FW_FILTER_BLOCK_ALL_SPLIT_APPS_IPV6_CONN_KEY,
+ 0x5cb3c5e, 0x6f64, 0x44f7, 0x81, 0xb1, 0xc8, 0x90, 0x56, 0x3f, 0xa2, 0x80);
+
+// {C854E73A-81C8-4814-9A55-55BAF2C3BD17}
+DEFINE_GUID(ST_FW_FILTER_BLOCK_ALL_SPLIT_APPS_IPV6_RECV_KEY,
+ 0xc854e73a, 0x81c8, 0x4814, 0x9a, 0x55, 0x55, 0xba, 0xf2, 0xc3, 0xbd, 0x17);
+
+//
+// This sublayer is defined and registered by `winfw`.
+// We're going to reuse it to avoid having different sublayers fight over
+// whether something should be blocked or permitted.
+//
+DEFINE_GUID(ST_FW_WINFW_BASELINE_SUBLAYER_KEY,
+ 0xc78056ff, 0x2bc1, 0x4211, 0xaa, 0xdd, 0x7f, 0x35, 0x8d, 0xef, 0x20, 0x2d);
+
+// {FDC95593-04EF-415C-AE68-46BD8B4821A8}
+DEFINE_GUID(ST_FW_PROVIDER_CONTEXT_KEY,
+ 0xfdc95593, 0x4ef, 0x415c, 0xae, 0x68, 0x46, 0xbd, 0x8b, 0x48, 0x21, 0xa8);
diff --git a/src/firewall/splitting.cpp b/src/firewall/splitting.cpp
new file mode 100644
index 0000000..1ff5d1c
--- /dev/null
+++ b/src/firewall/splitting.cpp
@@ -0,0 +1,420 @@
+#include "../util.h"
+#include "identifiers.h"
+#include "constants.h"
+#include "splitting.h"
+
+namespace firewall
+{
+
+//
+// RewriteBind()
+//
+// This is where the splitting happens.
+// Move socket binds from tunnel interface to the internet connected interface.
+//
+void
+RewriteBind
+(
+ CONTEXT *Context,
+ const FWPS_INCOMING_VALUES0 *FixedValues,
+ const FWPS_INCOMING_METADATA_VALUES0 *MetaValues,
+ UINT64 FilterId,
+ const void *ClassifyContext,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut
+)
+{
+ UNREFERENCED_PARAMETER(MetaValues);
+
+ UINT64 classifyHandle = 0;
+
+ auto status = FwpsAcquireClassifyHandle0
+ (
+ const_cast(ClassifyContext),
+ 0,
+ &classifyHandle
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("FwpsAcquireClassifyHandle0() failed 0x%X\n", status);
+
+ return;
+ }
+
+ FWPS_BIND_REQUEST0 *bindRequest = NULL;
+
+ status = FwpsAcquireWritableLayerDataPointer0
+ (
+ classifyHandle,
+ FilterId,
+ 0,
+ (PVOID*)&bindRequest,
+ ClassifyOut
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("FwpsAcquireWritableLayerDataPointer0() failed 0x%X\n", status);
+
+ goto Cleanup_handle;
+ }
+
+ //
+ // According to documentation, FwpsAcquireWritableLayerDataPointer0() will update the
+ // `actionType` and `rights` fields with poorly chosen values:
+ //
+ // ```
+ // classifyOut->actionType = FWP_ACTION_BLOCK
+ // classifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE
+ // ```
+ //
+ // However, in practice it seems to not make any changes to those fields.
+ // But if it did we'd want to ensure the fields have sane values.
+ //
+
+ ClassifyOut->actionType = FWP_ACTION_CONTINUE;
+ ClassifyOut->rights |= FWPS_RIGHT_ACTION_WRITE;
+
+ //
+ // There's a list with redirection history.
+ //
+ // This only ever comes into play if several callouts are fighting to redirect the bind.
+ //
+ // To prevent recursion, we need to check if we're on the list, and abort if so.
+ //
+
+ for (auto history = bindRequest->previousVersion;
+ history != NULL;
+ history = history->previousVersion)
+ {
+ if (history->modifierFilterId == FilterId)
+ {
+ DbgPrint("Aborting bind processing because already redirected by us\n");
+
+ goto Cleanup_data;
+ }
+ }
+
+ //
+ // Rewrite bind as applicable.
+ //
+
+ const bool ipv4 = FixedValues->layerId == FWPS_LAYER_ALE_BIND_REDIRECT_V4;
+
+ WdfWaitLockAcquire(Context->IpAddresses.Lock, NULL);
+
+ if (ipv4)
+ {
+ auto bindTarget = (SOCKADDR_IN*)&(bindRequest->localAddressAndPort);
+
+ DbgPrint("Bind request eligible for splitting: %d.%d.%d.%d:%d\n",
+ bindTarget->sin_addr.S_un.S_un_b.s_b1,
+ bindTarget->sin_addr.S_un.S_un_b.s_b2,
+ bindTarget->sin_addr.S_un.S_un_b.s_b3,
+ bindTarget->sin_addr.S_un.S_un_b.s_b4,
+ ntohs(bindTarget->sin_port)
+ );
+
+ if (IN4_IS_ADDR_UNSPECIFIED(&(bindTarget->sin_addr))
+ || IN4_ADDR_EQUAL(&(bindTarget->sin_addr), &(Context->IpAddresses.Addresses.TunnelIpv4)))
+ {
+ DbgPrint("SPLITTING\n");
+
+ bindTarget->sin_addr = Context->IpAddresses.Addresses.InternetIpv4;
+
+ ClassifyOut->actionType = FWP_ACTION_PERMIT;
+ ClassifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE;
+ }
+ }
+ else
+ {
+ auto bindTarget = (SOCKADDR_IN6*)&(bindRequest->localAddressAndPort);
+
+ DbgPrint("Bind request eligible for splitting: [%X:%X:%X:%X:%X:%X:%X:%X]:%d\n",
+ ntohs(bindTarget->sin6_addr.u.Word[0]),
+ ntohs(bindTarget->sin6_addr.u.Word[1]),
+ ntohs(bindTarget->sin6_addr.u.Word[2]),
+ ntohs(bindTarget->sin6_addr.u.Word[3]),
+ ntohs(bindTarget->sin6_addr.u.Word[4]),
+ ntohs(bindTarget->sin6_addr.u.Word[5]),
+ ntohs(bindTarget->sin6_addr.u.Word[6]),
+ ntohs(bindTarget->sin6_addr.u.Word[7]),
+ ntohs(bindTarget->sin6_port)
+ );
+
+ static const IN6_ADDR IN6_ADDR_ANY = { 0 };
+
+ if (IN6_ADDR_EQUAL(&(bindTarget->sin6_addr), &IN6_ADDR_ANY)
+ || IN6_ADDR_EQUAL(&(bindTarget->sin6_addr), &(Context->IpAddresses.Addresses.TunnelIpv6)))
+ {
+ DbgPrint("SPLITTING\n");
+
+ bindTarget->sin6_addr = Context->IpAddresses.Addresses.InternetIpv6;
+
+ ClassifyOut->actionType = FWP_ACTION_PERMIT;
+ ClassifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE;
+ }
+ }
+
+ WdfWaitLockRelease(Context->IpAddresses.Lock);
+
+Cleanup_data:
+
+ //
+ // Call the "apply" function even in instances where we've made no changes
+ // to the data, because it was deemed not necessary, or aborting for some other reason.
+ //
+ // This is the correct logic according to documentation.
+ //
+
+ FwpsApplyModifiedLayerData0(classifyHandle, (PVOID*)&bindRequest, 0);
+
+Cleanup_handle:
+
+ FwpsReleaseClassifyHandle0(classifyHandle);
+}
+
+//
+// RegisterFilterBindRedirectTx()
+//
+// Register WFP filters that will pass all bind requests through the bind callout
+// for validation/redirection.
+//
+// "Tx" (in transaction) suffix means there is no clean-up in failure paths.
+//
+NTSTATUS
+RegisterFilterBindRedirectTx
+(
+ HANDLE WfpSession,
+ bool RegisterIpv6
+)
+{
+ //
+ // Create filter that references callout.
+ // Not specifying any conditions makes it apply to all traffic.
+ //
+
+ FWPM_FILTER0 filter = { 0 };
+
+ const auto filterName = L"Mullvad Split Tunnel Bind Redirect Filter (IPv4)";
+ const auto filterDescription = L"Redirects certain binds away from tunnel interface";
+
+ filter.filterKey = ST_FW_FILTER_CLASSIFY_BIND_IPV4_KEY;
+ filter.displayData.name = const_cast(filterName);
+ filter.displayData.description = const_cast(filterDescription);
+ filter.flags = FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT | FWPM_FILTER_FLAG_HAS_PROVIDER_CONTEXT;
+ filter.providerKey = const_cast(&ST_FW_PROVIDER_KEY);
+ filter.layerKey = FWPM_LAYER_ALE_BIND_REDIRECT_V4;
+ filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
+ filter.weight.type = FWP_UINT64;
+ filter.weight.uint64 = const_cast(&ST_MAX_FILTER_WEIGHT);
+ filter.action.type = FWP_ACTION_CALLOUT_UNKNOWN;
+ filter.action.calloutKey = ST_FW_CALLOUT_CLASSIFY_BIND_IPV4_KEY;
+ filter.providerContextKey = ST_FW_PROVIDER_CONTEXT_KEY;
+
+ auto status = FwpmFilterAdd0(WfpSession, &filter, NULL, NULL);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ if (!RegisterIpv6)
+ {
+ return STATUS_SUCCESS;
+ }
+
+ //
+ // Again, for IPv6 also.
+ //
+
+ const auto filterNameIpv6 = L"Mullvad Split Tunnel Bind Redirect Filter (IPv6)";
+
+ filter.filterKey = ST_FW_FILTER_CLASSIFY_BIND_IPV6_KEY;
+ filter.displayData.name = const_cast(filterNameIpv6);
+ filter.layerKey = FWPM_LAYER_ALE_BIND_REDIRECT_V6;
+ filter.action.calloutKey = ST_FW_CALLOUT_CLASSIFY_BIND_IPV6_KEY;
+
+ return FwpmFilterAdd0(WfpSession, &filter, NULL, NULL);
+}
+
+//
+// RemoveFilterBindRedirectTx()
+//
+// Remove WFP filters that activate the bind callout.
+//
+// "Tx" (in transaction) suffix means there is no clean-up in failure paths.
+//
+NTSTATUS
+RemoveFilterBindRedirectTx
+(
+ HANDLE WfpSession,
+ bool RemoveIpv6
+)
+{
+ auto status = FwpmFilterDeleteByKey0(WfpSession, &ST_FW_FILTER_CLASSIFY_BIND_IPV4_KEY);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ if (!RemoveIpv6)
+ {
+ return STATUS_SUCCESS;
+ }
+
+ return FwpmFilterDeleteByKey0(WfpSession, &ST_FW_FILTER_CLASSIFY_BIND_IPV6_KEY);
+}
+
+//
+// RegisterFilterPermitSplitAppsTx()
+//
+// Register WFP filters that will pass all connection attempts through the
+// connection callouts for validation.
+//
+// "Tx" (in transaction) suffix means there is no clean-up in failure paths.
+//
+NTSTATUS
+RegisterFilterPermitSplitAppsTx
+(
+ HANDLE WfpSession,
+ const IN_ADDR *TunnelIpv4,
+ const IN6_ADDR *TunnelIpv6
+)
+{
+ //
+ // Create filter that references callout.
+ //
+ // The single condition is IP_LOCAL_ADDRESS != Tunnel.
+ //
+ // This ensures the callout is presented only with connections that are
+ // attempted outside the tunnel.
+ //
+ // Ipv4 outbound.
+ //
+
+ FWPM_FILTER0 filter = { 0 };
+
+ const auto filterName = L"Mullvad Split Tunnel Permissive Filter (IPv4)";
+ const auto filterDescription = L"Approves selected connections outside the tunnel";
+
+ filter.filterKey = ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV4_CONN_KEY;
+ filter.displayData.name = const_cast(filterName);
+ filter.displayData.description = const_cast(filterDescription);
+ filter.flags = FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT | FWPM_FILTER_FLAG_HAS_PROVIDER_CONTEXT;
+ filter.providerKey = const_cast(&ST_FW_PROVIDER_KEY);
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
+ filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
+ filter.weight.type = FWP_UINT64;
+ filter.weight.uint64 = const_cast(&ST_HIGH_FILTER_WEIGHT);
+ filter.action.type = FWP_ACTION_CALLOUT_UNKNOWN;
+ filter.action.calloutKey = ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV4_CONN_KEY;
+ filter.providerContextKey = ST_FW_PROVIDER_CONTEXT_KEY;
+
+ FWPM_FILTER_CONDITION0 cond;
+
+ cond.fieldKey = FWPM_CONDITION_IP_LOCAL_ADDRESS;
+ cond.matchType = FWP_MATCH_NOT_EQUAL;
+ cond.conditionValue.type = FWP_UINT32;
+ cond.conditionValue.uint32 = RtlUlongByteSwap(TunnelIpv4->s_addr);
+
+ filter.filterCondition = &cond;
+ filter.numFilterConditions = 1;
+
+ auto status = FwpmFilterAdd0(WfpSession, &filter, NULL, NULL);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ //
+ // Ipv4 inbound.
+ //
+
+ filter.filterKey = ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV4_RECV_KEY;
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
+ filter.action.calloutKey = ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV4_RECV_KEY;
+
+ status = FwpmFilterAdd0(WfpSession, &filter, NULL, NULL);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ if (TunnelIpv6 == NULL)
+ {
+ return STATUS_SUCCESS;
+ }
+
+ //
+ // IPv6 outbound.
+ //
+
+ const auto filterNameIpv6 = L"Mullvad Split Tunnel Permissive Filter (IPv6)";
+
+ filter.filterKey = ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV6_CONN_KEY;
+ filter.displayData.name = const_cast(filterNameIpv6);
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
+ filter.action.calloutKey = ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV6_CONN_KEY;
+
+ cond.conditionValue.type = FWP_BYTE_ARRAY16_TYPE;
+ cond.conditionValue.byteArray16 = (FWP_BYTE_ARRAY16*)TunnelIpv6->u.Byte;
+
+ status = FwpmFilterAdd0(WfpSession, &filter, NULL, NULL);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ //
+ // IPv6 inbound.
+ //
+
+ filter.filterKey = ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV6_RECV_KEY;
+ filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
+ filter.action.calloutKey = ST_FW_CALLOUT_PERMIT_SPLIT_APPS_IPV6_RECV_KEY;
+
+ return FwpmFilterAdd0(WfpSession, &filter, NULL, NULL);
+}
+
+NTSTATUS
+RemoveFilterPermitSplitAppsTx
+(
+ HANDLE WfpSession,
+ bool RemoveIpv6
+)
+{
+ auto status = FwpmFilterDeleteByKey0(WfpSession, &ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV4_CONN_KEY);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = FwpmFilterDeleteByKey0(WfpSession, &ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV4_RECV_KEY);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ if (!RemoveIpv6)
+ {
+ return STATUS_SUCCESS;
+ }
+
+ status = FwpmFilterDeleteByKey0(WfpSession, &ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV6_CONN_KEY);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ return FwpmFilterDeleteByKey0(WfpSession, &ST_FW_FILTER_PERMIT_SPLIT_APPS_IPV6_RECV_KEY);
+}
+
+} // namespace firewall
diff --git a/src/firewall/splitting.h b/src/firewall/splitting.h
new file mode 100644
index 0000000..8e62277
--- /dev/null
+++ b/src/firewall/splitting.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "wfp.h"
+#include "context.h"
+
+namespace firewall
+{
+
+void
+RewriteBind
+(
+ CONTEXT *Context,
+ const FWPS_INCOMING_VALUES0 *FixedValues,
+ const FWPS_INCOMING_METADATA_VALUES0 *MetaValues,
+ UINT64 FilterId,
+ const void *ClassifyContext,
+ FWPS_CLASSIFY_OUT0 *ClassifyOut
+);
+
+//
+// RegisterFilterBindRedirectTx()
+//
+// Register filters, with linked callout, that rewrites binds for
+// applications being split.
+//
+NTSTATUS
+RegisterFilterBindRedirectTx
+(
+ HANDLE WfpSession,
+ bool RegisterIpv6
+);
+
+NTSTATUS
+RemoveFilterBindRedirectTx
+(
+ HANDLE WfpSession,
+ bool RemoveIpv6
+);
+
+//
+// RegisterFilterPermitSplitAppsTx()
+//
+// Register filters, with linked callout, that permits non-tunnel connections
+// associated with applications being split.
+//
+// This ensures winfw filters are not applied to these apps.
+//
+NTSTATUS
+RegisterFilterPermitSplitAppsTx
+(
+ HANDLE WfpSession,
+ const IN_ADDR *TunnelIpv4,
+ const IN6_ADDR *TunnelIpv6
+);
+
+NTSTATUS
+RemoveFilterPermitSplitAppsTx
+(
+ HANDLE WfpSession,
+ bool RemoveIpv6
+);
+
+} // namespace firewall
diff --git a/src/firewall/wfp.h b/src/firewall/wfp.h
new file mode 100644
index 0000000..8dea147
--- /dev/null
+++ b/src/firewall/wfp.h
@@ -0,0 +1,18 @@
+#pragma once
+
+//
+// Magical include order with defines etc.
+// Infuriating.
+//
+
+#include
+#include
+#include
+#pragma warning(push)
+#pragma warning(disable:4201)
+#define NDIS630
+#include
+#include
+#pragma warning(pop)
+#include
+#include
diff --git a/src/ioctl.cpp b/src/ioctl.cpp
new file mode 100644
index 0000000..f844575
--- /dev/null
+++ b/src/ioctl.cpp
@@ -0,0 +1,1734 @@
+#include "ioctl.h"
+#include "devicecontext.h"
+#include "util.h"
+#include "ipaddr.h"
+#include "firewall/firewall.h"
+#include "defs/config.h"
+#include "defs/process.h"
+#include "defs/queryprocess.h"
+#include "validation.h"
+#include "eventing/eventing.h"
+#include "eventing/builder.h"
+
+namespace ioctl
+{
+
+namespace
+{
+
+//
+// Minimum buffer sizes for requests.
+//
+enum class MIN_REQUEST_SIZE
+{
+ SET_CONFIGURATION = sizeof(ST_CONFIGURATION_HEADER),
+ GET_CONFIGURATION = sizeof(SIZE_T),
+ REGISTER_PROCESSES = sizeof(ST_PROCESS_DISCOVERY_HEADER),
+ REGISTER_IP_ADDRESSES = sizeof(ST_IP_ADDRESSES),
+ GET_IP_ADDRESSES = sizeof(ST_IP_ADDRESSES),
+ GET_STATE = sizeof(SIZE_T),
+ QUERY_PROCESS = sizeof(ST_QUERY_PROCESS),
+ QUERY_PROCESS_RESPONSE = sizeof(ST_QUERY_PROCESS_RESPONSE),
+};
+
+bool VpnActive(const ST_IP_ADDRESSES *IpAddresses)
+{
+ return ip::ValidInternetIpv4Address(IpAddresses) && ip::ValidTunnelIpv4Address(IpAddresses);
+}
+
+NTSTATUS
+InitializeProcessRegistryMgmt
+(
+ PROCESS_REGISTRY_MGMT *Mgmt
+)
+{
+ auto status = WdfSpinLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &Mgmt->Lock);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfSpinLockCreate() failed 0x%X\n", status);
+
+ goto Abort;
+ }
+
+ status = procregistry::Initialize(&Mgmt->Instance, ST_PAGEABLE::NO);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("procregistry::Initialize() failed 0x%X\n", status);
+
+ goto Abort_Delete_Lock;
+ }
+
+ return STATUS_SUCCESS;
+
+Abort_Delete_Lock:
+
+ WdfObjectDelete(Mgmt->Lock);
+
+Abort:
+
+ Mgmt->Lock = NULL;
+ Mgmt->Instance = NULL;
+
+ return status;
+}
+
+void
+DestroyProcessRegistryMgmt
+(
+ PROCESS_REGISTRY_MGMT *Mgmt
+)
+{
+ if (Mgmt->Instance != NULL)
+ {
+ procregistry::TearDown(&Mgmt->Instance);
+ Mgmt->Instance = NULL;
+ }
+
+ if (Mgmt->Lock != NULL)
+ {
+ WdfObjectDelete(Mgmt->Lock);
+ Mgmt->Lock = NULL;
+ }
+}
+
+//
+// UpdateTargetSplitSetting()
+//
+// Updates the target split setting on a process registry entry.
+//
+// Target state is set to split if either of:
+//
+// - Imagename is included in config.
+// - Currently split by inheritance and parent has departed.
+//
+bool
+NTAPI
+UpdateTargetSplitSetting
+(
+ procregistry::PROCESS_REGISTRY_ENTRY *Entry,
+ void *Context
+)
+{
+ auto context = (ST_DEVICE_CONTEXT*)Context;
+
+ Entry->TargetSettings.Split = ST_PROCESS_SPLIT_STATUS_OFF;
+
+ if (registeredimage::HasEntryExact(context->RegisteredImage.Instance, &Entry->ImageName))
+ {
+ Entry->TargetSettings.Split = ST_PROCESS_SPLIT_STATUS_ON_BY_CONFIG;
+ }
+ else if (Entry->ParentProcessId == 0
+ && Entry->Settings.Split == ST_PROCESS_SPLIT_STATUS_ON_BY_INHERITANCE)
+ {
+ Entry->TargetSettings.Split = ST_PROCESS_SPLIT_STATUS_ON_BY_INHERITANCE;
+ }
+
+ return true;
+}
+
+//
+// ApplyFinalizeTargetSettings()
+//
+// NOTE: Applies the target split setting but does not update current settings
+// on the process registry entry under consideration.
+//
+// Manages transitions in settings changes:
+//
+// Not split -> split
+// Split -> not split
+//
+// Something worth noting is that a process being split may have firewall state, but a process
+// that's not being split will never have firewall state.
+//
+// This is contrary to a previous design that used additional filters to block non-tunnel traffic.
+//
+bool
+NTAPI
+ApplyFinalizeTargetSettings
+(
+ ST_DEVICE_CONTEXT *Context,
+ procregistry::PROCESS_REGISTRY_ENTRY *Entry
+)
+{
+ if (!util::SplittingEnabled(Entry->Settings.Split))
+ {
+ NT_ASSERT(!Entry->Settings.HasFirewallState);
+
+ if (!util::SplittingEnabled(Entry->TargetSettings.Split))
+ {
+ Entry->TargetSettings.HasFirewallState = false;
+
+ return true;
+ }
+
+ //
+ // Not split -> split
+ //
+
+ auto status = firewall::RegisterAppBecomingSplitTx2(Context->Firewall, &Entry->ImageName);
+
+ if (!NT_SUCCESS(status))
+ {
+ return false;
+ }
+
+ return Entry->TargetSettings.HasFirewallState = true;
+ }
+
+ if (util::SplittingEnabled(Entry->TargetSettings.Split))
+ {
+ Entry->TargetSettings.HasFirewallState = Entry->Settings.HasFirewallState;
+
+ return true;
+ }
+
+ //
+ // Split -> not split
+ //
+
+ if (Entry->Settings.HasFirewallState)
+ {
+ auto status = firewall::RegisterAppBecomingUnsplitTx2(Context->Firewall, &Entry->ImageName);
+
+ if (!NT_SUCCESS(status))
+ {
+ return false;
+ }
+ }
+
+ Entry->TargetSettings.HasFirewallState = false;
+
+ return true;
+}
+
+//
+// PropagateApplyTargetSettings()
+//
+// Traverse ancestry to see if parent/grandparent/etc is being split.
+// Then apply target split setting.
+//
+bool
+NTAPI
+PropagateApplyTargetSettings
+(
+ procregistry::PROCESS_REGISTRY_ENTRY *Entry,
+ void *Context
+)
+{
+ auto context = (ST_DEVICE_CONTEXT *)Context;
+
+ if (!util::SplittingEnabled(Entry->TargetSettings.Split))
+ {
+ auto currentEntry = Entry;
+
+ //
+ // In the current state of changing settings,
+ // we have to follow the ancestry all the way to the root.
+ //
+
+ for (;;)
+ {
+ const auto parent = procregistry::GetParentEntry(context->ProcessRegistry.Instance, currentEntry);
+
+ if (NULL == parent)
+ {
+ break;
+ }
+
+ if (util::SplittingEnabled(parent->TargetSettings.Split))
+ {
+ Entry->TargetSettings.Split = ST_PROCESS_SPLIT_STATUS_ON_BY_INHERITANCE;
+ break;
+ }
+
+ currentEntry = parent;
+ }
+ }
+
+ return ApplyFinalizeTargetSettings(context, Entry);
+}
+
+struct CONFIGURATION_COMPUTE_LENGTH_CONTEXT
+{
+ SIZE_T NumEntries;
+ SIZE_T TotalStringLength;
+};
+
+bool
+NTAPI
+GetConfigurationComputeLength
+(
+ LOWER_UNICODE_STRING *Entry,
+ void *Context
+)
+{
+ auto ctx = (CONFIGURATION_COMPUTE_LENGTH_CONTEXT*)Context;
+
+ ++(ctx->NumEntries);
+
+ ctx->TotalStringLength += Entry->Length;
+
+ return true;
+}
+
+struct CONFIGURATION_SERIALIZE_CONTEXT
+{
+ // Next entry that should be written.
+ ST_CONFIGURATION_ENTRY *Entry;
+
+ // Pointer where next string should be written.
+ UCHAR *StringDest;
+
+ // Offset where next string should be written.
+ SIZE_T StringOffset;
+};
+
+bool
+NTAPI
+GetConfigurationSerialize
+(
+ LOWER_UNICODE_STRING *Entry,
+ void *Context
+)
+{
+ auto ctx = (CONFIGURATION_SERIALIZE_CONTEXT*)Context;
+
+ //
+ // Copy data.
+ //
+
+ ctx->Entry->ImageNameOffset = ctx->StringOffset;
+ ctx->Entry->ImageNameLength = Entry->Length;
+
+ RtlCopyMemory(ctx->StringDest, Entry->Buffer, Entry->Length);
+
+ //
+ // Update context for next iteration.
+ //
+
+ ++(ctx->Entry);
+ ctx->StringDest += Entry->Length;
+ ctx->StringOffset += Entry->Length;
+
+ return true;
+}
+
+//
+// CallbackQueryProcess
+//
+// This callback is provided to the firewall for use with callouts.
+//
+// We don't need to worry about the current driver state, because if callouts
+// are active this means the current state is "engaged".
+//
+firewall::PROCESS_SPLIT_VERDICT
+CallbackQueryProcess
+(
+ HANDLE ProcessId,
+ void *RawContext
+)
+{
+ auto context = (ST_DEVICE_CONTEXT*)RawContext;
+
+ WdfSpinLockAcquire(context->ProcessRegistry.Lock);
+
+ auto process = procregistry::FindEntry(context->ProcessRegistry.Instance, ProcessId);
+
+ firewall::PROCESS_SPLIT_VERDICT verdict = firewall::PROCESS_SPLIT_VERDICT::UNKNOWN;
+
+ if (process != NULL)
+ {
+ verdict = (util::SplittingEnabled(process->Settings.Split)
+ ? firewall::PROCESS_SPLIT_VERDICT::DO_SPLIT
+ : firewall::PROCESS_SPLIT_VERDICT::DONT_SPLIT);
+ }
+
+ WdfSpinLockRelease(context->ProcessRegistry.Lock);
+
+ return verdict;
+}
+
+bool
+NTAPI
+DbgPrintConfiguration
+(
+ LOWER_UNICODE_STRING *Entry,
+ void *Context
+)
+{
+ UNREFERENCED_PARAMETER(Context);
+
+ DbgPrint("%wZ\n", Entry);
+
+ return true;
+}
+
+//
+// ClearApplySplitSetting()
+//
+// Clear splitting and notify responsible systems.
+//
+// Locks being held when called:
+//
+// Process event subsystem operation lock
+//
+bool
+NTAPI
+ClearApplySplitSetting
+(
+ procregistry::PROCESS_REGISTRY_ENTRY *Entry,
+ void *Context
+)
+{
+ auto context = (ST_DEVICE_CONTEXT *)Context;
+
+ Entry->TargetSettings.Split = ST_PROCESS_SPLIT_STATUS_OFF;
+ Entry->TargetSettings.HasFirewallState = false;
+
+ if (Entry->Settings.HasFirewallState)
+ {
+ auto status = firewall::RegisterAppBecomingUnsplitTx2(context->Firewall, &Entry->ImageName);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Failed to update firewall 0x%X\n", status);
+
+ return false;
+ }
+ }
+
+ return true;
+}
+
+//
+// RealizeAnnounceSettingsChange()
+//
+// Update previous, current settings.
+//
+// Analyze change and emit corresponding event.
+//
+bool
+NTAPI
+RealizeAnnounceSettingsChange
+(
+ procregistry::PROCESS_REGISTRY_ENTRY *Entry,
+ void *Context
+)
+{
+ auto context = (ST_DEVICE_CONTEXT *)Context;
+
+ Entry->PreviousSettings = Entry->Settings;
+ Entry->Settings = Entry->TargetSettings;
+
+ if (util::SplittingEnabled(Entry->Settings.Split))
+ {
+ if (!util::SplittingEnabled(Entry->PreviousSettings.Split))
+ {
+ auto evt = eventing::BuildStartSplittingEvent(Entry->ProcessId,
+ ST_SPLITTING_REASON_BY_CONFIG, &Entry->ImageName);
+
+ eventing::Emit(context->Eventing, &evt);
+ }
+ }
+ else
+ {
+ if (util::SplittingEnabled(Entry->PreviousSettings.Split))
+ {
+ auto evt = eventing::BuildStopSplittingEvent(Entry->ProcessId,
+ ST_SPLITTING_REASON_BY_CONFIG, &Entry->ImageName);
+
+ eventing::Emit(context->Eventing, &evt);
+ }
+ }
+
+ return true;
+}
+
+//
+// ClearRealizeAnnounceSettingsChange()
+//
+// Clear splitting. Then realize and announce.
+//
+bool
+NTAPI
+ClearRealizeAnnounceSettingsChange
+(
+ procregistry::PROCESS_REGISTRY_ENTRY *Entry,
+ void *Context
+)
+{
+ Entry->TargetSettings.Split = ST_PROCESS_SPLIT_STATUS_OFF;
+ Entry->TargetSettings.HasFirewallState = false;
+
+ return RealizeAnnounceSettingsChange(Entry, Context);
+}
+
+NTSTATUS
+SyncProcessRegistry
+(
+ ST_DEVICE_CONTEXT *Context
+)
+{
+ //
+ // The process management subsystem is locked out becase we're holding the state lock.
+ // This ensures there will be no structural changes to the process registry.
+ //
+ // There will be readers at DISPATCH (callouts).
+ // But we are free to make atomic updates to individual entries.
+ //
+ // Locking of the configuration is not required since we're in the serialized
+ // IOCTL handler path.
+ //
+
+ procregistry::ForEach(Context->ProcessRegistry.Instance, UpdateTargetSplitSetting, Context);
+
+ auto status = firewall::TransactionBegin(Context->Firewall);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not create firewall transaction: 0x%X\n", status);
+
+ return status;
+ }
+
+ auto successful = procregistry::ForEach(Context->ProcessRegistry.Instance, PropagateApplyTargetSettings, Context);
+
+ if (!successful)
+ {
+ DbgPrint("Could not add/remove firewall filters\n");
+
+ status = STATUS_UNSUCCESSFUL;
+
+ goto Abort;
+ }
+
+ status = firewall::TransactionCommit(Context->Firewall);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not commit firewall transaction\n");
+
+ goto Abort;
+ }
+
+ //
+ // No fallible operations beyond here.
+ //
+ // Send splitting events and finish off.
+ //
+
+ procregistry::ForEach(Context->ProcessRegistry.Instance, RealizeAnnounceSettingsChange, Context);
+
+ return STATUS_SUCCESS;
+
+Abort:
+
+ auto s2 = firewall::TransactionAbort(Context->Firewall);
+
+ if (!NT_SUCCESS(s2))
+ {
+ DbgPrint("Could not abort firewall transaction: 0x%X\n", s2);
+ }
+
+ return status;
+}
+
+NTSTATUS
+EnterEngagedState
+(
+ ST_DEVICE_CONTEXT *Context,
+ const ST_IP_ADDRESSES *IpAddresses
+)
+{
+ auto status = firewall::EnableSplitting(Context->Firewall, IpAddresses);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not enable splitting in firewall: 0x%X\n", status);
+
+ return status;
+ }
+
+ status = SyncProcessRegistry(Context);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not synchronize process registry with configuration: 0x%X\n", status);
+
+ auto s2 = firewall::DisableSplitting(Context->Firewall);
+
+ if (!NT_SUCCESS(s2))
+ {
+ DbgPrint("DisableSplitting() failed: 0x%X\n", s2);
+ }
+
+ return status;
+ }
+
+ Context->DriverState.State = ST_DRIVER_STATE_ENGAGED;
+
+ DbgPrint("Successful state transition READY -> ENGAGED\n");
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+LeaveEngagedState
+(
+ ST_DEVICE_CONTEXT *Context
+)
+{
+ auto status = firewall::DisableSplitting(Context->Firewall);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not disable splitting in firewall: 0x%X\n", status);
+
+ return status;
+ }
+
+ //
+ // This doesn't touch the firewall.
+ // It's already been reset as a result of the disable-call above.
+ //
+ procregistry::ForEach(Context->ProcessRegistry.Instance, ClearRealizeAnnounceSettingsChange, Context);
+
+ Context->DriverState.State = ST_DRIVER_STATE_READY;
+
+ DbgPrint("Successful state transition ENGAGED -> READY\n");
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+RegisterIpAddressesAtReady
+(
+ ST_DEVICE_CONTEXT *Context,
+ const ST_IP_ADDRESSES *newIpAddresses
+)
+{
+ //
+ // If there's no config registered we just store the addresses and succeed.
+ //
+ // No need to access the configuration exclusively:
+ //
+ // - We're in the serialized IOCTL handler path.
+ // - Config is only read from, not written to.
+ //
+
+ if (registeredimage::IsEmpty(Context->RegisteredImage.Instance))
+ {
+ Context->IpAddresses = *newIpAddresses;
+
+ return STATUS_SUCCESS;
+ }
+
+ //
+ // There's a configuration registered.
+ //
+ // However, if the VPN isn't active we can't enter the engaged state.
+ //
+
+ if (!VpnActive(newIpAddresses))
+ {
+ Context->IpAddresses = *newIpAddresses;
+
+ return STATUS_SUCCESS;
+ }
+
+ //
+ // Enter into engaged state.
+ //
+
+ auto status = EnterEngagedState(Context, newIpAddresses);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not enter engaged state: 0x%X", status);
+
+ return status;
+ }
+
+ Context->IpAddresses = *newIpAddresses;
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+RegisterIpAddressesAtEngaged
+(
+ ST_DEVICE_CONTEXT *Context,
+ const ST_IP_ADDRESSES *newIpAddresses
+)
+{
+ if (!VpnActive(newIpAddresses))
+ {
+ auto status = LeaveEngagedState(Context);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not leave engaged state: 0x%X", status);
+
+ return status;
+ }
+
+ Context->IpAddresses = *newIpAddresses;
+
+ return STATUS_SUCCESS;
+ }
+
+ //
+ // No state change required.
+ // Notify firewall so it can rewrite any filters with IP-conditions.
+ //
+
+ auto status = firewall::RegisterUpdatedIpAddresses(Context->Firewall, newIpAddresses);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not update firewall with new IPs: 0x%X", status);
+
+ return status;
+ }
+
+ Context->IpAddresses = *newIpAddresses;
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+RegisterConfigurationAtReady
+(
+ ST_DEVICE_CONTEXT *Context,
+ registeredimage::CONTEXT *Imageset
+)
+{
+ //
+ // If VPN is not active just store new configuration and succeed.
+ //
+
+ if (!VpnActive(&Context->IpAddresses))
+ {
+ auto oldConfiguration = Context->RegisteredImage.Instance;
+
+ Context->RegisteredImage.Instance = Imageset;
+
+ registeredimage::TearDown(&oldConfiguration);
+
+ return STATUS_SUCCESS;
+ }
+
+ //
+ // VPN is active so enter engaged state.
+ //
+
+ auto oldConfiguration = Context->RegisteredImage.Instance;
+
+ Context->RegisteredImage.Instance = Imageset;
+
+ auto status = EnterEngagedState(Context, &Context->IpAddresses);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not enter engaged state: 0x%X", status);
+
+ Context->RegisteredImage.Instance = oldConfiguration;
+
+ registeredimage::TearDown(&Imageset);
+
+ return status;
+ }
+
+ registeredimage::TearDown(&oldConfiguration);
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+RegisterConfigurationAtEngaged
+(
+ ST_DEVICE_CONTEXT *Context,
+ registeredimage::CONTEXT *Imageset
+)
+{
+ auto oldConfiguration = Context->RegisteredImage.Instance;
+
+ Context->RegisteredImage.Instance = Imageset;
+
+ //
+ // Update process registry to reflect new configuration.
+ //
+
+ auto status = SyncProcessRegistry(Context);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not synchronize process registry with configuration: 0x%X\n", status);
+
+ Context->RegisteredImage.Instance = oldConfiguration;
+
+ registeredimage::TearDown(&Imageset);
+
+ return status;
+ }
+
+ registeredimage::TearDown(&oldConfiguration);
+
+ return STATUS_SUCCESS;
+}
+
+void
+NTAPI
+CallbackAcquireStateLock
+(
+ void *Context
+)
+{
+ auto context = (ST_DEVICE_CONTEXT*)Context;
+
+ WdfWaitLockAcquire(context->DriverState.Lock, NULL);
+}
+
+void
+NTAPI
+CallbackReleaseStateLock
+(
+ void *Context
+)
+{
+ auto context = (ST_DEVICE_CONTEXT*)Context;
+
+ WdfWaitLockRelease(context->DriverState.Lock);
+}
+
+bool
+NTAPI
+CallbackEngagedStateActive
+(
+ void *Context
+)
+{
+ auto context = (ST_DEVICE_CONTEXT*)Context;
+
+ return context->DriverState.State == ST_DRIVER_STATE_ENGAGED;
+}
+
+NTSTATUS
+ResetInner
+(
+ ST_DEVICE_CONTEXT *Context
+)
+{
+ //
+ // Leave engaged state to minimize the impact if any of this fails.
+ //
+
+ if (Context->DriverState.State == ST_DRIVER_STATE_ENGAGED)
+ {
+ WdfWaitLockAcquire(Context->DriverState.Lock, NULL);
+
+ auto status = LeaveEngagedState(Context);
+
+ WdfWaitLockRelease(Context->DriverState.Lock);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not leave engaged state\n");
+ }
+ }
+
+ //
+ // Tear down everything in reverse order of initializing it.
+ //
+
+ procmgmt::TearDown(&Context->ProcessMgmt);
+
+ auto status = firewall::TearDown(&Context->Firewall);
+
+ if (!NT_SUCCESS(status))
+ {
+ //
+ // Filters or callouts could not be unregistered.
+ //
+ // There is no way to recover from this. The driver will not be able to unload.
+ //
+ // All moving parts in the system that depend on the state lock are stopped.
+ // So safe to update state without using the lock.
+ //
+
+ Context->DriverState.State = ST_DRIVER_STATE_ZOMBIE;
+
+ return status;
+ }
+
+ RtlZeroMemory(&Context->IpAddresses, sizeof(Context->IpAddresses));
+
+ procregistry::TearDown(&Context->ProcessRegistry.Instance);
+
+ registeredimage::TearDown((registeredimage::CONTEXT**)&Context->RegisteredImage.Instance);
+
+ procbroker::TearDown(&Context->ProcessEventBroker);
+
+ eventing::TearDown(&Context->Eventing);
+
+ Context->DriverState.State = ST_DRIVER_STATE_STARTED;
+
+ return STATUS_SUCCESS;
+}
+
+} // anonymous namespace
+
+NTSTATUS
+Initialize
+(
+ WDFDEVICE Device
+)
+{
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ //
+ // The context struct is cleared.
+ // Only state is set at this point.
+ //
+
+ auto status = eventing::Initialize(&context->Eventing, Device);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = procbroker::Initialize(&context->ProcessEventBroker);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_teardown_eventing;
+ }
+
+ status = registeredimage::Initialize
+ (
+ (registeredimage::CONTEXT**)&context->RegisteredImage.Instance,
+ ST_PAGEABLE::NO
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_teardown_procbroker;
+ }
+
+ status = InitializeProcessRegistryMgmt(&context->ProcessRegistry);
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_teardown_registeredimage;
+ }
+
+ firewall::CALLBACKS callbacks;
+
+ callbacks.QueryProcess = CallbackQueryProcess;
+ callbacks.Context = context;
+
+ status = firewall::Initialize
+ (
+ &context->Firewall,
+ WdfDeviceWdmGetDeviceObject(Device),
+ &callbacks,
+ context->ProcessEventBroker
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_teardown_process_registry;
+ }
+
+ status = procmgmt::Initialize
+ (
+ &context->ProcessMgmt,
+ context->ProcessEventBroker,
+ &context->ProcessRegistry,
+ &context->RegisteredImage,
+ context->Eventing,
+ context->Firewall,
+ CallbackAcquireStateLock,
+ CallbackReleaseStateLock,
+ CallbackEngagedStateActive,
+ context
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ goto Abort_teardown_firewall;
+ }
+
+ context->DriverState.State = ST_DRIVER_STATE_INITIALIZED;
+
+ DbgPrint("Successfully processed IOCTL_ST_INITIALIZE\n");
+
+ return STATUS_SUCCESS;
+
+Abort_teardown_firewall:
+
+ firewall::TearDown(&context->Firewall);
+
+Abort_teardown_process_registry:
+
+ DestroyProcessRegistryMgmt(&context->ProcessRegistry);
+
+Abort_teardown_registeredimage:
+
+ registeredimage::TearDown((registeredimage::CONTEXT**)&context->RegisteredImage.Instance);
+
+Abort_teardown_procbroker:
+
+ procbroker::TearDown(&context->ProcessEventBroker);
+
+Abort_teardown_eventing:
+
+ eventing::TearDown(&context->Eventing);
+
+ return status;
+}
+
+//
+// SetConfigurationPrepare()
+//
+// Validate and repackage configuration data into new registered image instance.
+//
+// This runs at PASSIVE, in order to be able to downcase the strings.
+//
+NTSTATUS
+SetConfigurationPrepare
+(
+ WDFREQUEST Request,
+ registeredimage::CONTEXT **Imageset
+)
+{
+ *Imageset = NULL;
+
+ PVOID buffer;
+ size_t bufferLength;
+
+ auto status = WdfRequestRetrieveInputBuffer(Request,
+ (size_t)MIN_REQUEST_SIZE::SET_CONFIGURATION, &buffer, &bufferLength);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not access configuration buffer provided to IOCTL: 0x%X", status);
+
+ return status;
+ }
+
+ if (!ValidateUserBufferConfiguration(buffer, bufferLength))
+ {
+ DbgPrint("Invalid configuration data in buffer provided to IOCTL\n");
+
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ auto header = (ST_CONFIGURATION_HEADER*)buffer;
+ auto entry = (ST_CONFIGURATION_ENTRY*)(header + 1);
+ auto stringBuffer = (UCHAR*)(entry + header->NumEntries);
+
+ if (header->NumEntries == 0)
+ {
+ DbgPrint("Cannot assign empty configuration\n");
+
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ //
+ // Create new instance for storing image names.
+ //
+
+ registeredimage::CONTEXT *imageset;
+
+ status = registeredimage::Initialize(&imageset, ST_PAGEABLE::NO);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not create new registered image instance: 0x%X\n", status);
+
+ return status;
+ }
+
+ //
+ // Insert each entry one by one.
+ //
+
+ for (auto i = 0; i < header->NumEntries; ++i, ++entry)
+ {
+ UNICODE_STRING s;
+
+ s.Length = entry->ImageNameLength;
+ s.MaximumLength = entry->ImageNameLength;
+ s.Buffer = (WCHAR*)(stringBuffer + entry->ImageNameOffset);
+
+ status = registeredimage::AddEntry(imageset, &s);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Could not insert new entry into registered image instance: 0x%X\n", status);
+
+ registeredimage::TearDown(&imageset);
+
+ return status;
+ }
+ }
+
+ *Imageset = imageset;
+
+ return STATUS_SUCCESS;
+}
+
+//
+// SetConfiguration()
+//
+// Store updated configuration.
+//
+// Possibly enter/leave engaged state depending on a number of factors.
+//
+NTSTATUS
+SetConfiguration
+(
+ WDFDEVICE Device,
+ registeredimage::CONTEXT *Imageset
+)
+{
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ NTSTATUS status = STATUS_UNSUCCESSFUL;
+
+ WdfWaitLockAcquire(context->DriverState.Lock, NULL);
+
+ switch (context->DriverState.State)
+ {
+ case ST_DRIVER_STATE_READY:
+ {
+ status = RegisterConfigurationAtReady(context, Imageset);
+
+ break;
+ }
+ case ST_DRIVER_STATE_ENGAGED:
+ {
+ status = RegisterConfigurationAtEngaged(context, Imageset);
+
+ break;
+ }
+ }
+
+ WdfWaitLockRelease(context->DriverState.Lock);
+
+ if (NT_SUCCESS(status))
+ {
+ DbgPrint("Successfully processed IOCTL_ST_SET_CONFIGURATION\n");
+
+ //
+ // No locking required since we're in a serialized IOCTL handler path.
+ //
+ registeredimage::ForEach
+ (
+ context->RegisteredImage.Instance,
+ DbgPrintConfiguration,
+ NULL
+ );
+ }
+
+ return status;
+}
+
+//
+// GetConfigurationComplete()
+//
+// Return current configuration to driver client.
+//
+// Locking is not required for the following reasons:
+//
+// - We're in the serialized IOCTL handler path.
+// - Config is only read from, not written to.
+//
+void
+GetConfigurationComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+)
+{
+ PVOID buffer;
+ size_t bufferLength;
+
+ auto status = WdfRequestRetrieveOutputBuffer(Request,
+ (size_t)MIN_REQUEST_SIZE::GET_CONFIGURATION, &buffer, &bufferLength);
+
+ if (!NT_SUCCESS(status))
+ {
+ WdfRequestComplete(Request, status);
+
+ return;
+ }
+
+ //
+ // Buffer is present and meets the minimum size requirements.
+ // This means we can "complete with information".
+ //
+
+ ULONG_PTR info = 0;
+
+ //
+ // Compute required buffer length.
+ //
+
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ CONFIGURATION_COMPUTE_LENGTH_CONTEXT computeContext;
+
+ computeContext.NumEntries = 0;
+ computeContext.TotalStringLength = 0;
+
+ registeredimage::ForEach(context->RegisteredImage.Instance,
+ GetConfigurationComputeLength, &computeContext);
+
+ SIZE_T requiredLength = sizeof(ST_CONFIGURATION_HEADER)
+ + (sizeof(ST_CONFIGURATION_ENTRY) * computeContext.NumEntries)
+ + computeContext.TotalStringLength;
+
+ //
+ // It's not possible to fail the request AND provide output data.
+ //
+ // Therefore, the only two types of valid input buffers are:
+ //
+ // # A buffer large enough to contain the settings.
+ // # A buffer of exactly sizeof(SIZE_T) bytes, to learn the required length.
+ //
+
+ if (bufferLength < requiredLength)
+ {
+ if (bufferLength == sizeof(SIZE_T))
+ {
+ status = STATUS_SUCCESS;
+
+ *(SIZE_T*)buffer = requiredLength;
+
+ info = sizeof(SIZE_T);
+ }
+ else
+ {
+ status = STATUS_BUFFER_TOO_SMALL;
+
+ info = 0;
+ }
+
+ goto Complete;
+ }
+
+ //
+ // Output buffer is OK.
+ // Serialize config into buffer.
+ //
+
+ auto header = (ST_CONFIGURATION_HEADER*)buffer;
+ auto entry = (ST_CONFIGURATION_ENTRY*)(header + 1);
+ auto stringBuffer = (UCHAR*)(entry + computeContext.NumEntries);
+
+ CONFIGURATION_SERIALIZE_CONTEXT serializeContext;
+
+ serializeContext.Entry = entry;
+ serializeContext.StringOffset = 0;
+ serializeContext.StringDest = stringBuffer;
+
+ registeredimage::ForEach(context->RegisteredImage.Instance,
+ GetConfigurationSerialize, &serializeContext);
+
+ //
+ // Finalize header.
+ //
+
+ header->NumEntries = computeContext.NumEntries;
+ header->TotalLength = requiredLength;
+
+ info = requiredLength;
+
+ status = STATUS_SUCCESS;
+
+Complete:
+
+ WdfRequestCompleteWithInformation(Request, status, info);
+}
+
+//
+// ClearConfiguration()
+//
+// Mark all processes as non-split and clear configuration.
+//
+NTSTATUS
+ClearConfiguration
+(
+ WDFDEVICE Device
+)
+{
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ WdfWaitLockAcquire(context->DriverState.Lock, NULL);
+
+ if (context->DriverState.State == ST_DRIVER_STATE_ENGAGED)
+ {
+ //
+ // Leave engaged state.
+ // (This updates the process registry and sends splitting events.)
+ //
+
+ auto status = LeaveEngagedState(context);
+
+ if (!NT_SUCCESS(status))
+ {
+ WdfWaitLockRelease(context->DriverState.Lock);
+
+ DbgPrint("Could not leave engaged state: 0x%X", status);
+
+ return status;
+ }
+ }
+
+ registeredimage::Reset(context->RegisteredImage.Instance);
+
+ WdfWaitLockRelease(context->DriverState.Lock);
+
+ DbgPrint("Successfully processed IOCTL_ST_CLEAR_CONFIGURATION\n");
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+RegisterProcesses
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+)
+{
+ PVOID buffer;
+ size_t bufferLength;
+
+ auto status = WdfRequestRetrieveInputBuffer(Request,
+ (size_t)MIN_REQUEST_SIZE::REGISTER_PROCESSES, &buffer, &bufferLength);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ if (!ValidateUserBufferProcesses(buffer, bufferLength))
+ {
+ DbgPrint("Invalid data provided to IOCTL_ST_REGISTER_PROCESSES\n");
+
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ auto header = (ST_PROCESS_DISCOVERY_HEADER*)buffer;
+ auto entry = (ST_PROCESS_DISCOVERY_ENTRY*)(header + 1);
+ auto stringBuffer = (UCHAR*)(entry + header->NumEntries);
+
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ NT_ASSERT(procregistry::IsEmpty(context->ProcessRegistry.Instance));
+
+ //
+ // Insert records one by one.
+ //
+ // We can't check the configuration to get accurate information on whether the process being
+ // inserted should have its traffic split.
+ //
+ // Because there is no configuration yet.
+ //
+
+ for (auto i = 0; i < header->NumEntries; ++i, ++entry)
+ {
+ UNICODE_STRING imagename;
+
+ imagename.Length = entry->ImageNameLength;
+ imagename.MaximumLength = entry->ImageNameLength;
+
+ if (entry->ImageNameLength == 0)
+ {
+ imagename.Buffer = NULL;
+ }
+ else
+ {
+ imagename.Buffer = (WCHAR*)(stringBuffer + entry->ImageNameOffset);
+ }
+
+ procregistry::PROCESS_REGISTRY_ENTRY registryEntry = { 0 };
+
+ status = procregistry::InitializeEntry
+ (
+ context->ProcessRegistry.Instance,
+ entry->ParentProcessId,
+ entry->ProcessId,
+ ST_PROCESS_SPLIT_STATUS_OFF,
+ &imagename,
+ ®istryEntry
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ procregistry::Reset(context->ProcessRegistry.Instance);
+
+ return status;
+ }
+
+ status = procregistry::AddEntry
+ (
+ context->ProcessRegistry.Instance,
+ ®istryEntry
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ procregistry::ReleaseEntry(®istryEntry);
+
+ procregistry::Reset(context->ProcessRegistry.Instance);
+
+ return status;
+ }
+ }
+
+ context->DriverState.State = ST_DRIVER_STATE_READY;
+
+ procmgmt::Activate(context->ProcessMgmt);
+
+ DbgPrint("Successfully processed IOCTL_ST_REGISTER_PROCESSES\n");
+
+ return STATUS_SUCCESS;
+}
+
+//
+// RegisterIpAddresses()
+//
+// Store updated set of IP addresses.
+//
+// Possibly enter/leave engaged state depending on a number of factors.
+//
+NTSTATUS
+RegisterIpAddresses
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+)
+{
+ PVOID buffer;
+ size_t bufferLength;
+
+ auto status = WdfRequestRetrieveInputBuffer(Request,
+ (size_t)MIN_REQUEST_SIZE::REGISTER_IP_ADDRESSES, &buffer, &bufferLength);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ if (bufferLength != sizeof(ST_IP_ADDRESSES))
+ {
+ DbgPrint("Invalid data provided to IOCTL_ST_REGISTER_IP_ADDRESSES\n");
+
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ auto newIpAddresses = (ST_IP_ADDRESSES*)buffer;
+
+ //
+ // New addresses seem OK, branch on current state.
+ //
+
+ status = STATUS_UNSUCCESSFUL;
+
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ WdfWaitLockAcquire(context->DriverState.Lock, NULL);
+
+ switch (context->DriverState.State)
+ {
+ case ST_DRIVER_STATE_READY:
+ {
+ status = RegisterIpAddressesAtReady(context, newIpAddresses);
+
+ break;
+ }
+ case ST_DRIVER_STATE_ENGAGED:
+ {
+ status = RegisterIpAddressesAtEngaged(context, newIpAddresses);
+
+ break;
+ }
+ }
+
+ WdfWaitLockRelease(context->DriverState.Lock);
+
+ if (NT_SUCCESS(status))
+ {
+ DbgPrint("Successfully processed IOCTL_ST_REGISTER_IP_ADDRESSES\n");
+ }
+
+ return status;
+}
+
+//
+// GetIpAddressesComplete()
+//
+// Return currently registered IP addresses to driver client.
+//
+// Locking is not required for the following reasons:
+//
+// - We're in the serialized IOCTL handler path.
+// - IP addresses struct is only read from, not written to.
+//
+void
+GetIpAddressesComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+)
+{
+ NT_ASSERT((size_t)MIN_REQUEST_SIZE::GET_IP_ADDRESSES >= sizeof(ST_IP_ADDRESSES));
+
+ PVOID buffer;
+
+ auto status = WdfRequestRetrieveOutputBuffer
+ (
+ Request,
+ (size_t)MIN_REQUEST_SIZE::GET_IP_ADDRESSES,
+ &buffer,
+ NULL
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ WdfRequestComplete(Request, status);
+
+ return;
+ }
+
+ //
+ // Copy IP addresses struct to output buffer.
+ //
+
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ RtlCopyMemory(buffer, &context->IpAddresses, sizeof(context->IpAddresses));
+
+ //
+ // Finish up.
+ //
+
+ WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, sizeof(context->IpAddresses));
+}
+
+void
+GetStateComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+)
+{
+ PVOID buffer;
+
+ auto status = WdfRequestRetrieveOutputBuffer
+ (
+ Request,
+ (size_t)MIN_REQUEST_SIZE::GET_STATE,
+ &buffer,
+ NULL
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Unable to retrieve client buffer or invalid buffer size\n");
+
+ WdfRequestComplete(Request, status);
+
+ return;
+ }
+
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ // Sample current state.
+ *(SIZE_T*)buffer = context->DriverState.State;
+
+ WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, sizeof(SIZE_T));
+}
+
+//
+// QueryProcessComplete()
+//
+// Returns information about specific process to driver client.
+//
+void
+QueryProcessComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+)
+{
+ PVOID buffer;
+ size_t bufferLength;
+
+ auto status = WdfRequestRetrieveInputBuffer
+ (
+ Request,
+ (size_t)MIN_REQUEST_SIZE::QUERY_PROCESS,
+ &buffer,
+ &bufferLength
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Unable to retrieve input buffer or buffer too small\n");
+
+ WdfRequestComplete(Request, status);
+
+ return;
+ }
+
+ if (bufferLength != (size_t)MIN_REQUEST_SIZE::QUERY_PROCESS)
+ {
+ DbgPrint("Invalid buffer size\n");
+
+ WdfRequestComplete(Request, STATUS_INVALID_BUFFER_SIZE);
+
+ return;
+ }
+
+ auto processId = ((ST_QUERY_PROCESS*)buffer)->ProcessId;
+
+ //
+ // Get the output buffer.
+ //
+ // We can't validate the buffer length just yet, because we don't know the
+ // length of the process image name.
+ //
+
+ status = WdfRequestRetrieveOutputBuffer
+ (
+ Request,
+ (size_t)MIN_REQUEST_SIZE::QUERY_PROCESS_RESPONSE,
+ &buffer,
+ &bufferLength
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Unable to retrieve output buffer or buffer too small\n");
+
+ WdfRequestComplete(Request, status);
+
+ return;
+ }
+
+ //
+ // Look up process.
+ //
+
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ WdfSpinLockAcquire(context->ProcessRegistry.Lock);
+
+ auto record = procregistry::FindEntry(context->ProcessRegistry.Instance, processId);
+
+ if (record == NULL)
+ {
+ WdfSpinLockRelease(context->ProcessRegistry.Lock);
+
+ DbgPrint("Process query for unknown process\n");
+
+ WdfRequestComplete(Request, STATUS_INVALID_HANDLE);
+
+ return;
+ }
+
+ //
+ // Definitively validate output buffer.
+ //
+
+ auto requiredLength = sizeof(ST_QUERY_PROCESS_RESPONSE)
+ - RTL_FIELD_SIZE(ST_QUERY_PROCESS_RESPONSE, ImageName)
+ + record->ImageName.Length;
+
+ if (bufferLength < requiredLength)
+ {
+ WdfSpinLockRelease(context->ProcessRegistry.Lock);
+
+ DbgPrint("Output buffer is too small\n");
+
+ WdfRequestComplete(Request, STATUS_BUFFER_TOO_SMALL);
+
+ return;
+ }
+
+ //
+ // Copy data and release lock.
+ //
+
+ auto response = (ST_QUERY_PROCESS_RESPONSE *)buffer;
+
+ response->ProcessId = record->ProcessId;
+ response->ParentProcessId = record->ParentProcessId;
+ response->Split = (util::SplittingEnabled(record->Settings.Split) ? TRUE : FALSE);
+ response->ImageNameLength = record->ImageName.Length;
+
+ RtlCopyMemory(&response->ImageName, record->ImageName.Buffer, record->ImageName.Length);
+
+ WdfSpinLockRelease(context->ProcessRegistry.Lock);
+
+ //
+ // Complete request.
+ //
+
+ WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, requiredLength);
+}
+
+void
+ResetComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+)
+{
+ auto context = DeviceGetSplitTunnelContext(Device);
+
+ //
+ // We're in the serialized IOCTL handler path so handlers that might update the state are
+ // locked out from executing.
+ //
+ // That's the first reason to not acquire the state lock.
+ //
+ // The second reason is that process management logic uses the state lock so we can't be
+ // holding the lock while trying to tear down the process management subsystem.
+ //
+
+ NTSTATUS status = STATUS_SUCCESS;
+
+ switch (context->DriverState.State)
+ {
+ case ST_DRIVER_STATE_STARTED:
+ case ST_DRIVER_STATE_ZOMBIE:
+ {
+ break;
+ }
+ default:
+ {
+ status = ResetInner(context);
+ }
+ }
+
+ if (NT_SUCCESS(status))
+ {
+ DbgPrint("Successfully processed IOCTL_ST_RESET\n");
+ }
+ else
+ {
+ DbgPrint("Failed to reset driver state\n");
+ }
+
+ WdfRequestComplete(Request, status);
+}
+
+} // namespace ioctl
diff --git a/src/ioctl.h b/src/ioctl.h
new file mode 100644
index 0000000..090baff
--- /dev/null
+++ b/src/ioctl.h
@@ -0,0 +1,98 @@
+#pragma once
+
+#include
+#include
+#include "containers/registeredimage.h"
+
+namespace ioctl
+{
+
+//
+// Initialize()
+//
+// Initialize subsystems and device context.
+//
+NTSTATUS
+Initialize
+(
+ WDFDEVICE Device
+);
+
+//
+// SetConfigurationPrepare()
+//
+// Parse client buffer into registeredimage instance.
+//
+// This should be called at PASSIVE, and the actual updating and
+// state transition may be performed at DISPATCH.
+//
+NTSTATUS
+SetConfigurationPrepare
+(
+ WDFREQUEST Request,
+ registeredimage::CONTEXT **Imageset
+);
+
+NTSTATUS
+SetConfiguration
+(
+ WDFDEVICE Device,
+ registeredimage::CONTEXT *Imageset
+);
+
+void
+GetConfigurationComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+);
+
+NTSTATUS
+ClearConfiguration
+(
+ WDFDEVICE Device
+);
+
+NTSTATUS
+RegisterProcesses
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+);
+
+NTSTATUS
+RegisterIpAddresses
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+);
+
+void
+GetIpAddressesComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+);
+
+void
+GetStateComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+);
+
+void
+QueryProcessComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+);
+
+void
+ResetComplete
+(
+ WDFDEVICE Device,
+ WDFREQUEST Request
+);
+
+} // namespace ioctl
diff --git a/src/ipaddr.cpp b/src/ipaddr.cpp
new file mode 100644
index 0000000..0a13d83
--- /dev/null
+++ b/src/ipaddr.cpp
@@ -0,0 +1,44 @@
+#include
+#include "ipaddr.h"
+#include "util.h"
+
+namespace ip
+{
+
+bool
+ValidTunnelIpv4Address
+(
+ const ST_IP_ADDRESSES *IpAddresses
+)
+{
+ return !util::IsEmptyRange(&IpAddresses->TunnelIpv4, sizeof(IpAddresses->TunnelIpv4));
+}
+
+bool
+ValidInternetIpv4Address
+(
+ const ST_IP_ADDRESSES *IpAddresses
+)
+{
+ return !util::IsEmptyRange(&IpAddresses->InternetIpv4, sizeof(IpAddresses->InternetIpv4));
+}
+
+bool
+ValidTunnelIpv6Address
+(
+ const ST_IP_ADDRESSES *IpAddresses
+)
+{
+ return !util::IsEmptyRange(&IpAddresses->TunnelIpv6, sizeof(IpAddresses->TunnelIpv6));
+}
+
+bool
+ValidInternetIpv6Address
+(
+ const ST_IP_ADDRESSES *IpAddresses
+)
+{
+ return !util::IsEmptyRange(&IpAddresses->InternetIpv6, sizeof(IpAddresses->InternetIpv6));
+}
+
+} // namespace ip
diff --git a/src/ipaddr.h b/src/ipaddr.h
new file mode 100644
index 0000000..64b9c59
--- /dev/null
+++ b/src/ipaddr.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include
+#include
+
+typedef struct tag_ST_IP_ADDRESSES
+{
+ IN_ADDR TunnelIpv4;
+ IN_ADDR InternetIpv4;
+
+ IN6_ADDR TunnelIpv6;
+ IN6_ADDR InternetIpv6;
+}
+ST_IP_ADDRESSES;
+
+namespace ip
+{
+
+bool
+ValidTunnelIpv4Address
+(
+ const ST_IP_ADDRESSES *IpAddresses
+);
+
+bool
+ValidInternetIpv4Address
+(
+ const ST_IP_ADDRESSES *IpAddresses
+);
+
+bool
+ValidTunnelIpv6Address
+(
+ const ST_IP_ADDRESSES *IpAddresses
+);
+
+bool
+ValidInternetIpv6Address
+(
+ const ST_IP_ADDRESSES *IpAddresses
+);
+
+} // namespace ip
diff --git a/src/mullvad-split-tunnel.inf b/src/mullvad-split-tunnel.inf
new file mode 100644
index 0000000..d5f3a4f
--- /dev/null
+++ b/src/mullvad-split-tunnel.inf
@@ -0,0 +1,86 @@
+;
+; mullvad-split-tunnel.inf
+;
+
+[Version]
+Signature="$WINDOWS NT$"
+Class=WFPCALLOUTS
+ClassGuid={57465043-616C-6C6F-7574-5F636C617373}
+Provider=%ManufacturerName%
+CatalogFile=mullvad-split-tunnel.cat
+DriverVer=
+
+[DestinationDirs]
+DefaultDestDir = 12
+mullvad-split-tunnel_Device_CoInstaller_CopyFiles = 11
+
+; ================= Class section =====================
+
+[ClassInstall32]
+AddReg=SplitTunnelClassReg
+
+[SplitTunnelClassReg]
+HKR,,,0,%ClassName%
+HKR,,Icon,,-5
+
+[SourceDisksNames]
+1 = %DiskName%,,,""
+
+[SourceDisksFiles]
+mullvad-split-tunnel.sys = 1,,
+WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll=1
+
+;*****************************************
+; Install Section
+;*****************************************
+
+[Manufacturer]
+%ManufacturerName%=Standard,NT$ARCH$
+
+[Standard.NT$ARCH$]
+%mullvad-split-tunnel.DeviceDesc%=mullvad-split-tunnel_Device, Root\mullvad-split-tunnel
+
+[mullvad-split-tunnel_Device.NT]
+CopyFiles=Drivers_Dir
+
+[Drivers_Dir]
+mullvad-split-tunnel.sys
+
+;-------------- Service installation
+[mullvad-split-tunnel_Device.NT.Services]
+AddService = mullvad-split-tunnel,%SPSVCINST_ASSOCSERVICE%, mullvad-split-tunnel_Service_Inst
+
+; -------------- mullvad-split-tunnel driver install sections
+[mullvad-split-tunnel_Service_Inst]
+DisplayName = %mullvad-split-tunnel.SVCDESC%
+ServiceType = 1 ; SERVICE_KERNEL_DRIVER
+StartType = 3 ; SERVICE_DEMAND_START
+ErrorControl = 1 ; SERVICE_ERROR_NORMAL
+ServiceBinary = %12%\mullvad-split-tunnel.sys
+
+;
+;--- mullvad-split-tunnel_Device Coinstaller installation ------
+;
+
+[mullvad-split-tunnel_Device.NT.CoInstallers]
+AddReg=mullvad-split-tunnel_Device_CoInstaller_AddReg
+CopyFiles=mullvad-split-tunnel_Device_CoInstaller_CopyFiles
+
+[mullvad-split-tunnel_Device_CoInstaller_AddReg]
+HKR,,CoInstallers32,0x00010000, "WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll,WdfCoInstaller"
+
+[mullvad-split-tunnel_Device_CoInstaller_CopyFiles]
+WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll
+
+[mullvad-split-tunnel_Device.NT.Wdf]
+KmdfService = mullvad-split-tunnel, mullvad-split-tunnel_wdfsect
+[mullvad-split-tunnel_wdfsect]
+KmdfLibraryVersion = $KMDFVERSION$
+
+[Strings]
+SPSVCINST_ASSOCSERVICE= 0x00000002
+ManufacturerName="Mullvad AB"
+ClassName="Mullvad Split Tunnel"
+DiskName = "Mullvad Split Tunnel Installation Disk"
+mullvad-split-tunnel.DeviceDesc = "Mullvad Split Tunnel Device"
+mullvad-split-tunnel.SVCDESC = "Mullvad Split Tunnel Service"
diff --git a/src/mullvad-split-tunnel.sln b/src/mullvad-split-tunnel.sln
new file mode 100644
index 0000000..a6a52b7
--- /dev/null
+++ b/src/mullvad-split-tunnel.sln
@@ -0,0 +1,35 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29609.76
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mullvad-split-tunnel", "mullvad-split-tunnel.vcxproj", "{5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|ARM64 = Debug|ARM64
+ Debug|x64 = Debug|x64
+ Release|ARM64 = Release|ARM64
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Debug|ARM64.Build.0 = Debug|ARM64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Debug|ARM64.Deploy.0 = Debug|ARM64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Debug|x64.ActiveCfg = Debug|x64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Debug|x64.Build.0 = Debug|x64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Debug|x64.Deploy.0 = Debug|x64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Release|ARM64.ActiveCfg = Release|ARM64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Release|ARM64.Build.0 = Release|ARM64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Release|ARM64.Deploy.0 = Release|ARM64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Release|x64.ActiveCfg = Release|x64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Release|x64.Build.0 = Release|x64
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}.Release|x64.Deploy.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {E03BD6C9-77C0-4B56-8A56-2A430043D615}
+ EndGlobalSection
+EndGlobal
diff --git a/src/mullvad-split-tunnel.vcxproj b/src/mullvad-split-tunnel.vcxproj
new file mode 100644
index 0000000..957da77
--- /dev/null
+++ b/src/mullvad-split-tunnel.vcxproj
@@ -0,0 +1,260 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+ Debug
+ ARM64
+
+
+ Release
+ ARM64
+
+
+
+ {5B2A6B2C-D052-43DA-8181-EACB5F93E5A9}
+ {1bc93793-694f-48fe-9372-81e2b05556fd}
+ v4.5
+ 12.0
+ Debug
+ Win32
+ mullvad_split_tunnel
+ $(LatestTargetPlatformVersion)
+
+
+
+ Windows7
+ true
+ WindowsKernelModeDriver10.0
+ Driver
+ KMDF
+ Desktop
+ 1
+ 11
+
+
+
+
+ Windows7
+ false
+ WindowsKernelModeDriver10.0
+ Driver
+ KMDF
+ Desktop
+ 1
+ 11
+
+
+
+
+ Windows7
+ true
+ WindowsKernelModeDriver10.0
+ Driver
+ KMDF
+ Desktop
+ 1
+ 11
+
+
+
+
+ Windows7
+ false
+ WindowsKernelModeDriver10.0
+ Driver
+ KMDF
+ Desktop
+ 1
+ 11
+
+
+
+
+
+
+
+
+
+
+
+
+ DbgengKernelDebugger
+ $(SolutionDir)..\bin\$(Platform)-$(Configuration)\
+ $(SolutionDir)..\bin\temp\$(Platform)-$(Configuration)\$(ProjectName)\
+ true
+
+
+ DbgengKernelDebugger
+ $(SolutionDir)..\bin\$(Platform)-$(Configuration)\
+ $(SolutionDir)..\bin\temp\$(Platform)-$(Configuration)\$(ProjectName)\
+ true
+
+
+ DbgengKernelDebugger
+ $(SolutionDir)..\bin\$(Platform)-$(Configuration)\
+ $(SolutionDir)..\bin\temp\$(Platform)-$(Configuration)\$(ProjectName)\
+ true
+
+
+ DbgengKernelDebugger
+ $(SolutionDir)..\bin\$(Platform)-$(Configuration)\
+ $(SolutionDir)..\bin\temp\$(Platform)-$(Configuration)\$(ProjectName)\
+ true
+
+
+
+ POOL_NX_OPTIN=1;_WIN64;_AMD64_;AMD64;DEBUG;%(PreprocessorDefinitions)
+ stdcpplatest
+
+
+ %(AdditionalDependencies);$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;$(DDK_LIB_PATH)\wdmsec.lib;Fwpkclnt.lib
+ /INTEGRITYCHECK %(AdditionalOptions)
+
+
+ 0.0.0.1
+
+
+ copy /y $(SolutionDir)..\bin\$(Platform)-$(Configuration)\mullvad-split-tunnel.pdb $(SolutionDir)..\bin\$(Platform)-$(Configuration)\mullvad-split-tunnel\mullvad-split-tunnel.pdb
+
+
+ custom-stampinf.bat "$(InfToolPath)stampinf.exe" $(InfArch) $(KMDF_VERSION_MAJOR).$(KMDF_VERSION_MINOR) "$(IntDir)mullvad-split-tunnel.inf" "$(OutDir)mullvad-split-tunnel.inf"
+
+
+
+
+ _ARM64_;ARM64;_USE_DECLSPECS_FOR_SAL=1;STD_CALL;%(PreprocessorDefinitions)
+ stdcpp17
+
+
+ %(AdditionalDependencies);$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;$(DDK_LIB_PATH)\wdmsec.lib;Fwpkclnt.lib
+ /INTEGRITYCHECK %(AdditionalOptions)
+
+
+ copy /y $(SolutionDir)..\bin\$(Platform)-$(Configuration)\mullvad-split-tunnel.pdb $(SolutionDir)..\bin\$(Platform)-$(Configuration)\mullvad-split-tunnel\mullvad-split-tunnel.pdb
+
+
+ custom-stampinf.bat "$(InfToolPath)stampinf.exe" $(InfArch) $(KMDF_VERSION_MAJOR).$(KMDF_VERSION_MINOR) "$(IntDir)mullvad-split-tunnel.inf" "$(OutDir)mullvad-split-tunnel.inf"
+
+
+ 0.0.0.1
+
+
+
+
+ %(AdditionalDependencies);$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;$(DDK_LIB_PATH)\wdmsec.lib;Fwpkclnt.lib
+ /INTEGRITYCHECK %(AdditionalOptions)
+
+
+ stdcpp17
+
+
+ copy /y $(SolutionDir)..\bin\$(Platform)-$(Configuration)\mullvad-split-tunnel.pdb $(SolutionDir)..\bin\$(Platform)-$(Configuration)\mullvad-split-tunnel\mullvad-split-tunnel.pdb
+
+
+ custom-stampinf.bat "$(InfToolPath)stampinf.exe" $(InfArch) $(KMDF_VERSION_MAJOR).$(KMDF_VERSION_MINOR) "$(IntDir)mullvad-split-tunnel.inf" "$(OutDir)mullvad-split-tunnel.inf"
+
+
+ 0.0.0.1
+
+
+
+
+ %(AdditionalDependencies);$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;$(DDK_LIB_PATH)\wdmsec.lib;Fwpkclnt.lib
+ /INTEGRITYCHECK %(AdditionalOptions)
+
+
+ stdcpplatest
+ POOL_NX_OPTIN=1;_WIN64;_AMD64_;AMD64;%(PreprocessorDefinitions)
+
+
+ 0.0.0.1
+
+
+ copy /y $(SolutionDir)..\bin\$(Platform)-$(Configuration)\mullvad-split-tunnel.pdb $(SolutionDir)..\bin\$(Platform)-$(Configuration)\mullvad-split-tunnel\mullvad-split-tunnel.pdb
+
+
+ custom-stampinf.bat "$(InfToolPath)stampinf.exe" $(InfArch) $(KMDF_VERSION_MAJOR).$(KMDF_VERSION_MINOR) "$(IntDir)mullvad-split-tunnel.inf" "$(OutDir)mullvad-split-tunnel.inf"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/mullvad-split-tunnel.vcxproj.filters b/src/mullvad-split-tunnel.vcxproj.filters
new file mode 100644
index 0000000..02de09d
--- /dev/null
+++ b/src/mullvad-split-tunnel.vcxproj.filters
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+ firewall
+
+
+ firewall
+
+
+ firewall
+
+
+ firewall
+
+
+
+ procmon
+
+
+ procmgmt
+
+
+ eventing
+
+
+ eventing
+
+
+ containers
+
+
+ containers
+
+
+ procbroker
+
+
+ firewall
+
+
+
+
+
+
+
+
+
+
+
+ firewall
+
+
+ firewall
+
+
+ firewall
+
+
+ firewall
+
+
+ firewall
+
+
+ firewall
+
+
+ firewall
+
+
+ firewall
+
+
+
+ defs
+
+
+ defs
+
+
+ defs
+
+
+ defs
+
+
+ defs
+
+
+ defs
+
+
+ procmon
+
+
+ procmon
+
+
+ procmgmt
+
+
+ procmgmt
+
+
+
+ eventing
+
+
+ eventing
+
+
+ eventing
+
+
+ defs
+
+
+ containers
+
+
+ containers
+
+
+ procbroker
+
+
+ procbroker
+
+
+ firewall
+
+
+ procmgmt
+
+
+
+
+
+
+
+ {928a5c2c-1bf7-4e3c-acc6-8ba6d886c732}
+
+
+ {290a2ff5-e1cf-4d3c-acc0-4ca5cdb2df6a}
+
+
+ {acf27993-d281-4696-855e-8d5ce53aa007}
+
+
+ {115be191-5ceb-46b8-b6ae-69469f030f45}
+
+
+ {8014a3a4-3238-4a49-9289-c6f34abf23dc}
+
+
+ {59fed122-0778-4ba5-96a5-35a86e1467d2}
+
+
+ {1d2963d7-a048-4609-b8e7-812f4c6ed7a9}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/mullvad-split-tunnel.vcxproj.user b/src/mullvad-split-tunnel.vcxproj.user
new file mode 100644
index 0000000..4ff9ec3
--- /dev/null
+++ b/src/mullvad-split-tunnel.vcxproj.user
@@ -0,0 +1,15 @@
+
+
+
+ Off
+
+
+ Off
+
+
+ Off
+
+
+ Off
+
+
\ No newline at end of file
diff --git a/src/procbroker/context.h b/src/procbroker/context.h
new file mode 100644
index 0000000..a9139c2
--- /dev/null
+++ b/src/procbroker/context.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include
+#include "procbroker.h"
+
+namespace procbroker
+{
+
+struct SUBSCRIPTION
+{
+ LIST_ENTRY ListEntry;
+ ST_PB_CALLBACK Callback;
+ void *ClientContext;
+};
+
+struct CONTEXT
+{
+ WDFWAITLOCK SubscriptionsLock;
+ LIST_ENTRY Subscriptions;
+};
+
+} // namespace procbroker
diff --git a/src/procbroker/procbroker.cpp b/src/procbroker/procbroker.cpp
new file mode 100644
index 0000000..16b76fb
--- /dev/null
+++ b/src/procbroker/procbroker.cpp
@@ -0,0 +1,138 @@
+#include "procbroker.h"
+#include "context.h"
+#include "../defs/types.h"
+
+namespace procbroker
+{
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context
+)
+{
+ auto context = (CONTEXT*)ExAllocatePoolWithTag(PagedPool, sizeof(CONTEXT), ST_POOL_TAG);
+
+ if (NULL == context)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ RtlZeroMemory(context, sizeof(*context));
+
+ auto status = WdfWaitLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &context->SubscriptionsLock);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfWaitLockCreate() failed 0x%X\n", status);
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+
+ return status;
+ }
+
+ InitializeListHead(&context->Subscriptions);
+
+ *Context = context;
+
+ return STATUS_SUCCESS;
+}
+
+void
+TearDown
+(
+ CONTEXT **Context
+)
+{
+ auto context = *Context;
+
+ LIST_ENTRY *record;
+
+ while ((record = RemoveHeadList(&context->Subscriptions)) != &context->Subscriptions)
+ {
+ ExFreePoolWithTag(record, ST_POOL_TAG);
+ }
+
+ WdfObjectDelete(context->SubscriptionsLock);
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+
+ *Context = NULL;
+}
+
+NTSTATUS
+Subscribe
+(
+ CONTEXT *Context,
+ ST_PB_CALLBACK Callback,
+ void *ClientContext
+)
+{
+ auto sub = (SUBSCRIPTION*)ExAllocatePoolWithTag(PagedPool, sizeof(SUBSCRIPTION), ST_POOL_TAG);
+
+ if (NULL == sub)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ RtlZeroMemory(sub, sizeof(SUBSCRIPTION));
+
+ sub->Callback = Callback;
+ sub->ClientContext = ClientContext;
+
+ WdfWaitLockAcquire(Context->SubscriptionsLock, NULL);
+
+ InsertTailList(&Context->Subscriptions, &sub->ListEntry);
+
+ WdfWaitLockRelease(Context->SubscriptionsLock);
+
+ return STATUS_SUCCESS;
+}
+
+void
+CancelSubscription
+(
+ CONTEXT *Context,
+ ST_PB_CALLBACK Callback
+)
+{
+ WdfWaitLockAcquire(Context->SubscriptionsLock, NULL);
+
+ for (auto entry = Context->Subscriptions.Flink;
+ entry != &Context->Subscriptions;
+ entry = entry->Flink)
+ {
+ if (((SUBSCRIPTION*)entry)->Callback == Callback)
+ {
+ RemoveEntryList(entry);
+
+ break;
+ }
+ }
+
+ WdfWaitLockRelease(Context->SubscriptionsLock);
+}
+
+void
+Publish
+(
+ CONTEXT *Context,
+ HANDLE ProcessId,
+ bool Arriving
+)
+{
+ WdfWaitLockAcquire(Context->SubscriptionsLock, NULL);
+
+ for (auto entry = Context->Subscriptions.Flink;
+ entry != &Context->Subscriptions;
+ entry = entry->Flink)
+ {
+ auto sub = (SUBSCRIPTION*)entry;
+
+ sub->Callback(ProcessId, Arriving, sub->ClientContext);
+ }
+
+ WdfWaitLockRelease(Context->SubscriptionsLock);
+}
+
+} // namespace procbroker
diff --git a/src/procbroker/procbroker.h b/src/procbroker/procbroker.h
new file mode 100644
index 0000000..327afe4
--- /dev/null
+++ b/src/procbroker/procbroker.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include
+
+//
+// Process event broker.
+//
+// Distributes events in the system to notify subsystems when
+// processes arrive and depart.
+//
+// Introduced to break the dependency between "procmgmt" and "firewall".
+//
+
+namespace procbroker
+{
+
+struct CONTEXT;
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context
+);
+
+void
+TearDown
+(
+ CONTEXT **Context
+);
+
+typedef void (NTAPI *ST_PB_CALLBACK)(HANDLE ProcessId, bool Arriving, void *Context);
+
+NTSTATUS
+Subscribe
+(
+ CONTEXT *Context,
+ ST_PB_CALLBACK Callback,
+ void *ClientContext
+);
+
+void
+CancelSubscription
+(
+ CONTEXT *Context,
+ ST_PB_CALLBACK Callback
+);
+
+void
+Publish
+(
+ CONTEXT *Context,
+ HANDLE ProcessId,
+ bool Arriving
+);
+
+} // namespace procbroker
diff --git a/src/procmgmt/callbacks.h b/src/procmgmt/callbacks.h
new file mode 100644
index 0000000..b44fe52
--- /dev/null
+++ b/src/procmgmt/callbacks.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include
+
+namespace procmgmt
+{
+
+typedef void (NTAPI *ACQUIRE_STATE_LOCK_FN)(void *context);
+typedef void (NTAPI *RELEASE_STATE_LOCK_FN)(void *context);
+typedef bool (NTAPI *ENGAGED_STATE_ACTIVE_FN)(void *context);
+
+} // namespace procmgmt
diff --git a/src/procmgmt/context.h b/src/procmgmt/context.h
new file mode 100644
index 0000000..6b73170
--- /dev/null
+++ b/src/procmgmt/context.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "../procmon/procmon.h"
+#include "../procbroker/procbroker.h"
+#include "../containers.h"
+#include "../eventing/eventing.h"
+#include "../firewall/firewall.h"
+#include "callbacks.h"
+
+namespace procmgmt
+{
+
+struct CONTEXT
+{
+ procmon::CONTEXT *ProcessMonitor;
+
+ procbroker::CONTEXT *ProcessEventBroker;
+
+ PROCESS_REGISTRY_MGMT *ProcessRegistry;
+
+ REGISTERED_IMAGE_MGMT *RegisteredImage;
+
+ eventing::CONTEXT *Eventing;
+
+ firewall::CONTEXT *Firewall;
+
+ ACQUIRE_STATE_LOCK_FN AcquireStateLock;
+ RELEASE_STATE_LOCK_FN ReleaseStateLock;
+ ENGAGED_STATE_ACTIVE_FN EngagedStateActive;
+
+ void *CallbackContext;
+};
+
+} // namespace procmgmt
diff --git a/src/procmgmt/procmgmt.cpp b/src/procmgmt/procmgmt.cpp
new file mode 100644
index 0000000..e3de42a
--- /dev/null
+++ b/src/procmgmt/procmgmt.cpp
@@ -0,0 +1,536 @@
+#include "procmgmt.h"
+#include "context.h"
+#include "../util.h"
+#include "../defs/events.h"
+#include "../eventing/builder.h"
+
+namespace procmgmt
+{
+
+namespace
+{
+
+//
+// ValidateCollision()
+//
+// Find and validate existing entry in process registry that prevented the insertion
+// of a a new entry, because they share the same PID.
+//
+bool
+ValidateCollision
+(
+ CONTEXT *Context,
+ const procregistry::PROCESS_REGISTRY_ENTRY *newEntry
+)
+{
+ auto processRegistry = Context->ProcessRegistry;
+
+ const auto existingEntry = procregistry::FindEntry(processRegistry->Instance, newEntry->ProcessId);
+
+ if (existingEntry == NULL)
+ {
+ DbgPrint("Validate PR collision - could not look up existing entry\n");
+
+ goto Abort_unlock_break;
+ }
+
+ if (existingEntry->ParentProcessId != newEntry->ParentProcessId)
+ {
+ DbgPrint("Validate PR collision - different parent process\n");
+
+ goto Abort_unlock_break;
+ }
+
+ if (existingEntry->ImageName.Length == 0)
+ {
+ if (newEntry->ImageName.Length != 0)
+ {
+ DbgPrint("Validate PR collision - "\
+ "registered entry is without image name but proposed entry is not\n");
+
+ goto Abort_unlock_break;
+ }
+
+ goto Approved;
+ }
+
+ //
+ // Both the existing entry and the proposed entry will have lower-case
+ // imagenames so it's straight forward to compare the strings.
+ //
+
+ const auto equalBytes = RtlCompareMemory
+ (
+ existingEntry->ImageName.Buffer,
+ newEntry->ImageName.Buffer,
+ newEntry->ImageName.Length
+ );
+
+ if (equalBytes != newEntry->ImageName.Length)
+ {
+ DbgPrint("Validate PR collision - mismatched image name\n");
+
+ goto Abort_unlock_break;
+ }
+
+Approved:
+
+ DbgPrint("Process registry collision validation has succeeded\n");
+
+ return true;
+
+Abort_unlock_break:
+
+ DbgPrint("Process registry collision validation has failed\n");
+
+ DbgPrint("Existing entry at %p\n", existingEntry);
+ DbgPrint("New proposed entry at %p\n", newEntry);
+
+ util::StopIfDebugBuild();
+
+ return false;
+}
+
+struct ArrivalEvent
+{
+ UINT32 SplittingReason;
+ bool EmitEvent;
+
+ //
+ // Successfully adding a new entry in the process registry makes the
+ // registry take ownership of the imagename buffer passed.
+ //
+ // Therefore, if we need to emit a splitting event for a successful addition,
+ // we have to duplicate the imagename here to preserve it.
+ //
+ LOWER_UNICODE_STRING Imagename;
+};
+
+void
+EvaluateSplitting
+(
+ CONTEXT *Context,
+ procregistry::PROCESS_REGISTRY_ENTRY *RegistryEntry,
+ ArrivalEvent *ArrivalEvent
+)
+{
+ auto registeredImage = Context->RegisteredImage->Instance;
+
+ if (registeredimage::HasEntryExact(registeredImage, &RegistryEntry->ImageName))
+ {
+ RegistryEntry->Settings.Split = ST_PROCESS_SPLIT_STATUS_ON_BY_CONFIG;
+ ArrivalEvent->SplittingReason |= ST_SPLITTING_REASON_BY_CONFIG;
+
+ goto Duplicate_imagename;
+ }
+
+ //
+ // Note that we're providing an entry which is not yet added to the registry.
+ // This may seem wrong but is totally fine.
+ //
+ auto processRegistry = Context->ProcessRegistry;
+
+ auto parent = procregistry::GetParentEntry(processRegistry->Instance, RegistryEntry);
+
+ if (parent == NULL || !util::SplittingEnabled(parent->Settings.Split))
+ {
+ return;
+ }
+
+ RegistryEntry->Settings.Split = ST_PROCESS_SPLIT_STATUS_ON_BY_INHERITANCE;
+ ArrivalEvent->SplittingReason |= ST_SPLITTING_REASON_BY_INHERITANCE;
+
+Duplicate_imagename:
+
+ ArrivalEvent->EmitEvent = true;
+
+ auto status = util::DuplicateString
+ (
+ &RegistryEntry->ImageName,
+ &ArrivalEvent->Imagename,
+ ST_PAGEABLE::NO
+ );
+
+ if (NT_SUCCESS(status))
+ {
+ return;
+ }
+
+ DbgPrint("Cannot emit splitting event for arriving process due to resource exhaustion\n");
+
+ ArrivalEvent->EmitEvent = false;
+}
+
+void
+HandleProcessArriving
+(
+ CONTEXT *Context,
+ const procmon::PROCESS_EVENT *Record
+)
+{
+ //DbgPrint("Process arriving: 0x%X\n", Record->ProcessId);
+ //DbgPrint(" Parent: 0x%X\n", Record->Details->ParentProcessId);
+ //DbgPrint(" Path: %wZ\n", Record->Details->Path);
+
+ //
+ // State lock is held and is locking out IOCTL handlers.
+ //
+ // The process registry lock will be required for updating the process registry.
+ // The configuration lock won't be required.
+ //
+
+ auto processRegistry = Context->ProcessRegistry;
+
+ procregistry::PROCESS_REGISTRY_ENTRY registryEntry = { 0 };
+
+ auto status = procregistry::InitializeEntry
+ (
+ processRegistry->Instance,
+ Record->Details->ParentProcessId,
+ Record->ProcessId,
+ ST_PROCESS_SPLIT_STATUS_OFF,
+ &(Record->Details->ImageName),
+ ®istryEntry
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Failed to initialize entry for arriving process: status 0x%X\n", status);
+ DbgPrint(" PID of arriving process %p\n", Record->ProcessId);
+
+ return;
+ }
+
+ ArrivalEvent arrivalEvent =
+ {
+ .SplittingReason = ST_SPLITTING_REASON_PROCESS_ARRIVING,
+ .EmitEvent = false
+ };
+
+ if (Context->EngagedStateActive(Context->CallbackContext))
+ {
+ EvaluateSplitting(Context, ®istryEntry, &arrivalEvent);
+ }
+
+ //
+ // Insert entry into registry.
+ //
+
+ WdfSpinLockAcquire(processRegistry->Lock);
+
+ status = procregistry::AddEntry(processRegistry->Instance, ®istryEntry);
+
+ WdfSpinLockRelease(processRegistry->Lock);
+
+ if (NT_SUCCESS(status))
+ {
+ //
+ // Entry was successfully added and we no longer own the imagename buffer
+ // referenced by the registry entry.
+ //
+
+ if (arrivalEvent.EmitEvent)
+ {
+ auto splittingEvent = eventing::BuildStartSplittingEvent
+ (
+ Record->ProcessId,
+ (ST_SPLITTING_STATUS_CHANGE_REASON)arrivalEvent.SplittingReason,
+ &arrivalEvent.Imagename
+ );
+
+ eventing::Emit(Context->Eventing, &splittingEvent);
+ }
+ }
+ else if (status == STATUS_DUPLICATE_OBJECTID)
+ {
+ //
+ // During driver initialization it may happen that the process registry is
+ // populated with processes that are also queued to the current function.
+ //
+ // This is usually fine, but has to be verified to ensure it's an exact duplicate
+ // and not just a PID collision.
+ //
+ // The latter would indicate that events are not being queued in an orderly fashion
+ // or went missing alltogether.
+ //
+ // In case the collision is approved - Do NOT emit an event since the corresponding
+ // event will already have been emitted.
+ //
+
+ auto validationStatus = ValidateCollision(Context, ®istryEntry);
+
+ if (!validationStatus && arrivalEvent.EmitEvent)
+ {
+ auto splittingErrorEvent = eventing::BuildStartSplittingErrorEvent
+ (
+ Record->ProcessId,
+ ®istryEntry.ImageName
+ );
+
+ eventing::Emit(Context->Eventing, &splittingErrorEvent);
+ }
+
+ procregistry::ReleaseEntry(®istryEntry);
+ }
+ else
+ {
+ //
+ // General error handling.
+ //
+
+ DbgPrint("Failed to add entry for arriving process: status 0x%X.\n", status);
+ DbgPrint(" PID of arriving process %p\n", Record->ProcessId);
+
+ if (arrivalEvent.EmitEvent)
+ {
+ auto splittingErrorEvent = eventing::BuildStartSplittingErrorEvent
+ (
+ Record->ProcessId,
+ ®istryEntry.ImageName
+ );
+
+ eventing::Emit(Context->Eventing, &splittingErrorEvent);
+ }
+
+ procregistry::ReleaseEntry(®istryEntry);
+ }
+
+ //
+ // Clean up event data.
+ //
+
+ if (arrivalEvent.Imagename.Buffer != NULL)
+ {
+ util::FreeStringBuffer(&arrivalEvent.Imagename);
+ }
+
+ //
+ // No need to update the firewall because the arriving process won't
+ // have any existing connections.
+ //
+}
+
+NTSTATUS
+UpdateFirewallDepartingProcess
+(
+ CONTEXT *Context,
+ procregistry::PROCESS_REGISTRY_ENTRY *registryEntry
+)
+{
+ //
+ // It's inferred that we're in the engaged state.
+ // Because we found a process record that has firewall state.
+ // But leave this assert here for now.
+ //
+ NT_ASSERT(Context->EngagedStateActive(Context->CallbackContext));
+
+ auto status = firewall::TransactionBegin(Context->Firewall);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Failed to create firewall transaction: 0x%X\n", status);
+
+ return status;
+ }
+
+ status = firewall::RegisterAppBecomingUnsplitTx2(Context->Firewall, ®istryEntry->ImageName);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Failed to update firewall: 0x%X\n", status);
+
+ auto s2 = firewall::TransactionAbort(Context->Firewall);
+
+ if (!NT_SUCCESS(s2))
+ {
+ DbgPrint("Failed to abort firewall transaction: 0x%X\n", s2);
+ }
+
+ return status;
+ }
+
+ status = firewall::TransactionCommit(Context->Firewall);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Failed to commit firewall transaction: 0x%X\n", status);
+
+ auto s2 = firewall::TransactionAbort(Context->Firewall);
+
+ if (!NT_SUCCESS(s2))
+ {
+ DbgPrint("Failed to abort firewall transaction: 0x%X\n", s2);
+ }
+ }
+
+ return status;
+}
+
+void
+HandleProcessDeparting
+(
+ CONTEXT *Context,
+ const procmon::PROCESS_EVENT *Record
+)
+{
+ //DbgPrint("Process departing: 0x%X\n", Record->ProcessId);
+
+ //
+ // We're still at PASSIVE_LEVEL and the state lock is held.
+ // IOCTL handlers are locked out.
+ //
+ // Complete all processing and acquire the spin lock only when
+ // updating the process tree.
+ //
+
+ auto processRegistry = Context->ProcessRegistry;
+
+ auto registryEntry = procregistry::FindEntry(processRegistry->Instance, Record->ProcessId);
+
+ if (NULL == registryEntry)
+ {
+ DbgPrint("Received process-departing event for unknown PID\n");
+
+ return;
+ }
+
+ if (registryEntry->Settings.HasFirewallState)
+ {
+ auto status = UpdateFirewallDepartingProcess(Context, registryEntry);
+
+ eventing::RAW_EVENT *evt = NULL;
+
+ if (NT_SUCCESS(status))
+ {
+ evt = eventing::BuildStopSplittingEvent(registryEntry->ProcessId,
+ ST_SPLITTING_REASON_PROCESS_DEPARTING, ®istryEntry->ImageName);
+ }
+ else
+ {
+ evt = eventing::BuildStopSplittingErrorEvent(registryEntry->ProcessId,
+ ®istryEntry->ImageName);
+ }
+
+ eventing::Emit(Context->Eventing, &evt);
+ }
+ else if (util::SplittingEnabled(registryEntry->Settings.Split))
+ {
+ auto splittingEvent = eventing::BuildStopSplittingEvent(Record->ProcessId,
+ ST_SPLITTING_REASON_PROCESS_DEPARTING, ®istryEntry->ImageName);
+
+ eventing::Emit(Context->Eventing, &splittingEvent);
+ }
+
+ WdfSpinLockAcquire(processRegistry->Lock);
+
+ procregistry::DeleteEntry(processRegistry->Instance, registryEntry);
+
+ WdfSpinLockRelease(processRegistry->Lock);
+}
+
+void
+NTAPI
+ProcessEventSink
+(
+ const procmon::PROCESS_EVENT *Event,
+ void *Context
+)
+{
+ auto context = (CONTEXT*)Context;
+
+ const auto arriving = (Event->Details != NULL);
+
+ context->AcquireStateLock(context->CallbackContext);
+
+ if (arriving)
+ {
+ HandleProcessArriving(context, Event);
+ }
+ else
+ {
+ HandleProcessDeparting(context, Event);
+ }
+
+ context->ReleaseStateLock(context->CallbackContext);
+
+ procbroker::Publish(context->ProcessEventBroker, Event->ProcessId, arriving);
+}
+
+} // anonymous namespace
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ procbroker::CONTEXT *ProcessEventBroker,
+ PROCESS_REGISTRY_MGMT *ProcessRegistry,
+ REGISTERED_IMAGE_MGMT *RegisteredImage,
+ eventing::CONTEXT *Eventing,
+ firewall::CONTEXT *Firewall,
+ ACQUIRE_STATE_LOCK_FN AcquireStateLock,
+ RELEASE_STATE_LOCK_FN ReleaseStateLock,
+ ENGAGED_STATE_ACTIVE_FN EngagedStateActive,
+ void *CallbackContext
+)
+{
+ auto context = (CONTEXT*)ExAllocatePoolWithTag(NonPagedPool, sizeof(CONTEXT), ST_POOL_TAG);
+
+ if (NULL == context)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ RtlZeroMemory(context, sizeof(*context));
+
+ auto status = procmon::Initialize(&context->ProcessMonitor, ProcessEventSink, context);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("procmon::Initialize() failed 0x%X\n", status);
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+
+ return status;
+ }
+
+ context->ProcessEventBroker = ProcessEventBroker;
+ context->ProcessRegistry = ProcessRegistry;
+ context->RegisteredImage = RegisteredImage;
+ context->Eventing = Eventing;
+ context->Firewall = Firewall;
+
+ context->AcquireStateLock = AcquireStateLock;
+ context->ReleaseStateLock = ReleaseStateLock;
+ context->EngagedStateActive = EngagedStateActive;
+ context->CallbackContext = CallbackContext;
+
+ *Context = context;
+
+ return STATUS_SUCCESS;
+}
+
+void
+TearDown
+(
+ CONTEXT **Context
+)
+{
+ auto context = *Context;
+
+ procmon::TearDown(&context->ProcessMonitor);
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+
+ *Context = NULL;
+}
+
+void
+Activate
+(
+ CONTEXT *Context
+)
+{
+ procmon::EnableDispatching(Context->ProcessMonitor);
+}
+
+} // namespace procmgmt
diff --git a/src/procmgmt/procmgmt.h b/src/procmgmt/procmgmt.h
new file mode 100644
index 0000000..9f58c8d
--- /dev/null
+++ b/src/procmgmt/procmgmt.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include
+#include
+#include "../procbroker/procbroker.h"
+#include "../containers.h"
+#include "../eventing/eventing.h"
+#include "../firewall/firewall.h"
+#include "callbacks.h"
+
+namespace procmgmt
+{
+
+struct CONTEXT;
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ procbroker::CONTEXT *ProcessEventBroker,
+ PROCESS_REGISTRY_MGMT *ProcessRegistry,
+ REGISTERED_IMAGE_MGMT *RegisteredImage,
+ eventing::CONTEXT *Eventing,
+ firewall::CONTEXT *Firewall,
+ ACQUIRE_STATE_LOCK_FN AcquireStateLock,
+ RELEASE_STATE_LOCK_FN ReleaseStateLock,
+ ENGAGED_STATE_ACTIVE_FN EngagedStateActive,
+ void *CallbackContext
+);
+
+void
+TearDown
+(
+ CONTEXT **Context
+);
+
+//
+// Activate()
+//
+// Until after you call Activate(), all process events are queued.
+// Call Activate() after the process registry is populated.
+//
+void
+Activate
+(
+ CONTEXT *Context
+);
+
+} // namespace procmgmt
diff --git a/src/procmon/context.h b/src/procmon/context.h
new file mode 100644
index 0000000..a9a2c99
--- /dev/null
+++ b/src/procmon/context.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include
+#include
+#include "procmon.h"
+
+namespace procmon
+{
+
+struct CONTEXT
+{
+ // The thread that services queued process events.
+ PETHREAD DispatchWorker;
+
+ // Lock to coordinate work on the queue.
+ WDFWAITLOCK QueueLock;
+
+ // Queue of incoming process events.
+ LIST_ENTRY EventQueue;
+
+ // Event that signals worker should exit.
+ KEVENT ExitWorker;
+
+ // Event that signals a new process event has been queued.
+ KEVENT WakeUpWorker;
+
+ //
+ // Initially events are not dispatched.
+ //
+ // This variable controls whether an event should only be queued or if the queue should be
+ // signalled as well.
+ //
+ bool DispatchingEnabled;
+
+ //
+ // Client callback function that receives process events.
+ // Single client only in this layer.
+ //
+ PROCESS_EVENT_SINK ProcessEventSink;
+
+ //
+ // Context to pass along when making the callback.
+ //
+ void *SinkContext;
+};
+
+} // namespace procmon
diff --git a/src/procmon/procmon.cpp b/src/procmon/procmon.cpp
new file mode 100644
index 0000000..f972d79
--- /dev/null
+++ b/src/procmon/procmon.cpp
@@ -0,0 +1,399 @@
+#include
+#include "procmon.h"
+#include "context.h"
+#include "../util.h"
+#include "../defs/types.h"
+
+namespace procmon
+{
+
+namespace
+{
+
+//
+// PsSetCreateProcessNotifyRoutineEx() is broken so you can't pass context.
+//
+// This isn't ideal, especially considering creating more than once instance of this "class" will
+// send all events to the most recently registered sink.
+//
+// But... There should never be more than one instance.
+// And this lets us keep a familiar interface towards clients, so just roll with it.
+//
+CONTEXT *g_Context = NULL;
+
+void
+SystemProcessEvent
+(
+ PEPROCESS Process,
+ HANDLE ProcessId,
+ PPS_CREATE_NOTIFY_INFO CreateInfo
+)
+{
+ //
+ // We want to offload the system thread this is being sent on.
+ // Build a self-contained event record and queue it to a dedicated thread.
+ //
+
+ PROCESS_EVENT *record = NULL;
+
+ if (CreateInfo != NULL)
+ {
+ //
+ // Process is arriving.
+ //
+ // First, get the filename so we can determine the size of the final
+ // buffer that needs to be allocated.
+ //
+
+ UNICODE_STRING *imageName;
+
+ auto status = util::GetDevicePathImageName(Process, &imageName);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("Dropping process event\n");
+ DbgPrint(" Could not determine image filename, status: 0x%X\n", status);
+ DbgPrint(" PID of arriving process %p\n", ProcessId);
+
+ return;
+ }
+
+ auto offsetDetails = util::RoundToMultiple(sizeof(PROCESS_EVENT), TYPE_ALIGNMENT(PROCESS_EVENT_DETAILS));
+ auto offsetStringBuffer = util::RoundToMultiple(offsetDetails + sizeof(PROCESS_EVENT_DETAILS), TYPE_ALIGNMENT(WCHAR));
+
+ auto allocationSize = offsetStringBuffer + imageName->Length;
+
+ record = (PROCESS_EVENT *)ExAllocatePoolWithTag(PagedPool, allocationSize, ST_POOL_TAG);
+
+ if (record == NULL)
+ {
+ DbgPrint("Dropping process event\n");
+ DbgPrint(" Failed to allocate memory\n");
+ DbgPrint(" Imagename of arriving process %wZ\n", imageName);
+ DbgPrint(" PID of arriving process %p\n", ProcessId);
+
+ ExFreePoolWithTag(imageName, ST_POOL_TAG);
+
+ return;
+ }
+
+ auto details = (PROCESS_EVENT_DETAILS*)(((CHAR*)record) + offsetDetails);
+ auto stringBuffer = (WCHAR*)(((CHAR*)record) + offsetStringBuffer);
+
+ InitializeListHead(&record->ListEntry);
+ record->ProcessId = ProcessId;
+ record->Details = details;
+
+ details->ParentProcessId = CreateInfo->ParentProcessId;
+ details->ImageName.Length = imageName->Length;
+ details->ImageName.MaximumLength = imageName->Length;
+ details->ImageName.Buffer = stringBuffer;
+
+ RtlCopyMemory(stringBuffer, imageName->Buffer, imageName->Length);
+ ExFreePoolWithTag(imageName, ST_POOL_TAG);
+ }
+ else
+ {
+ //
+ // Process is departing.
+ //
+
+ record = (PROCESS_EVENT *)ExAllocatePoolWithTag(PagedPool, sizeof(PROCESS_EVENT), ST_POOL_TAG);
+
+ if (record == NULL)
+ {
+ DbgPrint("Dropping process event\n");
+ DbgPrint(" Failed to allocate memory\n");
+ DbgPrint(" PID of departing process %p\n", ProcessId);
+
+ return;
+ }
+
+ InitializeListHead(&record->ListEntry);
+ record->ProcessId = ProcessId;
+ record->Details = NULL;
+ }
+
+ //
+ // Queue to worker thread.
+ //
+
+ WdfWaitLockAcquire(g_Context->QueueLock, NULL);
+
+ InsertTailList(&g_Context->EventQueue, &record->ListEntry);
+
+ if (g_Context->DispatchingEnabled)
+ {
+ KeSetEvent(&g_Context->WakeUpWorker, 0, FALSE);
+ }
+
+ WdfWaitLockRelease(g_Context->QueueLock);
+}
+
+void
+DispatchWorker
+(
+ PVOID StartContext
+)
+{
+ auto context = (CONTEXT *)StartContext;
+
+ for (;;)
+ {
+ KeWaitForSingleObject(&context->WakeUpWorker, Executive, KernelMode, FALSE, NULL);
+
+ WdfWaitLockAcquire(context->QueueLock, NULL);
+
+ if (0 != KeReadStateEvent(&context->ExitWorker))
+ {
+ WdfWaitLockRelease(context->QueueLock);
+
+ PsTerminateSystemThread(STATUS_SUCCESS);
+
+ return;
+ }
+
+ //
+ // Reparent the queue in order to release the lock sooner.
+ //
+
+ LIST_ENTRY queue;
+
+ util::ReparentList(&queue, &context->EventQueue);
+
+ KeClearEvent(&context->WakeUpWorker);
+
+ WdfWaitLockRelease(context->QueueLock);
+
+ //
+ // There are one or more records queued.
+ // Process all available records.
+ //
+
+ LIST_ENTRY *record;
+
+ while ((record = RemoveHeadList(&queue)) != &queue)
+ {
+ context->ProcessEventSink((PROCESS_EVENT*)record, context->SinkContext);
+
+ ExFreePoolWithTag(record, ST_POOL_TAG);
+ }
+ }
+}
+
+} // anonymous namespace
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ PROCESS_EVENT_SINK ProcessEventSink,
+ void *SinkContext
+)
+{
+ *Context = NULL;
+
+ bool notifyRoutineRegistered = false;
+
+ auto context = (CONTEXT*)ExAllocatePoolWithTag(NonPagedPool, sizeof(CONTEXT), ST_POOL_TAG);
+
+ if (NULL == context)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ RtlZeroMemory(context, sizeof(*context));
+
+ context->ProcessEventSink = ProcessEventSink;
+ context->SinkContext = SinkContext;
+
+ InitializeListHead(&context->EventQueue);
+
+ KeInitializeEvent(&context->ExitWorker, NotificationEvent, FALSE);
+ KeInitializeEvent(&context->WakeUpWorker, NotificationEvent, FALSE);
+
+ auto status = WdfWaitLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &context->QueueLock);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("WdfWaitLockCreate() failed 0x%X\n", status);
+
+ context->QueueLock = NULL;
+
+ goto Abort;
+ }
+
+ g_Context = context;
+
+ //
+ // It's alright to register for notifications before starting the worker thread.
+ //
+ // Events that come in before the thread is created are queued.
+ // So no event will be lost.
+ //
+ // Also, the thread doesn't own the queued events so nothing is leaked even
+ // if the thread fails to process events in a timely manner, or at all.
+ //
+ // Also, clean-up is simpler if thread creation is the last fallible operation.
+ //
+
+ status = PsSetCreateProcessNotifyRoutineEx(SystemProcessEvent, FALSE);
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("PsSetCreateProcessNotifyRoutineEx() failed 0x%X\n", status);
+
+ goto Abort;
+ }
+
+ notifyRoutineRegistered = true;
+
+ //
+ // Create the thread that will be servicing events.
+ //
+
+ OBJECT_ATTRIBUTES threadAttributes;
+
+ InitializeObjectAttributes(&threadAttributes, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
+
+ HANDLE threadHandle;
+
+ status = PsCreateSystemThread
+ (
+ &threadHandle,
+ THREAD_ALL_ACCESS,
+ &threadAttributes,
+ NULL,
+ NULL,
+ DispatchWorker,
+ context
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ DbgPrint("PsCreateSystemThread() failed 0x%X\n", status);
+ DbgPrint("Could not create process monitoring thread\n");
+
+ goto Abort;
+ }
+
+ //
+ // ObReference... will never fail if the handle is valid.
+ //
+
+ status = ObReferenceObjectByHandle
+ (
+ threadHandle,
+ THREAD_ALL_ACCESS,
+ NULL,
+ KernelMode,
+ (PVOID *)&context->DispatchWorker,
+ NULL
+ );
+
+ ZwClose(threadHandle);
+
+ *Context = context;
+
+ return STATUS_SUCCESS;
+
+Abort:
+
+ if (notifyRoutineRegistered)
+ {
+ PsSetCreateProcessNotifyRoutineEx(SystemProcessEvent, TRUE);
+
+ //
+ // Drain event queue to avoid leaking events.
+ //
+
+ LIST_ENTRY *record;
+
+ while ((record = RemoveHeadList(&context->EventQueue)) != &context->EventQueue)
+ {
+ ExFreePoolWithTag(record, ST_POOL_TAG);
+ }
+ }
+
+ if (context->QueueLock != NULL)
+ {
+ WdfObjectDelete(context->QueueLock);
+ }
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+ g_Context = NULL;
+
+ return status;
+}
+
+void
+TearDown
+(
+ CONTEXT **Context
+)
+{
+ auto context = *Context;
+
+ //
+ // Deregister notify routine so we stop queuing events.
+ // This can never fail according to documentation.
+ //
+
+ PsSetCreateProcessNotifyRoutineEx(SystemProcessEvent, TRUE);
+
+ //
+ // Tell worker thread to exit and wait for it to happen.
+ //
+
+ WdfWaitLockAcquire(context->QueueLock, NULL);
+
+ KeSetEvent(&context->ExitWorker, 0, FALSE);
+ KeSetEvent(&context->WakeUpWorker, 1, FALSE);
+
+ WdfWaitLockRelease(context->QueueLock);
+
+ KeWaitForSingleObject(context->DispatchWorker, Executive, KernelMode, FALSE, NULL);
+
+ ObDereferenceObject(context->DispatchWorker);
+
+ //
+ // Drain event queue to avoid leaking events.
+ //
+
+ LIST_ENTRY *record;
+
+ while ((record = RemoveHeadList(&context->EventQueue)) != &context->EventQueue)
+ {
+ ExFreePoolWithTag(record, ST_POOL_TAG);
+ }
+
+ //
+ // Release remaining resources.
+ //
+
+ WdfObjectDelete(context->QueueLock);
+
+ ExFreePoolWithTag(context, ST_POOL_TAG);
+
+ *Context = NULL;
+}
+
+void
+EnableDispatching
+(
+ CONTEXT *Context
+)
+{
+ WdfWaitLockAcquire(Context->QueueLock, NULL);
+
+ Context->DispatchingEnabled = true;
+
+ if (!IsListEmpty(&Context->EventQueue))
+ {
+ KeSetEvent(&Context->WakeUpWorker, 0, FALSE);
+ }
+
+ WdfWaitLockRelease(Context->QueueLock);
+}
+
+}
diff --git a/src/procmon/procmon.h b/src/procmon/procmon.h
new file mode 100644
index 0000000..daf55cd
--- /dev/null
+++ b/src/procmon/procmon.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include
+
+namespace procmon
+{
+
+typedef struct tag_PROCESS_EVENT_DETAILS
+{
+ HANDLE ParentProcessId;
+
+ // Device path using mixed case characters.
+ UNICODE_STRING ImageName;
+}
+PROCESS_EVENT_DETAILS;
+
+typedef struct tag_PROCESS_EVENT
+{
+ LIST_ENTRY ListEntry;
+
+ HANDLE ProcessId;
+
+ //
+ // `Details` will be present and valid for processes that are arriving.
+ // If a process is departing, this field is set to NULL.
+ //
+ PROCESS_EVENT_DETAILS *Details;
+}
+PROCESS_EVENT;
+
+typedef void (NTAPI *PROCESS_EVENT_SINK)(const PROCESS_EVENT *Event, void *Context);
+
+struct CONTEXT;
+
+NTSTATUS
+Initialize
+(
+ CONTEXT **Context,
+ PROCESS_EVENT_SINK ProcessEventSink,
+ void *SinkContext
+);
+
+void
+TearDown
+(
+ CONTEXT **Context
+);
+
+void
+EnableDispatching
+(
+ CONTEXT *Context
+);
+
+} // namespace procmon
diff --git a/src/public.h b/src/public.h
new file mode 100644
index 0000000..f51e361
--- /dev/null
+++ b/src/public.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "x64guard.h"
+#include "ipaddr.h"
+#include "defs/state.h"
+#include "defs/ioctl.h"
+#include "defs/config.h"
+#include "defs/process.h"
+#include "defs/queryprocess.h"
+#include "defs/events.h"
diff --git a/src/resource.rc b/src/resource.rc
new file mode 100644
index 0000000..fa8648d
--- /dev/null
+++ b/src/resource.rc
@@ -0,0 +1,34 @@
+#include "version.h"
+
+#define STRINGIFY(X) #X
+#define EXPANDSTR(X) STRINGIFY(X)
+#define MAKE_VERSION_STR(A,B,C,D) EXPANDSTR(A) "." EXPANDSTR(B) "." EXPANDSTR(C) "." EXPANDSTR(D)
+#define CALL(A,B) A B
+#define DRIVER_VERSION_STR_HELPER(X) CALL(MAKE_VERSION_STR,(X))
+
+#define DRIVER_VERSION DRIVER_VERSION_MAJOR,DRIVER_VERSION_MINOR,DRIVER_VERSION_PATCH,DRIVER_VERSION_BUILD
+#define DRIVER_VERSION_STR DRIVER_VERSION_STR_HELPER(DRIVER_VERSION)
+
+1 VERSIONINFO
+FILEVERSION DRIVER_VERSION
+PRODUCTVERSION DRIVER_VERSION
+BEGIN
+BLOCK "StringFileInfo"
+BEGIN
+ BLOCK "040904E4"
+ BEGIN
+ VALUE "CompanyName", "Mullvad VPN AB"
+ VALUE "FileDescription", "Split Tunnel Kernel Driver"
+ VALUE "FileVersion", DRIVER_VERSION_STR
+ VALUE "InternalName", "mullvad-split-tunnel"
+ VALUE "LegalCopyright", "(c) 2021 Mullvad VPN AB"
+ VALUE "OriginalFilename", "mullvad-split-tunnel.sys"
+ VALUE "ProductName", "Mullvad VPN"
+ VALUE "ProductVersion", DRIVER_VERSION_STR
+ END
+END
+BLOCK "VarFileInfo"
+BEGIN
+ VALUE "Translation", 0x409, 1252
+END
+END
diff --git a/src/util.cpp b/src/util.cpp
new file mode 100644
index 0000000..193de2f
--- /dev/null
+++ b/src/util.cpp
@@ -0,0 +1,339 @@
+#include
+#include "util.h"
+
+namespace util
+{
+
+void
+ReparentList(LIST_ENTRY *dest, LIST_ENTRY *src)
+{
+ //
+ // If it's an empty list there is nothing to reparent.
+ //
+
+ if (src->Flink == src)
+ {
+ InitializeListHead(dest);
+ return;
+ }
+
+ //
+ // Replace root node.
+ //
+
+ *dest = *src;
+
+ //
+ // Update links on first and last entry.
+ //
+
+ dest->Flink->Blink = dest;
+ dest->Blink->Flink = dest;
+
+ //
+ // Reinitialize original root node.
+ //
+
+ InitializeListHead(src);
+}
+
+typedef NTSTATUS (*QUERY_INFO_PROCESS) (
+ __in HANDLE ProcessHandle,
+ __in PROCESSINFOCLASS ProcessInformationClass,
+ __out_bcount(ProcessInformationLength) PVOID ProcessInformation,
+ __in ULONG ProcessInformationLength,
+ __out_opt PULONG ReturnLength
+);
+
+extern "C"
+NTSTATUS
+GetDevicePathImageName
+(
+ PEPROCESS Process,
+ UNICODE_STRING **ImageName
+)
+{
+ *ImageName = NULL;
+
+ HANDLE processHandle;
+
+ auto status = ObOpenObjectByPointer
+ (
+ Process,
+ OBJ_KERNEL_HANDLE,
+ NULL,
+ GENERIC_READ,
+ NULL,
+ KernelMode,
+ &processHandle
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ static QUERY_INFO_PROCESS QueryFunction = NULL;
+
+ if (QueryFunction == NULL)
+ {
+ DECLARE_CONST_UNICODE_STRING(queryName, L"ZwQueryInformationProcess");
+
+ QueryFunction = (QUERY_INFO_PROCESS)
+ MmGetSystemRoutineAddress((UNICODE_STRING*)&queryName);
+
+ if (NULL == QueryFunction)
+ {
+ // TODO: Use more appropriate error code
+ status = STATUS_NOT_CAPABLE;
+
+ goto Failure;
+ }
+ }
+
+ //
+ // Determine required size of name buffer.
+ //
+
+ ULONG bufferLength;
+
+ status = QueryFunction
+ (
+ processHandle,
+ ProcessImageFileName,
+ NULL,
+ 0,
+ &bufferLength
+ );
+
+ if (status != STATUS_INFO_LENGTH_MISMATCH)
+ {
+ goto Failure;
+ }
+
+ //
+ // Allocate name buffer.
+ //
+
+ *ImageName = (UNICODE_STRING*)ExAllocatePoolWithTag(PagedPool, bufferLength, ST_POOL_TAG);
+
+ if (NULL == *ImageName)
+ {
+ status = STATUS_INSUFFICIENT_RESOURCES;
+
+ goto Failure;
+ }
+
+ //
+ // Retrieve filename.
+ //
+
+ status = QueryFunction
+ (
+ processHandle,
+ ProcessImageFileName,
+ *ImageName,
+ bufferLength,
+ &bufferLength
+ );
+
+ if (NT_SUCCESS(status))
+ {
+ goto Cleanup;
+ }
+
+Failure:
+
+ if (*ImageName != NULL)
+ {
+ ExFreePoolWithTag(*ImageName, ST_POOL_TAG);
+ }
+
+Cleanup:
+
+ ZwClose(processHandle);
+
+ return status;
+}
+
+bool
+ValidateBufferRange
+(
+ void *Buffer,
+ void *BufferEnd,
+ SIZE_T RangeOffset,
+ SIZE_T RangeLength
+)
+{
+ if (RangeLength == 0)
+ {
+ return true;
+ }
+
+ auto range = (UCHAR*)Buffer + RangeOffset;
+ auto rangeEnd = range + RangeLength;
+
+ if (range < (UCHAR*)Buffer
+ || range >= (UCHAR*)BufferEnd
+ || rangeEnd < range
+ || rangeEnd > BufferEnd)
+ {
+ return false;
+ }
+
+ return true;
+}
+
+bool
+IsEmptyRange
+(
+ const void *Buffer,
+ SIZE_T Length
+)
+{
+ //
+ // TODO
+ //
+ // Assuming x64, round down `Length` and read QWORDs from the buffer.
+ // Then read the last few bytes in this silly byte-by-byte manner.
+ //
+
+ for (auto b = (const UCHAR*)Buffer; Length != 0; ++b, --Length)
+ {
+ if (*b != 0)
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+NTSTATUS
+AllocateCopyDowncaseString
+(
+ const UNICODE_STRING * const In,
+ LOWER_UNICODE_STRING *Out,
+ ST_PAGEABLE Pageable
+)
+{
+ //
+ // Unfortunately, there is no way to determine the required buffer size.
+ //
+ // It would be possible to allocate e.g. `In.Length * 1.5` bytes, and waste memory.
+ //
+ // We opt for the slightly less time efficient method of allocating an exact size
+ // twice and copying the string.
+ //
+
+ UNICODE_STRING lower;
+
+ auto status = RtlDowncaseUnicodeString(&lower, In, TRUE);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ const auto poolType = (Pageable == ST_PAGEABLE::YES) ? PagedPool : NonPagedPool;
+
+ auto finalBuffer = (PWCH)ExAllocatePoolWithTag(poolType, lower.Length, ST_POOL_TAG);
+
+ if (finalBuffer == NULL)
+ {
+ RtlFreeUnicodeString(&lower);
+
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ RtlCopyMemory(finalBuffer, lower.Buffer, lower.Length);
+
+ Out->Length = lower.Length;
+ Out->MaximumLength = lower.Length;
+ Out->Buffer = finalBuffer;
+
+ RtlFreeUnicodeString(&lower);
+
+ return STATUS_SUCCESS;
+}
+
+void
+FreeStringBuffer
+(
+ UNICODE_STRING *String
+)
+{
+ ExFreePoolWithTag(String->Buffer, ST_POOL_TAG);
+
+ String->Length = 0;
+ String->MaximumLength = 0;
+ String->Buffer = NULL;
+}
+
+void
+FreeStringBuffer
+(
+ LOWER_UNICODE_STRING *String
+)
+{
+ return FreeStringBuffer((UNICODE_STRING*)String);
+}
+
+NTSTATUS
+DuplicateString
+(
+ const UNICODE_STRING *Src,
+ UNICODE_STRING *Dest,
+ ST_PAGEABLE Pageable
+)
+{
+ const auto poolType = (Pageable == ST_PAGEABLE::YES) ? PagedPool : NonPagedPool;
+
+ auto buffer = (PWCH)ExAllocatePoolWithTag(poolType, Src->Length, ST_POOL_TAG);
+
+ if (NULL == buffer)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ RtlCopyMemory(buffer, Src->Buffer, Src->Length);
+
+ Dest->Length = Src->Length;
+ Dest->MaximumLength = Src->Length;
+ Dest->Buffer = buffer;
+
+ return STATUS_SUCCESS;
+}
+
+NTSTATUS
+DuplicateString
+(
+ const LOWER_UNICODE_STRING *Src,
+ LOWER_UNICODE_STRING *Dest,
+ ST_PAGEABLE Pageable
+)
+{
+ return DuplicateString((const UNICODE_STRING*)Src, (UNICODE_STRING*)Dest, Pageable);
+}
+
+void
+StopIfDebugBuild
+(
+)
+{
+#ifdef DEBUG
+ DbgBreakPoint();
+#endif
+}
+
+bool
+SplittingEnabled
+(
+ ST_PROCESS_SPLIT_STATUS Status
+)
+{
+ return (Status == ST_PROCESS_SPLIT_STATUS_ON_BY_CONFIG
+ || Status == ST_PROCESS_SPLIT_STATUS_ON_BY_INHERITANCE);
+}
+
+} // namespace util
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..e105683
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,121 @@
+#pragma once
+
+#include
+#include "defs/types.h"
+
+#define bswapw(s) (((s & 0xFF) << 8) | ((s >> 8) & 0xFF))
+
+#define ntohs(s) bswapw(s)
+#define htons(s) bswapw(s)
+
+template
+bool bool_cast(T t)
+{
+ return t != 0;
+}
+
+namespace util
+{
+
+//
+// N.B. m has to be a power of two.
+//
+inline SIZE_T RoundToMultiple(SIZE_T v, SIZE_T m)
+{
+ return ((v + m - 1) & ~(m - 1));
+}
+
+void
+ReparentList(LIST_ENTRY *dest, LIST_ENTRY *src);
+
+//
+// GetDevicePathImageName()
+//
+// Returns the device path of the process binary.
+// I.e. the returned path begins with `\Device\HarddiskVolumeX\`
+// rather than a symbolic link of the form `\??\C:\`.
+//
+// A UNICODE_STRING structure and an associated filename buffer
+// is allocated and returned.
+//
+// TODO: The type PEPROCESS seems to require C-linkage on any function
+// that uses it as an argument. Fix, maybe.
+//
+extern "C"
+NTSTATUS
+GetDevicePathImageName
+(
+ PEPROCESS Process,
+ UNICODE_STRING **ImageName
+);
+
+bool
+ValidateBufferRange
+(
+ void *Buffer,
+ void *BufferEnd,
+ SIZE_T RangeOffset,
+ SIZE_T RangeLength
+);
+
+bool
+IsEmptyRange
+(
+ const void *Buffer,
+ SIZE_T Length
+);
+
+//
+// AllocateCopyDowncaseString()
+//
+// Make a lower case copy of the string.
+// `Out->Buffer` is allocated and assigned.
+//
+NTSTATUS
+AllocateCopyDowncaseString
+(
+ const UNICODE_STRING * const In,
+ LOWER_UNICODE_STRING *Out,
+ ST_PAGEABLE Pageable
+);
+
+void
+FreeStringBuffer
+(
+ UNICODE_STRING *String
+);
+
+void
+FreeStringBuffer
+(
+ LOWER_UNICODE_STRING *String
+);
+
+NTSTATUS
+DuplicateString
+(
+ const UNICODE_STRING *Src,
+ UNICODE_STRING *Dest,
+ ST_PAGEABLE Pageable
+);
+
+NTSTATUS
+DuplicateString
+(
+ const LOWER_UNICODE_STRING *Src,
+ LOWER_UNICODE_STRING *Dest,
+ ST_PAGEABLE Pageable
+);
+
+void
+StopIfDebugBuild
+(
+);
+
+bool
+SplittingEnabled
+(
+ ST_PROCESS_SPLIT_STATUS Status
+);
+
+} // namespace util
diff --git a/src/validation.cpp b/src/validation.cpp
new file mode 100644
index 0000000..e91e9f3
--- /dev/null
+++ b/src/validation.cpp
@@ -0,0 +1,108 @@
+#include "validation.h"
+#include "defs/config.h"
+#include "defs/process.h"
+#include "util.h"
+
+bool
+ValidateUserBufferConfiguration
+(
+ void *Buffer,
+ size_t BufferLength
+)
+{
+ auto bufferEnd = (UCHAR*)Buffer + BufferLength;
+
+ if (BufferLength < sizeof(ST_CONFIGURATION_HEADER)
+ || bufferEnd < (UCHAR*)Buffer)
+ {
+ return false;
+ }
+
+ auto header = (ST_CONFIGURATION_HEADER*)Buffer;
+
+ if (header->TotalLength != BufferLength)
+ {
+ return false;
+ }
+
+ auto stringBuffer = (UCHAR*)Buffer
+ + sizeof(ST_CONFIGURATION_HEADER)
+ + (sizeof(ST_CONFIGURATION_ENTRY) * header->NumEntries);
+
+ if (stringBuffer < (UCHAR*)Buffer
+ || stringBuffer >= bufferEnd)
+ {
+ return false;
+ }
+
+ //
+ // Verify that all strings reside within the string buffer.
+ //
+
+ auto entry = (ST_CONFIGURATION_ENTRY*)(header + 1);
+
+ for (auto i = 0; i < header->NumEntries; ++i, ++entry)
+ {
+ const auto valid = util::ValidateBufferRange(stringBuffer, bufferEnd,
+ entry->ImageNameOffset, entry->ImageNameLength);
+
+ if (!valid)
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool
+ValidateUserBufferProcesses
+(
+ void *Buffer,
+ size_t BufferLength
+)
+{
+ auto bufferEnd = (UCHAR*)Buffer + BufferLength;
+
+ if (BufferLength < sizeof(ST_PROCESS_DISCOVERY_HEADER)
+ || bufferEnd < (UCHAR*)Buffer)
+ {
+ return false;
+ }
+
+ auto header = (ST_PROCESS_DISCOVERY_HEADER*)Buffer;
+
+ if (header->TotalLength != BufferLength)
+ {
+ return false;
+ }
+
+ auto stringBuffer = (UCHAR*)Buffer
+ + sizeof(ST_PROCESS_DISCOVERY_HEADER)
+ + (sizeof(ST_PROCESS_DISCOVERY_ENTRY) * header->NumEntries);
+
+ if (stringBuffer < (UCHAR*)Buffer
+ || stringBuffer >= bufferEnd)
+ {
+ return false;
+ }
+
+ //
+ // Verify that all strings reside within the string buffer.
+ //
+
+ auto entry = (ST_PROCESS_DISCOVERY_ENTRY*)(header + 1);
+
+ for (auto i = 0; i < header->NumEntries; ++i, ++entry)
+ {
+ const auto valid = util::ValidateBufferRange(stringBuffer, bufferEnd,
+ entry->ImageNameOffset, entry->ImageNameLength);
+
+ if (!valid)
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/src/validation.h b/src/validation.h
new file mode 100644
index 0000000..77cbf11
--- /dev/null
+++ b/src/validation.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+
+//
+// ValidateUserBufferConfiguration()
+//
+// Validates configuration data sent by user mode.
+//
+bool
+ValidateUserBufferConfiguration
+(
+ void *Buffer,
+ size_t BufferLength
+);
+
+//
+// ValidateUserBufferProcesses()
+//
+// Validates process data sent by user mode.
+//
+bool
+ValidateUserBufferProcesses
+(
+ void *Buffer,
+ size_t BufferLength
+);
diff --git a/src/version.h b/src/version.h
new file mode 100644
index 0000000..7dc329f
--- /dev/null
+++ b/src/version.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#define DRIVER_VERSION_MAJOR 0
+#define DRIVER_VERSION_MINOR 1
+#define DRIVER_VERSION_PATCH 0
+#define DRIVER_VERSION_BUILD 0
diff --git a/src/x64guard.h b/src/x64guard.h
new file mode 100644
index 0000000..1809372
--- /dev/null
+++ b/src/x64guard.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#ifdef NTDDI_VERSION // kernel
+ #ifndef _AMD64_
+ #error The only supported compilation target is x64
+ #endif
+#else // user
+ #ifdef WIN32
+ #error The only supported compilation target is x64
+ #endif
+#endif
diff --git a/testing/proc.cpp b/testing/proc.cpp
new file mode 100644
index 0000000..bc6c869
--- /dev/null
+++ b/testing/proc.cpp
@@ -0,0 +1,280 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include