diff --git a/net/adblock-fast/LICENSE b/net/adblock-fast/LICENSE
new file mode 100644
index 0000000000..0ad25db4bd
--- /dev/null
+++ b/net/adblock-fast/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are 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.
+
+ 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.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ 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 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 work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ 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 AGPL, see
+.
diff --git a/net/adblock-fast/Makefile b/net/adblock-fast/Makefile
index e1bd93dc97..a8a1acb69d 100644
--- a/net/adblock-fast/Makefile
+++ b/net/adblock-fast/Makefile
@@ -4,8 +4,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=adblock-fast
-PKG_VERSION:=1.2.1
-PKG_RELEASE:=7
+PKG_VERSION:=1.2.2
+PKG_RELEASE:=6
PKG_MAINTAINER:=Stan Grishin
PKG_LICENSE:=AGPL-3.0-or-later
@@ -15,12 +15,15 @@ define Package/adblock-fast
SECTION:=net
CATEGORY:=Network
TITLE:=AdBlock Fast Service
- URL:=https://github.com/stangri/adblock-fast/
+ URL:=https://github.com/mossdef-org/adblock-fast/
PKGARCH:=all
DEPENDS:= \
- +jshn \
+curl \
+resolveip \
+ +ucode \
+ +ucode-mod-fs \
+ +ucode-mod-uci \
+ +ucode-mod-ubus \
+!BUSYBOX_DEFAULT_AWK:gawk \
+!BUSYBOX_DEFAULT_GREP:grep \
+!BUSYBOX_DEFAULT_SED:sed \
@@ -46,31 +49,21 @@ endef
define Package/adblock-fast/install
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/adblock-fast $(1)/etc/init.d/adblock-fast
- $(SED) "s|^\(readonly PKG_VERSION\).*|\1='$(PKG_VERSION)-r$(PKG_RELEASE)'|" $(1)/etc/init.d/adblock-fast
+ $(INSTALL_DIR) $(1)/lib/adblock-fast
+ $(INSTALL_DATA) ./files/lib/adblock-fast/adblock-fast.uc $(1)/lib/adblock-fast/adblock-fast.uc
+ $(INSTALL_DATA) ./files/lib/adblock-fast/cli.uc $(1)/lib/adblock-fast/cli.uc
+ $(SED) "s|^\(\tversion:\).*|\1 '$(PKG_VERSION)-r$(PKG_RELEASE)',|" $(1)/lib/adblock-fast/adblock-fast.uc
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/adblock-fast $(1)/etc/config/adblock-fast
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) ./files/etc/uci-defaults/90-adblock-fast $(1)/etc/uci-defaults/90-adblock-fast
endef
-define Package/adblock-fast/postinst
-#!/bin/sh
-# check if we are on real system
-if [ -z "$${IPKG_INSTROOT}" ]; then
- /etc/init.d/adblock-fast enable
-fi
-exit 0
-endef
-
define Package/adblock-fast/prerm
#!/bin/sh
-# check if we are on real system
if [ -z "$${IPKG_INSTROOT}" ]; then
- echo -n "Stopping adblock-fast service... "
- { /etc/init.d/adblock-fast stop && \
- /etc/init.d/adblock-fast killcache; } >/dev/null 2>&1 && echo "OK" || echo "FAIL"
- echo -n "Removing rc.d symlink for adblock-fast... "
- /etc/init.d/adblock-fast disable >/dev/null 2>&1 && echo "OK" || echo "FAIL"
+ echo -n "Removing adblock-fast cache... "
+ /etc/init.d/adblock-fast killcache >/dev/null 2>&1 && echo "OK" || echo "FAIL"
fi
exit 0
endef
diff --git a/net/adblock-fast/files/etc/init.d/adblock-fast b/net/adblock-fast/files/etc/init.d/adblock-fast
old mode 100755
new mode 100644
index 73d53153fb..5971da1299
--- a/net/adblock-fast/files/etc/init.d/adblock-fast
+++ b/net/adblock-fast/files/etc/init.d/adblock-fast
@@ -1,10 +1,9 @@
#!/bin/sh /etc/rc.common
-# Copyright 2023-2025 MOSSDeF, Stan Grishin (stangri@melmac.ca)
-# shellcheck disable=SC2015,SC3023,SC3043
+# Copyright 2023-2026 MOSSDeF, Stan Grishin (stangri@melmac.ca)
+# shellcheck disable=SC2015,SC3043
# shellcheck disable=SC2034
START=20
-# shellcheck disable=SC2034
USE_PROCD=1
LC_ALL=C
@@ -23,2678 +22,65 @@ if type extra_command 1>/dev/null 2>&1; then
fi
readonly packageName='adblock-fast'
-readonly PKG_VERSION='dev-test'
-readonly packageCompat='11'
-readonly serviceName="$packageName $PKG_VERSION"
-readonly packageMemoryThreshold='33554432'
-readonly packageConfigFile="/etc/config/${packageName}"
-readonly dnsmasqUnifiedFile="/var/run/${packageName}/${packageName}.dnsmasq"
-readonly dnsmasqAddnhostsFile="/var/run/${packageName}/dnsmasq.addnhosts"
-readonly dnsmasqAddnhostsCache="/var/run/${packageName}/dnsmasq.addnhosts.cache"
-readonly dnsmasqAddnhostsGzip="${packageName}.dnsmasq.addnhosts.gz"
-readonly dnsmasqAddnhostsOutputFormatFilter='s|^|127.0.0.1 |;s|$||'
-readonly dnsmasqAddnhostsOutputFormatFilterIPv6='s|^|:: |;s|$||'
-readonly dnsmasqAddnhostsOutputParseFilter='s|^127.0.0.1 ||;s|^:: ||;'
-readonly dnsmasqAddnhostsGrepPatternIPv4='s|^|^127\.0\.0\.1 |'
-readonly dnsmasqAddnhostsGrepPatternIPv6='s|^|^:: |'
-readonly dnsmasqConfFile="$dnsmasqUnifiedFile"
-readonly dnsmasqConfCache="/var/run/${packageName}/dnsmasq.conf.cache"
-readonly dnsmasqConfGzip="${packageName}.dnsmasq.conf.gz"
-readonly dnsmasqConfOutputFormatFilter='s|^|local=/|;s|$|/|'
-readonly dnsmasqConfOutputParseFilter='s|local=/||;s|/$||;'
-readonly dnsmasqConfGrepPattern='s|^|^local=/|;s|$|/$|'
-readonly dnsmasqIpsetFile="$dnsmasqUnifiedFile"
-readonly dnsmasqIpsetCache="/var/run/${packageName}/dnsmasq.ipset.cache"
-readonly dnsmasqIpsetGzip="${packageName}.dnsmasq.ipset.gz"
-readonly dnsmasqIpsetOutputFormatFilter='s|^|ipset=/|;s|$|/adb|'
-readonly dnsmasqIpsetOutputParseFilter='s|ipset=/||;s|/adb$||;'
-readonly dnsmasqIpsetGrepPattern='s|^|^ipset=/|;s|$|/adb$|'
-readonly dnsmasqNftsetFile="$dnsmasqUnifiedFile"
-readonly dnsmasqNftsetCache="/var/run/${packageName}/dnsmasq.nftset.cache"
-readonly dnsmasqNftsetGzip="${packageName}.dnsmasq.nftset.gz"
-readonly dnsmasqNftsetOutputFormatFilter='s|^|nftset=/|;s|$|/4#inet#fw4#adb4|'
-readonly dnsmasqNftsetOutputFormatFilterIPv6='s|^|nftset=/|;s|$|/4#inet#fw4#adb4,6#inet#fw4#adb6|'
-readonly dnsmasqNftsetOutputParseFilter='s|nftset=/||;s|/4#.*$||;'
-readonly dnsmasqNftsetGrepPattern='s|^|^nftset=/|;s|$|/4#.*$|'
-readonly dnsmasqServersFile="/var/run/${packageName}/dnsmasq.servers"
-readonly dnsmasqServersCache="/var/run/${packageName}/dnsmasq.servers.cache"
-readonly dnsmasqServersGzip="${packageName}.dnsmasq.servers.gz"
-readonly dnsmasqServersOutputFormatFilter='s|^|server=/|;s|$|/|'
-readonly dnsmasqServersAllowFilter='s|(.*)|server=/\1/#|'
-readonly dnsmasqServersBlockedCountFilter='\|/#|d'
-readonly dnsmasqServersOutputParseFilter='s|server=/||;s|/.*$||;'
-readonly dnsmasqServersGrepPattern='s|^|^server=/|;s|$|/$|'
-readonly smartdnsDomainSetFile="/var/run/${packageName}/smartdns.domainset"
-readonly smartdnsDomainSetCache="/var/run/${packageName}/smartdns.domainset.cache"
-readonly smartdnsDomainSetConfig="/var/run/${packageName}/smartdns.domainset.conf"
-readonly smartdnsDomainSetGzip="${packageName}.smartdns.domainset.gz"
-readonly smartdnsDomainSetOutputFormatFilter=''
-readonly smartdnsDomainSetOutputParseFilter=''
-readonly smartdnsIpsetFile="/var/run/${packageName}/smartdns.ipset"
-readonly smartdnsIpsetCache="/var/run/${packageName}/smartdns.ipset.cache"
-readonly smartdnsIpsetConfig="/var/run/${packageName}/smartdns.ipset.conf"
-readonly smartdnsIpsetGzip="${packageName}.smartdns.ipset.gz"
-readonly smartdnsIpsetOutputFormatFilter=''
-readonly smartdnsIpsetOutputParseFilter=''
-readonly smartdnsNftsetFile="/var/run/${packageName}/smartdns.nftset"
-readonly smartdnsNftsetCache="/var/run/${packageName}/smartdns.nftset.cache"
-readonly smartdnsNftsetConfig="/var/run/${packageName}/smartdns.nftset.conf"
-readonly smartdnsNftsetGzip="${packageName}.smartdns.nftset.gz"
-readonly smartdnsNftsetOutputFormatFilter=''
-readonly smartdnsNftsetOutputParseFilter=''
-readonly unboundFile="/var/lib/unbound/adb_list.${packageName}"
-readonly unboundCache="/var/run/${packageName}/unbound.cache"
-readonly unboundGzip="${packageName}.unbound.gz"
-readonly unboundOutputFormatFilter='s|^|local-zone: "|;s|$|." always_nxdomain|'
-readonly unboundOutputParseFilter='s|^local-zone: "||;s|." always_nxdomain$||;'
-
-readonly ALLOWED_TMP="/var/${packageName}.allowed.tmp"
-readonly A_TMP="/var/${packageName}.a.tmp"
-readonly B_TMP="/var/${packageName}.b.tmp"
-readonly SED_TMP="/var/${packageName}.sed.tmp"
-readonly uciConfigFile="/etc/config/${packageName}"
-readonly runningConfigFile="/dev/shm/${packageName}"
-readonly runningStatusFile="/dev/shm/${packageName}.status.json"
-readonly runningStatusFileLock="/var/lock/${packageName}.lock"
-readonly hostsFilter='/localhost/d;/^#/d;/^[^0-9]/d;s/^0\.0\.0\.0.//;s/^127\.0\.0\.1.//;s/[[:space:]]*#.*$//;s/[[:cntrl:]]$//;s/[[:space:]]//g;/[`~!@#\$%\^&\*()=+;:"'\'',<>?/\|[{}]/d;/]/d;/\./!d;/^$/d;/[^[:alnum:]_.-]/d;'
-# Validating domains filter
-readonly domainsFilter='/^#/d;s/[[:space:]]*#.*|[[:space:]]*$|[[:cntrl:]]$//g;/^[[:space:]]*$/d;/^-|^\.|\.\.|-$|\.$|^[0-9.]+$|^[^[:alnum:]]|[`~!@#\$%\^&\*()=+;:"'"'"',<>?/\|{}]/d;/\./!d'
-# Lax domains filter
-#readonly domainsFilter='/^#/d;s/[[:space:]]*#.*|[[:space:]]*$|[[:cntrl:]]$//g;/^[[:space:]]*$/d;/^[^[:alnum:]._-]|[`~!@#\$%\^&\*()=+;:"'"'"',<>?/\|{}]/d;/\./!d'
-readonly adBlockPlusFilter='/^#/d;/^!/d;s/[[:space:]]*#.*$//;s/^||//;s/\^$//;s/[[:space:]]*$//;s/[[:cntrl:]]$//;/[[:space:]]/d;/[`~!@#\$%\^&\*()=+;:"'\'',<>?/\|[{}]/d;/]/d;/\./!d;/^$/d;/[^[:alnum:]_.-]/d;'
-readonly dnsmasqFileFilter='\|^server=/[[:alnum:]_.-].*/|!d;s|server=/||;s|/.*$||'
-readonly dnsmasq2FileFilter='\|^local=/[[:alnum:]_.-].*/|!d;s|local=/||;s|/.*$||'
-readonly dnsmasq3FileFilter='\|^address=/[[:alnum:]_.-].*/|!d;s|address=/||;s|/.*$||'
-readonly _DOT_='.'
-readonly __DOT__='[w]'
-readonly _OK_='\033[0;32m\xe2\x9c\x93\033[0m'
-readonly __OK__='\033[0;32m[\xe2\x9c\x93]\033[0m'
-readonly _FAIL_='\033[0;31m\xe2\x9c\x97\033[0m'
-readonly __FAIL__='\033[0;31m[\xe2\x9c\x97]\033[0m'
-readonly _WARN_='\033[0;33m\xe2\x9c\x94\033[0m'
-readonly __WARN__='\033[0;33m[\xe2\x9c\x94]\033[0m'
-readonly _ERROR_='\033[0;31m[ERROR]\033[0m'
-readonly _WARNING_='\033[0;33m[WARN]\033[0m'
-# shellcheck disable=SC2155
-readonly ipset="$(command -v ipset)"
-# shellcheck disable=SC2155
-readonly nft="$(command -v nft)"
-readonly canaryDomainsMozilla='use-application-dns.net'
-readonly canaryDomainsiCloud='mask.icloud.com mask-h2.icloud.com'
-readonly triggersReload='parallel_downloads debug download_timeout allowed_domain blocked_domain allowed_url blocked_url dns config_update_enabled config_update_url dnsmasq_config_file_url curl_additional_param curl_max_file_size curl_retry'
-readonly triggersRestart='compressed_cache compressed_cache_dir force_dns led force_dns_port'
-
-# Silence "Command failed: Not found" for redundant procd service delete calls
-__UBUS_BIN="$(command -v ubus || echo /bin/ubus)"
-ubus() {
- if [ "$1" = "call" ] && [ "$2" = "service" ] && [ "$3" = "delete" ]; then
- "$__UBUS_BIN" "$@" >/dev/null 2>&1 || true
- else
- "$__UBUS_BIN" "$@"
- fi
-}
-
-dl_command=
-dl_flag=
-isSSLSupported=
-loadEnvironmentFlag=
-loadPackageConfigFlag=
-outputAllowFilter=
-outputBlockedCountFilter=
-outputFilter=
-outputFilterIPv6=
-outputFile=
-outputGzip=
-outputCache=
-stripToDomainsFilter=
-triggerStatus=
-awk='awk'
-allowed_url=
-blocked_url=
-fw4_restart_flag=
-adbf_boot_flag=
-dnsmasq_features=
-dnsmasq_ubus=
-
-# package config variables
-allow_non_ascii=
-canary_domains_icloud=
-canary_domains_mozilla=
-compressed_cache=
-config_update_enabled=
-debug_init_script=
-debug_performance=
-dnsmasq_sanity_check=
-dnsmasq_validity_check=
-enabled=
-force_dns=
-ipv6_enabled=
-parallel_downloads=
-procd_trigger_wan6=
-sanity_check=
-update_config_sizes=
-allowed_domain=
-blocked_domain=
-compressed_cache_dir=
-config_update_url=
-curl_additional_param=
-curl_max_file_size=
-curl_retry=
-dns=
-dnsmasq_config_file_url=
-dnsmasq_instance=
-download_timeout=
-force_dns_interface=
-force_dns_port=
-heartbeat_domain=
-heartbeat_sleep_timeout=
-led=
-pause_timeout=
-procd_boot_wan_timeout=
-smartdns_instance=
-verbosity=
+readonly _ucode="ucode -S -L /lib/${packageName} /lib/${packageName}/cli.uc --"
+_procd_svc_data=
+_fw4_restart=
# shellcheck disable=SC1091
. "${IPKG_INSTROOT}/lib/functions.sh"
# shellcheck disable=SC1091
. "${IPKG_INSTROOT}/lib/functions/network.sh"
-# shellcheck disable=SC1091
-. "${IPKG_INSTROOT}/usr/share/libubox/jshn.sh"
-append_newline() { is_newline_ending "$1" || echo '' >> "$1"; }
-check_ipset() { { command -v ipset && /usr/sbin/ipset help hash:net; } >/dev/null 2>&1; }
-check_nft() { command -v nft >/dev/null 2>&1; }
-check_dnsmasq() { command -v dnsmasq >/dev/null 2>&1; }
-check_dnsmasq_feature () {
- [ -z "$dnsmasq_features" ] && dnsmasq_features="$(dnsmasq --version | grep -m1 'Compile time options:' | cut -d: -f2) "
- case "$1" in
- idn) [ "${dnsmasq_features#* IDN }" != "$dnsmasq_features" ];;
- ipset) [ "${dnsmasq_features#* ipset }" != "$dnsmasq_features" ];;
- nftset) [ "${dnsmasq_features#* nftset }" != "$dnsmasq_features" ];;
- esac
-}
-check_dnsmasq_ipset() { check_ipset && check_dnsmasq_feature 'ipset'; }
-check_dnsmasq_nftset() { check_nft && check_dnsmasq_feature 'nftset'; }
-check_smartdns() { command -v smartdns >/dev/null 2>&1; }
-check_smartdns_ipset() { check_smartdns && check_ipset; }
-check_smartdns_nftset() { check_smartdns && check_nft; }
-check_unbound() { command -v unbound >/dev/null 2>&1; }
-append_url() {
- local cfg="$1" allow_var="${2:-allowed_url}" block_var="${3:-blocked_url}"
- local old_value
- local en action url
- config_get_bool en "$cfg" enabled '1'
- config_get action "$cfg" action 'block'
- config_get url "$cfg" url
- if [ "$en" = '1' ]; then
- if [ "$action" = 'allow' ]; then
- old_value=$(eval echo "\$$allow_var")
- old_value="${old_value:+$old_value }${url}"
- eval "$allow_var"="\$old_value"
- else
- old_value=$(eval echo "\$$block_var")
- old_value="${old_value:+$old_value }${url}"
- eval "$block_var"="\$old_value"
- fi
- fi
-}
-adb_config_cache() {
- local param="$1" var="$2"
- local _reload="$triggersReload"
- local _restart="$triggersRestart"
- local i ret
- case "$param" in
- create|set)
- cp -f "$uciConfigFile" "$runningConfigFile"
- ;;
- get)
- case "$var" in
- trigger_fw4)
- if [ -s "$runningConfigFile" ]; then
- local UCI_CONFIG_DIR="${runningConfigFile%/*}"
- is_fw4_restart_needed && ret='true'
- fi
- printf "%b" "$ret"
- return
- ;;
- trigger_service)
- local old_allowed_url old_blocked_url
- if [ ! -s "$runningConfigFile" ]; then
- ret='on_boot'
- elif ! cmp -s "$uciConfigFile" "$runningConfigFile"; then
-# ret='restart'
-# else
- local current_allowed_url current_blocked_url
- config_load "$uciConfigFile"
- config_foreach append_url 'file_url' current_allowed_url current_blocked_url
- if [ -z "$allowed_url" ] || [ -z "$blocked_url" ]; then
- config_load "$runningConfigFile"
- config_foreach append_url 'file_url' allowed_url blocked_url
- fi
- for i in $_reload; do
- local val_current val_old UCI_CONFIG_DIR
- case "$i" in
- allowed_url)
- val_current="$current_allowed_url"
- val_old="$allowed_url"
- ;;
- blocked_url)
- val_current="$current_blocked_url"
- val_old="$blocked_url"
- ;;
- *)
- UCI_CONFIG_DIR=
- val_current="$(uci_get "$packageName" 'config' "$i")"
- UCI_CONFIG_DIR="${runningConfigFile%/*}"
- val_old="$(uci_get "$packageName" 'config' "$i")"
- ;;
- esac
- if [ "$val_current" != "$val_old" ]; then
- ret='download'
- unset _restart
- break
- fi
- done
- for i in $_restart; do
- local val_current val_old UCI_CONFIG_DIR
- UCI_CONFIG_DIR=
- val_current="$(uci_get "$packageName" 'config' "$i")"
- UCI_CONFIG_DIR="${runningConfigFile%/*}"
- val_old="$(uci_get "$packageName" 'config' "$i")"
- if [ "$val_current" != "$val_old" ]; then
- ret='restart'
- break
- fi
- done
- fi
- printf "%b" "$ret"
- return
- ;;
- *)
- local UCI_CONFIG_DIR="${runningConfigFile%/*}"
- ret="$(uci_get "$packageName" 'config' "$var")"
- printf "%b" "$ret"
- return
- ;;
- esac
- ;;
- esac
-}
-count_blocked_domains() {
- if [ -n "$outputBlockedCountFilter" ]; then
- [ -f "$outputFile" ] && sed "$outputBlockedCountFilter" "$outputFile" | wc -l || echo '0'
- else
- [ -f "$outputFile" ] && wc -l < "$outputFile" || echo '0'
- fi
-}
-debug() { local __i __j; for __i in "$@"; do eval "__j=\$$__i"; echo "${__i}: ${__j} "; done; }
-debug_log() { local __i __j; for __i in "$@"; do eval "__j=\$$__i"; logger -t "$packageName" "${__i}: ${__j} "; done; }
-dns_set_output_values() {
- case "$1" in
- dnsmasq.addnhosts)
- outputFilter="$dnsmasqAddnhostsOutputFormatFilter"
- outputFile="$dnsmasqAddnhostsFile"
- outputCache="$dnsmasqAddnhostsCache"
- outputGzip="${compressed_cache_dir}/${dnsmasqAddnhostsGzip}"
- outputParseFilter="$dnsmasqAddnhostsOutputParseFilter"
- if [ -n "$ipv6_enabled" ]; then
- outputFilterIPv6="$dnsmasqAddnhostsOutputFormatFilterIPv6"
- fi
- ;;
- dnsmasq.conf)
- outputFilter="$dnsmasqConfOutputFormatFilter"
- outputFile="$dnsmasqConfFile"
- outputCache="$dnsmasqConfCache"
- outputGzip="${compressed_cache_dir}/${dnsmasqConfGzip}"
- outputParseFilter="$dnsmasqConfOutputParseFilter"
- ;;
- dnsmasq.ipset)
- outputFilter="$dnsmasqIpsetOutputFormatFilter"
- outputFile="$dnsmasqIpsetFile"
- outputCache="$dnsmasqIpsetCache"
- outputGzip="${compressed_cache_dir}/${dnsmasqIpsetGzip}"
- outputParseFilter="$dnsmasqIpsetOutputParseFilter"
- ;;
- dnsmasq.nftset)
- if [ -n "$ipv6_enabled" ]; then
- outputFilter="$dnsmasqNftsetOutputFormatFilterIPv6"
- else
- outputFilter="$dnsmasqNftsetOutputFormatFilter"
- fi
- outputFile="$dnsmasqNftsetFile"
- outputCache="$dnsmasqNftsetCache"
- outputGzip="${compressed_cache_dir}/${dnsmasqNftsetGzip}"
- outputParseFilter="$dnsmasqNftsetOutputParseFilter"
- ;;
- dnsmasq.servers)
- outputFilter="$dnsmasqServersOutputFormatFilter"
- outputFile="$dnsmasqServersFile"
- outputCache="$dnsmasqServersCache"
- outputGzip="${compressed_cache_dir}/${dnsmasqServersGzip}"
- outputParseFilter="$dnsmasqServersOutputParseFilter"
- outputAllowFilter="$dnsmasqServersAllowFilter"
- outputBlockedCountFilter="$dnsmasqServersBlockedCountFilter"
- ;;
- smartdns.domainset)
- outputFilter="$smartdnsDomainSetOutputFormatFilter"
- outputFile="$smartdnsDomainSetFile"
- outputCache="$smartdnsDomainSetCache"
- outputGzip="${compressed_cache_dir}/${smartdnsDomainSetGzip}"
- outputConfig="$smartdnsDomainSetConfig"
- outputParseFilter="$smartdnsDomainSetOutputParseFilter"
- ;;
- smartdns.ipset)
- outputFilter="$smartdnsIpsetOutputFormatFilter"
- outputFile="$smartdnsIpsetFile"
- outputCache="$smartdnsIpsetCache"
- outputGzip="${compressed_cache_dir}/${smartdnsIpsetGzip}"
- outputConfig="$smartdnsIpsetConfig"
- outputParseFilter="$smartdnsIpsetOutputParseFilter"
- ;;
- smartdns.nftset)
- outputFilter="$smartdnsNftsetOutputFormatFilter"
- outputFile="$smartdnsNftsetFile"
- outputCache="$smartdnsNftsetCache"
- outputGzip="${compressed_cache_dir}/${smartdnsNftsetGzip}"
- outputConfig="$smartdnsNftsetConfig"
- outputParseFilter="$smartdnsNftsetOutputParseFilter"
- ;;
- unbound.adb_list)
- outputFilter="$unboundOutputFormatFilter"
- outputFile="$unboundFile"
- outputCache="$unboundCache"
- outputGzip="${compressed_cache_dir}/${unboundGzip}"
- outputParseFilter="$unboundOutputParseFilter"
- ;;
- esac
- resolver 'on_load'
-}
-dnsmasq_hup() { killall -q -s HUP dnsmasq; }
-dnsmasq_kill() { killall -q -s KILL dnsmasq; }
-dnsmasq_restart() { /etc/init.d/dnsmasq restart >/dev/null 2>&1; }
-is_enabled() { uci_get "$1" 'config' 'enabled' '0'; }
-is_fw4_restart_needed() {
- [ -n "$fw4_restart_flag" ] && return 0
- local dns force_dns
- dns="$(uci_get "$packageName" 'config' 'dns' 'dnsmasq.servers')"
- force_dns="$(uci_get "$packageName" 'config' 'force_dns' '1')"
- if [ "$force_dns" = '1' ]; then
- return 0
- elif [ "$dns" = 'dnsmasq.ipset' ]; then
- return 0
- elif [ "$dns" = 'dnsmasq.nftset' ]; then
- return 0
- elif [ "$dns" = 'smartdns.ipset' ]; then
- return 0
- elif [ "$dns" = 'smartdns.nftset' ]; then
- return 0
- else
- return 1
- fi
-}
-is_integer() { case "$1" in ''|*[!0-9]*) return 1;; esac; [ "$1" -ge 1 ] && [ "$1" -le 65535 ] || return 1; return 0; }
-is_greater() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
-is_greater_or_equal() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" = "$2"; }
-# shellcheck disable=SC3057
-is_https_url() { [ "${1:0:8}" = "https://" ]; }
-is_newline_ending() { [ "$(tail -c1 "$1" | wc -l)" -ne '0' ]; }
-is_port_listening() {
- local hex
- is_integer "$1" || return 1
- hex="$(printf '%04X' "$1")"
- # TCP: state 0A == LISTEN
- if awk -v h="$hex" 'NR>1{split($2,a,":"); if (toupper(a[2])==h && $4=="0A") {found=1}} END{exit found?0:1}' /proc/net/tcp /proc/net/tcp6 2>/dev/null; then
- return 0
- fi
- # UDP: presence indicates a bound socket
- if awk -v h="$hex" 'NR>1{split($2,a,":"); if (toupper(a[2])==h) {found=1}} END{exit found?0:1}' /proc/net/udp /proc/net/udp6 2>/dev/null; then
- return 0
- fi
- return 1
-}
-is_present() { command -v "$1" >/dev/null 2>&1; }
-is_running() {
- local i j
- i="$(json get status)"
- j="$(ubus_get_data status)"
- if [ "$i" = 'statusStopped' ] || [ -z "${i}${j}" ]; then
- return 1
- else
- return 0
- fi
-}
-ipset() { "$ipset" "$@" >/dev/null 2>&1; }
-get_mem_available() {
- local ram swap
- ram="$( ubus call system info | jsonfilter -e '@.memory.available' )"
- swap="$( ubus call system info | jsonfilter -e '@.swap.free' )"
- echo "$((ram + swap))";
-}
-get_mem_total() {
- local ram swap
- ram="$( ubus call system info | jsonfilter -e '@.memory.total' )"
- swap="$( ubus call system info | jsonfilter -e '@.swap.total' )"
- echo "$((ram + swap))";
-}
-led_on(){ if [ -n "${1}" ] && [ -e "${1}/trigger" ]; then echo 'default-on' > "${1}/trigger" 2>&1; fi; }
-led_off(){ if [ -n "${1}" ] && [ -e "${1}/trigger" ]; then echo 'none' > "${1}/trigger" 2>&1; fi; }
-logger() { /usr/bin/logger -t "$packageName" "$@"; }
-logger_debug() { [ -n "$debug_performance" ] && /usr/bin/logger -t "$packageName [$$]" "$@"; }
-nft() { "$nft" "$@" >/dev/null 2>&1; }
-output_dot() { output 1 "$_DOT_"; output 2 "$__DOT__"; }
-output_ok() { output 1 "$_OK_"; output 2 "$__OK__\n"; }
-output_okn() { output 1 "$_OK_\n"; output 2 "$__OK__\n"; }
-output_warn() { output 1 "$_WARN_"; output 2 "$__WARN__\n"; }
-output_warnn() { output 1 "$_WARN_\n"; output 2 "$__WARN__\n"; }
-output_fail() { output 1 "$_FAIL_"; output 2 "$__FAIL__\n"; }
-output_failn() { output 1 "$_FAIL_\n"; output 2 "$__FAIL__\n"; }
-output_dns() {
- case "$dns" in
- dnsmasq.*) output 2 "[DNSM] $*";;
- smartdns.*) output 2 "[SMRT] $*";;
- unbound.*) output 2 "[UNBD] $*";;
- esac
-}
-output_error() { output "${_ERROR_} $*!\n"; }
-output_warning() { output "${_WARNING_} $*!\n"; }
-print_json_bool() { json_init; json_add_boolean "$1" "$2"; json_dump; json_cleanup; }
-print_json_int() { json_init; json_add_int "$1" "$2"; json_dump; json_cleanup; }
-print_json_string() { json_init; json_add_string "$1" "$2"; json_dump; json_cleanup; }
-sanitize_domain() { printf '%s' "$1" | sed -E 's#^[a-z]+://##; s#/.*$##; s/:.*$//'; }
-sanitize_dir() { [ -d "$(readlink -fn "$1")" ] && readlink -fn "$1"; }
-smartdns_restart() { /etc/init.d/smartdns restart >/dev/null 2>&1; }
-# shellcheck disable=SC3060
-str_contains() { [ "${1//$2}" != "$1" ]; }
-str_contains_word() { echo "$1" | grep -qw "$2"; }
-str_first_word() { echo "${1%% *}"; }
-# shellcheck disable=SC2018,SC2019
-str_to_lower() { echo "$1" | tr 'A-Z' 'a-z'; }
-# shellcheck disable=SC2018,SC2019
-str_to_upper() { echo "$1" | tr 'a-z' 'A-Z'; }
-# shellcheck disable=SC3060
-str_replace() { echo "${1//$2/$3}"; }
-ubus_get_data() { ubus call service list "{\"name\":\"$packageName\"}" | jsonfilter -e "@['${packageName}'].instances.main.data.${1}"; }
-ubus_get_ports() { ubus call service list "{\"name\":\"$packageName\"}" | jsonfilter -e "@['${packageName}'].instances.main.data.firewall.*.dest_port"; }
-uci_get_protocol() { uci_get 'network' "$1" 'proto'; }
-unbound_restart() { /etc/init.d/unbound restart >/dev/null 2>&1; }
-
-json() {
- {
- flock -x 209
- local status message stats i
- local action="$1" param="$2" value="$3"; shift 3; local info="$*";
- local _current_namespace="$_JSON_PREFIX"
- json_set_namespace "${packageName//-/_}_"
- [ "$param" = 'error' ] && param='errors'
- [ "$param" = 'warning' ] && param='warnings'
- { json_load_file "$runningStatusFile" || json_init; } >/dev/null 2>&1
- { json_select 'data' || { json_add_object 'data'; json_close_object; json_select 'data'; }; } >/dev/null 2>&1
- case "${action}:${param}" in
- 'get:errors'|'get:warnings')
- json_select "$param" >/dev/null 2>&1 || return
- if [ -z "$value" ]; then
- json_get_keys i
- else
- json_select "$value" >/dev/null 2>&1
- case "${info:-code}" in
- 'code'|'info') json_get_var 'i' "$info" >/dev/null 2>&1;;
- esac
- fi
- printf "%b" "$i"
- json_set_namespace "$_current_namespace"
- return
- ;;
- get:*)
- json_get_var 'i' "$param" >/dev/null 2>&1
- printf "%b" "$i"
- json_set_namespace "$_current_namespace"
- return
- ;;
- 'add:errors'|'add:warnings')
- { json_select "$param" || json_add_array "$param"; } >/dev/null 2>&1
- json_add_object ""
- json_add_string 'code' "$value"
- json_add_string 'info' "$info"
- json_close_object
- json_select ..
- ;;
- add:*)
- json_add_string "$param" "$value"
- ;;
- 'del:all')
- json_add_string status ''
- json_add_string message ''
- json_add_string stats ''
- json_add_array errors
- json_close_array
- json_add_array warnings
- json_close_array
- ;;
- 'del:errors'|'del:warnings')
- json_add_array "$param"
- json_close_array
- ;;
- del:*)
- json_add_string "$param" ''
- ;;
- 'set:status'|'set:message'|'set:stats')
- json_add_string "$param" "$value"
- ;;
- esac
- json_add_string 'version' "$PKG_VERSION"
- json_add_string 'packageCompat' "$packageCompat"
- json_select ..
- mkdir -p "${runningStatusFile%/*}"
- json_dump > "$runningStatusFile"
- sync
- json_set_namespace "$_current_namespace"
- } 209>"$runningStatusFileLock"
-}
-
-get_local_filesize() {
- local file="$1" size
- [ -f "$file" ] || return 0
- if is_present stat; then
- size="$(stat -c%s "$file")"
- elif is_present wc; then
- size="$(wc -c < "$file")"
- fi
-# shellcheck disable=SC3037
- echo -en "$size"
-}
-
-get_url_filesize() {
- local url="$1" size size_command timeout_sec=2
- [ -n "$url" ] || return 0
- if is_present 'curl'; then
- # shellcheck disable=SC1017
- size_command='curl --silent --insecure --fail --head --request GET'
- size="$($size_command --connect-timeout $timeout_sec "$url" | awk -F": " '{IGNORECASE=1}/content-length/ {gsub(/\r/, ""); print $2}' )"
- fi
-
- # Check if size is empty and fallback to uclient-fetch if necessary
- if [ -z "$size" ] && is_present 'uclient-fetch' ; then
- # shellcheck disable=SC1017
- size_command='uclient-fetch --spider'
- size="$($size_command --timeout $timeout_sec "$url" -O /dev/null 2>&1 | sed -n '/^Download/ s/.*(\([0-9]*\) bytes).*/\1/p')"
- fi
- # shellcheck disable=SC3037
- echo -en "$size"
-}
-
-# shellcheck disable=SC3060
-output() {
- [ -z "$verbosity" ] && verbosity="$(uci_get "$packageName" 'config' 'verbosity' '1')"
- [ "$#" -ne '1' ] && {
- case "$1" in [0-9]) [ $((verbosity & $1)) -gt 0 ] && shift || return 0;; esac }
- local msg="$*" queue="/dev/shm/$packageName-output"
- [ -t 1 ] && printf "%b" "$msg"
- [ "$msg" != "${msg//\\n}" ] && {
- [ -s "$queue" ] && msg="$(cat "$queue")${msg}" && rm -f "$queue"
- msg="$(printf "%b" "$msg" | sed 's/\x1b\[[0-9;]*m//g')"
- logger -t "$packageName [$$]" "$(printf "%b" "$msg")"
- } || printf "%b" "$msg" >> "$queue"
-}
-
-uci_add_list_if_new() {
- local PACKAGE="$1"
- local CONFIG="$2"
- local OPTION="$3"
- local VALUE="$4"
- local i
- [ -n "$PACKAGE" ] && [ -n "$CONFIG" ] && [ -n "$OPTION" ] && [ -n "$VALUE" ] || return 1
- for i in $(uci_get "$PACKAGE" "$CONFIG" "$OPTION"); do
- [ "$i" = "$VALUE" ] && return 0
- done
- uci_add_list "$PACKAGE" "$CONFIG" "$OPTION" "$VALUE"
-}
-
-uci_changes() {
- local PACKAGE="$1"
- local CONFIG="$2"
- local OPTION="$3"
- [ -s "${UCI_CONFIG_DIR:-/etc/config/}${PACKAGE}" ] && \
- [ -n "$(/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} changes "$PACKAGE${CONFIG:+.$CONFIG}${OPTION:+.$OPTION}")" ]
-}
-
-get_text() {
- local r="$1"; shift;
- case "$r" in
- errorConfigValidationFail) printf "The %s config validation failed" "$packageName";;
- errorServiceDisabled) printf "The %s is currently disabled" "$packageName";;
- errorNoDnsmasqIpset)
- printf "The dnsmasq ipset support is enabled in %s, but dnsmasq is either not installed or installed dnsmasq does not support ipset" "$packageName";;
- errorNoIpset)
- printf "The dnsmasq ipset support is enabled in %s, but ipset is either not installed or installed ipset does not support 'hash:net' type" "$packageName";;
- errorNoDnsmasqNftset)
- printf "The dnsmasq nft set support is enabled in %s, but dnsmasq is either not installed or installed dnsmasq does not support nft set" "$packageName";;
- errorNoNft) printf "The dnsmasq nft sets support is enabled in %s, but nft is not installed" "$packageName";;
- errorNoWanGateway) printf "The %s failed to discover WAN gateway" "$serviceName";;
- errorOutputDirCreate) printf "Failed to create directory for %s file" "$@";;
- errorOutputFileCreate) printf "Failed to create %s file" "$@";;
- errorFailDNSReload) printf "Failed to restart/reload DNS resolver";;
- errorSharedMemory) printf "Failed to access shared memory";;
- errorSorting) printf "Failed to sort data file";;
- errorOptimization) printf "Failed to optimize data file";;
- errorAllowListProcessing) printf "Failed to process allow-list";;
- errorDataFileFormatting) printf "Failed to format data file";;
- errorCopyingDataFile) printf "Failed to copy data file to '%s'" "$@";;
- errorMovingDataFile) printf "Failed to move data file to '%s'" "$@";;
- errorCreatingCompressedCache) printf "Failed to create compressed cache";;
- errorRemovingTempFiles) printf "Failed to remove temporary files";;
- errorRestoreCompressedCache) printf "Failed to unpack compressed cache";;
- errorRestoreCache) printf "Failed to move '%s' to '%s'" "$outputCache" "$outputFile";;
- errorOhSnap) printf "Failed to create block-list or restart DNS resolver";;
- errorStopping) printf "Failed to stop %s" "$serviceName";;
- errorDNSReload) printf "Failed to reload/restart DNS resolver";;
- errorDownloadingConfigUpdate) printf "Failed to download Config Update file";;
- errorDownloadingList) printf "Failed to download %s" "$@";;
- errorParsingConfigUpdate) printf "Failed to parse Config Update file";;
- errorParsingList) printf "Failed to parse";;
- errorNoSSLSupport) printf "No HTTPS/SSL support on device";;
- errorCreatingDirectory) printf "Failed to create output/cache/gzip file directory";;
- errorDetectingFileType) printf "Failed to detect format";;
- errorNothingToDo) printf "No blocked list URLs nor blocked-domains enabled";;
- errorTooLittleRam) printf "Free ram (%s) is not enough to process all enabled block-lists" "$@";;
- errorCreatingBackupFile) printf "Failed to create backup file %s" "$@";;
- errorDeletingDataFile) printf "Failed to delete data file %s" "$@";;
- errorRestoringBackupFile) printf "Failed to restore backup file %s" "$@";;
- errorNoOutputFile) printf "Failed to create final block-list %s" "$@";;
- errorNoHeartbeat) printf "Heartbeat domain is not accessible after resolver restart";;
-
- statusNoInstall) printf "The %s is not installed or not found" "$serviceName";;
- statusStopped) printf "stopped";;
- statusStarting) printf "starting";;
- statusRestarting) printf "restarting";;
- statusForceReloading) printf "force-reloading";;
- statusDownloading) printf "downloading";;
- statusProcessing) printf "processing";;
- statusFail) printf "failed to start";;
- statusSuccess) printf "success";;
- statusTriggerBootWait) printf "waiting for trigger (on_boot)";;
- statusTriggerStartWait) printf "waiting for trigger (on_start)";;
-
- warningExternalDnsmasqConfig)
- printf "Use of external dnsmasq config file detected, please set 'dns' option to 'dnsmasq.conf'";;
- warningMissingRecommendedPackages) printf "Some recommended packages are missing";;
- warningInvalidCompressedCacheDir) printf "Invalid compressed cache directory '%s'" "$@";;
- warningFreeRamCheckFail) printf "Can't detect free RAM";;
- warningSanityCheckTLD) printf "Sanity check discovered TLDs in %s" "$@";;
- warningSanityCheckLeadingDot) printf "Sanity check discovered leading dots in %s" "$@";;
- warningInvalidDomainsRemoved) printf "Removed %s invalid domain entries from block-list (domains starting with -/./numbers or containing invalid patterns)" "$@";;
-
- *) printf "Unknown error/warning '%s'" "$@";;
- esac
-}
-
-load_network() {
- local param="$1"
- local i j wan_if wan_gw
- local counter wan_if_timeout="$procd_boot_wan_timeout" wan_gw_timeout='5'
- counter=0
- while [ -z "$wan_if" ]; do
- network_flush_cache
- network_find_wan wan_if
- if [ -n "$wan_if" ]; then
- output 1 "WAN interface found: '${wan_if}'.\n"
- output 2 "[BOOT] WAN interface found: '${wan_if}'.\n"
- break
- fi
- if [ "$counter" -gt "$wan_if_timeout" ]; then
- output 1 "WAN interface timeout, assuming 'wan'.\n"
- output 2 "[BOOT] WAN interface timeout, assuming 'wan'.\n"
- wan_if='wan'
- break
- fi
- counter=$((counter+1))
- output 1 "Waiting to discover WAN Interface...\n"
- output 2 "[BOOT] Waiting to discover WAN Interface...\n"
- sleep 1
- done
-
- counter=0
- if [ "$(uci_get_protocol "$wan_if")" = 'pppoe' ]; then
- wan_gw_timeout=$((wan_gw_timeout+10))
- fi
- while [ "$counter" -le "$wan_gw_timeout" ]; do
- network_flush_cache
- network_get_gateway wan_gw "$wan_if"
- if [ -n "$wan_gw" ]; then
- output 1 "WAN gateway found: '${wan_gw}.'\n"
- output 2 "[BOOT] WAN gateway found: '${wan_gw}.'\n"
- return 0
- fi
- counter=$((counter+1))
- output 1 "Waiting to discover $wan_if gateway...\n"
- output 2 "[BOOT] Waiting to discover $wan_if gateway...\n"
- sleep 1
- done
- json add error 'errorNoWanGateway'
- output_error "$(get_text 'errorNoWanGateway')"
- return 1
-}
-
-detect_file_type() {
- local file="$1"
- if [ "$(head -1 "$file")" = '[Adblock Plus]' ] || \
- grep -q '^||' "$file"; then
- echo 'adblockplus'
- elif grep -q '^server=' "$file"; then
- echo 'dnsmasq'
- elif grep -q '^local=' "$file"; then
- echo 'dnsmasq2'
- elif grep -q '^address=' "$file"; then
- echo 'dnsmasq3'
- elif grep -q -e '^0\.0\.0\.0\s' -e '^127\.0\.0\.1\s' "$file"; then
- echo 'hosts'
- elif [ -n "$(sed "$domainsFilter" "$file" | head -1)" ]; then
- echo 'domains'
- fi
-}
-
-load_package_config() {
- config_load "$packageName"
- config_get_bool allow_non_ascii 'config' 'allow_non_ascii' '0'
- config_get_bool canary_domains_icloud 'config' 'canary_domains_icloud' '0'
- config_get_bool canary_domains_mozilla 'config' 'canary_domains_mozilla' '0'
- config_get_bool compressed_cache 'config' 'compressed_cache' '0'
- config_get_bool config_update_enabled 'config' 'config_update_enabled' '0'
- config_get_bool debug_init_script 'config' 'debug_init_script' '0'
- config_get_bool debug_performance 'config' 'debug_performance' '0'
- config_get_bool dnsmasq_sanity_check 'config' 'dnsmasq_sanity_check' '1'
- config_get_bool dnsmasq_validity_check 'config' 'dnsmasq_validity_check' '0'
- config_get_bool enabled 'config' 'enabled' '0'
- config_get_bool force_dns 'config' 'force_dns' '1'
- config_get_bool ipv6_enabled 'config' 'ipv6_enabled' '0'
- config_get_bool parallel_downloads 'config' 'parallel_downloads' '1'
- config_get_bool procd_trigger_wan6 'config' 'procd_trigger_wan6' '0'
- config_get_bool update_config_sizes 'config' 'update_config_sizes' '1'
- config_get allowed_domain 'config' 'allowed_domain'
- config_get blocked_domain 'config' 'blocked_domain'
- config_get compressed_cache_dir 'config' 'compressed_cache_dir' '/etc'
- config_get config_update_url 'config' 'config_update_url' 'https://cdn.jsdelivr.net/gh/openwrt/packages/net/adblock-fast/files/adblock-fast.config.update'
- config_get curl_additional_param 'config' 'curl_additional_param'
- config_get curl_max_file_size 'config' 'curl_max_file_size'
- config_get curl_retry 'config' 'curl_retry' '3'
- config_get dns 'config' 'dns' 'dnsmasq.servers'
- config_get dnsmasq_config_file_url 'config' 'dnsmasq_config_file_url'
- config_get dnsmasq_instance 'config' 'dnsmasq_instance' '*'
- config_get download_timeout 'config' 'download_timeout' '20'
- config_get force_dns_interface 'config' 'force_dns_interface' 'lan'
- config_get force_dns_port 'config' 'force_dns_port' '53 853'
- config_get heartbeat_domain 'config' 'heartbeat_domain' 'heartbeat.melmac.ca'
- config_get heartbeat_sleep_timeout 'config' 'heartbeat_sleep_timeout' '10'
- config_get led 'config' 'led'
- config_get pause_timeout 'config' 'pause_timeout' '20'
- config_get procd_boot_wan_timeout 'config' 'procd_boot_wan_timeout' '60'
- config_get smartdns_instance 'config' 'smartdns_instance' '*'
- config_get verbosity 'config' 'verbosity' '2'
-
- [ "$allow_non_ascii" = '1' ] || unset allow_non_ascii
- [ "$canary_domains_icloud" = '1' ] || unset canary_domains_icloud
- [ "$canary_domains_mozilla" = '1' ] || unset canary_domains_mozilla
- [ "$compressed_cache" = '1' ] || unset compressed_cache
- [ "$config_update_enabled" = '1' ] || unset config_update_enabled
- [ "$debug_init_script" = '1' ] || unset debug_init_script
- [ "$debug_performance" = '1' ] || unset debug_performance
- [ "$dnsmasq_sanity_check" = '1' ] || unset dnsmasq_sanity_check
- [ "$dnsmasq_validity_check" = '1' ] || unset dnsmasq_validity_check
- [ "$enabled" = '1' ] || unset enabled
- [ "$force_dns" = '1' ] || unset force_dns
- [ "$ipv6_enabled" = '1' ] || unset ipv6_enabled
- [ "$parallel_downloads" = '1' ] || unset parallel_downloads
- [ "$procd_trigger_wan6" = '1' ] || unset procd_trigger_wan6
- [ "$update_config_sizes" = '1' ] || unset update_config_sizes
-
- dns_set_output_values "$dns"
- [ "$heartbeat_domain" = '-' ] && unset heartbeat_domain || heartbeat_domain="$(sanitize_domain "$heartbeat_domain")"
- if [ "$(sanitize_dir "$compressed_cache_dir")" = '/' ]; then
- compressed_cache_dir=''
- elif [ -n "$(sanitize_dir "$compressed_cache_dir")" ]; then
- compressed_cache_dir="$(sanitize_dir "$compressed_cache_dir")"
- else
- compressed_cache_dir="/etc"
- fi
-
- unset loadEnvironmentFlag
- loadPackageConfigFlag='true'
-}
-
-load_dl_command() {
- # Prefer curl because it supports the file:// scheme.
- if is_present 'curl'; then
- dl_command='curl -f --silent --insecure'
- dl_command="${dl_command}${curl_additional_param:+ $curl_additional_param}"
- dl_command="${dl_command}${curl_max_file_size:+ --max-filesize $curl_max_file_size}"
- dl_command="${dl_command}${curl_retry:+ --retry $curl_retry}"
- dl_command="${dl_command}${download_timeout:+ --connect-timeout $download_timeout}"
- dl_flag='-o'
- elif is_present '/usr/libexec/wget-ssl'; then
- dl_command='/usr/libexec/wget-ssl --no-check-certificate -q'
- dl_command="${dl_command}${download_timeout:+ --timeout $download_timeout}"
- dl_flag="-O"
- size_command='/usr/libexec/wget-ssl --no-check-certificate -q -O /dev/null --server-response'
- size_command="${size_command}${download_timeout:+ --timeout $download_timeout}"
- elif is_present wget && wget --version 2>/dev/null | grep -q "+https"; then
- dl_command="wget --no-check-certificate -q"
- dl_command="${dl_command}${download_timeout:+ --timeout $download_timeout}"
- dl_flag="-O"
- size_command='wget --no-check-certificate -q -O /dev/null --server-response'
- size_command="${size_command}${download_timeout:+ --timeout $download_timeout}"
- else
- dl_command="uclient-fetch --no-check-certificate -q"
- dl_command="${dl_command}${download_timeout:+ --timeout $download_timeout}"
- dl_flag="-O"
- fi
- if curl --version 2>/dev/null | grep -q "Protocols: .*https.*" \
- || wget --version 2>/dev/null | grep -q "+ssl"; then
- isSSLSupported='true'
- else
- unset isSSLSupported
- fi
-}
-
-load_environment() {
- local i j
- local param="$1" validation_result="$2"
-
- [ -z "$loadEnvironmentFlag" ] || return 0
- [ -n "$loadPackageConfigFlag" ] || load_package_config
-
- if [ -z "$enabled" ]; then
- json add error 'errorServiceDisabled'
- output_error "$(get_text 'errorServiceDisabled')"
- output "Run the following commands before starting service again:\n"
- output "uci set ${packageName}.config.enabled='1'; uci commit $packageName;\n"
- return 1
- fi
-
- if [ -n "$validation_result" ] && [ "$validation_result" != '0' ]; then
- output 1 "$_FAIL_\n"
- json add error 'errorConfigValidationFail'
- output_error "$(get_text 'errorConfigValidationFail')"
- output "Please check if the '$packageConfigFile' contains correct values for config options.\n"
- return 1
- fi
-
- if [ -n "$debug_init_script" ]; then
- exec 1>>"/tmp/$packageName.log"
- exec 2>&1
- set -x
- fi
-
- # Check for resolver presence and error out on start
- case "$dns" in
- dnsmasq.*)
- if ! check_dnsmasq; then
- [ "$param" != 'quiet' ] && { json add error 'errorDNSReload'; output_error "Resolver 'dnsmasq' not found"; }
- return 1
- fi
- if check_dnsmasq_feature 'idn'; then
- allow_non_ascii=''
- fi
- ;;
- smartdns.*)
- if ! check_smartdns; then
- [ "$param" != 'quiet' ] && { json add error 'errorDNSReload'; output_error "Resolver 'smartdns' not found"; }
- return 1
- fi
- allow_non_ascii=''
- ;;
- unbound.*)
- if ! check_unbound; then
- [ "$param" != 'quiet' ] && { json add error 'errorDNSReload'; output_error "Resolver 'unbound' not found"; }
- return 1
- fi
- allow_non_ascii='true'
- ;;
- esac
-
- case "$dns" in
- dnsmasq.ipset)
- if ! check_dnsmasq_feature 'ipset'; then
- if [ "$param" != 'quiet' ]; then
- json add error 'errorNoDnsmasqIpset'
-# output_error "$(get_text 'errorNoDnsmasqIpset')"
- fi
- dns='dnsmasq.servers'
- fi
- if ! ipset help hash:net; then
- if [ "$param" != 'quiet' ]; then
- json add error 'errorNoIpset'
-# output_error "$(get_text 'errorNoIpset')"
- fi
- dns='dnsmasq.servers'
- fi
- ;;
- dnsmasq.nftset)
- if ! check_dnsmasq_feature 'nftset'; then
- if [ "$param" != 'quiet' ]; then
- json add error 'errorNoDnsmasqNftset'
-# output_error "$(get_text 'errorNoDnsmasqNftset')"
- fi
- dns='dnsmasq.servers'
- fi
- if [ -z "$nft" ]; then
- if [ "$param" != 'quiet' ]; then
- json add error 'errorNoNft'
-# output_error "$(get_text 'errorNoNft')"
- fi
- dns='dnsmasq.servers'
- fi
- ;;
- smartdns.ipset)
- if ! ipset help hash:net; then
- if [ "$param" != 'quiet' ]; then
- json add error 'errorNoIpset'
-# output_error "$(get_text 'errorNoIpset')"
- fi
- dns='smartdns.domainset'
- fi
- ;;
- smartdns.nftset)
- if [ -z "$nft" ]; then
- if [ "$param" != 'quiet' ]; then
- json add error 'errorNoNft'
-# output_error "$(get_text 'errorNoNft')"
- fi
- dns='smartdns.domainset'
- fi
- ;;
- esac
-
- if [ -n "$dnsmasq_config_file_url" ]; then
- unset update_config_sizes
- case "$dns" in
- dnsmasq.conf) :;;
- *)
- dns='dnsmasq.conf'
- if [ "$param" != 'quiet' ]; then
- json add warning 'warningExternalDnsmasqConfig'
- fi
- ;;
- esac
- fi
-
- [ "$dns" = 'dnsmasq.addnhosts' ] || rm -f "$dnsmasqAddnhostsFile" "$dnsmasqAddnhostsCache" "${compressed_cache_dir}/${dnsmasqAddnhostsGzip}"
- [ "$dns" = 'dnsmasq.conf' ] || rm -f "$dnsmasqConfCache" "${compressed_cache_dir}/${dnsmasqConfGzip}"
- [ "$dns" = 'dnsmasq.ipset' ] || rm -f "$dnsmasqIpsetCache" "${compressed_cache_dir}/${dnsmasqIpsetGzip}"
- [ "$dns" = 'dnsmasq.nftset' ] || rm -f "$dnsmasqNftsetCache" "${compressed_cache_dir}/${dnsmasqNftsetGzip}"
- [ "$dns" = 'dnsmasq.servers' ] || rm -f "$dnsmasqServersFile" "$dnsmasqServersCache" "${compressed_cache_dir}/${dnsmasqServersGzip}"
- [ "$dns" = 'smartdns.domainset' ] || rm -f "$smartdnsDomainSetFile" "$smartdnsDomainSetCache" "${compressed_cache_dir}/${smartdnsDomainSetGzip}" "$smartdnsDomainSetConfig"
- [ "$dns" = 'smartdns.ipset' ] || rm -f "$smartdnsIpsetFile" "$smartdnsIpsetCache" "${compressed_cache_dir}/${smartdnsIpsetGzip}" "$smartdnsIpsetConfig"
- [ "$dns" = 'smartdns.nftset' ] || rm -f "$smartdnsNftsetFile" "$smartdnsNftsetCache" "${compressed_cache_dir}/${smartdnsNftsetGzip}" "$smartdnsNftsetConfig"
- [ "$dns" = 'unbound.adb_list' ] || rm -f "$unboundFile" "$unboundCache" "${compressed_cache_dir}/${unboundGzip}"
-
- for i in "$runningConfigFile" "$runningStatusFile" "$outputFile" "$outputCache" "$outputGzip" "$outputConfig"; do
- [ -n "$i" ] || continue
- if ! mkdir -p "${i%/*}"; then
- if [ "$param" != 'quiet' ]; then
- json add error 'errorOutputDirCreate' "$i"
- fi
- fi
- done
-
- is_present 'gawk' && awk='gawk'
- if ! is_present '/usr/libexec/grep-gnu' || ! is_present '/usr/libexec/sed-gnu' || \
- ! is_present '/usr/libexec/sort-coreutils' || ! is_present 'gawk'; then
- local s
- is_present 'gawk' || { json add warning 'warningMissingRecommendedPackages' 'gawk'; s="${s:+$s }gawk"; }
- is_present '/usr/libexec/grep-gnu' || { json add warning 'warningMissingRecommendedPackages' 'grep'; s="${s:+$s }grep"; }
- is_present '/usr/libexec/sed-gnu' || { json add warning 'warningMissingRecommendedPackages' 'sed'; s="${s:+$s }sed"; }
- is_present '/usr/libexec/sort-coreutils' || { json add warning 'warningMissingRecommendedPackages' 'coreutils-sort'; s="${s:+$s }coreutils-sort"; }
- if [ "$param" != 'quiet' ]; then
- output_warning "$(get_text 'warningMissingRecommendedPackages'), install them by running:"
- output "opkg update; opkg --force-overwrite install $s;"
- fi
- fi
-
- load_dl_command
-
- led="${led:+/sys/class/leds/$led}"
- config_load "$packageName"
- config_foreach append_url 'file_url' allowed_url blocked_url
- loadEnvironmentFlag='true'
- adb_file 'test_cache' && return 0
- adb_file 'test_gzip' && return 0
- if [ "$param" = 'on_boot' ]; then
- load_network "$param"
- return "$?"
- else
- return 0
- fi
-}
-
-resolver() {
- _dnsmasq_instance_get_confdir() {
- local cfg_file
- [ -z "$dnsmasq_ubus" ] && dnsmasq_ubus="$(ubus call service list '{"name":"dnsmasq"}')"
- cfg_file="$(echo "$dnsmasq_ubus" | jsonfilter -e "@.dnsmasq.instances.${1}.command" \
- | awk '{gsub(/\\\//,"/");gsub(/[][",]/,"");for(i=1;i<=NF;i++)if($i=="-C"){print $(i+1);exit}}')"
- awk -F= '/^conf-dir=/{print $2; exit}' "$cfg_file"
- }
- _dnsmasq_instance_config() {
- local cfg="$1" param="$2" confdir
- [ -s "/etc/config/dhcp" ] || return 0
- [ -n "$(uci_get dhcp "$cfg")" ] || return 1
- case "$param" in
- dnsmasq.addnhosts)
- # clean up other dnsmasq configs
- confdir="$(_dnsmasq_instance_get_confdir "$cfg")"
- [ -n "$confdir" ] && rm -f "${confdir}/${packageName}"
- uci_remove_list 'dhcp' "$cfg" 'addnmount' "$dnsmasqConfFile"
- if [ "$(uci_get 'dhcp' "$cfg" 'serversfile')" = "$dnsmasqServersFile" ]; then
- uci_remove 'dhcp' "$cfg" 'serversfile'
- fi
- # add dnsmasq addnhosts config
- uci_add_list_if_new 'dhcp' "$cfg" 'addnhosts' "$dnsmasqAddnhostsFile"
- ;;
- cleanup|unbound.adb_list)
- # clean up all dnsmasq configs
- confdir="$(_dnsmasq_instance_get_confdir "$cfg")"
- [ -n "$confdir" ] && rm -f "${confdir}/${packageName}"
- uci_remove_list 'dhcp' "$cfg" 'addnhosts' "$dnsmasqAddnhostsFile"
- uci_remove_list 'dhcp' "$cfg" 'addnmount' "$dnsmasqConfFile"
- if [ "$(uci_get 'dhcp' "$cfg" 'serversfile')" = "$dnsmasqServersFile" ]; then
- uci_remove 'dhcp' "$cfg" 'serversfile'
- fi
- ;;
- dnsmasq.conf|dnsmasq.ipset|dnsmasq.nftset)
- # clean up other dnsmasq configs
- uci_remove_list 'dhcp' "$cfg" 'addnhosts' "$dnsmasqAddnhostsFile"
- if [ "$(uci_get 'dhcp' "$cfg" 'serversfile')" = "$dnsmasqServersFile" ]; then
- uci_remove 'dhcp' "$cfg" 'serversfile'
- fi
- # add dnsmasq conf addnmount to point to adblock-fast file
- uci_add_list_if_new 'dhcp' "$cfg" 'addnmount' "$dnsmasqConfFile"
- # add softlink to adblock-fast file
- confdir="$(_dnsmasq_instance_get_confdir "$cfg")"
- [ -n "$confdir" ] || return 1
- ln -sf "$dnsmasqConfFile" "${confdir}/${packageName}"
- chmod 660 "${confdir}/${packageName}"
- chown -h root:dnsmasq "${confdir}/${packageName}" >/dev/null 2>/dev/null
- ;;
- dnsmasq.servers)
- # clean up other dnsmasq configs
- uci_remove_list 'dhcp' "$cfg" 'addnhosts' "$dnsmasqAddnhostsFile"
- confdir="$(_dnsmasq_instance_get_confdir "$cfg")"
- [ -n "$confdir" ] && rm -f "${confdir}/${packageName}"
- uci_remove_list 'dhcp' "$cfg" 'addnmount' "$dnsmasqConfFile"
- # add dnsmasq servers config
- if [ "$(uci_get 'dhcp' "$cfg" 'serversfile')" != "$dnsmasqServersFile" ]; then
- uci_set 'dhcp' "$cfg" 'serversfile' "$dnsmasqServersFile"
- fi
- ;;
- esac
- }
-# shellcheck disable=SC2317
- _dnsmasq_instance_append_force_dns_port() {
- local cfg="$1" instance_port
- [ -s "/etc/config/dhcp" ] || return 0
- [ -n "$(uci_get 'dhcp' "$cfg")" ] || return 1
- config_get instance_port "$cfg" 'port' '53'
- str_contains_word "$force_dns_port" "$instance_port" || force_dns_port="${force_dns_port:+$force_dns_port }${instance_port}"
- }
- _smartdns_instance_append_force_dns_port() {
- local cfg="$1" instance_port
- [ -s "/etc/config/smartdns" ] || return 0
- [ -n "$(uci_get 'smartdns' "$cfg")" ] || return 1
- config_get instance_port "$cfg" 'port' '53'
- str_contains_word "$force_dns_port" "$instance_port" || force_dns_port="${force_dns_port:+$force_dns_port }${instance_port}"
- }
- _smartdns_instance_config() {
- local cfg="$1" param="$2"
- [ -s "/etc/config/smartdns" ] || return 0
- [ -n "$(uci_get 'smartdns' "$cfg")" ] || return 1
- case "$param" in
- cleanup)
- uci_remove_list 'smartdns' "$cfg" 'conf_files' "$outputConfig"
- rm -f "$outputConfig"
- ;;
- smartdns.domainset)
- { echo "domain-set -name adblock-fast -file $outputFile"; \
- echo "domain-rules /domain-set:adblock-fast/ -a #"; } > "$outputConfig"
- uci_add_list_if_new 'smartdns' "$cfg" 'conf_files' "$outputConfig"
- ;;
- smartdns.ipset)
- { echo "domain-set -name adblock-fast -file $outputFile"; \
- echo "domain-rules /domain-set:adblock-fast/ -ipset adb"; } > "$outputConfig"
- uci_add_list_if_new 'smartdns' "$cfg" 'conf_files' "$outputConfig"
- ;;
- smartdns.nftset)
- local nftset="#4:inet#fw4#adb4"
- [ -n "$ipv6_enabled" ] && nftset="${nftset},#6:inet#fw4#adb6"
- { echo "domain-set -name adblock-fast -file $outputFile"; \
- echo "domain-rules /domain-set:adblock-fast/ -nftset $nftset"; } > "$outputConfig"
- uci_add_list_if_new 'smartdns' "$cfg" 'conf_files' "$outputConfig"
- ;;
- esac
- }
-# shellcheck disable=SC2317,SC2329
- _unbound_instance_append_force_dns_port() {
- [ -s "/etc/config/unbound" ] || return 0
- [ -n "$(uci_get 'unbound' "$cfg")" ] || return 1
- local cfg="$1" instance_port
- config_get instance_port "$cfg" 'listen_port' '53'
- str_contains_word "$force_dns_port" "$instance_port" || force_dns_port="${force_dns_port:+$force_dns_port }${instance_port}"
- }
-
- local i resolver_name="${dns%%.*}"
- [ -z "$1" ] && return 0
- case $1 in
- cleanup)
- rm -f "$dnsmasqAddnhostsFile" "$dnsmasqAddnhostsCache" "${compressed_cache_dir}/${dnsmasqAddnhostsGzip}"
- rm -f "$dnsmasqConfCache" "${compressed_cache_dir}/${dnsmasqConfGzip}"
- rm -f "$dnsmasqIpsetCache" "${compressed_cache_dir}/${dnsmasqIpsetGzip}"
- rm -f "$dnsmasqNftsetCache" "${compressed_cache_dir}/${dnsmasqNftsetGzip}"
- rm -f "$dnsmasqServersFile" "$dnsmasqServersCache" "${compressed_cache_dir}/${dnsmasqServersGzip}"
- rm -f "$smartdnsDomainSetFile" "$smartdnsDomainSetCache" "${compressed_cache_dir}/${smartdnsDomainSetGzip}" "$smartdnsDomainSetConfig"
- rm -f "$smartdnsIpsetFile" "$smartdnsIpsetCache" "${compressed_cache_dir}/${smartdnsIpsetGzip}" "$smartdnsIpsetConfig"
- rm -f "$smartdnsNftsetFile" "$smartdnsNftsetCache" "${compressed_cache_dir}/${smartdnsNftsetGzip}" "$smartdnsNftsetConfig"
- rm -f "$unboundFile" "$unboundCache" "${compressed_cache_dir}/${unboundGzip}"
- if [ -s "/etc/config/dhcp" ]; then
- config_load 'dhcp'
- config_foreach _dnsmasq_instance_config 'dnsmasq' 'cleanup'
- uci_changes 'dhcp' && uci_commit 'dhcp'
- fi
- if [ -s "/etc/config/smartdns" ]; then
- config_load 'smartdns'
- config_foreach _smartdns_instance_config 'smartdns' 'cleanup'
- uci_changes 'smartdns' && uci_commit 'smartdns'
- fi
- ;;
- on_load)
- :
- ;;
- on_stop|quiet|quiet_restart)
- eval "${resolver_name}_restart"
- return $?
- ;;
- on_start)
- if ! adb_file 'test'; then
- json set status 'statusFail'
- json add error 'errorOutputFileCreate' "$outputFile"
- return 1
- fi
- output 1 "Cycling $resolver_name "
- resolver 'update_config' && \
- resolver 'test' && \
- resolver 'sanity' && \
- resolver 'restart' && \
- resolver 'heartbeat' || resolver 'revert'
- output 1 '\n'
- ;;
- test)
- case "$dns" in
- dnsmasq.*)
- output_dns "Testing $dns configuration "
- if dnsmasq --test >/dev/null 2>/dev/null; then
- output_ok
- return 0
- else
- output_fail
- return 1
- fi
- ;;
- smartdns.*)
- return 0
- ;;
- unbound.*)
- return 0
- ;;
- esac
- ;;
- restart)
- output_dns "Restarting $resolver_name "
- json set message "Restarting $resolver_name"
- if eval "${resolver_name}_restart"; then
- json set status 'statusSuccess'
- led_on "$led"
- output_ok
- return 0
- else
- output_fail
- json set status 'statusFail'
- json add error 'errorDNSReload'
- return 1
- fi
- ;;
- sanity)
- [ -n "$sanity_check" ] || return 0
- output_dns "Sanity check for $dns TLDs "
- if ! grep -qvE '\.|server:' "$outputFile"; then
- output_ok
- else
- json add warning 'warningSanityCheckTLD' "$outputFile"
- output_warn
- fi
- output_dns "Sanity check for $dns leading dots "
- case "$dns" in
- dnsmasq.*)
- if ! grep -q '/\.' "$outputFile"; then
- output_ok
- else
- json add warning 'warningSanityCheckLeadingDot' "$outputFile"
- output_warn
- fi
- ;;
- smartdns.*)
- if ! grep -q '^\.' "$outputFile"; then
- output_ok
- else
- json add warning 'warningSanityCheckLeadingDot' "$outputFile"
- output_warn
- fi
- ;;
- unbound.*)
- if ! grep -q '"\.' "$outputFile"; then
- output_ok
- else
- json add warning 'warningSanityCheckLeadingDot' "$outputFile"
- output_warn
- fi
- ;;
- esac
- ;;
- heartbeat)
- [ -n "$heartbeat_domain" ] || return 0
- is_integer "$heartbeat_sleep_timeout" || return 0
- output_dns "Probing $heartbeat_domain for $heartbeat_sleep_timeout seconds "
- json set message "Testing resolver on $heartbeat_domain"
- local i=0
- while [ "$i" -lt "$heartbeat_sleep_timeout" ]; do
- if resolveip "$heartbeat_domain" >/dev/null 2>&1; then
- output_ok
- return 0
- fi
- output_dot
- i=$((i+1))
- sleep 1
- done
- output_fail
- json set status 'statusFail'
- json add error 'errorNoHeartbeat'
- return 1
- ;;
- revert)
- output 1 "Resetting/Restarting $resolver_name "
- output_dns "Resetting $resolver_name "
- resolver 'cleanup'
- output_ok
- output_dns "Restarting $resolver_name "
- if eval "${resolver_name}_restart"; then
- led_off "$led"
- output_ok
- return 0
- else
- output_fail
- json set status 'statusFail'
- json add error 'errorDNSReload'
- return 1
- fi
- ;;
- update_config)
- output_dns "Updating $resolver_name configuration "
- case "$dns" in
- dnsmasq.*)
- config_load 'dhcp'
- if [ "$dnsmasq_instance" = "*" ]; then
- config_foreach _dnsmasq_instance_config 'dnsmasq' "$dns"
- config_foreach _dnsmasq_instance_append_force_dns_port 'dnsmasq'
- elif [ -n "$dnsmasq_instance" ]; then
- for i in $dnsmasq_instance; do
- _dnsmasq_instance_config "@dnsmasq[$i]" "$dns" || _dnsmasq_instance_config "$i" "$dns"
- _dnsmasq_instance_append_force_dns_port "@dnsmasq[$i]" || _dnsmasq_instance_append_force_dns_port "$i"
- done
- fi
- uci_changes 'dhcp' && uci_commit 'dhcp'
- if adb_file 'test'; then
- chmod 660 "$outputFile"
- chown root:dnsmasq "$outputFile" >/dev/null 2>/dev/null
- else
- json set status 'statusFail'
- json add error 'errorNoOutputFile' "$outputFile"
- return 1
- fi
- ;;
- smartdns.*)
- config_load 'smartdns'
- if [ "$smartdns_instance" = "*" ]; then
- config_foreach _smartdns_instance_config 'smartdns' "$dns"
- config_foreach _smartdns_instance_append_force_dns_port 'smartdns'
- elif [ -n "$smartdns_instance" ]; then
- for i in $smartdns_instance; do
- _smartdns_instance_config "@smartdns[$i]" "$dns" || _smartdns_instance_config "$i" "$dns"
- _smartdns_instance_append_force_dns_port "@smartdns[$i]" || _smartdns_instance_append_force_dns_port "$i"
- done
- fi
- uci_changes 'smartdns' && uci_commit 'smartdns'
- chmod 660 "$outputFile" "$outputConfig"
- chown root:root "$outputFile" "$outputConfig" >/dev/null 2>/dev/null
- ;;
- unbound.*)
- config_load 'unbound'
- config_foreach _unbound_instance_append_force_dns_port 'unbound'
- chmod 660 "$outputFile"
- chown root:unbound "$outputFile" >/dev/null 2>/dev/null
- ;;
- esac
- output_ok
- ;;
- esac
-}
-
-adb_file() {
- local R_TMP
- case "$1" in
- create|backup)
- [ -s "$outputFile" ] && { mv -f "$outputFile" "$outputCache"; } >/dev/null 2>/dev/null
- return $?
- ;;
- restore|use)
- [ -s "$outputCache" ] && mv "$outputCache" "$outputFile" >/dev/null 2>/dev/null
- return $?
- ;;
- test|test_file)
- [ -s "$outputFile" ]
- return $?
- ;;
- test_cache)
- [ -s "$outputCache" ]
- return $?
- ;;
- test_gzip)
- [ -s "$outputGzip" ] && gzip -t -c "$outputGzip" >/dev/null 2>/dev/null
- return $?
- ;;
- create_gzip)
- [ -s "$outputFile" ] || return 1
- rm -f "$outputGzip" >/dev/null 2>/dev/null
- R_TMP="$(mktemp -q -t "${packageName}_tmp.XXXXXXXX")"
- if gzip < "$outputFile" > "$R_TMP"; then
- if mv "$R_TMP" "$outputGzip"; then
- rm -f "$R_TMP"
- return 0
- else
- rm -f "$R_TMP"
- return 1
- fi
- else
- return 1
- fi
- ;;
- expand|unpack|unpack_gzip)
- [ -s "$outputGzip" ] && gzip -dc < "$outputGzip" > "$outputCache"
- return $?
- ;;
- remove_cache)
- rm -f "$outputCache" >/dev/null 2>/dev/null
- ;;
- remove_gzip)
- rm -f "$outputGzip" >/dev/null 2>/dev/null
- ;;
- esac
-}
-
-process_file_url_wrapper() {
- if [ "$2" != '0' ]; then
- json add error 'errorConfigValidationFail'
- fi
- if [ -n "$parallel_downloads" ]; then
- process_file_url "$1" &
- else
- process_file_url "$1"
- fi
-}
-
-process_file_url() {
- _sanitize_source() {
- local type="$1" file="$2"
- case "$type" in
- hosts)
- sed -i '/# Title: StevenBlack/,/# Custom host records are listed here/d' "$file"
-# sed -i -E '/^(.*)[\t ](local|localhost|localhost.localdomain)$/d;/^255.255.255.255[\t ]broadcasthost$/d;/^0.0.0.0[\t ]0.0.0.0$/d' "$file"
-# sed -i -E '/^(.*)[\t ](ip6-localhost|ip6-loopback|ip6-localnet|ip6-mcastprefix|ip6-allnodes|ip6-allrouters|ip6-allhosts)/d' "$file"
- ;;
- esac
- }
-# url and action are set by load_validate_file_url_section or passed as 2nd and 3rd parameter
- local cfg="$1" new_size
- local label type D_TMP R_TMP filter
- if [ -z "$cfg" ] || [ -n "${2}${3}" ]; then
- url="$2"
- action="$3"
- fi
-
- [ "$enabled" = '1' ] || return 0
- [ -n "$url" ] || return 1
-
- label="${url##*//}"
- label="${label%%/*}"
- label="${name:-$label}"
- label="List: $label"
- case "$action" in
- allow) type='Allowed'; D_TMP="$ALLOWED_TMP"
- ;;
- block) type='Blocked'; D_TMP="$B_TMP"
- ;;
- file) type='File'; D_TMP="$B_TMP"
- ;;
- esac
- if is_https_url "$url" && [ -z "$isSSLSupported" ]; then
- output 1 "$_FAIL_"
- output 2 "[ DL ] $type $label $__FAIL__\n"
- json add error 'errorNoSSLSupport' "${name:-$url}"
- return 0
- fi
- R_TMP="$(mktemp -q -t "${packageName}_tmp.XXXXXXXX")"
- if [ -z "$url" ] || ! $dl_command "$url" "$dl_flag" "$R_TMP" 2>/dev/null || \
- [ ! -s "$R_TMP" ]; then
- output 1 "$_FAIL_"
- output 2 "[ DL ] $type $label $__FAIL__\n"
- json add error 'errorDownloadingList' "${name:-$url}"
- else
- append_newline "$R_TMP"
- [ -n "$cfg" ] && new_size="$(get_local_filesize "$R_TMP")"
- if [ -n "$new_size" ] && [ "$size" != "$new_size" ]; then
- uci_set "$packageName" "$cfg" 'size' "$new_size"
- fi
- format="$(detect_file_type "$R_TMP")"
- case "$format" in
- adblockplus) filter="$adBlockPlusFilter";;
- dnsmasq) filter="$dnsmasqFileFilter";;
- dnsmasq2) filter="$dnsmasq2FileFilter";;
- dnsmasq3) filter="$dnsmasq3FileFilter";;
- domains) filter="$domainsFilter";;
- hosts)
- filter="$hostsFilter"
- _sanitize_source 'hosts' "$R_TMP"
- ;;
- *)
- output 1 "$_FAIL_"
- output 2 "[ DL ] $type $label $__FAIL__\n"
- json add error 'errorDetectingFileType' "${name:-$url}"
- rm -f "$R_TMP"
- return 0
- ;;
- esac
- if [ -n "$filter" ] && [ "$action" != 'file' ]; then
- sed -i "$filter" "$R_TMP"
- fi
- if [ ! -s "$R_TMP" ]; then
- output 1 "$_FAIL_"
- output 2 "[ DL ] $type $label ($format) $__FAIL__\n"
- json add error 'errorParsingList' "${name:-$url}"
- else
- append_newline "$R_TMP"
- cat "${R_TMP}" >> "$D_TMP"
- output 1 "$_OK_"
- output 2 "[ DL ] $type $label ($format) $__OK__\n"
- fi
- fi
- rm -f "$R_TMP"
- return 0
-}
-
-download_dnsmasq_file() {
- json set message "$(get_text 'statusDownloading')..."
- json set status 'statusDownloading'
-
- rm -f "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP" "$outputFile" "$outputCache" "$outputGzip"
- if [ "$(get_mem_available)" -lt "$packageMemoryThreshold" ]; then
- output 'Low free memory, restarting resolver '
- if resolver 'quiet_restart'; then
- output_okn
- else
- output_failn
- fi
- fi
- touch "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP"
- output 1 'Downloading dnsmasq file '
- process_file_url '' "$dnsmasq_config_file_url" 'file'
- output_dns 'Moving dnsmasq file '
- if mv "$B_TMP" "$outputFile"; then
- output_ok
- else
- output_fail
- json add error 'errorMovingDataFile' "$i"
- fi
- output 1 '\n'
-}
-
-download_lists() {
-# shellcheck disable=SC2317,SC2329
- _ram_check() {
- _config_calculate_sizes() {
- local cfg="$1"
- local en size url
- config_get_bool en "$cfg" enabled '1'
- config_get size "$cfg" size
- config_get url "$cfg" url
- [ "$en" = '0' ] && return 0
- [ -n "$size" ] || size="$(get_url_filesize "$url")"
- [ -n "$size" ] && total_sizes=$((total_sizes+size))
- }
- local i free_mem total_sizes
- free_mem="$(get_mem_available)"
- if [ -z "$free_mem" ]; then
- json add warning 'warningFreeRamCheckFail'
- output_warning "$(get_text 'warningFreeRamCheckFail')"
- return 0
- fi
- config_load "$packageName"
- config_foreach _config_calculate_sizes 'file_url'
- if [ $((free_mem)) -lt $((total_sizes * 2)) ]; then
- json add error 'errorTooLittleRam' "$free_mem"
- return 1
- else
- return 0
- fi
- }
- local hf j=0 R_TMP
- local step_title start_time end_time elapsed
-
- _ram_check || return 1
-
- json set message "$(get_text 'statusDownloading')..."
- json set status 'statusDownloading'
-
- rm -f "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP" "$outputFile" "$outputCache" "$outputGzip"
- if [ "$(get_mem_total)" -lt "$packageMemoryThreshold" ]; then
- output 'Low free memory, restarting resolver '
- if resolver 'quiet_restart'; then
- output_okn
- else
- output_failn
- fi
- fi
- touch "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP"
- output 1 'Downloading lists '
- config_load "$packageName"
- config_foreach load_validate_file_url_section 'file_url' process_file_url_wrapper
- wait
- if uci_changes "$packageName"; then
- output 2 "[PROC] Saving updated file sizes "
- if [ -n "$update_config_sizes" ] && uci_commit "$packageName"; then output_ok; else output_fail; fi
- fi
- output 1 '\n'
-
- if [ -n "$canary_domains_icloud" ]; then
- canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsiCloud}"
- fi
- if [ -n "$canary_domains_mozilla" ]; then
- canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsMozilla}"
- fi
-
- output 1 'Processing downloads '
-
- start_time=$(date +%s)
- step_title='Sorting combined block-list'
- output 2 "[PROC] $step_title "
- json set status 'statusProcessing'
- json set message "$(get_text 'statusProcessing'): $step_title"
- append_newline "$B_TMP"
- {
- for hf in $blocked_domain $canaryDomains; do
- [ -n "$hf" ] && echo "$hf"
- done
- } | sed "$domainsFilter" >> "$B_TMP"
- sed -i '/^[[:space:]]*$/d' "$B_TMP"
- [ ! -s "$B_TMP" ] && return 1
-
- if [ -n "$allow_non_ascii" ]; then
- if sort -u "$B_TMP" > "$A_TMP"; then
- output_ok
- else
- output_fail
- json add error 'errorSorting'
- fi
- else
- if sort -u "$B_TMP" | grep -E -v '[^a-zA-Z0-9=/.-]' > "$A_TMP"; then
- output_ok
- else
- output_fail
- json add error 'errorSorting'
- fi
- fi
- end_time=$(date +%s)
- elapsed=$(( end_time - start_time ))
- logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s"
-
- case "$dns" in
- 'dnsmasq.conf' | 'dnsmasq.ipset' | 'dnsmasq.nftset' | 'dnsmasq.servers' | \
- 'smartdns.domainset' | 'smartdns.ipset' | 'smartdns.nftset' | \
- 'unbound.adb_list' )
- start_time=$(date +%s)
- step_title='Optimizing combined block-list'
- output 2 "[PROC] ${step_title} "
- json set message "$(get_text 'statusProcessing'): ${step_title}"
-# shellcheck disable=SC2016
- if $awk -F "." '{for(i=NF;i>0;i--) printf "%s%s", $i, (i>1?".":"\n")}' "$A_TMP" > "$B_TMP"; then
- if sort "$B_TMP" > "$A_TMP"; then
- if $awk '
- NR==1 {prev=$0; print; next}
- {
- len=length(prev)
- if(substr($0,1,len)==prev && substr($0,len+1,1)==".") next
- print
- prev=$0
- }
- ' "$A_TMP" > "$B_TMP"; then
- if $awk -F "." '{for(i=NF;i>0;i--) printf "%s%s", $i, (i>1?".":"\n")}' "$B_TMP" > "$A_TMP"; then
- if sort -u "$A_TMP" > "$B_TMP"; then
- output_ok
- else
- output_fail
- json add error 'errorOptimization'
- mv "$A_TMP" "$B_TMP"
- fi
- else
- output_fail
- json add error 'errorOptimization'
- fi
- else
- output_fail
- json add error 'errorOptimization'
- mv "$A_TMP" "$B_TMP"
- fi
- else
- output_fail
- json add error 'errorOptimization'
- fi
- else
- output_fail
- json add error 'errorOptimization'
- mv "$A_TMP" "$B_TMP"
- fi
- end_time=$(date +%s)
- elapsed=$(( end_time - start_time ))
- logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s"
- ;;
- *)
- mv "$A_TMP" "$B_TMP"
- ;;
- esac
-
- if [ -n "$allowed_domain" ] || [ -s "$ALLOWED_TMP" ]; then
- start_time=$(date +%s)
- step_title='Removing allowed domains from combined block-list'
- output 2 "[PROC] ${step_title} "
- json set message "$(get_text 'statusProcessing'): ${step_title}"
- local allowed_domains_from_dl
- [ -s "$ALLOWED_TMP" ] && allowed_domains_from_dl="$(sed '/^[[:space:]]*$/d' "$ALLOWED_TMP")"
- allowed_domain="${allowed_domain}${allowed_domains_from_dl:+ $allowed_domains_from_dl}"
- for hf in ${allowed_domain}; do
- hf="$(echo "$hf" | sed 's/\./\\./g')"
- echo "/(^|\.)${hf}$/d;"
- done > "$SED_TMP"
- # if only doing exact matches, may be faster to add $hf to $ALLOWED_TMP and then
- # grep -vFf "$ALLOWED_TMP" "$B_TMP" > "$A_TMP" && mv "$A_TMP" "$B_TMP"
- if [ -s "$SED_TMP" ]; then
- if sed -E -f "$SED_TMP" "$B_TMP" > "$A_TMP" && mv "$A_TMP" "$B_TMP"; then
- output_ok
- else
- output_fail
- json add error 'errorAllowListProcessing'
- fi
- else
- output_fail
- json add error 'errorAllowListProcessing'
- fi
- end_time=$(date +%s)
- elapsed=$(( end_time - start_time ))
- logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s"
- fi
-
- start_time=$(date +%s)
- step_title='Formatting combined block-list file'
- output 2 "[PROC] ${step_title} "
- json set message "$(get_text 'statusProcessing'): ${step_title}"
- if [ -z "$outputFilterIPv6" ]; then
- if sed "$outputFilter" "$B_TMP" > "$A_TMP"; then
- output_ok
- else
- output_fail
- json add error 'errorDataFileFormatting'
- fi
- else
- case "$dns" in
- dnsmasq.addnhosts)
- if sed "$outputFilter" "$B_TMP" > "$A_TMP" && \
- sed "$outputFilterIPv6" "$B_TMP" >> "$A_TMP"; then
- output_ok
- else
- output_fail
- json add error 'errorDataFileFormatting'
- fi
- ;;
- esac
- fi
- end_time=$(date +%s)
- elapsed=$(( end_time - start_time ))
- logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s"
-
- if [ -n "$outputAllowFilter" ] && [ -n "$allowed_domain" ]; then
- rm -f "$SED_TMP"; touch "$SED_TMP";
- start_time=$(date +%s)
- step_title="Explicitly allowing domains in ${dns}"
- output 2 "[PROC] ${step_title} "
- json set message "$(get_text 'statusProcessing'): ${step_title}"
- for hf in ${allowed_domain}; do
- echo "$hf" | sed -E "$outputAllowFilter" >> "$SED_TMP"
- done
- if [ -s "$SED_TMP" ]; then
- if cat "$SED_TMP" "$A_TMP" > "$B_TMP"; then
- output_ok
- else
- output_fail
- json add error 'errorAllowListProcessing'
- fi
- else
- output_fail
- json add error 'errorAllowListProcessing'
- fi
- end_time=$(date +%s)
- elapsed=$(( end_time - start_time ))
- logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s"
- else
- mv "$A_TMP" "$B_TMP"
- fi
-
- start_time=$(date +%s)
- step_title="Setting up ${dns} file"
- output 2 "[PROC] ${step_title} "
- json set message "$(get_text 'statusProcessing'): ${step_title}"
-
- case "$dns" in
- unbound.adb_list)
- if mv "$B_TMP" "$outputFile"; then
- output_ok
- else
- output_fail
- json add error 'errorMovingDataFile' "$outputFile"
- fi
- sed -i '1 i\server:' "$outputFile"
- ;;
- *)
- if mv "$B_TMP" "$outputFile"; then
- output_ok
- else
- output_fail
- json add error 'errorMovingDataFile' "$outputFile"
- fi
- ;;
- esac
-
- # Validate and remove invalid domain entries (RFC 1123 compliant)
- if [ -n "$dnsmasq_validity_check" ]; then
- case "$dns" in
- dnsmasq.conf|dnsmasq.ipset|dnsmasq.nftset|dnsmasq.servers|dnsmasq.addnhosts)
- start_time=$(date +%s)
- step_title='Validating domain entries'
- output 2 "[PROC] ${step_title} "
- json set message "$(get_text 'statusProcessing'): ${step_title}"
- invalid_file="/tmp/${packageName}.invalid.tmp"
- rm -f "$invalid_file"
- # Fast validation: remove entries where domain:
- # - starts with dash or dot (invalid per RFC)
- # - is all numeric with dots (IP-like, invalid for domain)
- # - has consecutive dots
- # - ends with dash or dot (invalid per RFC)
- sed "$outputParseFilter" "$outputFile" | \
- grep -E '^-|^\.|^[0-9.]+$|\.\.|-$|\.$' > "$invalid_file" 2>/dev/null || true
- if [ -s "$invalid_file" ]; then
- invalid_count=$(wc -l < "$invalid_file" 2>/dev/null || echo 0)
- if [ "$invalid_count" -gt 0 ]; then
- # Create pattern file for grep -vFf (fastest removal method)
- # Use appropriate prefix based on dns type
- case "$dns" in
- dnsmasq.conf)
- sed "$dnsmasqConfGrepPattern" "$invalid_file" > "${invalid_file}.pat" 2>/dev/null
- ;;
- dnsmasq.ipset)
- sed "$dnsmasqIpsetGrepPattern" "$invalid_file" > "${invalid_file}.pat" 2>/dev/null
- ;;
- dnsmasq.nftset)
- sed "$dnsmasqNftsetGrepPattern" "$invalid_file" > "${invalid_file}.pat" 2>/dev/null
- ;;
- dnsmasq.servers)
- sed "$dnsmasqServersGrepPattern" "$invalid_file" > "${invalid_file}.pat" 2>/dev/null
- ;;
- dnsmasq.addnhosts)
- # Create patterns for both IPv4 and IPv6 formats
- { sed "$dnsmasqAddnhostsGrepPatternIPv4" "$invalid_file"; sed "$dnsmasqAddnhostsGrepPatternIPv6" "$invalid_file"; } > "${invalid_file}.pat" 2>/dev/null
- ;;
- esac
- # Remove invalid entries
- grep -vFf "${invalid_file}.pat" "$outputFile" > "${outputFile}.valid" 2>/dev/null && \
- mv "${outputFile}.valid" "$outputFile" 2>/dev/null
- # Report (limit to first 20 for performance)
- logger -t "$packageName" "Removed $invalid_count invalid entries from ${dns}."
- json add warning 'warningInvalidDomainsRemoved' "$invalid_count"
- rm -f "${invalid_file}.pat"
- fi
- rm -f "$invalid_file"
- fi
- if [ "${invalid_count:-0}" -gt 0 ]; then
- output_warn
- else
- output_ok
- fi
- end_time=$(date +%s)
- elapsed=$(( end_time - start_time ))
- logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s"
- ;;
- esac
- fi
-
- step_title='Removing temporary files'
- output 2 "[PROC] ${step_title} "
- json set message "$(get_text 'statusProcessing'): ${step_title}"
- if rm -f "/tmp/${packageName}_tmp."* "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP" "$outputCache"; then
- output_ok
- else
- output_fail
- json add error 'errorRemovingTempFiles'
- fi
- output 1 '\n'
-}
-
-adb_config_update() {
-# shellcheck disable=SC2317,SC2329
- _cleanup_missing_urls() {
- local cfg="$1" url size
- config_get url "$cfg" url
- if [ -z "$url" ]; then
- uci_delete "$packageName" "$cfg"
- fi
- }
- local R_TMP label
- local param="${1:-quiet}"
- load_package_config
- load_dl_command
- label="${config_update_url##*//}"
- label="${label%%/*}";
- [ -n "$enabled" ] || return 0
- [ -n "$config_update_enabled" ] || return 0
-
- if [ "$param" != 'download' ]; then
- adb_file 'test' && return 0
- adb_file 'test_cache' && return 0
- adb_file 'test_gzip' && return 0
- fi
-
- output 1 'Updating config '
- output 2 "[ DL ] Config Update: $label "
- R_TMP="$(mktemp -q -t "${packageName}_tmp.XXXXXXXX")"
- if ! $dl_command "$config_update_url" "$dl_flag" "$R_TMP" 2>/dev/null || [ ! -s "$R_TMP" ]; then
- append_newline "$R_TMP"
- output_failn
- json add error 'errorDownloadingConfigUpdate'
- else
- if [ -s "$R_TMP" ] && sed -f "$R_TMP" -i "$packageConfigFile" 2>/dev/null; then
- output_okn
- else
- output_failn
- json add error 'errorParsingConfigUpdate'
- fi
- fi
- rm -f "$R_TMP"
- config_load "$packageName"
- config_foreach _cleanup_missing_urls 'file_url'
- uci_changes "$packageName" && uci_commit "$packageName"
- return 0
-}
-
-# shellcheck disable=SC2120
+boot() { rc_procd start_service 'on_boot' && service_started 'on_boot'; }
start_service() {
- local status error param="${1:-on_start}"
- local action p iface k
- status="$(json get status)"
- error="$(json get error)"
- json del all
-
- case "$param" in
- on_boot)
- if adb_file 'test_gzip' || adb_file 'test_cache'; then
- unset adbf_boot_flag
- else
- return 0
- fi
- ;;
- esac
-
- adb_config_update "$param"
- load_environment "$param" "$(load_validate_config)" || return 1
-
- action="$(adb_config_cache get trigger_service)"
- fw4_restart_flag="$(adb_config_cache get trigger_fw4)"
-
- if [ -n "$error" ]; then
- action='download'
- elif ! adb_file 'test'; then
- if adb_file 'test_gzip' || adb_file 'test_cache'; then
- action='restore'
- else
- action='download'
- fi
- elif [ "$status" = "statusSuccess" ]; then
- action='skip'
- fi
-
- case "${action}:${param}" in
- on_boot:*|*:on_boot|*:on_pause)
- if adb_file 'test_gzip' || adb_file 'test_cache'; then
- action='restore'
- else
- action='download'
- fi
- ;;
- download:*|*:download)
- action='download';;
- restart:*)
- action='restart';;
- restore:*)
- action='restore';;
- skip:*)
- action='skip';;
- *:*)
- action='download';;
- esac
-
- if [ "$action" = 'restore' ]; then
- output 1 "Starting $serviceName...\n"
- output 2 "[INIT] Starting $serviceName...\n"
- json set status 'statusStarting'
- if adb_file 'test_gzip' && ! adb_file 'test_cache' && ! adb_file 'test'; then
- output 1 'Found compressed cache file, unpacking it '
- output 2 '[INIT] Found compressed cache file, unpacking it '
- json set message 'found compressed cache file, unpacking it.'
- if adb_file 'unpack_gzip'; then
- output_okn
- else
- output_failn
- output_error "$(get_text 'errorRestoreCompressedCache')"
- action='download'
- fi
- fi
- if adb_file 'test_cache' && ! adb_file 'test'; then
- output 1 'Found cache file, reusing it '
- output 2 '[INIT] Found cache file, reusing it '
- json set message 'found cache file, reusing it.'
- if adb_file 'restore'; then
- unset sanity_check
- unset heartbeat_domain
- output_okn
- resolver 'on_start'
- else
- output_failn
- output_error "$(get_text 'errorRestoreCache')"
- action='download'
- fi
- fi
- fi
-
- if [ "$action" = 'download' ]; then
- if [ -z "$blocked_url" ] && [ -z "$blocked_domain" ]; then
- json set status 'statusFail'
- json add error 'errorNothingToDo'
- else
- if ! adb_file 'test' || adb_file 'test_cache' || adb_file 'test_gzip'; then
- output 1 "Force-reloading $serviceName...\n"
- output 2 "[INIT] Force-reloading $serviceName...\n"
- json set status 'statusForceReloading'
- else
- output 1 "Starting $serviceName...\n"
- output 2 "[INIT] Starting $serviceName...\n"
- json set status 'statusStarting'
- fi
- resolver 'cleanup'
- if [ "$dns" = 'dnsmasq.conf' ] && [ -n "$dnsmasq_config_file_url" ]; then
- download_dnsmasq_file
- else
- download_lists
- fi
- resolver 'on_start'
- fi
- fi
-
- if [ "$action" = 'restart' ]; then
- output 1 "Restarting $serviceName...\n"
- output 2 "[INIT] Restarting $serviceName...\n"
- json set status 'statusRestarting'
- unset sanity_check
- unset heartbeat_domain
- resolver 'on_start'
- fi
-
- if [ "$action" = 'start' ]; then
- output 1 "Starting $serviceName...\n"
- output 2 "[INIT] Starting $serviceName...\n"
- json set status 'statusStarting'
- unset sanity_check
- unset heartbeat_domain
- resolver 'on_start'
- fi
-
- if adb_file 'test' && [ "$(json get status)" != "statusFail" ]; then
- json del message
- json set status 'statusSuccess'
- json set stats "$serviceName is blocking $(count_blocked_domains) domains (with ${dns})"
- status_service 'on_start_success'
- else
- json set status 'statusFail'
- json add error 'errorOhSnap'
- status_service 'on_start_failure'
- resolver 'revert'
- fi
-
- adb_config_cache 'create'
-
- procd_open_instance 'main'
- procd_set_param command /bin/true
- procd_set_param stdout 1
- procd_set_param stderr 1
- procd_open_data
- json_add_string 'version' "$PKG_VERSION"
- json_add_string 'status' "$(json get status)"
- json_add_int 'packageCompat' "$packageCompat"
- json_add_int 'entries' "$(count_blocked_domains)"
- json_add_array 'errors'
- for k in $(json get errors); do
- json_add_object "$k"
- json_add_string 'code' "$(json get error "$k" 'code')"
- json_add_string 'info' "$(json get error "$k" 'info')"
- json_close_object
- done
- json_close_array
- json_add_array 'warnings'
- for k in $(json get warnings); do
- json_add_object "$k"
- json_add_string 'code' "$(json get warning "$k" 'code')"
- json_add_string 'info' "$(json get warning "$k" 'info')"
- json_close_object
- done
- json_close_array
- json_add_array firewall
- if [ -n "$force_dns" ]; then
-# shellcheck disable=SC3060
- for p in ${force_dns_port/,/ }; do
- if is_port_listening "$p"; then
- for iface in $force_dns_interface; do
- json_add_object ''
- json_add_string type 'redirect'
- json_add_string target 'DNAT'
- json_add_string src "$iface"
- json_add_string proto 'tcp udp'
- json_add_string src_dport '53'
- json_add_string dest_port "$p"
- json_add_string family 'any'
- json_add_boolean reflection '0'
- json_close_object
- done
- else
- for iface in $force_dns_interface; do
- json_add_object ''
- json_add_string type 'rule'
- json_add_string src "$iface"
- json_add_string dest '*'
- json_add_string proto 'tcp udp'
- json_add_string dest_port "$p"
- json_add_string target 'REJECT'
- json_close_object
- done
- fi
- done
- fi
- case "$dns" in
- dnsmasq.ipset|smartdns.ipset)
- json_add_object ''
- json_add_string type 'ipset'
- json_add_string name 'adb'
- json_add_string match 'dest_net'
- json_add_string storage 'hash'
- json_close_object
- for iface in $force_dns_interface; do
- json_add_object ''
- json_add_string type 'rule'
- json_add_string ipset 'adb'
- json_add_string src "$iface"
- json_add_string dest '*'
- json_add_string proto 'tcp udp'
- json_add_string target 'REJECT'
- json_close_object
- done
- ;;
- dnsmasq.nftset|smartdns.nftset)
- json_add_object ''
- json_add_string type 'ipset'
- json_add_string name 'adb4'
- json_add_string family '4'
- json_add_string match 'dest_net'
- json_close_object
- for iface in $force_dns_interface; do
- json_add_object ''
- json_add_string type 'rule'
- json_add_string ipset 'adb4'
- json_add_string src "$iface"
- json_add_string dest '*'
- json_add_string proto 'tcp udp'
- json_add_string target 'REJECT'
- json_close_object
- done
- if [ -n "$ipv6_enabled" ]; then
- json_add_object ''
- json_add_string type 'ipset'
- json_add_string name 'adb6'
- json_add_string family '6'
- json_add_string match 'dest_net'
- json_close_object
- for iface in $force_dns_interface; do
- json_add_object ''
- json_add_string type 'rule'
- json_add_string ipset 'adb6'
- json_add_string src "$iface"
- json_add_string dest '*'
- json_add_string proto 'tcp udp'
- json_add_string target 'REJECT'
- json_close_object
- done
- fi
- ;;
- esac
- json_close_array
- procd_close_data
- procd_close_instance
- return 0
-}
-
-status_service() {
- local param="$1"
- local c status message error warning stats text
- local code info
- load_package_config
- status="$(json get status)"
- message="$(json get message)"
- error="$(json get error)"
- warning="$(json get warning)"
- stats="$(json get stats)"
- if [ "$status" = "statusSuccess" ]; then
- output 1 "* $stats\n"
- output 2 "[STAT] $stats\n"
- else
- [ -n "$status" ] && status="$(get_text "$status")"
- status="${status}${status:+${message:+: $message}}"
- case "$(adb_file 'test_cache'; echo $?:$(adb_file 'test_gzip'; echo $?))" in
- "0:0")
- message="cache file and compressed cache file found"
- ;;
- "0:1")
- message="cache file found"
- ;;
- "1:0")
- message="compressed cache file found"
- ;;
- *)
- unset message
- ;;
- esac
- status="${status}${status:+${message:+ ($message)}}"
- [ -n "$status" ] && output "$serviceName $status.\n"
- fi
- [ "$param" != 'quiet' ] || return 0
- if [ -n "$error" ]; then
- for c in $error; do
- code="$(json get error "$c" 'code')"
- info="$(json get error "$c" 'info')"
- output_error "$(get_text "$code" "$info")"
- done
- fi
- if [ -n "$warning" ]; then
- for c in $warning; do
- code="$(json get warning "$c" 'code')"
- info="$(json get warning "$c" 'info')"
- output_warning "$(get_text "$code" "$info")"
- done
- fi
-}
-
-# shellcheck disable=SC2120
-stop_service() {
- load_package_config
- if adb_file 'test'; then
- output 1 "Stopping $serviceName... "
- output 2 "[STOP] Stopping $serviceName... "
- adb_file 'create'
- if resolver 'on_stop'; then
- ipset -q -! flush adb > /dev/null 2>&1
- ipset -q -! destroy adb > /dev/null 2>&1
- nft delete set inet fw4 adb4 > /dev/null 2>&1
- nft delete set inet fw4 adb6 > /dev/null 2>&1
- led_off "$led"
- output_okn
- json set status 'statusStopped'
- json del message
- else
- output_failn;
- json set status 'statusFail'
- json add error 'errorStopping'
- output_error "$(get_text 'errorStopping')"
- fi
- fi
- return 0
-}
-
-boot() {
-# ubus -t 30 wait_for network.interface 2>/dev/null
- adbf_boot_flag=1
- rc_procd start_service 'on_boot' && service_started 'on_boot'
+ local param="${1:-on_start}"
+ _procd_svc_data="$($_ucode start "$param")"
}
+service_data() { [ -n "$_procd_svc_data" ] && eval "$_procd_svc_data"; }
+stop_service() { eval "$($_ucode stop)"; }
reload_service() { rc_procd start_service 'reload'; }
restart_service() { rc_procd start_service 'restart'; }
-service_stopped() { is_fw4_restart_needed && procd_set_config_changed firewall; }
+status_service() { $_ucode status "$@"; }
+service_stopped() { [ "$_fw4_restart" = 1 ] && procd_set_config_changed firewall; }
+service_started() { [ "$_fw4_restart" = 1 ] && procd_set_config_changed firewall; }
service_triggers() {
- local wan wan6 i
- if [ -n "$adbf_boot_flag" ]; then
- output 1 'Setting trigger (on_boot) '
- output 2 '[TRIG] Setting trigger (on_boot) '
- procd_add_raw_trigger "interface.*.up" 5000 "/etc/init.d/${packageName}" start && output_okn || output_failn
- triggerStatus='statusTriggerBootWait'
- else
- procd_open_validate
- load_validate_file_url_section
- procd_close_validate
+ local wan wan6 procd_trigger_wan6 i
+ if [ ! -s "/dev/shm/${packageName}" ]; then
+ procd_add_raw_trigger "interface.*.up" 5000 "/etc/init.d/${packageName}" start
+ else
network_flush_cache
network_find_wan wan
wan="${wan:-wan}"
- if [ -n "$procd_trigger_wan6" ]; then
+ procd_trigger_wan6="$(uci -q get "${packageName}.config.procd_trigger_wan6")"
+ if [ "$procd_trigger_wan6" = '1' ]; then
network_find_wan6 wan6
wan6="${wan6:-wan6}"
fi
- output 1 "Setting trigger${wan6:+s} for $wan ${wan6:+$wan6 }"
- output 2 "[TRIG] Setting trigger${wan6:+s} for $wan ${wan6:+$wan6 }"
for i in $wan $wan6; do
- procd_add_interface_trigger "interface.*" "$i" "/etc/init.d/${packageName}" start && output_ok || output_fail
+ procd_add_interface_trigger "interface.*" "$i" "/etc/init.d/${packageName}" start "on_${i}"
done
- output 1 '\n'
procd_add_config_trigger "config.change" "$packageName" "/etc/init.d/${packageName}" reload
- triggerStatus='statusTriggerStartWait'
+ procd_open_validate
+ load_validate_config
+ load_validate_file_url_section
+ procd_close_validate
fi
}
-
-service_started() {
- local start_time end_time elapsed step_title
- if [ -n "$compressed_cache" ] && ! adb_file 'test_gzip' && adb_file 'test'; then
- start_time=$(date +%s)
- step_title="Creating ${dns} compressed cache"
- output 1 "${step_title} "
- output 2 "[PROC] ${step_title} "
- json set message "$(get_text 'statusProcessing'): ${step_title}"
- if adb_file 'create_gzip'; then
- output_okn
- else
- output_failn
- json add error 'errorCreatingCompressedCache'
- fi
- end_time=$(date +%s)
- elapsed=$(( end_time - start_time ))
- logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s"
- else
- adb_file 'remove_gzip'
- fi
- is_fw4_restart_needed && procd_set_config_changed firewall
- [ -z "$(json get status)" ] && json set status "$triggerStatus"
-}
-
-allow() {
- local c hf string="$1"
- load_package_config
- if ! adb_file 'test'; then
- output "No block-list ('$outputFile') found.\n"
- return 0
- elif [ -z "$string" ]; then
- output "Usage: /etc/init.d/${packageName} allow 'domain' ...\n"
- return 0
- elif [ -n "$dnsmasq_config_file_url" ]; then
- output "Allowing individual domains is not possible when using external dnsmasq config file.\n"
- return 0
- fi
- case "$dns" in
- dnsmasq.*)
- output 1 'Allowing domains and restarting dnsmasq '
- output 2 '[PROC] Allowing domains \n'
- for c in $string; do
- output 2 " $c "
- hf="$(echo "$c" | sed 's/\./\\./g')"
- if sed -i "\:\(/\|\.\)${hf}/:d" "$outputFile"; then
- output_ok
- else
- output_fail
- fi
- if [ -n "$outputAllowFilter" ]; then
- if echo "$c" | sed -E "$outputAllowFilter" >> "$outputFile"; then
- output_ok
- else
- output_fail
- fi
- fi
- if uci_add_list_if_new "${packageName}" 'config' 'allowed_domain' "$c"; then
- output_ok
- else
- output_fail
- fi
- done
- if [ -n "$compressed_cache" ]; then
- output 2 '[PROC] Creating compressed cache '
- if adb_file 'create_gzip'; then
- output_ok
- else
- output_fail
- fi
- fi
- output 2 '[PROC] Committing changes to config '
- if uci_commit "$packageName"; then
- allowed_domain="$(uci_get "$packageName" 'config' 'allowed_domain')"
- adb_config_cache 'create'
- json set stats "$serviceName is blocking $(count_blocked_domains) domains (with ${dns})"
- output_ok
- if [ "$dns" = 'dnsmasq.ipset' ]; then
- output 2 '[PROC] Flushing adb ipset '
- if ipset -q -! flush adb; then output_ok; else output_fail; fi
- fi
- if [ "$dns" = 'dnsmasq.nftset' ]; then
- output 2 '[PROC] Flushing adb nft sets '
- nft flush set inet fw4 adb6
- if nft flush set inet fw4 adb4; then output_ok; else output_fail; fi
- fi
- output_dns 'Restarting dnsmasq '
- if dnsmasq_restart; then output_ok; else output_fail; fi
- else
- output_fail
- fi
- output 1 '\n'
- ;;
- smartdns.*)
- output 1 'Allowing domains and restarting smartdns '
- output 2 '[PROC] Allowing domains \n'
- for c in $string; do
- output 2 " $c "
- hf="$(echo "$c" | sed 's/\./\\./g')"
- if sed -i "\:\(\"\|\.\)${hf}\":d" "$outputFile" && \
- uci_add_list_if_new "$packageName" 'config' 'allowed_domain' "$string"; then
- output_ok
- else
- output_fail
- fi
- done
- if [ -n "$compressed_cache" ]; then
- output 2 '[PROC] Creating compressed cache '
- if adb_file 'create_gzip'; then
- output_ok
- else
- output_fail
- fi
- fi
- output 2 '[PROC] Committing changes to config '
- if uci_commit "$packageName"; then
- allowed_domain="$(uci_get "$packageName" 'config' 'allowed_domain')"
- adb_config_cache 'create'
- json set stats "$serviceName is blocking $(count_blocked_domains) domains (with ${dns})"
- output_ok;
- output_dns 'Restarting SmartDNS '
- if smartdns_restart; then output_ok; else output_fail; fi
- else
- output_fail
- fi
- output 1 '\n'
- ;;
- unbound.*)
- output 1 'Allowing domains and restarting Unbound '
- output 2 '[PROC] Allowing domains \n'
- for c in $string; do
- output 2 " $c "
- hf="$(echo "$c" | sed 's/\./\\./g')"
- if sed -i "\:\(\"\|\.\)${hf}\":d" "$outputFile" && \
- uci_add_list_if_new "$packageName" 'config' 'allowed_domain' "$string"; then
- output_ok
- else
- output_fail
- fi
- done
- if [ -n "$compressed_cache" ]; then
- output 2 '[PROC] Creating compressed cache '
- if adb_file 'create_gzip'; then
- output_ok
- else
- output_failn
- fi
- fi
- output 2 '[PROC] Committing changes to config '
- if uci_commit "$packageName"; then
- allowed_domain="$(uci_get "$packageName" 'config' 'allowed_domain')"
- adb_config_cache 'create'
- json set stats "$serviceName is blocking $(count_blocked_domains) domains (with ${dns})"
- output_ok;
- output_dns 'Restarting Unbound '
- if unbound_restart; then output_ok; else output_fail; fi
- else
- output_fail
- fi
- output 1 '\n'
- ;;
- esac
-}
-
-check() {
- local c param="$1"
- load_package_config
- if ! adb_file 'test'; then
- output "No block-list ('$outputFile') found.\n"
- return 0
- elif [ -z "$param" ]; then
- output "Usage: /etc/init.d/${packageName} check 'domain' ...\n"
- return 0
- fi
- for string in ${param}; do
- c="$(grep -c -E "$string" "$outputFile")"
- if [ "$c" -gt 0 ]; then
- if [ "$c" -eq 1 ]; then
- output 1 "Found 1 match for '$string' in '$outputFile'.\n"
- output 2 "[PROC] Found 1 match for '$string' in '$outputFile'.\n"
- else
- output 1 "Found $c matches for '$string' in '$outputFile'.\n"
- output 2 "[PROC] Found $c matches for '$string' in '$outputFile'.\n"
- fi
- if [ "$c" -le 20 ]; then
- grep "$string" "$outputFile" | sed "$outputParseFilter"
- fi
- else
- output 1 "The '$string' is not found in current block-list ('$outputFile').\n"
- output 2 "[PROC] The '$string' is not found in current block-list ('$outputFile').\n"
- fi
- done
-}
-
-check_tld() {
- local c param="$1"
- load_package_config
- if ! adb_file 'test'; then
- output "No block-list ('$outputFile') found.\n"
- return 0
- fi
- c="$(grep -cvE '\.|server:' "$outputFile")"
- if [ "$c" -gt 0 ]; then
- if [ "$c" -eq 1 ]; then
- output 1 "Found 1 match for TLD in '$outputFile'.\n"
- output 2 "[PROC] Found 1 match for TLD in '$outputFile'.\n"
- else
- output 1 "Found $c matches for TLDs in '$outputFile'.\n"
- output 2 "[PROC] Found $c matches for TLDs in '$outputFile'.\n"
- fi
- if [ "$c" -le 20 ]; then
- grep -vE '\.|server:' "$outputFile" | sed "$outputParseFilter"
- fi
- else
- output 1 "No TLD was found in current block-list ('$outputFile').\n"
- output 2 "[PROC] No TLD was found in current block-list ('$outputFile').\n"
- fi
-}
-
-check_leading_dot() {
- local c param="$1"
- local string
- load_package_config
- if ! adb_file 'test'; then
- output "No block-list ('$outputFile') found.\n"
- return 0
- fi
- case "$dns" in
- dnsmasq.*) string='/\.';;
- smartdns.*) string='^\.';;
- unbound.*) string='"\.';;
- esac
- c="$(grep -c "$string" "$outputFile")"
- if [ "$c" -gt 0 ]; then
- if [ "$c" -eq 1 ]; then
- output 1 "Found 1 match for leading-dot domain in '$outputFile'.\n"
- output 2 "[PROC] Found 1 match for leading-dot domain in '$outputFile'.\n"
- else
- output 1 "Found $c matches for leading-dot domains in '$outputFile'.\n"
- output 2 "[PROC] Found $c matches for leading-dot domains in '$outputFile'.\n"
- fi
- if [ "$c" -le 20 ]; then
- grep "$string" "$outputFile" | sed "$outputParseFilter"
- fi
- else
- output 1 "No leading-dot domain was found in current block-list ('$outputFile').\n"
- output 2 "[PROC] No leading-dot domain was found in current block-list ('$outputFile').\n"
- fi
-}
-
-check_lists() {
-# shellcheck disable=SC2317,SC2329
- _check_list() {
- local cfg="$1"
- local en size url name R_TMP string c
- config_get_bool en "$cfg" enabled '1'
- config_get action "$cfg" action 'block'
- config_get url "$cfg" url
- config_get name "$cfg" name
- name="${name:-$url}"
-
- [ "$en" = '0' ] && return 0
- [ "$action" != 'block' ] && return 0
-
- output 1 "Checking ${name}: "
- output 2 "[ DL ] $name "
-
- if is_https_url "$url" && [ -z "$isSSLSupported" ]; then
- output_failn
- return 1
- fi
- R_TMP="$(mktemp -q -t "${packageName}_tmp.XXXXXXXX")"
- if [ -z "$url" ] || ! $dl_command "$url" "$dl_flag" "$R_TMP" 2>/dev/null || \
- [ ! -s "$R_TMP" ]; then
- output_failn
- return 1
- else
- output 2 "$__OK__\n"
- fi
- append_newline "$R_TMP"
- for string in ${param}; do
- c="$(grep -c -E "$string" "$R_TMP")"
- if [ "$c" -gt 0 ]; then
- if [ "$c" -eq 1 ]; then
- output 1 "found 1 match for '$string'.\n"
- output 2 "[PROC] Found 1 match for '$string' in '$url'.\n"
- else
- output 1 "found $c matches for '$string'.\n"
- output 2 "[PROC] Found $c matches for '$string' in '$url'.\n"
- fi
- grep "$string" "$R_TMP"
- else
- output 1 "'$string' not found.\n"
- output 2 "[PROC] The '$string' is not found in '$url'.\n"
- fi
- done
- rm -f "$R_TMP"
- }
- local param="$1"
- load_package_config
- load_dl_command
- if [ -z "$param" ]; then
- output "Usage: /etc/init.d/${packageName} check_lists 'domain' ...\n"
- return 0
- fi
- config_load "$packageName"
- config_foreach _check_list 'file_url'
- return 0
-}
-
-dl() { rc_procd start_service 'download' && service_started 'download'; }
-
-killcache() {
- load_package_config
- rm -f "$dnsmasqAddnhostsCache" "${compressed_cache_dir}/${dnsmasqAddnhostsGzip}"
- rm -f "$dnsmasqConfCache" "${compressed_cache_dir}/${dnsmasqConfGzip}"
- rm -f "$dnsmasqIpsetCache" "${compressed_cache_dir}/${dnsmasqIpsetGzip}"
- rm -f "$dnsmasqNftsetCache" "${compressed_cache_dir}/${dnsmasqNftsetGzip}"
- rm -f "$dnsmasqServersCache" "${compressed_cache_dir}/${dnsmasqServersGzip}"
- rm -f "$smartdnsDomainSetCache" "${compressed_cache_dir}/${smartdnsDomainSetGzip}"
- rm -f "$smartdnsIpsetCache" "${compressed_cache_dir}/${smartdnsIpsetGzip}"
- rm -f "$smartdnsNftsetCache" "${compressed_cache_dir}/${smartdnsNftsetGzip}"
- rm -f "$unboundCache" "${compressed_cache_dir}/${unboundGzip}"
- resolver 'cleanup'
- return 0
-}
-
-pause() {
- load_package_config
- local timeout="${1:-$pause_timeout}"
- stop_service 'on_pause'
- output 1 "Sleeping for $timeout seconds... "
- output 2 "[PROC] Sleeping for $timeout seconds... "
- if is_integer "$timeout" && sleep "$timeout"; then
- output_okn
- else
- output_failn
- fi
- start_service 'on_pause'
-}
-
-show_blocklist() {
- load_package_config
- sed "$stripToDomainsFilter" "$outputFile"
-}
-
-sizes() {
-# shellcheck disable=SC2329
- _config_add_url_size() {
- local cfg="$1" url name size
- config_get url "$cfg" url
- config_get name "$cfg" name
- size="$(get_url_filesize "$url")"
- output "${name:-$url}${size:+: $size} "
- if [ -n "$size" ]; then
- uci_set "$packageName" "$cfg" 'size' "$size"
- output_okn
- else
- output_failn
- fi
- }
- local i
- load_package_config
- load_dl_command
- config_load "$packageName"
- config_foreach _config_add_url_size 'file_url'
- [ -n "$update_config_sizes" ] && uci_changes "$packageName" && uci_commit "$packageName"
-}
-
-version() { echo "$PKG_VERSION"; }
+allow() { $_ucode allow "$@"; }
+check() { $_ucode check "$@"; }
+check_tld() { $_ucode check_tld "$@"; }
+check_leading_dot() { $_ucode check_leading_dot "$@"; }
+check_lists() { $_ucode check_lists "$@"; }
+dl() { rc_procd start_service 'download' && service_started; }
+killcache() { $_ucode killcache "$@"; }
+pause() { $_ucode pause "$@"; }
+show_blocklist() { $_ucode show_blocklist "$@"; }
+sizes() { $_ucode sizes "$@"; }
+version() { $_ucode version "$@"; }
# shellcheck disable=SC2120
load_validate_file_url_section() {
- uci_load_validate "$packageName" "$packageName" "$1" "$2" \
+ uci_load_validate "$packageName" 'file_url' "$1" "$2" \
'enabled:bool:1' \
'action:or("allow", "block"):block' \
'size:or(uinteger, "")' \
@@ -2703,8 +89,9 @@ load_validate_file_url_section() {
;
}
+# shellcheck disable=SC2120
load_validate_config() {
- uci_load_validate "$packageName" "$packageName" "$1" "${2}${3:+ $3}" \
+ uci_load_validate "$packageName" "$packageName" "$1" "${2}${3:+ ${3}}" \
'enabled:bool:0' \
'force_dns:bool:1' \
'force_dns_interface:list(network):lan' \
@@ -2730,8 +117,8 @@ load_validate_config() {
'procd_boot_wan_timeout:integer:60' \
'led:or("", "none", file, device, string)' \
'dns:or("dnsmasq.addnhosts", "dnsmasq.conf", "dnsmasq.ipset", "dnsmasq.nftset", "dnsmasq.servers", "smartdns.domainset", "smartdns.ipset", "smartdns.nftset", "unbound.adb_list"):dnsmasq.servers' \
- 'dnsmasq_instance:list(or(integer, string)):*' \
- 'smartdns_instance:list(or(integer, string)):*' \
+ 'dnsmasq_instance:list(or("*", "-", uinteger, uci("dhcp", "@dnsmasq"))):*' \
+ 'smartdns_instance:list(or("*", "-", uinteger, uci("smartdns", "@smartdns"))):*' \
'heartbeat_domain:or("-", string):heartbeat.melmac.ca' \
'heartbeat_sleep_timeout:range(1,60):10' \
'dnsmasq_sanity_check:bool:1' \
diff --git a/net/adblock-fast/files/etc/uci-defaults/90-adblock-fast b/net/adblock-fast/files/etc/uci-defaults/90-adblock-fast
index 2414540029..0e2cd03312 100644
--- a/net/adblock-fast/files/etc/uci-defaults/90-adblock-fast
+++ b/net/adblock-fast/files/etc/uci-defaults/90-adblock-fast
@@ -1,181 +1,65 @@
#!/bin/sh
-# Copyright 2023 MOSSDeF, Stan Grishin (stangri@melmac.ca)
-# shellcheck disable=SC2015,SC3043,SC3060
+# Copyright 2023-2026 MOSSDeF, Stan Grishin (stangri@melmac.ca)
+# shellcheck disable=SC2015,SC3043
+readonly pkg='adblock-fast'
-readonly adbFunctionsFile='/etc/init.d/adblock-fast'
-if [ -s "$adbFunctionsFile" ]; then
-# shellcheck source=../../etc/init.d/adblock-fast
- . "$adbFunctionsFile"
-else
- printf "%b: adblock-fast init.d file (%s) not found! \n" '\033[0;31mERROR\033[0m' "$adbFunctionsFile"
-fi
+# ── Transition to list names ─────────────────────────────────────────
+# Adds 'name' to file_url sections that lack one, using the pristine default config
-# Transition from simple-adblock
-_enable_url() {
- local cfg="$1" url="$2" action="$3"
- local u a
- config_get u "$cfg" 'url'
- config_get a "$cfg" 'action' 'block'
- if [ "$u" = "$url" ] && [ "$a" = "$action" ]; then
- uci_remove "$packageName" "$cfg" 'enabled' && _found=1
- fi
-}
+# Find pristine default: apk uses .apk-new, opkg uses -opkg
+pristine=''
+for f in "/etc/config/${pkg}.apk-new" "/etc/config/${pkg}-opkg"; do
+ [ -s "$f" ] && pristine="$f" && break
+done
-enable_add_url() {
- local url="$1" action="$2" _found
- config_load "$packageName"
- config_foreach _enable_url 'file_url' "$url" "$action"
- if [ -z "$_found" ]; then
- uci_add "$packageName" 'file_url'
- uci_set "$packageName" '@file_url[-1]' 'url' "$url"
- uci_set "$packageName" '@file_url[-1]' 'size' "$(get_url_filesize "$url")"
- uci_set "$packageName" '@file_url[-1]' 'action' "$action"
- fi
-}
+_find_name() { grep -B1 "$1" "$pristine" 2>/dev/null | head -1 | cut -d "'" -f2; }
-if [ -s '/etc/config/simple-adblock' ] \
- && [ ! -s '/etc/config/adblock-fast-opkg' ] \
- && [ "$(uci_get adblock-fast config enabled)" = '0' ]; then
- cp -f '/etc/config/adblock-fast' '/etc/config/adblock-fast-opkg'
- enabled="$(uci_get simple-adblock config enabled)"
- if [ -x '/etc/init.d/simple-adblock' ]; then
- output "Stopping and disabling simple-adblock "
- if /etc/init.d/simple-adblock stop >/dev/null 2>&1 \
- && /etc/init.d/simple-adblock disable \
- && uci_set simple-adblock config enabled 0 \
- && uci_commit simple-adblock; then
- output_okn
- else
- output_failn
- fi
- else
- output "Disabling simple-adblock."
- if uci_set simple-adblock config enabled 0 \
- && uci_commit simple-adblock; then
- output_okn
- else
- output_failn
- fi
- fi
- output "Migrating simple-adblock config file "
- for i in allow_non_ascii canary_domains_icloud canary_domains_mozilla \
- compressed_cache compressed_cache_dir config_update_enabled \
- curl_additional_param curl_max_file_size curl_retry download_timeout \
- debug dns dns_instance dnsmasq_config_file_url force_dns led \
- parallel_downloads procd_trigger_wan6 procd_boot_wan_timeout verbosity; do
- j="$(uci_get simple-adblock.config.${i})"
- [ -n "$j" ] && uci_set "$packageName" config "$i" "$j"
- done
- [ -n "$enabled" ] && uci_set "$packageName" config enabled "$enabled"
- j="$(uci_get simple-adblock config config_update_url)"
- if [ "${j//simple-adblock/}" = "$j" ]; then
- uci_set "$packageName" config config_update_url "$j"
- fi
- ccd="$(uci_get simple-adblock config compressed_cache_dir '/etc')"
- for j in $(uci_get simple-adblock config allowed_domain); do
- [ -n "$j" ] && uci_add_list "$packageName" config allowed_domain "$j"
- done
- for j in $(uci_get simple-adblock config blocked_domain); do
- [ -n "$j" ] && uci_add_list "$packageName" config blocked_domain "$j"
- done
- for j in $(uci_get simple-adblock config force_dns_port); do
- [ -n "$j" ] && uci_add_list "$packageName" config force_dns_port "$j"
- done
- output_okn
-
- for i in allowed_domains_url blocked_adblockplus_url blocked_domains_url \
- blocked_hosts_url; do
- output "Migrating simple-adblock ${i} "
- for j in $(uci_get simple-adblock config "$i"); do
- if [ "$i" = 'allowed_domains_url' ]; then
- enable_add_url "$j" 'allow'
- else
- enable_add_url "$j" 'block'
+if [ -n "$pristine" ]; then
+ # shellcheck disable=SC1091
+ . /lib/functions.sh
+ add_name() {
+ local cfg="$1" url name label
+ config_get url "$cfg" 'url'
+ config_get name "$cfg" 'name'
+ if [ -z "$name" ]; then
+ label="${url##*//}"; label="${label%%/*}"
+ name="$(_find_name "$url")"
+ if [ -n "$name" ]; then
+ uci set "${pkg}.${cfg}.name=${name}"
+ printf " %s: %s\n" "$label" "$name" >&2
fi
- done
- output_okn
- done
- uci_commit "$packageName"
- output "Migrating simple-adblock cache file(s) "
- for i in '/var/run/simple-adblock/dnsmasq.addnhosts.cache' \
- '/var/run/simple-adblock/dnsmasq.conf.cache' \
- '/var/run/simple-adblock/dnsmasq.ipset.cache' \
- '/var/run/simple-adblock/dnsmasq.nftset.cache' \
- '/var/run/simple-adblock/dnsmasq.servers.cache' \
- '/var/run/simple-adblock/unbound.cache'; do
- if [ -s "$i" ]; then
- current_dir="$(dirname "$i")"
- mkdir -p "${current_dir//simple-adblock/adblock-fast}"
- mv -f "$i" "${i//simple-adblock/adblock-fast}" && output_okn || output_failn
fi
- done
- for i in 'simple-adblock.dnsmasq.addnhosts.gz' \
- 'simple-adblock.dnsmasq.conf.gz' \
- 'simple-adblock.dnsmasq.ipset.gz' \
- 'simple-adblock.dnsmasq.nftset.gz' \
- 'simple-adblock.dnsmasq.servers.gz' \
- 'simple-adblock.unbound.gz'; do
- i="${ccd}/${i}"
- if [ -s "$i" ]; then
- mkdir -p "${ccd//simple-adblock/adblock-fast}"
- mv -f "$i" "${i//simple-adblock/adblock-fast}" && output_okn || output_failn
- fi
- done
- output_okn
-fi
-
-# Transition to list names
-_find_name() { grep -B1 "$1" "/etc/config/${packageName}-opkg" | head -1 | cut -d "'" -f2; }
-
-add_name() {
- local cfg="$1"
- local url name label
- config_get url "$cfg" 'url'
- config_get name "$cfg" 'name'
- if [ -z "$name" ]; then
- label="${url##*//}"
- label="${label%%/*}";
- output "Finding name for ${label}: "
- name="$(_find_name "$url")"
- if [ -n "$name" ]; then
- uci_set "$packageName" "$cfg" 'name' "$name"
- output "$name "
- output_okn
- else
- output "Unknown "
- output_failn
- fi
- else
- output "Name for ${label} already set to ${name} "
- output_okn
- fi
-}
-
-if [ -s "/etc/config/${packageName}-opkg" ] && ! grep -q 'option name' "/etc/config/${packageName}"; then
- config_load "$packageName"
+ }
+ config_load "$pkg"
config_foreach add_name 'file_url'
fi
-# migrate to 1.2.0
-oldval="$(uci_get "$packageName" 'config' 'debug')"
+# ── Migrate to 1.2.0 ────────────────────────────────────────────────
+
+oldval="$(uci -q get "${pkg}.config.debug")"
if [ -n "$oldval" ]; then
- uci_set "$packageName" 'config' 'debug_init_script' "$oldval"
- uci_remove "$packageName" 'config' 'debug'
-fi
-oldval="$(uci_get "$packageName" 'config' 'proc_debug')"
-if [ -n "$oldval" ]; then
- uci_set "$packageName" 'config' 'debug_performance' "$oldval"
- uci_remove "$packageName" 'config' 'proc_debug'
+ uci set "${pkg}.config.debug_init_script=${oldval}"
+ uci -q delete "${pkg}.config.debug"
fi
-# migrate sanity_check to dnsmasq_sanity_check
-if [ -z "$(uci_get "$packageName" 'config' 'dnsmasq_sanity_check')" ] && [ -n "$(uci_get "$packageName" 'config' 'sanity_check')" ]; then
- oldval="$(uci_get "$packageName" 'config' 'sanity_check')"
- uci_set "$packageName" 'config' 'dnsmasq_sanity_check' "$oldval"
- uci_remove "$packageName" 'config' 'sanity_check'
+oldval="$(uci -q get "${pkg}.config.proc_debug")"
+if [ -n "$oldval" ]; then
+ uci set "${pkg}.config.debug_performance=${oldval}"
+ uci -q delete "${pkg}.config.proc_debug"
fi
-uci_changes "$packageName" && uci_commit "$packageName"
+# ── Migrate sanity_check → dnsmasq_sanity_check ─────────────────────
+
+if [ -z "$(uci -q get "${pkg}.config.dnsmasq_sanity_check")" ] \
+ && [ -n "$(uci -q get "${pkg}.config.sanity_check")" ]; then
+ oldval="$(uci -q get "${pkg}.config.sanity_check")"
+ uci set "${pkg}.config.dnsmasq_sanity_check=${oldval}"
+ uci -q delete "${pkg}.config.sanity_check"
+fi
+
+# ── Commit if anything changed ───────────────────────────────────────
+
+[ -n "$(uci -q changes "$pkg" 2>/dev/null)" ] && uci commit "$pkg"
exit 0
diff --git a/net/adblock-fast/files/lib/adblock-fast/adblock-fast.uc b/net/adblock-fast/files/lib/adblock-fast/adblock-fast.uc
new file mode 100644
index 0000000000..2967c0ec6f
--- /dev/null
+++ b/net/adblock-fast/files/lib/adblock-fast/adblock-fast.uc
@@ -0,0 +1,2849 @@
+'use strict';
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Copyright 2023-2026 MOSSDeF, Stan Grishin (stangri@melmac.ca).
+//
+// Main ucode module for adblock-fast.
+// All business logic lives here; the init script is a thin procd wrapper.
+
+import { readfile, writefile, popen, stat, unlink, rename, open, glob, mkdir, mkstemp, symlink, chmod, chown, realpath, lsdir, access, dirname } from 'fs';
+import { cursor } from 'uci';
+import { connect } from 'ubus';
+
+// ── Constants ───────────────────────────────────────────────────────
+
+const pkg = {
+ name: 'adblock-fast',
+ version: 'dev-test',
+ compat: '13',
+ memory_threshold: 33554432,
+ config_file: '/etc/config/adblock-fast',
+ dnsmasq_file: '/var/run/adblock-fast/adblock-fast.dnsmasq',
+ run_file: '/dev/shm/adblock-fast',
+ triggers: {
+ reload: 'parallel_downloads debug download_timeout allowed_domain blocked_domain allowed_url blocked_url dns config_update_enabled config_update_url dnsmasq_config_file_url curl_additional_param curl_max_file_size curl_retry',
+ restart: 'compressed_cache compressed_cache_dir force_dns led force_dns_port',
+ },
+};
+pkg.service_name = pkg.name + ' ' + pkg.version;
+
+const dns_modes = {
+ 'dnsmasq.addnhosts': {
+ file: '/var/run/' + pkg.name + '/dnsmasq.addnhosts',
+ cache: '/var/run/' + pkg.name + '/dnsmasq.addnhosts.cache',
+ gzip: pkg.name + '.dnsmasq.addnhosts.gz',
+ format_filter: 's|^|127.0.0.1 |;s|$||',
+ format_filter_ipv6: 's|^|:: |;s|$||',
+ parse_filter: 's|^127.0.0.1 ||;s|^:: ||;',
+ grep_pattern_ipv4: 's|^|^127\\.0\\.0\\.1 |',
+ grep_pattern_ipv6: 's|^|^:: |',
+ },
+ 'dnsmasq.conf': {
+ file: pkg.dnsmasq_file,
+ cache: '/var/run/' + pkg.name + '/dnsmasq.conf.cache',
+ gzip: pkg.name + '.dnsmasq.conf.gz',
+ format_filter: 's|^|local=/|;s|$|/|',
+ parse_filter: 's|local=/||;s|/$||;',
+ grep_pattern: 's|^|^local=/|;s|$|/$|',
+ },
+ 'dnsmasq.ipset': {
+ file: pkg.dnsmasq_file,
+ cache: '/var/run/' + pkg.name + '/dnsmasq.ipset.cache',
+ gzip: pkg.name + '.dnsmasq.ipset.gz',
+ format_filter: 's|^|ipset=/|;s|$|/adb|',
+ parse_filter: 's|ipset=/||;s|/adb$||;',
+ grep_pattern: 's|^|^ipset=/|;s|$|/adb$|',
+ },
+ 'dnsmasq.nftset': {
+ file: pkg.dnsmasq_file,
+ cache: '/var/run/' + pkg.name + '/dnsmasq.nftset.cache',
+ gzip: pkg.name + '.dnsmasq.nftset.gz',
+ format_filter: 's|^|nftset=/|;s|$|/4#inet#fw4#adb4|',
+ format_filter_ipv6: 's|^|nftset=/|;s|$|/4#inet#fw4#adb4,6#inet#fw4#adb6|',
+ parse_filter: 's|nftset=/||;s|/4#.*$||;',
+ grep_pattern: 's|^|^nftset=/|;s|$|/4#.*$|',
+ },
+ 'dnsmasq.servers': {
+ file: '/var/run/' + pkg.name + '/dnsmasq.servers',
+ cache: '/var/run/' + pkg.name + '/dnsmasq.servers.cache',
+ gzip: pkg.name + '.dnsmasq.servers.gz',
+ format_filter: 's|^|server=/|;s|$|/|',
+ parse_filter: 's|server=/||;s|/.*$||;',
+ grep_pattern: 's|^|^server=/|;s|$|/$|',
+ allow_filter: 's|(.*)|server=/\\1/#|',
+ blocked_count_filter: '\\|/#|d',
+ },
+ 'smartdns.domainset': {
+ file: '/var/run/' + pkg.name + '/smartdns.domainset',
+ cache: '/var/run/' + pkg.name + '/smartdns.domainset.cache',
+ gzip: pkg.name + '.smartdns.domainset.gz',
+ config: '/var/run/' + pkg.name + '/smartdns.domainset.conf',
+ format_filter: '',
+ parse_filter: '',
+ },
+ 'smartdns.ipset': {
+ file: '/var/run/' + pkg.name + '/smartdns.ipset',
+ cache: '/var/run/' + pkg.name + '/smartdns.ipset.cache',
+ gzip: pkg.name + '.smartdns.ipset.gz',
+ config: '/var/run/' + pkg.name + '/smartdns.ipset.conf',
+ format_filter: '',
+ parse_filter: '',
+ },
+ 'smartdns.nftset': {
+ file: '/var/run/' + pkg.name + '/smartdns.nftset',
+ cache: '/var/run/' + pkg.name + '/smartdns.nftset.cache',
+ gzip: pkg.name + '.smartdns.nftset.gz',
+ config: '/var/run/' + pkg.name + '/smartdns.nftset.conf',
+ format_filter: '',
+ parse_filter: '',
+ },
+ 'unbound.adb_list': {
+ file: '/var/lib/unbound/adb_list.' + pkg.name,
+ cache: '/var/run/' + pkg.name + '/unbound.cache',
+ gzip: pkg.name + '.unbound.gz',
+ format_filter: 's|^|local-zone: "|;s|$|." always_nxdomain|',
+ parse_filter: 's|^local-zone: "||;s|." always_nxdomain$||;',
+ },
+};
+
+const tmp = {
+ allowed: '/var/' + pkg.name + '.allowed.tmp',
+ a: '/var/' + pkg.name + '.a.tmp',
+ b: '/var/' + pkg.name + '.b.tmp',
+ sed: '/var/' + pkg.name + '.sed.tmp',
+};
+
+const list_formats = {
+ adblockplus: {
+ first_line: '[Adblock Plus]',
+ detect: "'^||'",
+ filter: "/^#/d;/^!/d;s/[[:space:]]*#.*$//;s/^||//;s/\\^$//;s/[[:space:]]*$//;s/[[:cntrl:]]$//;/[[:space:]]/d;/[`~!@#\\$%\\^&\\*()=+;:\"',<>?/\\|[{}]/d;/]/d;/\\./!d;/^$/d;/[^[:alnum:]_.-]/d;",
+ },
+ dnsmasq: {
+ detect: "'^server='",
+ filter: "\\|^server=/[[:alnum:]_.-].*/|!d;s|server=/||;s|/.*$||",
+ },
+ dnsmasq2: {
+ detect: "'^local='",
+ filter: "\\|^local=/[[:alnum:]_.-].*/|!d;s|local=/||;s|/.*$||",
+ },
+ dnsmasq3: {
+ detect: "'^address='",
+ filter: "\\|^address=/[[:alnum:]_.-].*/|!d;s|address=/||;s|/.*$||",
+ },
+ hosts: {
+ detect: "-e '^0\\.0\\.0\\.0\\s' -e '^127\\.0\\.0\\.1\\s'",
+ filter: "/localhost/d;/^#/d;/^[^0-9]/d;s/^0\\.0\\.0\\.0.//;s/^127\\.0\\.0\\.1.//;s/[[:space:]]*#.*$//;s/[[:cntrl:]]$//;s/[[:space:]]//g;/[`~!@#\\$%\\^&\\*()=+;:\"',<>?/\\|[{}]/d;/]/d;/\\./!d;/^$/d;/[^[:alnum:]_.-]/d;",
+ },
+ domains: {
+ filter: "/^#/d;s/[[:space:]]*#.*//;s/[[:space:]]*$//;s/[[:cntrl:]]$//;/^[[:space:]]*$/d;/[[:space:]]/d;/^-/d;/^\\./d;/\\.\\./d;/-$/d;/\\.$/d;/^[0-9.]*$/d;/^[^[:alnum:]]/d;/[`~!@#\\$%\\^&\\*()=+;:\"',<>?/\\|{}]/d;/\\./!d",
+ },
+};
+
+const sym = {
+ dot: ['.', '[w]'],
+ ok: ['\033[0;32m✓\033[0m', '\033[0;32m[✓]\033[0m'],
+ fail: ['\033[0;31m✗\033[0m', '\033[0;31m[✗]\033[0m'],
+ warn: ['\033[0;33m✔\033[0m', '\033[0;33m[✔]\033[0m'],
+ ERR: '\033[0;31m[ERROR]\033[0m',
+ WARN: '\033[0;33m[WARN]\033[0m',
+};
+
+const canary = {
+ mozilla: 'use-application-dns.net',
+ icloud: 'mask.icloud.com mask-h2.icloud.com',
+};
+
+// ── Mutable Module State ────────────────────────────────────────────
+
+let state = {
+ script_name: pkg.name,
+ is_tty: false,
+ output_queue: '',
+ fw4_restart: false,
+};
+
+// ── Environment (platform capabilities, cached detection) ───────────
+
+let env = {
+ // Platform capabilities (set by env.detect())
+ dnsmasq_installed: false,
+ dnsmasq_features: '',
+ smartdns_installed: false,
+ unbound_installed: false,
+ ipset_supported: false,
+ nft_installed: false,
+ awk_cmd: 'awk',
+
+ // Resolver service info (cached)
+ dnsmasq_ubus: null,
+
+ // Downloader (set lazily by env.get_downloader())
+ _dl_cache: null,
+
+ // Guard flags
+ _detected: false,
+ _config_loaded: false,
+ _loaded: false,
+};
+
+let dns_output = {
+ allow_filter: '',
+ blocked_count_filter: '',
+ filter: '',
+ filter_ipv6: '',
+ file: '',
+ gzip: '',
+ cache: '',
+ config: '',
+ parse_filter: '',
+};
+
+// Config values loaded by env.load_config()
+let cfg = {};
+
+// ── Shell / System Helpers ──────────────────────────────────────────
+
+function shell_quote(s) {
+ return "'" + replace('' + s, "'", "'\\''") + "'";
+}
+
+function cmd_output(c) {
+ let p = popen(c, 'r');
+ if (!p) return '';
+ let data = p.read('all') || '';
+ p.close();
+ return trim(data);
+}
+
+function cmd_rc(c) {
+ return system(c + ' >/dev/null 2>&1');
+}
+
+function ensure_trailing_newline(file) {
+ let fh = open(file, 'r+');
+ if (!fh) return;
+ if (fh.seek(-1, 2) && fh.read(1) != '\n')
+ fh.write('\n');
+ fh.close();
+}
+
+function mkdir_p(path) {
+ if (!path || stat(path)?.type == 'directory') return true;
+ let parent = dirname(path);
+ if (parent && parent != path) mkdir_p(parent);
+ return mkdir(path) != null;
+}
+
+function is_present(cmd) {
+ if (index(cmd, '/') >= 0)
+ return access(cmd, 'x') == true;
+ for (let dir in ['/usr/sbin', '/usr/bin', '/sbin', '/bin'])
+ if (access(dir + '/' + cmd, 'x') == true) return true;
+ return false;
+}
+
+function is_integer(v) {
+ if (v == null || v == '') return false;
+ if (!match('' + v, /^[0-9]+$/)) return false;
+ let n = int(v);
+ return n >= 1 && n <= 65535;
+}
+
+function is_https_url(url) {
+ return substr('' + url, 0, 8) == 'https://';
+}
+
+function sanitize_domain(d) {
+ d = replace('' + d, /^[a-z]+:\/\//, '');
+ d = replace(d, /\/.*$/, '');
+ d = replace(d, /:.*$/, '');
+ return d;
+}
+
+function sanitize_dir(d) {
+ let r = realpath(d);
+ if (r && stat(r)?.type == 'directory') return r;
+ return null;
+}
+
+function str_contains_word(haystack, needle) {
+ if (!haystack || !needle) return false;
+ return index(split('' + haystack, /\s+/), needle) >= 0;
+}
+
+// ── Environment Detection ───────────────────────────────────────────
+
+env.detect = function() {
+ if (env._detected) return;
+ env.dnsmasq_installed = is_present('dnsmasq');
+ env.smartdns_installed = is_present('smartdns');
+ env.unbound_installed = is_present('unbound');
+ env.nft_installed = is_present('nft');
+ env.ipset_supported = is_present('ipset') && cmd_rc('/usr/sbin/ipset help hash:net') == 0;
+ if (is_present('gawk')) env.awk_cmd = 'gawk';
+ if (env.dnsmasq_installed && !env.dnsmasq_features) {
+ let raw = cmd_output('dnsmasq --version');
+ let m = match(raw, /Compile time options:(.+)/);
+ env.dnsmasq_features = (m ? m[1] : '') + ' ';
+ }
+ env._detected = true;
+};
+
+env.get_downloader = function() {
+ if (env._dl_cache) return env._dl_cache;
+ let command, flag, ssl_supported;
+ if (is_present('curl')) {
+ command = 'curl -f --silent --insecure';
+ if (cfg.curl_additional_param) command += ' ' + cfg.curl_additional_param;
+ if (cfg.curl_max_file_size) command += ' --max-filesize ' + cfg.curl_max_file_size;
+ if (cfg.curl_retry) command += ' --retry ' + cfg.curl_retry;
+ if (cfg.download_timeout) command += ' --connect-timeout ' + cfg.download_timeout;
+ flag = '-o';
+ } else if (is_present('/usr/libexec/wget-ssl')) {
+ command = '/usr/libexec/wget-ssl --no-check-certificate -q';
+ if (cfg.download_timeout) command += ' --timeout ' + cfg.download_timeout;
+ flag = '-O';
+ } else if (is_present('wget') && cmd_rc("wget --version 2>/dev/null | grep -q '+https'") == 0) {
+ command = 'wget --no-check-certificate -q';
+ if (cfg.download_timeout) command += ' --timeout ' + cfg.download_timeout;
+ flag = '-O';
+ } else {
+ command = 'uclient-fetch --no-check-certificate -q';
+ if (cfg.download_timeout) command += ' --timeout ' + cfg.download_timeout;
+ flag = '-O';
+ }
+ ssl_supported = cmd_rc("curl --version 2>/dev/null | grep -q 'Protocols: .*https.*'") == 0 ||
+ cmd_rc("wget --version 2>/dev/null | grep -q '+ssl'") == 0;
+ env._dl_cache = { command, flag, ssl_supported };
+ return env._dl_cache;
+};
+
+// ── Shell Command Wrappers ──────────────────────────────────────────
+
+function sed_filter(expr, input, output) {
+ return system(sprintf('sed %s %s > %s',
+ shell_quote(expr), shell_quote(input), shell_quote(output))) == 0;
+}
+
+function sed_inplace(expr, file) {
+ return system(sprintf('sed -i %s %s',
+ shell_quote(expr), shell_quote(file))) == 0;
+}
+
+function sed_script(script, input, output) {
+ return system(sprintf('sed -E -f %s %s > %s',
+ shell_quote(script), shell_quote(input), shell_quote(output))) == 0;
+}
+
+function sort_file(input, output, unique) {
+ return system(sprintf('sort %s%s > %s',
+ unique ? '-u ' : '', shell_quote(input), shell_quote(output))) == 0;
+}
+
+function gzip_test(file) {
+ return system(sprintf('gzip -t -c %s >/dev/null 2>/dev/null',
+ shell_quote(file))) == 0;
+}
+
+function gzip_compress(input, output) {
+ return system(sprintf('gzip < %s > %s',
+ shell_quote(input), shell_quote(output))) == 0;
+}
+
+function gzip_decompress(input, output) {
+ return system(sprintf('gzip -dc < %s > %s',
+ shell_quote(input), shell_quote(output))) == 0;
+}
+
+function grep_test(pattern, file, flags) {
+ return cmd_rc(sprintf('grep %s %s %s',
+ flags || '-q', shell_quote(pattern), shell_quote(file))) == 0;
+}
+
+function grep_count(pattern, file, flags) {
+ return int(trim(cmd_output(sprintf('grep %s %s %s',
+ flags || '-c', shell_quote(pattern), shell_quote(file)))) || '0');
+}
+
+function grep_output(pattern, file, flags) {
+ return cmd_output(sprintf('grep %s %s %s',
+ flags || '', shell_quote(pattern), shell_quote(file)));
+}
+
+function grep_exclude_file(patfile, input, output) {
+ return system(sprintf('grep -vFf %s %s > %s 2>/dev/null',
+ shell_quote(patfile), shell_quote(input), shell_quote(output))) == 0;
+}
+
+function count_lines(file, filter_expr) {
+ if (filter_expr)
+ return int(trim(cmd_output(sprintf('sed %s %s | wc -l',
+ shell_quote(filter_expr), shell_quote(file)))) || '0');
+ return int(trim(cmd_output('wc -l < ' + shell_quote(file))) || '0');
+}
+
+function awk_reverse_labels(input, output) {
+ return system(sprintf("%s -F '.' '{for(i=NF;i>0;i--) printf \"%%s%%s\", $i, (i>1?\".\":\"\\n\")}' %s > %s",
+ env.awk_cmd, shell_quote(input), shell_quote(output))) == 0;
+}
+
+function awk_dedup_subdomains(input, output) {
+ return system(sprintf("%s 'NR==1{prev=$0;print;next}{len=length(prev);if(substr($0,1,len)==prev && substr($0,len+1,1)==\".\") next;print;prev=$0}' %s > %s",
+ env.awk_cmd, shell_quote(input), shell_quote(output))) == 0;
+}
+
+function download(url, dest) {
+ let dlt = env.get_downloader();
+ return system(sprintf('%s %s %s %s 2>/dev/null',
+ dlt.command, shell_quote(url), dlt.flag, shell_quote(dest))) == 0;
+}
+
+function service_restart(name) {
+ return system(sprintf('/etc/init.d/%s restart >/dev/null 2>&1', name)) == 0;
+}
+
+function service_enabled(name) {
+ return system(sprintf('/etc/init.d/%s enabled >/dev/null 2>&1', name)) == 0;
+}
+
+// ── Memory / System Info ────────────────────────────────────────────
+
+function get_mem_available() {
+ let conn = connect();
+ if (!conn) return 0;
+ let info = conn.call('system', 'info');
+ conn.disconnect();
+ if (!info) return 0;
+ let ram = info?.memory?.available || 0;
+ let swap = info?.swap?.free || 0;
+ return ram + swap;
+}
+
+function get_mem_total() {
+ let conn = connect();
+ if (!conn) return 0;
+ let info = conn.call('system', 'info');
+ conn.disconnect();
+ if (!info) return 0;
+ let ram = info?.memory?.total || 0;
+ let swap = info?.swap?.total || 0;
+ return ram + swap;
+}
+
+function led_on(l) {
+ if (l && stat(l + '/trigger'))
+ writefile(l + '/trigger', 'default-on\n');
+}
+
+function led_off(l) {
+ if (l && stat(l + '/trigger'))
+ writefile(l + '/trigger', 'none\n');
+}
+
+function logger(msg) {
+ system('/usr/bin/logger -t ' + shell_quote(state.script_name) + ' ' + shell_quote(msg));
+}
+
+function logger_debug(msg) {
+ if (cfg.debug_performance)
+ system('/usr/bin/logger -t ' + shell_quote(state.script_name) + ' ' + shell_quote(msg));
+}
+
+// ── Output Management ───────────────────────────────────────────────
+
+let _write = function(level, ...args) {
+ if (!cfg.verbosity)
+ cfg.verbosity = int(uci(pkg.name).get(pkg.name, 'config', 'verbosity') || '1');
+ let msg = join('', args);
+ if (level != null && (cfg.verbosity & level) == 0) return;
+
+ // Print to stderr (terminal)
+ if (state.is_tty)
+ warn(replace(msg, /\\n/g, '\n'));
+
+ // Queue for logger: flush on newline
+ if (index(msg, '\\n') >= 0 || index(msg, '\n') >= 0) {
+ msg = state.output_queue + msg;
+ state.output_queue = '';
+ let clean = replace(msg, /\x1b\[[0-9;]*m/g, '');
+ clean = replace(clean, /\\n/g, '\n');
+ clean = trim(clean);
+ if (clean != '')
+ system('/usr/bin/logger -t ' + shell_quote(state.script_name) + ' ' + shell_quote(clean));
+ } else {
+ state.output_queue += msg;
+ }
+};
+
+let output = {
+ _write: _write,
+ info: function(...args) { _write(1, ...args); },
+ verbose: function(...args) { _write(2, ...args); },
+ print: function(...args) { _write(null, ...args); },
+ ok: function() { _write(1, sym.ok[0]); _write(2, sym.ok[1] + '\\n'); },
+ okn: function() { _write(1, sym.ok[0] + '\\n'); _write(2, sym.ok[1] + '\\n'); },
+ fail: function() { _write(1, sym.fail[0]); _write(2, sym.fail[1] + '\\n'); },
+ failn: function() { _write(1, sym.fail[0] + '\\n'); _write(2, sym.fail[1] + '\\n'); },
+ warn: function() { _write(1, sym.warn[0]); _write(2, sym.warn[1] + '\\n'); },
+ warnn: function() { _write(1, sym.warn[0] + '\\n'); _write(2, sym.warn[1] + '\\n'); },
+ dot: function() { _write(1, sym.dot[0]); _write(2, sym.dot[1]); },
+ dns: function(msg) {
+ if (!cfg.dns) return;
+ let d = '' + cfg.dns;
+ if (index(d, 'dnsmasq.') == 0) _write(2, '[DNSM] ' + msg);
+ else if (index(d, 'smartdns.') == 0) _write(2, '[SMRT] ' + msg);
+ else if (index(d, 'unbound.') == 0) _write(2, '[UNBD] ' + msg);
+ },
+ error: function(msg) { _write(null, sym.ERR + ' ' + msg + '!\\n'); },
+ warning: function(msg) { _write(null, sym.WARN + ' ' + msg + '!\\n'); },
+};
+
+// ── UCI Cursor ──────────────────────────────────────────────────────
+
+let _cursor = null;
+let _cursor_loaded = {};
+
+function uci(config, reload) {
+ if (!_cursor) _cursor = cursor();
+ if (!_cursor_loaded[config] || reload) {
+ _cursor.load(config);
+ _cursor_loaded[config] = true;
+ }
+ return _cursor;
+}
+
+function uci_has_changes(config) {
+ return length(uci(config).changes(config) || []) > 0;
+}
+
+function uci_list_add_if_new(config, section, option, value) {
+ if (!config || !section || !option || !value) return false;
+ let ctx = uci(config);
+ let current = ctx.get(config, section, option);
+ if (type(current) == 'array' && index(current, value) >= 0) return true;
+ if (current == value) return true;
+ ctx.list_append(config, section, option, value);
+ ctx.save(config);
+ return true;
+}
+
+// ── Status Data ─────────────────────────────────────────────────────
+
+let status_data = {
+ status: '',
+ message: '',
+ stats: '',
+ errors: [],
+ warnings: [],
+};
+
+function _load_status_from_ubus() {
+ let conn = connect();
+ if (!conn) return;
+ let svc = conn.call('service', 'list', { name: pkg.name });
+ conn.disconnect();
+ let data = svc?.[pkg.name]?.data;
+ if (!data) return;
+ status_data.status = data.status || '';
+ status_data.message = data.message || '';
+ status_data.stats = data.stats || '';
+ status_data.errors = data.errors || [];
+ status_data.warnings = data.warnings || [];
+}
+
+function _update_ubus_status() {
+ let conn = connect();
+ if (!conn) return;
+ let svc = conn.call('service', 'list', { name: pkg.name });
+ let data = svc?.[pkg.name]?.data;
+ if (!data) { conn.disconnect(); return; }
+ data.status = status_data.status;
+ data.message = status_data.message;
+ data.stats = status_data.stats;
+ data.errors = [];
+ for (let e in status_data.errors)
+ push(data.errors, { code: e.code, info: e.info });
+ data.warnings = [];
+ for (let e in status_data.warnings)
+ push(data.warnings, { code: e.code, info: e.info });
+ conn.call('service', 'set_data', { name: pkg.name, data: data });
+ conn.disconnect();
+}
+
+function _status_reset() {
+ status_data.status = '';
+ status_data.message = '';
+ status_data.stats = '';
+ status_data.errors = [];
+ status_data.warnings = [];
+}
+
+// ── get_text ────────────────────────────────────────────────────────
+
+function get_text(r, ...args) {
+ let a = args[0] || '';
+ switch (r) {
+ case 'errorConfigValidationFail': return sprintf("The %s config validation failed", pkg.name);
+ case 'errorServiceDisabled': return sprintf("The %s is currently disabled", pkg.name);
+ case 'errorNoDnsmasqIpset': return sprintf("The dnsmasq ipset support is enabled in %s, but dnsmasq is either not installed or installed dnsmasq does not support ipset", pkg.name);
+ case 'errorNoIpset': return sprintf("The dnsmasq ipset support is enabled in %s, but ipset is either not installed or installed ipset does not support 'hash:net' type", pkg.name);
+ case 'errorNoDnsmasqNftset': return sprintf("The dnsmasq nft set support is enabled in %s, but dnsmasq is either not installed or installed dnsmasq does not support nft set", pkg.name);
+ case 'errorNoNft': return sprintf("The dnsmasq nft sets support is enabled in %s, but nft is not installed", pkg.name);
+ case 'errorNoWanGateway': return sprintf("The %s failed to discover WAN gateway", pkg.service_name);
+ case 'errorOutputDirCreate': return sprintf("Failed to create directory for %s file", a);
+ case 'errorOutputFileCreate': return sprintf("Failed to create %s file", a);
+ case 'errorFailDNSReload': return "Failed to restart/reload DNS resolver";
+ case 'errorSharedMemory': return "Failed to access shared memory";
+ case 'errorSorting': return "Failed to sort data file";
+ case 'errorOptimization': return "Failed to optimize data file";
+ case 'errorAllowListProcessing': return "Failed to process allow-list";
+ case 'errorDataFileFormatting': return "Failed to format data file";
+ case 'errorCopyingDataFile': return sprintf("Failed to copy data file to '%s'", a);
+ case 'errorMovingDataFile': return sprintf("Failed to move data file to '%s'", a);
+ case 'errorCreatingCompressedCache': return "Failed to create compressed cache";
+ case 'errorRemovingTempFiles': return "Failed to remove temporary files";
+ case 'errorRestoreCompressedCache': return "Failed to unpack compressed cache";
+ case 'errorRestoreCache': return sprintf("Failed to move '%s' to '%s'", dns_output.cache, dns_output.file);
+ case 'errorOhSnap': return "Failed to create block-list or restart DNS resolver";
+ case 'errorStopping': return sprintf("Failed to stop %s", pkg.service_name);
+ case 'errorDNSReload': return "Failed to reload/restart DNS resolver";
+ case 'errorDownloadingConfigUpdate': return "Failed to download Config Update file";
+ case 'errorDownloadingList': return sprintf("Failed to download %s", a);
+ case 'errorParsingConfigUpdate': return "Failed to parse Config Update file";
+ case 'errorParsingList': return "Failed to parse";
+ case 'errorNoSSLSupport': return "No HTTPS/SSL support on device";
+ case 'errorCreatingDirectory': return "Failed to create output/cache/gzip file directory";
+ case 'errorDetectingFileType': return "Failed to detect format";
+ case 'errorNothingToDo': return "No blocked list URLs nor blocked-domains enabled";
+ case 'errorTooLittleRam': return sprintf("Free ram (%s) is not enough to process all enabled block-lists", a);
+ case 'errorCreatingBackupFile': return sprintf("Failed to create backup file %s", a);
+ case 'errorDeletingDataFile': return sprintf("Failed to delete data file %s", a);
+ case 'errorRestoringBackupFile': return sprintf("Failed to restore backup file %s", a);
+ case 'errorNoOutputFile': return sprintf("Failed to create final block-list %s", a);
+ case 'errorNoHeartbeat': return "Heartbeat domain is not accessible after resolver restart";
+ case 'statusNoInstall': return sprintf("The %s is not installed or not found", pkg.service_name);
+ case 'statusStopped': return "stopped";
+ case 'statusStarting': return "starting";
+ case 'statusRestarting': return "restarting";
+ case 'statusForceReloading': return "force-reloading";
+ case 'statusDownloading': return "downloading";
+ case 'statusProcessing': return "processing";
+ case 'statusFail': return "failed to start";
+ case 'statusSuccess': return "success";
+ case 'statusTriggerBootWait': return "waiting for trigger (on_boot)";
+ case 'statusTriggerStartWait': return "waiting for trigger (on_start)";
+ case 'warningExternalDnsmasqConfig': return "Use of external dnsmasq config file detected, please set 'dns' option to 'dnsmasq.conf'";
+ case 'warningMissingRecommendedPackages': return "Some recommended packages are missing";
+ case 'warningInvalidCompressedCacheDir': return sprintf("Invalid compressed cache directory '%s'", a);
+ case 'warningFreeRamCheckFail': return "Can't detect free RAM";
+ case 'warningSanityCheckTLD': return sprintf("Sanity check discovered TLDs in %s", a);
+ case 'warningSanityCheckLeadingDot': return sprintf("Sanity check discovered leading dots in %s", a);
+ case 'warningInvalidDomainsRemoved': return sprintf("Removed %s invalid domain entries from block-list (domains starting with -/./numbers or containing invalid patterns)", a);
+ default: return sprintf("Unknown error/warning '%s'", a);
+ }
+}
+
+// ── Resolver Checks (env methods) ───────────────────────────────────
+
+env.check_dnsmasq = function() { env.detect(); return env.dnsmasq_installed; };
+env.check_smartdns = function() { env.detect(); return env.smartdns_installed; };
+env.check_unbound = function() { env.detect(); return env.unbound_installed; };
+env.check_ipset = function() { env.detect(); return env.ipset_supported; };
+env.check_nft = function() { env.detect(); return env.nft_installed; };
+
+env.check_dnsmasq_feature = function(feat) {
+ env.detect();
+ switch (feat) {
+ case 'idn': return index(env.dnsmasq_features, ' IDN ') >= 0;
+ case 'ipset': return index(env.dnsmasq_features, ' ipset ') >= 0;
+ case 'nftset': return index(env.dnsmasq_features, ' nftset ') >= 0;
+ }
+ return false;
+};
+
+env.check_dnsmasq_ipset = function() { return env.check_ipset() && env.check_dnsmasq_feature('ipset'); };
+env.check_dnsmasq_nftset = function() { return env.check_nft() && env.check_dnsmasq_feature('nftset'); };
+
+// ── Port/Firewall Helpers ───────────────────────────────────────────
+
+function is_port_listening(port) {
+ if (!is_integer(port)) return false;
+ let port_hex = sprintf('%04X', int(port));
+ for (let path in ['/proc/net/tcp', '/proc/net/tcp6']) {
+ let lines = split(readfile(path) || '', '\n');
+ for (let i = 1; i < length(lines); i++) {
+ let fields = split(trim(lines[i]), /\s+/);
+ if (length(fields) < 4) continue;
+ if (uc(split(fields[1], ':')[1]) == port_hex && fields[3] == '0A')
+ return true;
+ }
+ }
+ for (let path in ['/proc/net/udp', '/proc/net/udp6']) {
+ let lines = split(readfile(path) || '', '\n');
+ for (let i = 1; i < length(lines); i++) {
+ let fields = split(trim(lines[i]), /\s+/);
+ if (length(fields) < 2) continue;
+ if (uc(split(fields[1], ':')[1]) == port_hex)
+ return true;
+ }
+ }
+ return false;
+}
+
+function is_fw4_restart_needed() {
+ if (state.fw4_restart) return true;
+ let d = (uci(pkg.name).get(pkg.name, 'config', 'dns') ?? 'dnsmasq.servers');
+ let fd = (uci(pkg.name).get(pkg.name, 'config', 'force_dns') ?? '1');
+ if (fd == '1') return true;
+ if (d == 'dnsmasq.ipset' || d == 'dnsmasq.nftset' ||
+ d == 'smartdns.ipset' || d == 'smartdns.nftset') return true;
+ return false;
+}
+
+// ── File Size Helpers ───────────────────────────────────────────────
+
+function get_local_filesize(file) {
+ let s = stat(file);
+ return s ? s.size : null;
+}
+
+function get_url_filesize(url) { // ucode-lsp disable
+ if (!url) return null;
+ let size = '';
+ if (is_present('curl')) {
+ size = cmd_output(sprintf("curl --silent --insecure --fail --head --request GET --connect-timeout 2 %s | awk -F': ' '{IGNORECASE=1}/content-length/ {gsub(/\\r/, \"\"); print $2}'", shell_quote(url)));
+ }
+ if (!size && is_present('uclient-fetch')) {
+ size = cmd_output(sprintf("uclient-fetch --spider --timeout 2 %s -O /dev/null 2>&1 | sed -n '/^Download/ s/.*\\(\\([0-9]*\\) bytes\\).*/\\1/p'", shell_quote(url)));
+ }
+ return size ? size : null;
+}
+
+// ── count_blocked_domains ───────────────────────────────────────────
+
+function count_blocked_domains() {
+ if (!dns_output.file || !stat(dns_output.file)) return '0';
+ if (dns_output.blocked_count_filter)
+ return '' + count_lines(dns_output.file, dns_output.blocked_count_filter);
+ return '' + count_lines(dns_output.file);
+}
+
+// ── DNS Output Values ───────────────────────────────────────────────
+
+env.dns_set_output_values = function(d) {
+ let dc = dns_modes[d];
+ if (!dc) return;
+ dns_output.file = dc.file;
+ dns_output.cache = dc.cache;
+ dns_output.gzip = cfg.compressed_cache_dir + '/' + dc.gzip;
+ dns_output.parse_filter = dc.parse_filter;
+ dns_output.config = dc.config || '';
+ dns_output.allow_filter = dc.allow_filter || '';
+ dns_output.blocked_count_filter = dc.blocked_count_filter || '';
+ dns_output.filter_ipv6 = '';
+ if (d == 'dnsmasq.nftset' && cfg.ipv6_enabled && dc.format_filter_ipv6)
+ dns_output.filter = dc.format_filter_ipv6;
+ else
+ dns_output.filter = dc.format_filter;
+ if (d == 'dnsmasq.addnhosts' && cfg.ipv6_enabled && dc.format_filter_ipv6)
+ dns_output.filter_ipv6 = dc.format_filter_ipv6;
+};
+
+// ── adb_file ────────────────────────────────────────────────────────
+
+function adb_file(action) {
+ switch (action) {
+ case 'create':
+ case 'backup':
+ if (stat(dns_output.file)?.size > 0)
+ return rename(dns_output.file, dns_output.cache) == true;
+ return false;
+ case 'restore':
+ case 'use':
+ if (stat(dns_output.cache)?.size > 0)
+ return rename(dns_output.cache, dns_output.file) == true;
+ return false;
+ case 'test':
+ case 'test_file':
+ return (stat(dns_output.file)?.size > 0);
+ case 'test_cache':
+ return (stat(dns_output.cache)?.size > 0);
+ case 'test_gzip':
+ return (stat(dns_output.gzip)?.size > 0) && gzip_test(dns_output.gzip);
+ case 'create_gzip':
+ if (!(stat(dns_output.file)?.size > 0)) return false;
+ unlink(dns_output.gzip);
+ // Write temp file in same directory as destination to avoid cross-filesystem rename
+ let gz_tmp = dns_output.gzip + '.tmp';
+ if (gzip_compress(dns_output.file, gz_tmp)) {
+ if (rename(gz_tmp, dns_output.gzip)) {
+ return true;
+ }
+ unlink(gz_tmp);
+ }
+ return false;
+ case 'expand':
+ case 'unpack':
+ case 'unpack_gzip':
+ if (stat(dns_output.gzip)?.size > 0)
+ return gzip_decompress(dns_output.gzip, dns_output.cache);
+ return false;
+ case 'remove_cache':
+ unlink(dns_output.cache);
+ return true;
+ case 'remove_gzip':
+ unlink(dns_output.gzip);
+ return true;
+ }
+ return false;
+}
+
+// ── Declarative Config Schema ───────────────────────────────────────
+// Each entry: [type, default] — mirrors the shell load_validate_config() spec.
+
+const config_schema = { // ucode-lsp disable
+ // Booleans
+ allow_non_ascii: ['bool', false],
+ canary_domains_icloud: ['bool', false],
+ canary_domains_mozilla: ['bool', false],
+ compressed_cache: ['bool', false],
+ config_update_enabled: ['bool', false],
+ debug_init_script: ['bool', false],
+ debug_performance: ['bool', false],
+ dnsmasq_sanity_check: ['bool', true],
+ dnsmasq_validity_check: ['bool', false],
+ enabled: ['bool', false],
+ force_dns: ['bool', true],
+ ipv6_enabled: ['bool', false],
+ parallel_downloads: ['bool', true],
+ procd_trigger_wan6: ['bool', false],
+ update_config_sizes: ['bool', true],
+ // Strings
+ config_update_url: ['string', 'https://cdn.jsdelivr.net/gh/openwrt/packages/net/adblock-fast/files/adblock-fast.config.update'],
+ curl_additional_param: ['string'],
+ curl_max_file_size: ['string'],
+ curl_retry: ['string', '3'],
+ dns: ['string', 'dnsmasq.servers'],
+ dnsmasq_config_file_url: ['string'],
+ download_timeout: ['string', '20'],
+ heartbeat_sleep_timeout: ['string', '10'],
+ led: ['string'],
+ pause_timeout: ['string', '20'],
+ procd_boot_wan_timeout: ['string', '60'],
+ // Integers
+ verbosity: ['int', 2],
+ // Lists
+ allowed_domain: ['list'],
+ blocked_domain: ['list'],
+ dnsmasq_instance: ['list', '*'],
+ force_dns_interface: ['list', 'lan'],
+ force_dns_port: ['list', '53 853'],
+ smartdns_instance: ['list', '*'],
+ // Domain (sanitized, '-' means disabled)
+ heartbeat_domain: ['domain', 'heartbeat.melmac.ca'],
+ // Directory (validated via realpath)
+ compressed_cache_dir: ['dir', '/etc'],
+};
+
+// ── parse_options ───────────────────────────────────────────────────
+
+function parse_options(raw, schema) { // ucode-lsp disable
+ let result = {};
+ for (let key in schema) {
+ let spec = schema[key];
+ let v = raw[key];
+ switch (spec[0]) {
+ case 'bool':
+ result[key] = (v == null) ? spec[1] : (v == '1' || v == 'yes' || v == 'on' || v == 'true');
+ break;
+ case 'string':
+ result[key] = (v == null) ? (spec[1] ?? null) : '' + v;
+ break;
+ case 'int':
+ result[key] = (v == null) ? (spec[1] ?? 0) : int(v);
+ break;
+ case 'list':
+ if (v == null) { result[key] = spec[1] ?? null; }
+ else { result[key] = replace((type(v) == 'array') ? join(' ', v) : '' + v, /,/g, ' '); }
+ break;
+ case 'domain':
+ if (v == null || v == '-') result[key] = spec[1] ?? null;
+ else result[key] = sanitize_domain('' + v) || spec[1] || null;
+ break;
+ case 'dir':
+ let d = sanitize_dir('' + (v ?? spec[1] ?? ''));
+ result[key] = (d == '/') ? '' : (d || spec[1] || '');
+ break;
+ }
+ }
+ return result;
+}
+
+// ── env.load_config ─────────────────────────────────────────────────
+
+env.load_config = function() {
+ if (env._config_loaded) return;
+ state.is_tty = system('[ -t 2 ]') == 0 ? true : false;
+ let raw = uci(pkg.name, true).get_all(pkg.name, 'config') || {};
+ cfg = parse_options(raw, config_schema);
+ env.dns_set_output_values(cfg.dns);
+ env._loaded = false;
+ env._detected = false;
+ env._dl_cache = null;
+ env._config_loaded = true;
+};
+
+// ── load_dl_command ─────────────────────────────────────────────────
+
+// Thin wrapper for backward compat; real logic is in env.get_downloader()
+function load_dl_command() { env.get_downloader(); }
+
+
+// ── detect_file_type ────────────────────────────────────────────────
+
+function detect_file_type(file) {
+ let first_line = split(readfile(file) || '', '\n')[0];
+ for (let name in keys(list_formats)) {
+ let fmt = list_formats[name];
+ if (fmt.first_line && first_line == fmt.first_line) return name;
+ if (fmt.detect && cmd_rc("grep -q " + fmt.detect + " " + shell_quote(file)) == 0) return name;
+ }
+ if (list_formats.domains) {
+ let test = cmd_output(sprintf("sed %s %s 2>/dev/null | head -1", shell_quote(list_formats.domains.filter), shell_quote(file)));
+ if (test) return 'domains';
+ }
+ return null;
+}
+
+// ── adb_config_cache ────────────────────────────────────────────────
+
+function adb_config_cache(action, variable) {
+ switch (action) {
+ case 'create':
+ case 'set':
+ writefile(pkg.run_file, readfile(pkg.config_file) || '');
+ return;
+ case 'get':
+ switch (variable) {
+ case 'trigger_fw4':
+ if (stat(pkg.run_file)?.size > 0) {
+ if (is_fw4_restart_needed()) return 'true';
+ }
+ return '';
+ case 'trigger_service':
+ if (!(stat(pkg.run_file)?.size > 0)) return 'on_boot';
+ if ((readfile(pkg.config_file) || '') != (readfile(pkg.run_file) || '')) {
+ // Config changed — determine if reload or restart
+ let run_dir = dirname(pkg.run_file);
+ let cached = cursor(run_dir);
+ cached.load(pkg.name);
+ let reload_triggers = split(pkg.triggers.reload, ' ');
+ for (let t in reload_triggers) {
+ if (!t) continue;
+ if (t == 'allowed_url' || t == 'blocked_url') continue;
+ let val_current = uci(pkg.name).get(pkg.name, 'config', t);
+ let val_old = cached.get(pkg.name, 'config', t);
+ if ('' + (val_current ?? '') != '' + (val_old ?? '')) return 'download';
+ }
+ let restart_triggers = split(pkg.triggers.restart, ' ');
+ for (let t in restart_triggers) {
+ if (!t) continue;
+ let val_current = uci(pkg.name).get(pkg.name, 'config', t);
+ let val_old = cached.get(pkg.name, 'config', t);
+ if ('' + (val_current ?? '') != '' + (val_old ?? '')) return 'restart';
+ }
+ }
+ return '';
+ default: {
+ let run_dir = dirname(pkg.run_file);
+ let old_cfg = cursor(run_dir);
+ old_cfg.load(pkg.name);
+ return old_cfg.get(pkg.name, 'config', variable) ?? '';
+ }
+ }
+ }
+}
+
+// ── append_url (collect file_url sections) ──────────────────────────
+
+function append_urls() { // ucode-lsp disable
+ cfg.allowed_url = '';
+ cfg.blocked_url = '';
+ uci(pkg.name).foreach(pkg.name, 'file_url', (s) => {
+ if (s.enabled == '0') return;
+ let url = s.url;
+ if (!url) return;
+ if ((s.action || 'block') == 'allow')
+ cfg.allowed_url = (cfg.allowed_url ? cfg.allowed_url + ' ' : '') + url;
+ else
+ cfg.blocked_url = (cfg.blocked_url ? cfg.blocked_url + ' ' : '') + url;
+ });
+}
+
+// ── env.load ────────────────────────────────────────────────────────
+
+env.load = function(param, validation_result) {
+ if (env._loaded) return true;
+ env.load_config();
+
+ if (!cfg.enabled) {
+ push(status_data.errors, { code: 'errorServiceDisabled', info: '' });
+ output.error(get_text('errorServiceDisabled'));
+ output.print("Run the following commands before starting service again:\\n");
+ output.print("uci set " + pkg.name + ".config.enabled='1'; uci commit " + pkg.name + ";\\n");
+ return false;
+ }
+
+ if (validation_result && validation_result != '0') {
+ output.info(sym.fail[0] + '\\n');
+ push(status_data.errors, { code: 'errorConfigValidationFail', info: '' });
+ output.error(get_text('errorConfigValidationFail'));
+ output.print("Please check if the '" + pkg.config_file + "' contains correct values for config options.\\n");
+ return false;
+ }
+
+ // ── nested helpers ──────────────────────────────────────────────
+
+ let _check_resolver_environment = function() {
+ // Check resolver presence
+ let dns_family = split(cfg.dns, '.')[0];
+ switch (dns_family) {
+ case 'dnsmasq':
+ if (!env.check_dnsmasq()) {
+ if (param != 'quiet') {
+ push(status_data.errors, { code: 'errorDNSReload', info: '' });
+ output.error("Resolver 'dnsmasq' not found");
+ }
+ return false;
+ }
+ if (env.check_dnsmasq_feature('idn')) cfg.allow_non_ascii = false;
+ break;
+ case 'smartdns':
+ if (!env.check_smartdns()) {
+ if (param != 'quiet') {
+ push(status_data.errors, { code: 'errorDNSReload', info: '' });
+ output.error("Resolver 'smartdns' not found");
+ }
+ return false;
+ }
+ cfg.allow_non_ascii = false;
+ break;
+ case 'unbound':
+ if (!env.check_unbound()) {
+ if (param != 'quiet') {
+ push(status_data.errors, { code: 'errorDNSReload', info: '' });
+ output.error("Resolver 'unbound' not found");
+ }
+ return false;
+ }
+ cfg.allow_non_ascii = true;
+ break;
+ }
+
+ // Check specific cfg.dns mode support
+ switch (cfg.dns) {
+ case 'dnsmasq.ipset':
+ if (!env.check_dnsmasq_feature('ipset')) {
+ if (param != 'quiet') push(status_data.errors, { code: 'errorNoDnsmasqIpset', info: '' });
+ cfg.dns = 'dnsmasq.servers';
+ }
+ if (!env.check_ipset()) {
+ if (param != 'quiet') push(status_data.errors, { code: 'errorNoIpset', info: '' });
+ cfg.dns = 'dnsmasq.servers';
+ }
+ break;
+ case 'dnsmasq.nftset':
+ if (!env.check_dnsmasq_feature('nftset')) {
+ if (param != 'quiet') push(status_data.errors, { code: 'errorNoDnsmasqNftset', info: '' });
+ cfg.dns = 'dnsmasq.servers';
+ }
+ if (!env.check_nft()) {
+ if (param != 'quiet') push(status_data.errors, { code: 'errorNoNft', info: '' });
+ cfg.dns = 'dnsmasq.servers';
+ }
+ break;
+ case 'smartdns.ipset':
+ if (!env.check_ipset()) {
+ if (param != 'quiet') push(status_data.errors, { code: 'errorNoIpset', info: '' });
+ cfg.dns = 'smartdns.domainset';
+ }
+ break;
+ case 'smartdns.nftset':
+ if (!env.check_nft()) {
+ if (param != 'quiet') push(status_data.errors, { code: 'errorNoNft', info: '' });
+ cfg.dns = 'smartdns.domainset';
+ }
+ break;
+ }
+
+ if (cfg.dnsmasq_config_file_url) {
+ cfg.update_config_sizes = false;
+ if (cfg.dns != 'dnsmasq.conf') {
+ cfg.dns = 'dnsmasq.conf';
+ if (param != 'quiet')
+ push(status_data.warnings, { code: 'warningExternalDnsmasqConfig', info: '' });
+ }
+ }
+
+ // Re-sync dns_output after any cfg.dns fallback
+ env.dns_set_output_values(cfg.dns);
+
+ // Clean up files for non-active cfg.dns modes
+ for (let mode in dns_modes) {
+ if (mode == cfg.dns) continue;
+ let dc = dns_modes[mode];
+ unlink(dc.cache);
+ unlink(cfg.compressed_cache_dir + '/' + dc.gzip);
+ if (dc.file != pkg.dnsmasq_file) unlink(dc.file);
+ if (dc.config) unlink(dc.config);
+ }
+
+ return true;
+ };
+
+ let _setup_directories = function() {
+ let dirs = [pkg.run_file, dns_output.file, dns_output.cache, dns_output.gzip, dns_output.config];
+ for (let f in dirs) {
+ if (!f) continue;
+ let dir = dirname(f);
+ if (!mkdir_p(dir)) {
+ if (param != 'quiet')
+ push(status_data.errors, { code: 'errorOutputDirCreate', info: f });
+ }
+ }
+ };
+
+ let _check_recommended_packages = function() {
+ let bins = {
+ gawk: ['gawk', 'gawk'],
+ grep: ['/usr/libexec/grep-gnu', 'grep'],
+ sed: ['/usr/libexec/sed-gnu', 'sed'],
+ sort: ['/usr/libexec/sort-coreutils', 'coreutils-sort'],
+ };
+ let missing = [];
+ for (let key in bins) {
+ if (!is_present(bins[key][0])) {
+ push(status_data.warnings, { code: 'warningMissingRecommendedPackages', info: bins[key][1] });
+ push(missing, bins[key][1]);
+ }
+ }
+ if (length(missing) && param != 'quiet') {
+ output.warning(get_text('warningMissingRecommendedPackages') + ', install them by running:');
+ output.print('opkg update; opkg --force-overwrite install ' + join(' ', missing) + ';');
+ }
+ };
+
+ let _check_wan_gateway = function() {
+ let ub = connect();
+ if (!ub) return false;
+ let dump = ub.call('network.interface', 'dump');
+ ub.disconnect();
+ if (!dump?.interface) return false;
+ for (let iface in dump.interface) {
+ for (let r in (iface.route || []))
+ if (r.target == '0.0.0.0') return true;
+ }
+ return false;
+ };
+
+ // ── param-driven branches ───────────────────────────────────────
+
+ switch (param) {
+ case 'on_boot':
+ // Minimal: just config + dns_output (for cache restore)
+ break;
+
+ case 'on_start':
+ case 'download':
+ case 'reload':
+ case 'restart':
+ default:
+ // Full pipeline
+ env.detect();
+ if (!_check_resolver_environment()) return false;
+ _setup_directories();
+ _check_recommended_packages();
+ if (!_check_wan_gateway()) {
+ push(status_data.errors, { code: 'errorNoWanGateway', info: '' });
+ output.error(get_text('errorNoWanGateway'));
+ return false;
+ }
+ append_urls();
+ if (cfg.led) cfg.led = '/sys/class/leds/' + cfg.led;
+ break;
+
+ case 'quiet':
+ env.detect();
+ _check_resolver_environment();
+ break;
+
+ case 'rpcd':
+ env.detect();
+ break;
+ }
+
+ env._loaded = true;
+ return true;
+};
+
+// ── resolver ────────────────────────────────────────────────────────
+
+function _dnsmasq_instance_get_confdir(inst) {
+ // Get the UCI section name for this instance
+ let uci_name = uci('dhcp').get('dhcp', inst, '.name') || inst;
+ // Cache dnsmasq service info via ubus
+ if (!env.dnsmasq_ubus) {
+ let ub = connect();
+ if (ub) {
+ env.dnsmasq_ubus = ub.call('service', 'list', { name: 'dnsmasq' });
+ ub.disconnect();
+ }
+ }
+ // Extract the -C config file from the dnsmasq instance command line
+ let cfg_file = null;
+ let cmd_arr = env.dnsmasq_ubus?.dnsmasq?.instances?.[uci_name]?.command;
+ if (type(cmd_arr) == 'array') {
+ for (let i = 0; i < length(cmd_arr); i++)
+ if (cmd_arr[i] == '-C' && i + 1 < length(cmd_arr)) { cfg_file = cmd_arr[i + 1]; break; }
+ }
+ if (!cfg_file) return null;
+ // Parse conf-dir from the dnsmasq config file
+ let content = readfile(cfg_file) || '';
+ if (!content) return null;
+ for (let line in split(content, '\n')) {
+ let m = match(line, /^conf-dir=(.+)$/);
+ if (m) return m[1];
+ }
+ return null;
+}
+
+function _dnsmasq_instance_config(inst, param) {
+ if (!stat('/etc/config/dhcp')?.size) return;
+ let dhcp = uci('dhcp');
+ if (!dhcp.get('dhcp', inst)) return;
+ let confdir;
+ let addnhostsFile = dns_modes['dnsmasq.addnhosts'].file;
+ let confFile = dns_modes['dnsmasq.conf'].file;
+ let serversFile = dns_modes['dnsmasq.servers'].file;
+ switch (param) {
+ case 'dnsmasq.addnhosts':
+ confdir = _dnsmasq_instance_get_confdir(inst);
+ if (confdir) unlink(confdir + '/' + pkg.name);
+ dhcp.list_remove('dhcp', inst, 'addnmount', confFile);
+ if (dhcp.get('dhcp', inst, 'serversfile') == serversFile)
+ dhcp.delete('dhcp', inst, 'serversfile');
+ uci_list_add_if_new('dhcp', inst, 'addnhosts', addnhostsFile);
+ break;
+ case 'cleanup':
+ case 'unbound.adb_list':
+ confdir = _dnsmasq_instance_get_confdir(inst);
+ if (confdir) unlink(confdir + '/' + pkg.name);
+ dhcp.list_remove('dhcp', inst, 'addnhosts', addnhostsFile);
+ dhcp.list_remove('dhcp', inst, 'addnmount', confFile);
+ if (dhcp.get('dhcp', inst, 'serversfile') == serversFile)
+ dhcp.delete('dhcp', inst, 'serversfile');
+ break;
+ case 'dnsmasq.conf':
+ case 'dnsmasq.ipset':
+ case 'dnsmasq.nftset':
+ dhcp.list_remove('dhcp', inst, 'addnhosts', addnhostsFile);
+ if (dhcp.get('dhcp', inst, 'serversfile') == serversFile)
+ dhcp.delete('dhcp', inst, 'serversfile');
+ uci_list_add_if_new('dhcp', inst, 'addnmount', confFile);
+ confdir = _dnsmasq_instance_get_confdir(inst);
+ if (!confdir) { dhcp.save('dhcp'); return; }
+ unlink(confdir + '/' + pkg.name);
+ symlink(confFile, confdir + '/' + pkg.name);
+ chmod(confdir + '/' + pkg.name, 0660);
+ chown(confdir + '/' + pkg.name, 'root', 'dnsmasq');
+ break;
+ case 'dnsmasq.servers':
+ dhcp.list_remove('dhcp', inst, 'addnhosts', addnhostsFile);
+ confdir = _dnsmasq_instance_get_confdir(inst);
+ if (confdir) unlink(confdir + '/' + pkg.name);
+ dhcp.list_remove('dhcp', inst, 'addnmount', confFile);
+ if (dhcp.get('dhcp', inst, 'serversfile') != serversFile)
+ dhcp.set('dhcp', inst, 'serversfile', serversFile);
+ break;
+ }
+ dhcp.save('dhcp');
+}
+
+function _dnsmasq_instance_append_force_dns_port(inst) {
+ if (!stat('/etc/config/dhcp')?.size) return;
+ let dhcp = uci('dhcp');
+ if (!dhcp.get('dhcp', inst)) return;
+ let instance_port = dhcp.get('dhcp', inst, 'port') ?? '53';
+ if (!str_contains_word(cfg.force_dns_port, instance_port))
+ cfg.force_dns_port = (cfg.force_dns_port ? cfg.force_dns_port + ' ' : '') + instance_port;
+}
+
+function _smartdns_instance_config(inst, param) {
+ if (!stat('/etc/config/smartdns')?.size) return;
+ let sdns = uci('smartdns');
+ if (!sdns.get('smartdns', inst)) return;
+ switch (param) {
+ case 'cleanup':
+ sdns.list_remove('smartdns', inst, 'conf_files', dns_output.config);
+ sdns.save('smartdns');
+ unlink(dns_output.config);
+ break;
+ case 'smartdns.domainset':
+ writefile(dns_output.config,
+ 'domain-set -name adblock-fast -file ' + dns_output.file + '\n' +
+ 'domain-rules /domain-set:adblock-fast/ -a #\n');
+ uci_list_add_if_new('smartdns', inst, 'conf_files', dns_output.config);
+ break;
+ case 'smartdns.ipset':
+ writefile(dns_output.config,
+ 'domain-set -name adblock-fast -file ' + dns_output.file + '\n' +
+ 'domain-rules /domain-set:adblock-fast/ -ipset adb\n');
+ uci_list_add_if_new('smartdns', inst, 'conf_files', dns_output.config);
+ break;
+ case 'smartdns.nftset':
+ let nftset = '#4:inet#fw4#adb4';
+ if (cfg.ipv6_enabled) nftset += ',#6:inet#fw4#adb6';
+ writefile(dns_output.config,
+ 'domain-set -name adblock-fast -file ' + dns_output.file + '\n' +
+ 'domain-rules /domain-set:adblock-fast/ -nftset ' + nftset + '\n');
+ uci_list_add_if_new('smartdns', inst, 'conf_files', dns_output.config);
+ break;
+ }
+}
+
+function _smartdns_instance_append_force_dns_port(inst) {
+ if (!stat('/etc/config/smartdns')?.size) return;
+ let sdns = uci('smartdns');
+ if (!sdns.get('smartdns', inst)) return;
+ let instance_port = sdns.get('smartdns', inst, 'port') ?? '53';
+ if (!str_contains_word(cfg.force_dns_port, instance_port))
+ cfg.force_dns_port = (cfg.force_dns_port ? cfg.force_dns_port + ' ' : '') + instance_port;
+}
+
+function _unbound_instance_append_force_dns_port(inst) {
+ if (!stat('/etc/config/unbound')?.size) return;
+ let ubnd = uci('unbound');
+ if (!ubnd.get('unbound', inst)) return;
+ let instance_port = ubnd.get('unbound', inst, 'listen_port') ?? '53';
+ if (!str_contains_word(cfg.force_dns_port, instance_port))
+ cfg.force_dns_port = (cfg.force_dns_port ? cfg.force_dns_port + ' ' : '') + instance_port;
+}
+
+function _get_dnsmasq_instances() {
+ let result = [];
+ let dhcp_cur = cursor();
+ dhcp_cur.load('dhcp');
+ if (cfg.dnsmasq_instance == '*') {
+ dhcp_cur.foreach('dhcp', 'dnsmasq', (s) => push(result, s['.name']));
+ } else if (cfg.dnsmasq_instance) {
+ for (let inst in split('' + cfg.dnsmasq_instance, /\s+/)) {
+ if (!inst) continue;
+ // Try @dnsmasq[N] index style, resolve to section name
+ let s = dhcp_cur.get_all('dhcp', '@dnsmasq[' + inst + ']');
+ push(result, s?.['.name'] || inst);
+ }
+ }
+ return result;
+}
+
+function _get_smartdns_instances() {
+ let result = [];
+ let sdns_cur = cursor();
+ sdns_cur.load('smartdns');
+ if (cfg.smartdns_instance == '*') {
+ sdns_cur.foreach('smartdns', 'smartdns', (s) => push(result, s['.name']));
+ } else if (cfg.smartdns_instance) {
+ for (let inst in split('' + cfg.smartdns_instance, /\s+/)) {
+ if (!inst) continue;
+ let s = sdns_cur.get_all('smartdns', '@smartdns[' + inst + ']');
+ push(result, s?.['.name'] || inst);
+ }
+ }
+ return result;
+}
+
+function resolver(action) {
+ let resolver_name = split(cfg.dns, '.')[0];
+ if (!action) return true;
+
+ switch (action) {
+ case 'cleanup':
+ for (let mode in dns_modes) {
+ let dc = dns_modes[mode];
+ unlink(dc.cache);
+ unlink(cfg.compressed_cache_dir + '/' + dc.gzip);
+ if (dc.file != pkg.dnsmasq_file) unlink(dc.file);
+ if (dc.config) unlink(dc.config);
+ }
+ if (stat('/etc/config/dhcp')?.size) {
+ for (let name in _get_dnsmasq_instances())
+ _dnsmasq_instance_config(name, 'cleanup');
+ if (uci_has_changes('dhcp')) uci('dhcp').commit('dhcp');
+ }
+ if (stat('/etc/config/smartdns')?.size) {
+ for (let name in _get_smartdns_instances())
+ _smartdns_instance_config(name, 'cleanup');
+ if (uci_has_changes('smartdns')) uci('smartdns').commit('smartdns');
+ }
+ break;
+
+ case 'on_stop':
+ case 'quiet':
+ case 'quiet_restart':
+ return service_restart(resolver_name);
+
+ case 'on_start':
+ if (!adb_file('test')) {
+ status_data.status = 'statusFail';
+ push(status_data.errors, { code: 'errorOutputFileCreate', info: dns_output.file });
+ return false;
+ }
+ output.info('Cycling ' + resolver_name + ' ');
+ if (resolver('update_config') && resolver('test') && resolver('sanity') && resolver('restart') && resolver('heartbeat')) {
+ // success
+ } else {
+ resolver('revert');
+ }
+ output.info('\\n');
+ break;
+
+ case 'test':
+ switch (cfg.dns) {
+ case 'dnsmasq.addnhosts':
+ case 'dnsmasq.conf':
+ case 'dnsmasq.ipset':
+ case 'dnsmasq.nftset':
+ case 'dnsmasq.servers':
+ output.dns('Testing ' + cfg.dns + ' configuration ');
+ if (cmd_rc('dnsmasq --test') == 0) {
+ output.ok();
+ return true;
+ }
+ output.fail();
+ return false;
+ default:
+ return true;
+ }
+
+ case 'restart':
+ output.dns('Restarting ' + resolver_name + ' ');
+ status_data.message = 'Restarting ' + resolver_name;
+ if (service_restart(resolver_name)) {
+ status_data.status = 'statusSuccess';
+ led_on(cfg.led);
+ output.ok();
+ return true;
+ }
+ output.fail();
+ status_data.status = 'statusFail';
+ push(status_data.errors, { code: 'errorDNSReload', info: '' });
+ return false;
+
+ case 'sanity':
+ if (!cfg.dnsmasq_sanity_check) return true;
+ output.dns('Sanity check for ' + cfg.dns + ' TLDs ');
+ if (!grep_test('\\.|server:', dns_output.file, '-qvE')) {
+ output.ok();
+ } else {
+ push(status_data.warnings, { code: 'warningSanityCheckTLD', info: dns_output.file });
+ output.warn();
+ }
+ output.dns('Sanity check for ' + cfg.dns + ' leading dots ');
+ let dot_pattern;
+ switch (split(cfg.dns, '.')[0]) {
+ case 'dnsmasq': dot_pattern = '/\\.'; break;
+ case 'smartdns': dot_pattern = '^\\.'; break;
+ case 'unbound': dot_pattern = '"\\.'; break;
+ }
+ if (dot_pattern && !grep_test(dot_pattern, dns_output.file)) {
+ output.ok();
+ } else {
+ push(status_data.warnings, { code: 'warningSanityCheckLeadingDot', info: dns_output.file });
+ output.warn();
+ }
+ return true;
+
+ case 'heartbeat':
+ if (!cfg.heartbeat_domain) return true;
+ if (!is_integer(cfg.heartbeat_sleep_timeout)) return true;
+ output.dns('Probing ' + cfg.heartbeat_domain + ' for ' + cfg.heartbeat_sleep_timeout + ' seconds ');
+ status_data.message = 'Testing resolver on ' + cfg.heartbeat_domain;
+ let timeout = int(cfg.heartbeat_sleep_timeout);
+ for (let i = 0; i < timeout; i++) {
+ if (cmd_rc('resolveip ' + shell_quote(cfg.heartbeat_domain)) == 0) {
+ output.ok();
+ return true;
+ }
+ output.dot();
+ system('sleep 1');
+ }
+ output.fail();
+ status_data.status = 'statusFail';
+ push(status_data.errors, { code: 'errorNoHeartbeat', info: '' });
+ return false;
+
+ case 'revert':
+ output.info('Resetting/Restarting ' + resolver_name + ' ');
+ output.dns('Resetting ' + resolver_name + ' ');
+ resolver('cleanup');
+ output.ok();
+ output.dns('Restarting ' + resolver_name + ' ');
+ if (service_restart(resolver_name)) {
+ led_off(cfg.led);
+ output.ok();
+ return true;
+ }
+ output.fail();
+ status_data.status = 'statusFail';
+ push(status_data.errors, { code: 'errorDNSReload', info: '' });
+ return false;
+
+ case 'update_config':
+ output.dns('Updating ' + resolver_name + ' configuration ');
+ switch (split(cfg.dns, '.')[0]) {
+ case 'dnsmasq':
+ for (let name in _get_dnsmasq_instances()) {
+ _dnsmasq_instance_config(name, cfg.dns);
+ _dnsmasq_instance_append_force_dns_port(name);
+ }
+ if (uci_has_changes('dhcp')) uci('dhcp').commit('dhcp');
+ if (adb_file('test')) {
+ chmod(dns_output.file, 0660);
+ chown(dns_output.file, 'root', 'dnsmasq');
+ } else {
+ status_data.status = 'statusFail';
+ push(status_data.errors, { code: 'errorNoOutputFile', info: dns_output.file });
+ return false;
+ }
+ break;
+ case 'smartdns':
+ for (let name in _get_smartdns_instances()) {
+ _smartdns_instance_config(name, cfg.dns);
+ _smartdns_instance_append_force_dns_port(name);
+ }
+ if (uci_has_changes('smartdns')) uci('smartdns').commit('smartdns');
+ chmod(dns_output.file, 0660);
+ chmod(dns_output.config, 0660);
+ chown(dns_output.file, 'root', 'root');
+ chown(dns_output.config, 'root', 'root');
+ break;
+ case 'unbound':
+ let ubnd_cur = cursor();
+ ubnd_cur.load('unbound');
+ ubnd_cur.foreach('unbound', 'unbound', (s) => _unbound_instance_append_force_dns_port(s['.name']));
+ chmod(dns_output.file, 0660);
+ chown(dns_output.file, 'root', 'unbound');
+ break;
+ }
+ output.ok();
+ return true;
+ }
+ return true;
+}
+
+// ── process_file_url ────────────────────────────────────────────────
+
+function process_file_url(section, url_override, action_override) {
+ let url, file_action, name, size_val;
+
+ if (section && !url_override) {
+ let sec_cur = cursor();
+ sec_cur.load(pkg.name);
+ let en = sec_cur.get(pkg.name, section, 'enabled');
+ if (en == '0') return true;
+ url = sec_cur.get(pkg.name, section, 'url');
+ file_action = sec_cur.get(pkg.name, section, 'action') || 'block';
+ name = sec_cur.get(pkg.name, section, 'name');
+ size_val = sec_cur.get(pkg.name, section, 'size');
+ } else {
+ url = url_override;
+ file_action = action_override || 'block';
+ }
+
+ if (!cfg.enabled) return true;
+ if (!url) return false;
+
+ let label = replace(url, /^[a-z]+:\/\//, '');
+ label = replace(label, /\/.*$/, '');
+ label = name || label;
+ label = 'List: ' + label;
+
+ let type_name, d_tmp;
+ switch (file_action) {
+ case 'allow': type_name = 'Allowed'; d_tmp = tmp.allowed; break;
+ case 'block': type_name = 'Blocked'; d_tmp = tmp.b; break;
+ case 'file': type_name = 'File'; d_tmp = tmp.b; break;
+ }
+
+ if (is_https_url(url) && !env.get_downloader().ssl_supported) {
+ output.info(sym.fail[0]);
+ output.verbose('[ DL ] ' + type_name + ' ' + label + ' ' + sym.fail[1] + '\\n');
+ push(status_data.errors, { code: 'errorNoSSLSupport', info: name || url });
+ return true;
+ }
+
+ let r_tmp = trim(cmd_output('mktemp -q -t "' + pkg.name + '_tmp.XXXXXXXX"'));
+ if (!url || !download(url, r_tmp) || !(stat(r_tmp)?.size > 0)) {
+ output.info(sym.fail[0]);
+ output.verbose('[ DL ] ' + type_name + ' ' + label + ' ' + sym.fail[1] + '\\n');
+ push(status_data.errors, { code: 'errorDownloadingList', info: name || url });
+ } else {
+ // Ensure newline at end
+ ensure_trailing_newline(r_tmp);
+
+ // Update size in config if changed
+ if (section) {
+ let new_size = get_local_filesize(r_tmp);
+ if (new_size != null && ('' + size_val) != ('' + new_size))
+ uci(pkg.name).set(pkg.name, section, 'size', '' + new_size);
+ uci(pkg.name).save(pkg.name);
+ }
+
+ let format = detect_file_type(r_tmp);
+ let filter = list_formats[format]?.filter;
+ if (!filter) {
+ output.info(sym.fail[0]);
+ output.verbose('[ DL ] ' + type_name + ' ' + label + ' ' + sym.fail[1] + '\\n');
+ push(status_data.errors, { code: 'errorDetectingFileType', info: name || url });
+ unlink(r_tmp);
+ return true;
+ }
+ if (format == 'hosts')
+ sed_inplace('/# Title: StevenBlack/,/# Custom host records are listed here/d', r_tmp);
+
+ if (filter && file_action != 'file')
+ sed_inplace(filter, r_tmp);
+
+ if (!(stat(r_tmp)?.size > 0)) {
+ output.info(sym.fail[0]);
+ output.verbose('[ DL ] ' + type_name + ' ' + label + ' (' + format + ') ' + sym.fail[1] + '\\n');
+ push(status_data.errors, { code: 'errorParsingList', info: name || url });
+ } else {
+ // Ensure file ends with newline, then append to accumulator
+ ensure_trailing_newline(r_tmp);
+ let inp = open(r_tmp, 'r');
+ let out = open(d_tmp, 'a');
+ if (inp && out) {
+ let chunk;
+ while ((chunk = inp.read(65536)) && length(chunk))
+ out.write(chunk);
+ }
+ if (inp) inp.close();
+ if (out) out.close();
+ output.info(sym.ok[0]);
+ output.verbose('[ DL ] ' + type_name + ' ' + label + ' (' + format + ') ' + sym.ok[1] + '\\n');
+ }
+ }
+ unlink(r_tmp);
+ return true;
+}
+
+// ── download_dnsmasq_file ───────────────────────────────────────────
+
+function download_dnsmasq_file() {
+ status_data.message = get_text('statusDownloading') + '...';
+ status_data.status = 'statusDownloading';
+
+ for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed, dns_output.file, dns_output.cache]) unlink(f);
+ if (get_mem_available() < pkg.memory_threshold) {
+ output.print('Low free memory, restarting resolver ');
+ if (resolver('quiet_restart')) output.okn(); else output.failn();
+ }
+ for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed]) writefile(f, '');
+ output.info('Downloading dnsmasq file ');
+ process_file_url(null, cfg.dnsmasq_config_file_url, 'file');
+ output.dns('Moving dnsmasq file ');
+ if (rename(tmp.b, dns_output.file)) {
+ output.ok();
+ } else {
+ output.fail();
+ push(status_data.errors, { code: 'errorMovingDataFile', info: dns_output.file });
+ }
+ output.info('\\n');
+}
+
+// ── download_lists ──────────────────────────────────────────────────
+
+function download_lists() {
+ // RAM check
+ let free_mem = get_mem_available();
+ if (!free_mem) {
+ push(status_data.warnings, { code: 'warningFreeRamCheckFail', info: '' });
+ output.warning(get_text('warningFreeRamCheckFail'));
+ } else {
+ let total_sizes = 0;
+ uci(pkg.name).foreach(pkg.name, 'file_url', (s) => {
+ if (s.enabled == '0') return;
+ let sz = s.size;
+ if (!sz && s.url) sz = get_url_filesize(s.url);
+ if (sz) total_sizes += int('' + sz);
+ });
+ if (free_mem < total_sizes * 2) {
+ push(status_data.errors, { code: 'errorTooLittleRam', info: '' + free_mem });
+ return false;
+ }
+ }
+
+ status_data.message = get_text('statusDownloading') + '...';
+ status_data.status = 'statusDownloading';
+
+ for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed, dns_output.file, dns_output.cache, dns_output.gzip]) unlink(f);
+ if (get_mem_total() < pkg.memory_threshold) {
+ output.print('Low free memory, restarting resolver ');
+ if (resolver('quiet_restart')) output.okn(); else output.failn();
+ }
+ for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed]) writefile(f, '');
+
+ output.info('Downloading lists ');
+
+ // Process each file_url section
+ let download_cfgs = [];
+ uci(pkg.name).foreach(pkg.name, 'file_url', (s) => push(download_cfgs, s['.name']));
+
+ for (let cfg_name in download_cfgs)
+ process_file_url(cfg_name);
+
+ if (uci_has_changes(pkg.name)) {
+ output.verbose('[PROC] Saving updated file sizes ');
+ if (cfg.update_config_sizes && uci(pkg.name).commit(pkg.name))
+ output.ok();
+ else
+ output.fail();
+ }
+ output.info('\\n');
+
+ // Add canary domains and cfg.blocked_domain
+ let canaryDomains = '';
+ if (cfg.canary_domains_icloud)
+ canaryDomains += (canaryDomains ? ' ' : '') + canary.icloud;
+ if (cfg.canary_domains_mozilla)
+ canaryDomains += (canaryDomains ? ' ' : '') + canary.mozilla;
+
+ output.info('Processing downloads ');
+
+ let start_time, end_time, elapsed, step_title;
+
+ // Sort combined block-list
+ start_time = time();
+ step_title = 'Sorting combined block-list';
+ output.verbose('[PROC] ' + step_title + ' ');
+ status_data.status = 'statusProcessing';
+ status_data.message = get_text('statusProcessing') + ': ' + step_title;
+
+ // Append cfg.blocked_domain and canary domains
+ ensure_trailing_newline(tmp.b);
+ let extra_domains = '';
+ for (let hf in split((cfg.blocked_domain || '') + ' ' + canaryDomains, /\s+/)) {
+ if (hf) extra_domains += hf + '\n';
+ }
+ if (extra_domains) {
+ let fd = popen(sprintf("sed %s >> %s", shell_quote(list_formats.domains.filter), shell_quote(tmp.b)), 'w');
+ if (fd) { fd.write(extra_domains); fd.close(); }
+ }
+ sed_inplace('/^[[:space:]]*$/d', tmp.b);
+
+ if (!(stat(tmp.b)?.size > 0)) return false;
+
+ if (cfg.allow_non_ascii) {
+ if (sort_file(tmp.b, tmp.a, true))
+ output.ok();
+ else { output.fail(); push(status_data.errors, { code: 'errorSorting', info: '' }); }
+ } else {
+ if (system(sprintf("sort -u %s | grep -E -v '[^a-zA-Z0-9=/.-]' > %s", shell_quote(tmp.b), shell_quote(tmp.a))) == 0)
+ output.ok();
+ else { output.fail(); push(status_data.errors, { code: 'errorSorting', info: '' }); }
+ }
+ end_time = time();
+ elapsed = end_time - start_time;
+ logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's');
+
+ // Optimization (subdomain dedup)
+ let needs_optimization = (cfg.dns == 'dnsmasq.conf' || cfg.dns == 'dnsmasq.ipset' || cfg.dns == 'dnsmasq.nftset' ||
+ cfg.dns == 'dnsmasq.servers' || cfg.dns == 'smartdns.domainset' || cfg.dns == 'smartdns.ipset' ||
+ cfg.dns == 'smartdns.nftset' || cfg.dns == 'unbound.adb_list');
+
+ if (needs_optimization) {
+ start_time = time();
+ step_title = 'Optimizing combined block-list';
+ output.verbose('[PROC] ' + step_title + ' ');
+ status_data.message = get_text('statusProcessing') + ': ' + step_title;
+
+ let ok = awk_reverse_labels(tmp.a, tmp.b);
+ if (ok) ok = sort_file(tmp.b, tmp.a);
+ if (ok) ok = awk_dedup_subdomains(tmp.a, tmp.b);
+ if (ok) ok = awk_reverse_labels(tmp.b, tmp.a);
+ if (ok) ok = sort_file(tmp.a, tmp.b, true);
+ if (ok) { output.ok(); }
+ else {
+ output.fail();
+ push(status_data.errors, { code: 'errorOptimization', info: '' });
+ rename(tmp.a, tmp.b);
+ }
+ end_time = time();
+ elapsed = end_time - start_time;
+ logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's');
+ } else {
+ rename(tmp.a, tmp.b);
+ }
+
+ // Remove allowed domains
+ if (cfg.allowed_domain || (stat(tmp.allowed)?.size > 0)) {
+ start_time = time();
+ step_title = 'Removing allowed domains from combined block-list';
+ output.verbose('[PROC] ' + step_title + ' ');
+ status_data.message = get_text('statusProcessing') + ': ' + step_title;
+
+ let allowed_extra = '';
+ if (stat(tmp.allowed)?.size > 0)
+ allowed_extra = trim(cmd_output(sprintf("sed '/^[[:space:]]*$/d' %s", shell_quote(tmp.allowed))));
+ let all_allowed = (cfg.allowed_domain || '') + (allowed_extra ? ' ' + allowed_extra : '');
+
+ let sed_content = '';
+ for (let hf in split(all_allowed, /\s+/)) {
+ if (!hf) continue;
+ let escaped = replace(hf, /\./g, '\\.');
+ sed_content += '/(^|\\.)' + escaped + '$/d;\n';
+ }
+ if (sed_content) {
+ writefile(tmp.sed, sed_content);
+ if (sed_script(tmp.sed, tmp.b, tmp.a) && rename(tmp.a, tmp.b))
+ output.ok();
+ else { output.fail(); push(status_data.errors, { code: 'errorAllowListProcessing', info: '' }); }
+ } else {
+ output.fail();
+ push(status_data.errors, { code: 'errorAllowListProcessing', info: '' });
+ }
+ end_time = time();
+ elapsed = end_time - start_time;
+ logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's');
+ }
+
+ // Format combined block-list
+ start_time = time();
+ step_title = 'Formatting combined block-list file';
+ output.verbose('[PROC] ' + step_title + ' ');
+ status_data.message = get_text('statusProcessing') + ': ' + step_title;
+
+ if (!dns_output.filter_ipv6) {
+ if (dns_output.filter) {
+ if (sed_filter(dns_output.filter, tmp.b, tmp.a))
+ output.ok();
+ else { output.fail(); push(status_data.errors, { code: 'errorDataFileFormatting', info: '' }); }
+ } else {
+ writefile(tmp.a, readfile(tmp.b) || '');
+ output.ok();
+ }
+ } else {
+ if (cfg.dns == 'dnsmasq.addnhosts') {
+ if (sed_filter(dns_output.filter, tmp.b, tmp.a) &&
+ system(sprintf('sed %s %s >> %s', shell_quote(dns_output.filter_ipv6), shell_quote(tmp.b), shell_quote(tmp.a))) == 0)
+ output.ok();
+ else { output.fail(); push(status_data.errors, { code: 'errorDataFileFormatting', info: '' }); }
+ }
+ }
+ end_time = time();
+ elapsed = end_time - start_time;
+ logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's');
+
+ // Explicitly allow domains in servers mode
+ if (dns_output.allow_filter && cfg.allowed_domain) {
+ unlink(tmp.sed); writefile(tmp.sed, '');
+ start_time = time();
+ step_title = 'Explicitly allowing domains in ' + cfg.dns;
+ output.verbose('[PROC] ' + step_title + ' ');
+ status_data.message = get_text('statusProcessing') + ': ' + step_title;
+ let allow_input = '';
+ for (let hf in split('' + cfg.allowed_domain, /\s+/))
+ if (hf) allow_input += hf + '\n';
+ if (allow_input)
+ system(sprintf("printf '%%s' %s | sed -E '%s' >> %s", shell_quote(allow_input), dns_output.allow_filter, shell_quote(tmp.sed)));
+ if (stat(tmp.sed)?.size > 0) {
+ if (writefile(tmp.b, (readfile(tmp.sed) || '') + (readfile(tmp.a) || '')))
+ output.ok();
+ else { output.fail(); push(status_data.errors, { code: 'errorAllowListProcessing', info: '' }); }
+ } else {
+ output.fail();
+ push(status_data.errors, { code: 'errorAllowListProcessing', info: '' });
+ }
+ end_time = time();
+ elapsed = end_time - start_time;
+ logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's');
+ } else {
+ rename(tmp.a, tmp.b);
+ }
+
+ // Move to output file
+ start_time = time();
+ step_title = 'Setting up ' + cfg.dns + ' file';
+ output.verbose('[PROC] ' + step_title + ' ');
+ status_data.message = get_text('statusProcessing') + ': ' + step_title;
+
+ if (rename(tmp.b, dns_output.file)) {
+ output.ok();
+ } else {
+ output.fail();
+ push(status_data.errors, { code: 'errorMovingDataFile', info: dns_output.file });
+ }
+ if (cfg.dns == 'unbound.adb_list')
+ sed_inplace('1 i\\server:', dns_output.file);
+
+ // Validity check
+ if (cfg.dnsmasq_validity_check && index(cfg.dns, 'dnsmasq.') == 0) {
+ start_time = time();
+ step_title = 'Validating domain entries';
+ output.verbose('[PROC] ' + step_title + ' ');
+ status_data.message = get_text('statusProcessing') + ': ' + step_title;
+ let invalid_file = '/tmp/' + pkg.name + '.invalid.tmp';
+ unlink(invalid_file);
+ system(sprintf("sed '%s' %s | grep -E '^-|^\\.|^[0-9.]+$|\\.\\.|\\-$|\\.$' > %s 2>/dev/null || true",
+ dns_output.parse_filter, shell_quote(dns_output.file), shell_quote(invalid_file)));
+ let invalid_count = 0;
+ if (stat(invalid_file)?.size > 0) {
+ invalid_count = int(trim(cmd_output('wc -l < ' + shell_quote(invalid_file))) || '0');
+ if (invalid_count > 0) {
+ let dc = dns_modes[cfg.dns];
+ let grep_pat = dc ? dc.grep_pattern : null;
+ if (cfg.dns == 'dnsmasq.addnhosts' && dc) {
+ system(sprintf("{ sed '%s' %s; sed '%s' %s; } > %s.pat 2>/dev/null",
+ dc.grep_pattern_ipv4, shell_quote(invalid_file),
+ dc.grep_pattern_ipv6, shell_quote(invalid_file),
+ shell_quote(invalid_file)));
+ grep_pat = null;
+ }
+ if (grep_pat)
+ sed_filter(grep_pat, invalid_file, invalid_file + '.pat');
+ grep_exclude_file(invalid_file + '.pat', dns_output.file, dns_output.file + '.valid');
+ rename(dns_output.file + '.valid', dns_output.file);
+ logger(sprintf('Removed %d invalid entries from %s.', invalid_count, cfg.dns));
+ push(status_data.warnings, { code: 'warningInvalidDomainsRemoved', info: '' + invalid_count });
+ unlink(invalid_file + '.pat');
+ }
+ unlink(invalid_file);
+ }
+ if (invalid_count > 0) output.warn(); else output.ok();
+ end_time = time();
+ elapsed = end_time - start_time;
+ logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's');
+ }
+
+ // Remove temporary files
+ step_title = 'Removing temporary files';
+ output.verbose('[PROC] ' + step_title + ' ');
+ status_data.message = get_text('statusProcessing') + ': ' + step_title;
+ for (let f in glob('/tmp/' + pkg.name + '_tmp.*') || []) unlink(f);
+ for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed, dns_output.cache]) unlink(f);
+ output.ok();
+ output.info('\\n');
+ return true;
+}
+
+// ── adb_config_update ───────────────────────────────────────────────
+
+function adb_config_update(param) {
+ param = param || 'quiet';
+ env.load_config();
+ let label = replace('' + cfg.config_update_url, /^[a-z]+:\/\//, '');
+ label = replace(label, /\/.*$/, '');
+ if (!cfg.enabled) return;
+ if (!cfg.config_update_enabled) return;
+
+ if (param != 'download') {
+ if (adb_file('test')) return;
+ if (adb_file('test_cache')) return;
+ if (adb_file('test_gzip')) return;
+ }
+
+ output.info('Updating config ');
+ output.verbose('[ DL ] Config Update: ' + label + ' ');
+ let r_tmp = trim(cmd_output('mktemp -q -t "' + pkg.name + '_tmp.XXXXXXXX"'));
+ if (!download(cfg.config_update_url, r_tmp) || !(stat(r_tmp)?.size > 0)) {
+ output.failn();
+ push(status_data.errors, { code: 'errorDownloadingConfigUpdate', info: '' });
+ } else {
+ if (system(sprintf("sed -f %s -i %s 2>/dev/null", shell_quote(r_tmp), shell_quote(pkg.config_file))) == 0)
+ output.okn();
+ else { output.failn(); push(status_data.errors, { code: 'errorParsingConfigUpdate', info: '' }); }
+ }
+ unlink(r_tmp);
+ // Cleanup missing URLs (refresh cursor after sed modified config)
+ let to_delete = [];
+ uci(pkg.name, true).foreach(pkg.name, 'file_url', (s) => {
+ if (!s.url) push(to_delete, s['.name']);
+ });
+ for (let name in to_delete)
+ uci(pkg.name).delete(pkg.name, name);
+ uci(pkg.name).save(pkg.name);
+ if (uci_has_changes(pkg.name))
+ uci(pkg.name).commit(pkg.name);
+}
+
+// ── get_file_url_list ────────────────────────────────────────────────
+
+function get_file_url_list() {
+ let files = [];
+ uci(pkg.name).foreach(pkg.name, 'file_url', (s) => {
+ let size = s.size;
+ if (!size && s.url) size = get_url_filesize(s.url);
+ push(files, { name: s.name || s.url, url: s.url, size: size || '' });
+ });
+ return files;
+}
+
+// ── _build_procd_data ───────────────────────────────────────────────
+
+function _build_procd_data() {
+ let result = {};
+ result.version = pkg.version;
+ result.status = status_data.status;
+ result.message = status_data.message;
+ result.stats = status_data.stats;
+ result.packageCompat = int(pkg.compat);
+ result.entries = int(count_blocked_domains());
+ result.dns = cfg.dns;
+ result.outputFile = dns_output.file;
+ result.outputCache = dns_output.cache;
+
+ let gzip_path = cfg.compressed_cache_dir
+ ? cfg.compressed_cache_dir + '/' + dns_output.gzip
+ : '';
+ result.outputGzip = gzip_path;
+
+ // Force DNS
+ let force_dns_ports = [];
+ if (cfg.force_dns && cfg.force_dns_port) {
+ force_dns_ports = split('' + cfg.force_dns_port, /[\s,]+/);
+ }
+ result.force_dns_active = length(force_dns_ports) > 0;
+ result.force_dns_ports = force_dns_ports;
+
+ // Platform support
+ result.platform = {
+ ipset_installed: env.ipset_supported,
+ nft_installed: env.nft_installed,
+ dnsmasq_installed: env.dnsmasq_installed,
+ dnsmasq_ipset_support: env.check_dnsmasq_ipset(),
+ dnsmasq_nftset_support: env.check_dnsmasq_nftset(),
+ smartdns_installed: env.smartdns_installed,
+ smartdns_ipset_support: env.smartdns_installed && env.ipset_supported,
+ smartdns_nftset_support: env.smartdns_installed && env.nft_installed,
+ unbound_installed: env.unbound_installed,
+ leds: lsdir('/sys/class/leds') || [],
+ };
+
+ // File URL sizes
+ result.file_url = get_file_url_list();
+
+ // Errors
+ result.errors = [];
+ for (let e in status_data.errors)
+ push(result.errors, { code: e.code, info: e.info });
+
+ // Warnings
+ result.warnings = [];
+ for (let e in status_data.warnings)
+ push(result.warnings, { code: e.code, info: e.info });
+
+ // Firewall rules
+ result.firewall = [];
+ if (cfg.force_dns) {
+ let ports = split(replace('' + cfg.force_dns_port, /,/g, ' '), /\s+/);
+ for (let p in ports) {
+ if (!p) continue;
+ let ifaces = split('' + cfg.force_dns_interface, /\s+/);
+ if (is_port_listening(p)) {
+ for (let iface in ifaces) {
+ if (!iface) continue;
+ push(result.firewall, {
+ type: 'redirect', target: 'DNAT', src: iface,
+ proto: 'tcp udp', src_dport: '53', dest_port: '' + p,
+ family: 'any', reflection: false,
+ });
+ }
+ } else {
+ for (let iface in ifaces) {
+ if (!iface) continue;
+ push(result.firewall, {
+ type: 'rule', src: iface, dest: '*',
+ proto: 'tcp udp', dest_port: '' + p, target: 'REJECT',
+ });
+ }
+ }
+ }
+ }
+
+ // fw4 restart flag (consumed by init script as shell variable)
+ result.fw4_restart_needed = is_fw4_restart_needed();
+
+ // ipset/nftset firewall rules
+ switch (cfg.dns) {
+ case 'dnsmasq.ipset':
+ case 'smartdns.ipset':
+ push(result.firewall, { type: 'ipset', name: 'adb', match: 'dest_net', storage: 'hash' });
+ for (let iface in split('' + (cfg.force_dns_interface), /\s+/)) {
+ if (!iface) continue;
+ push(result.firewall, { type: 'rule', ipset: 'adb', src: iface, dest: '*', proto: 'tcp udp', target: 'REJECT' });
+ }
+ break;
+ case 'dnsmasq.nftset':
+ case 'smartdns.nftset':
+ push(result.firewall, { type: 'ipset', name: 'adb4', family: '4', match: 'dest_net' });
+ for (let iface in split('' + (cfg.force_dns_interface), /\s+/)) {
+ if (!iface) continue;
+ push(result.firewall, { type: 'rule', ipset: 'adb4', src: iface, dest: '*', proto: 'tcp udp', target: 'REJECT' });
+ }
+ if (cfg.ipv6_enabled) {
+ push(result.firewall, { type: 'ipset', name: 'adb6', family: '6', match: 'dest_net' });
+ for (let iface in split('' + (cfg.force_dns_interface), /\s+/)) {
+ if (!iface) continue;
+ push(result.firewall, { type: 'rule', ipset: 'adb6', src: iface, dest: '*', proto: 'tcp udp', target: 'REJECT' });
+ }
+ }
+ break;
+ }
+
+ return result;
+}
+
+// ── emit_procd_shell ────────────────────────────────────────────────
+// Converts _build_procd_data() result into json_add_* shell commands
+// for safe use between procd_open_data / procd_close_data.
+
+function emit_procd_shell(data) {
+ if (!data) return '';
+ let lines = [];
+
+ if (data.fw4_restart_needed)
+ push(lines, '_fw4_restart=1');
+
+ // Minimal data (e.g. from stop) — only emit shell variables
+ if (!data.version)
+ return join('\n', lines) + '\n';
+
+ push(lines, 'json_add_string version ' + shell_quote(data.version || ''));
+ push(lines, 'json_add_string status ' + shell_quote(data.status || ''));
+ push(lines, 'json_add_string message ' + shell_quote(data.message || ''));
+ push(lines, 'json_add_string stats ' + shell_quote(data.stats || ''));
+ push(lines, 'json_add_int packageCompat ' + shell_quote('' + (data.packageCompat || 0)));
+ push(lines, 'json_add_int entries ' + shell_quote('' + (data.entries || 0)));
+ push(lines, 'json_add_string dns ' + shell_quote(data.dns || ''));
+ push(lines, 'json_add_string outputFile ' + shell_quote(data.outputFile || ''));
+ push(lines, 'json_add_string outputCache ' + shell_quote(data.outputCache || ''));
+ push(lines, 'json_add_string outputGzip ' + shell_quote(data.outputGzip || ''));
+ push(lines, 'json_add_boolean force_dns_active ' + shell_quote(data.force_dns_active ? '1' : '0'));
+
+ push(lines, 'json_add_array force_dns_ports');
+ for (let p in (data.force_dns_ports || []))
+ push(lines, 'json_add_string \'\' ' + shell_quote('' + p));
+ push(lines, 'json_close_array');
+
+ // Platform support
+ push(lines, 'json_add_object platform');
+ let plat = data.platform || {};
+ push(lines, 'json_add_boolean ipset_installed ' + shell_quote(plat.ipset_installed ? '1' : '0'));
+ push(lines, 'json_add_boolean nft_installed ' + shell_quote(plat.nft_installed ? '1' : '0'));
+ push(lines, 'json_add_boolean dnsmasq_installed ' + shell_quote(plat.dnsmasq_installed ? '1' : '0'));
+ push(lines, 'json_add_boolean dnsmasq_ipset_support ' + shell_quote(plat.dnsmasq_ipset_support ? '1' : '0'));
+ push(lines, 'json_add_boolean dnsmasq_nftset_support ' + shell_quote(plat.dnsmasq_nftset_support ? '1' : '0'));
+ push(lines, 'json_add_boolean smartdns_installed ' + shell_quote(plat.smartdns_installed ? '1' : '0'));
+ push(lines, 'json_add_boolean smartdns_ipset_support ' + shell_quote(plat.smartdns_ipset_support ? '1' : '0'));
+ push(lines, 'json_add_boolean smartdns_nftset_support ' + shell_quote(plat.smartdns_nftset_support ? '1' : '0'));
+ push(lines, 'json_add_boolean unbound_installed ' + shell_quote(plat.unbound_installed ? '1' : '0'));
+ push(lines, 'json_add_array leds');
+ for (let led in (plat.leds || []))
+ push(lines, 'json_add_string \'\' ' + shell_quote('' + led));
+ push(lines, 'json_close_array');
+ push(lines, 'json_close_object');
+
+ // File URL sizes
+ push(lines, 'json_add_array file_url');
+ for (let f in (data.file_url || [])) {
+ push(lines, "json_add_object ''");
+ push(lines, 'json_add_string name ' + shell_quote(f.name || ''));
+ push(lines, 'json_add_string url ' + shell_quote(f.url || ''));
+ push(lines, 'json_add_string size ' + shell_quote('' + (f.size || '')));
+ push(lines, 'json_close_object');
+ }
+ push(lines, 'json_close_array');
+
+ push(lines, 'json_add_array errors');
+ for (let e in (data.errors || [])) {
+ push(lines, "json_add_object ''");
+ push(lines, 'json_add_string code ' + shell_quote(e.code || ''));
+ push(lines, 'json_add_string info ' + shell_quote(e.info || ''));
+ push(lines, 'json_close_object');
+ }
+ push(lines, 'json_close_array');
+
+ push(lines, 'json_add_array warnings');
+ for (let w in (data.warnings || [])) {
+ push(lines, "json_add_object ''");
+ push(lines, 'json_add_string code ' + shell_quote(w.code || ''));
+ push(lines, 'json_add_string info ' + shell_quote(w.info || ''));
+ push(lines, 'json_close_object');
+ }
+ push(lines, 'json_close_array');
+
+ push(lines, 'json_add_array firewall');
+ for (let rule in (data.firewall || [])) {
+ push(lines, "json_add_object ''");
+ for (let k in keys(rule)) {
+ let v = rule[k];
+ if (type(v) == 'bool')
+ push(lines, 'json_add_boolean ' + k + ' ' + shell_quote(v ? '1' : '0'));
+ else if (type(v) == 'int')
+ push(lines, 'json_add_int ' + k + ' ' + shell_quote('' + v));
+ else
+ push(lines, 'json_add_string ' + k + ' ' + shell_quote('' + v));
+ }
+ push(lines, 'json_close_object');
+ }
+ push(lines, 'json_close_array');
+
+ return join('\n', lines) + '\n';
+}
+
+// ── status_service ──────────────────────────────────────────────────
+
+function status_service(param) {
+ env.load_config();
+ // When called from start() the in-memory status_data is already correct;
+ // reloading from ubus would overwrite it with stale data.
+ if (param != 'on_start_success' && param != 'on_start_failure')
+ _load_status_from_ubus();
+ let status = status_data.status;
+ let message = status_data.message;
+ let stats = status_data.stats;
+
+ if (status == 'statusSuccess') {
+ output.info('* ' + stats + '\\n');
+ output.verbose('[STAT] ' + stats + '\\n');
+ } else {
+ if (status) status = get_text(status);
+ if (status && message) status += ': ' + message;
+ let cache_info = '';
+ let has_cache = adb_file('test_cache');
+ let has_gzip = adb_file('test_gzip');
+ if (has_cache && has_gzip) cache_info = 'cache file and compressed cache file found';
+ else if (has_cache) cache_info = 'cache file found';
+ else if (has_gzip) cache_info = 'compressed cache file found';
+ if (status && cache_info) status += ' (' + cache_info + ')';
+ if (status) output.print(pkg.service_name + ' ' + status + '.\\n');
+ }
+
+ if (param == 'quiet' || param == 'on_start_success' || param == 'on_start_failure') return;
+
+ for (let e in status_data.errors)
+ output.error(get_text(e.code, e.info));
+ for (let e in status_data.warnings)
+ output.warning(get_text(e.code, e.info));
+}
+
+// ── start ───────────────────────────────────────────────────────────
+// Returns JSON object for procd_open_data (status, firewall[], errors[], warnings[])
+
+function start(args) {
+ let param = (args && args[0]) || 'on_start';
+
+ _load_status_from_ubus();
+ let prev_status = status_data.status;
+ let prev_errors = length(status_data.errors) > 0;
+ _status_reset();
+
+ if (param == 'on_boot') {
+ env.load(param); // on_boot: just loads config + dns_output
+ if (!adb_file('test_gzip') && !adb_file('test_cache'))
+ return null;
+ }
+
+ adb_config_update(param);
+ if (!env.load(param)) return null; // memoized if already called above
+
+ let action = adb_config_cache('get', 'trigger_service');
+ state.fw4_restart = adb_config_cache('get', 'trigger_fw4');
+
+ if (prev_errors) {
+ action = 'download';
+ } else if (!adb_file('test')) {
+ if (adb_file('test_gzip') || adb_file('test_cache'))
+ action = 'restore';
+ else
+ action = 'download';
+ } else if (prev_status == 'statusSuccess') {
+ action = 'skip';
+ }
+
+ // Normalize action based on param
+ let combo = (action || '') + ':' + param;
+ if (index(combo, 'on_boot') >= 0 || param == 'on_pause') {
+ action = (adb_file('test_gzip') || adb_file('test_cache')) ? 'restore' : 'download';
+ } else if (param == 'download' || action == 'download') {
+ action = 'download';
+ } else if (action == 'restart') {
+ action = 'restart';
+ } else if (action == 'restore') {
+ action = 'restore';
+ } else if (action == 'skip') {
+ action = 'skip';
+ } else if (!action) {
+ action = 'download';
+ }
+
+ if (action == 'restore') {
+ output.info('Starting ' + pkg.service_name + '...\\n');
+ output.verbose('[INIT] Starting ' + pkg.service_name + '...\\n');
+ status_data.status = 'statusStarting';
+ if (adb_file('test_gzip') && !adb_file('test_cache') && !adb_file('test')) {
+ output.info('Found compressed cache file, unpacking it ');
+ output.verbose('[INIT] Found compressed cache file, unpacking it ');
+ status_data.message = 'found compressed cache file, unpacking it.';
+ if (adb_file('unpack_gzip')) {
+ output.okn();
+ } else {
+ output.failn();
+ output.error(get_text('errorRestoreCompressedCache'));
+ action = 'download';
+ }
+ }
+ if (adb_file('test_cache') && !adb_file('test')) {
+ output.info('Found cache file, reusing it ');
+ output.verbose('[INIT] Found cache file, reusing it ');
+ status_data.message = 'found cache file, reusing it.';
+ if (adb_file('restore')) {
+ cfg.dnsmasq_sanity_check = false;
+ cfg.heartbeat_domain = null;
+ output.okn();
+ resolver('on_start');
+ } else {
+ output.failn();
+ output.error(get_text('errorRestoreCache'));
+ action = 'download';
+ }
+ }
+ }
+
+ if (action == 'download') {
+ if (!cfg.blocked_url && !cfg.blocked_domain) {
+ status_data.status = 'statusFail';
+ push(status_data.errors, { code: 'errorNothingToDo', info: '' });
+ } else {
+ if (!adb_file('test') || adb_file('test_cache') || adb_file('test_gzip')) {
+ output.info('Force-reloading ' + pkg.service_name + '...\\n');
+ output.verbose('[INIT] Force-reloading ' + pkg.service_name + '...\\n');
+ status_data.status = 'statusForceReloading';
+ } else {
+ output.info('Starting ' + pkg.service_name + '...\\n');
+ output.verbose('[INIT] Starting ' + pkg.service_name + '...\\n');
+ status_data.status = 'statusStarting';
+ }
+ resolver('cleanup');
+ if (cfg.dns == 'dnsmasq.conf' && cfg.dnsmasq_config_file_url)
+ download_dnsmasq_file();
+ else
+ download_lists();
+ resolver('on_start');
+ }
+ }
+
+ if (action == 'restart') {
+ output.info('Restarting ' + pkg.service_name + '...\\n');
+ output.verbose('[INIT] Restarting ' + pkg.service_name + '...\\n');
+ status_data.status = 'statusRestarting';
+ cfg.dnsmasq_sanity_check = false;
+ cfg.heartbeat_domain = null;
+ resolver('on_start');
+ }
+
+ if (action == 'start') {
+ output.info('Starting ' + pkg.service_name + '...\\n');
+ output.verbose('[INIT] Starting ' + pkg.service_name + '...\\n');
+ status_data.status = 'statusStarting';
+ cfg.dnsmasq_sanity_check = false;
+ cfg.heartbeat_domain = null;
+ resolver('on_start');
+ }
+
+ let final_status = status_data.status;
+ if (adb_file('test') && final_status != 'statusFail') {
+ status_data.message = '';
+ status_data.status = 'statusSuccess';
+ status_data.stats = pkg.service_name + ' is blocking ' + count_blocked_domains() + ' domains (with ' + cfg.dns + ')';
+ status_service('on_start_success');
+ } else {
+ status_data.status = 'statusFail';
+ push(status_data.errors, { code: 'errorOhSnap', info: '' });
+ status_service('on_start_failure');
+ resolver('revert');
+ }
+
+ // Compressed cache: create or remove
+ if (cfg.compressed_cache && !adb_file('test_gzip') && adb_file('test')) {
+ let start_time = time();
+ let step_title = 'Creating ' + cfg.dns + ' compressed cache';
+ output.info(step_title + ' ');
+ output.verbose('[PROC] ' + step_title + ' ');
+ status_data.message = get_text('statusProcessing') + ': ' + step_title;
+ if (adb_file('create_gzip'))
+ output.okn();
+ else {
+ output.failn();
+ push(status_data.errors, { code: 'errorCreatingCompressedCache', info: '' });
+ }
+ let end_time = time();
+ let elapsed = end_time - start_time;
+ logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's');
+ } else {
+ adb_file('remove_gzip');
+ }
+
+ adb_config_cache('create');
+
+ return _build_procd_data();
+}
+
+// ── dl ──────────────────────────────────────────────────────────────
+
+function dl() {
+ return start(['download']);
+}
+
+// ── stop ────────────────────────────────────────────────────────────
+
+function stop() {
+ env.load_config();
+ if (adb_file('test')) {
+ output.info('Stopping ' + pkg.service_name + '... ');
+ output.verbose('[STOP] Stopping ' + pkg.service_name + '... ');
+ adb_file('create');
+ if (resolver('on_stop')) {
+ system('ipset -q -! flush adb 2>/dev/null; ipset -q -! destroy adb 2>/dev/null');
+ system('nft delete set inet fw4 adb4 2>/dev/null; nft delete set inet fw4 adb6 2>/dev/null');
+ led_off(cfg.led);
+ output.okn();
+ status_data.status = 'statusStopped';
+ status_data.message = '';
+ } else {
+ output.failn();
+ status_data.status = 'statusFail';
+ push(status_data.errors, { code: 'errorStopping', info: '' });
+ output.error(get_text('errorStopping'));
+ }
+ }
+ return { fw4_restart_needed: is_fw4_restart_needed() };
+}
+
+// ── Extra Commands ──────────────────────────────────────────────────
+
+function allow(string) {
+ env.load_config();
+ if (!adb_file('test')) {
+ output.print("No block-list ('" + dns_output.file + "') found.\\n");
+ return;
+ }
+ if (!string) {
+ output.print("Usage: /etc/init.d/" + pkg.name + " allow 'domain' ...\\n");
+ return;
+ }
+ if (cfg.dnsmasq_config_file_url) {
+ output.print("Allowing individual domains is not possible when using external dnsmasq config file.\\n");
+ return;
+ }
+
+ let resolver_name = split(cfg.dns, '.')[0];
+ output.info('Allowing domains and restarting ' + resolver_name + ' ');
+ output.verbose('[PROC] Allowing domains \\n');
+
+ for (let c in split('' + string, /\s+/)) {
+ if (!c) continue;
+ output.verbose(' ' + c + ' ');
+ let escaped = replace(c, /\./g, '\\.');
+ switch (split(cfg.dns, '.')[0]) {
+ case 'dnsmasq':
+ sed_inplace(sprintf('\\:/\\(/%s\\|.%s\\):d', escaped, escaped), dns_output.file);
+ break;
+ case 'smartdns':
+ case 'unbound':
+ sed_inplace(sprintf('\\:\\("%s\\|.%s"\\):d', escaped, escaped), dns_output.file);
+ break;
+ }
+ output.ok();
+ if (dns_output.allow_filter) {
+ system(sprintf("echo %s | sed -E '%s' >> %s", shell_quote(c), dns_output.allow_filter, shell_quote(dns_output.file)));
+ output.ok();
+ }
+ uci_list_add_if_new(pkg.name, 'config', 'allowed_domain', c);
+ output.ok();
+ }
+
+ if (cfg.compressed_cache) {
+ output.verbose('[PROC] Creating compressed cache ');
+ if (adb_file('create_gzip')) output.ok(); else output.fail();
+ }
+ output.verbose('[PROC] Committing changes to config ');
+ if (uci(pkg.name).commit(pkg.name)) {
+ let ad = uci(pkg.name).get(pkg.name, 'config', 'allowed_domain');
+ cfg.allowed_domain = ad ? replace((type(ad) == 'array') ? join(' ', ad) : '' + ad, /,/g, ' ') : null;
+ adb_config_cache('create');
+ status_data.stats = pkg.service_name + ' is blocking ' + count_blocked_domains() + ' domains (with ' + cfg.dns + ')';
+ output.ok();
+ if (cfg.dns == 'dnsmasq.ipset') {
+ output.verbose('[PROC] Flushing adb ipset ');
+ if (system('ipset -q -! flush adb 2>/dev/null') == 0) output.ok(); else output.fail();
+ }
+ if (cfg.dns == 'dnsmasq.nftset') {
+ output.verbose('[PROC] Flushing adb nft sets ');
+ system('nft flush set inet fw4 adb6 2>/dev/null');
+ if (system('nft flush set inet fw4 adb4 2>/dev/null') == 0) output.ok(); else output.fail();
+ }
+ output.dns('Restarting ' + resolver_name + ' ');
+ if (service_restart(resolver_name)) output.ok(); else output.fail();
+ } else {
+ output.fail();
+ }
+ _update_ubus_status();
+ output.info('\\n');
+}
+
+function check(param) {
+ env.load_config();
+ if (!adb_file('test')) {
+ output.print("No block-list ('" + dns_output.file + "') found.\\n");
+ return;
+ }
+ if (!param) {
+ output.print("Usage: /etc/init.d/" + pkg.name + " check 'domain' ...\\n");
+ return;
+ }
+ for (let string in split('' + param, /\s+/)) {
+ if (!string) continue;
+ let c = grep_count(string, dns_output.file, '-c -E');
+ if (c > 0) {
+ let word = (c == 1) ? '1 match' : c + ' matches';
+ output.info("Found " + word + " for '" + string + "' in '" + dns_output.file + "'.\\n");
+ output.verbose("[PROC] Found " + word + " for '" + string + "' in '" + dns_output.file + "'.\\n");
+ if (c <= 20) {
+ let matches = grep_output(string, dns_output.file);
+ if (dns_output.parse_filter)
+ matches = cmd_output(sprintf("grep %s %s | sed '%s'", shell_quote(string), shell_quote(dns_output.file), dns_output.parse_filter));
+ if (matches) output.print(matches + '\\n');
+ }
+ } else {
+ output.info("The '" + string + "' is not found in current block-list ('" + dns_output.file + "').\\n");
+ output.verbose("[PROC] The '" + string + "' is not found in current block-list ('" + dns_output.file + "').\\n");
+ }
+ }
+}
+
+function check_tld() {
+ env.load_config();
+ if (!adb_file('test')) {
+ output.print("No block-list ('" + dns_output.file + "') found.\\n");
+ return;
+ }
+ let c = grep_count('\\.|server:', dns_output.file, '-cvE');
+ if (c > 0) {
+ let word = (c == 1) ? '1 match for TLD' : c + ' matches for TLDs';
+ output.info("Found " + word + " in '" + dns_output.file + "'.\\n");
+ output.verbose("[PROC] Found " + word + " in '" + dns_output.file + "'.\\n");
+ if (c <= 20) {
+ let matches = grep_output('\\.|server:', dns_output.file, '-vE');
+ if (dns_output.parse_filter)
+ matches = cmd_output(sprintf("grep -vE '\\.|server:' %s | sed '%s'", shell_quote(dns_output.file), dns_output.parse_filter));
+ if (matches) output.print(matches + '\\n');
+ }
+ } else {
+ output.info("No TLD was found in current block-list ('" + dns_output.file + "').\\n");
+ output.verbose("[PROC] No TLD was found in current block-list ('" + dns_output.file + "').\\n");
+ }
+}
+
+function check_leading_dot() {
+ env.load_config();
+ if (!adb_file('test')) {
+ output.print("No block-list ('" + dns_output.file + "') found.\\n");
+ return;
+ }
+ let search_string = '';
+ switch (split(cfg.dns, '.')[0]) {
+ case 'dnsmasq': search_string = '/\\.'; break;
+ case 'smartdns': search_string = '^\\.'; break;
+ case 'unbound': search_string = '"\\.'; break;
+ default: return;
+ }
+ let c = grep_count(search_string, dns_output.file);
+ if (c > 0) {
+ let word = (c == 1) ? '1 match for leading-dot domain' : c + ' matches for leading-dot domains';
+ output.info("Found " + word + " in '" + dns_output.file + "'.\\n");
+ output.verbose("[PROC] Found " + word + " in '" + dns_output.file + "'.\\n");
+ if (c <= 20) {
+ let matches = grep_output(search_string, dns_output.file);
+ if (dns_output.parse_filter)
+ matches = cmd_output(sprintf("grep %s %s | sed '%s'", shell_quote(search_string), shell_quote(dns_output.file), dns_output.parse_filter));
+ if (matches) output.print(matches + '\\n');
+ }
+ } else {
+ output.info("No leading-dot domain was found in current block-list ('" + dns_output.file + "').\\n");
+ output.verbose("[PROC] No leading-dot domain was found in current block-list ('" + dns_output.file + "').\\n");
+ }
+}
+
+function check_lists(param) {
+ env.load_config();
+ if (!param) {
+ output.print("Usage: /etc/init.d/" + pkg.name + " check_lists 'domain' ...\\n");
+ return;
+ }
+ uci(pkg.name).foreach(pkg.name, 'file_url', (s) => {
+ if (s.enabled == '0') return;
+ if ((s.action || 'block') != 'block') return;
+ let url = s.url;
+ let name = s.name || url;
+ if (!url) return;
+
+ output.info('Checking ' + name + ': ');
+ output.verbose('[ DL ] ' + name + ' ');
+
+ if (is_https_url(url) && !env.get_downloader().ssl_supported) {
+ output.failn();
+ return;
+ }
+ let r_tmp = trim(cmd_output('mktemp -q -t "' + pkg.name + '_tmp.XXXXXXXX"'));
+ if (!download(url, r_tmp) || !(stat(r_tmp)?.size > 0)) {
+ output.failn();
+ return;
+ }
+ output.verbose(sym.ok[1] + '\\n');
+ ensure_trailing_newline(r_tmp);
+
+ for (let string in split('' + param, /\s+/)) {
+ if (!string) continue;
+ let c = grep_count(string, r_tmp, '-c -E');
+ if (c > 0) {
+ let word = (c == 1) ? '1 match' : c + ' matches';
+ output.info("found " + word + " for '" + string + "'.\\n");
+ output.verbose("[PROC] Found " + word + " for '" + string + "' in '" + url + "'.\\n");
+ let matches = grep_output(string, r_tmp);
+ if (matches) output.print(matches + '\\n');
+ } else {
+ output.info("'" + string + "' not found.\\n");
+ output.verbose("[PROC] The '" + string + "' is not found in '" + url + "'.\\n");
+ }
+ }
+ unlink(r_tmp);
+ });
+}
+
+function killcache() {
+ env.load_config();
+ for (let mode in dns_modes) {
+ let dc = dns_modes[mode];
+ unlink(dc.cache);
+ unlink(cfg.compressed_cache_dir + '/' + dc.gzip);
+ }
+ resolver('cleanup');
+}
+
+function pause(timeout) {
+ env.load_config();
+ timeout = timeout || cfg.pause_timeout || '20';
+ stop();
+ output.info('Sleeping for ' + timeout + ' seconds... ');
+ output.verbose('[PROC] Sleeping for ' + timeout + ' seconds... ');
+ if (is_integer(timeout) && system('sleep ' + timeout) == 0)
+ output.okn();
+ else
+ output.failn();
+ let result = start(['on_pause']);
+ if (result) {
+ let conn = connect();
+ if (conn) {
+ conn.call('service', 'set_data', { name: pkg.name, data: result });
+ conn.disconnect();
+ }
+ }
+}
+
+function show_blocklist() {
+ env.load_config();
+ if (dns_output.file && dns_output.parse_filter)
+ system(sprintf("sed '%s' %s", dns_output.parse_filter, shell_quote(dns_output.file)));
+ else if (dns_output.file)
+ print(readfile(dns_output.file) || '');
+}
+
+function sizes() {
+ env.load_config();
+ uci(pkg.name).foreach(pkg.name, 'file_url', (s) => {
+ let size = get_url_filesize(s.url);
+ output.print((s.name || s.url) + (size ? ': ' + size : '') + ' ');
+ if (size) {
+ uci(pkg.name).set(pkg.name, s['.name'], 'size', '' + size);
+ output.okn();
+ } else {
+ output.failn();
+ }
+ });
+ uci(pkg.name).save(pkg.name);
+ if (cfg.update_config_sizes && length(uci(pkg.name).changes(pkg.name) || []))
+ uci(pkg.name).commit(pkg.name);
+}
+
+// ── get_network_trigger_info (for service_triggers) ─────────────────
+
+function get_network_trigger_info() {
+ env.load_config();
+ let result = { procd_trigger_wan6: cfg.procd_trigger_wan6 };
+ return result;
+}
+
+// ── rpcd Data Functions ─────────────────────────────────────────────
+
+function get_init_status(name) {
+ name = name || pkg.name;
+ env.load('rpcd');
+
+ // Read pre-computed data from procd service (like PBR)
+ let conn = connect();
+ let ubus_data = conn ? conn.call('service', 'list', { name: pkg.name }) : null;
+ if (conn) conn.disconnect();
+ let svc_data = ubus_data?.[pkg.name]?.data;
+
+ // Gzip path (for live file-existence checks)
+ let gzip_path = svc_data?.outputGzip || '';
+ if (!gzip_path && cfg.compressed_cache_dir)
+ gzip_path = cfg.compressed_cache_dir + '/' + dns_output.gzip;
+
+ let result = {};
+ result[name] = {
+ version: pkg.version,
+ packageCompat: int(pkg.compat),
+
+ // Live-computed (cheap stat/uci checks)
+ enabled: service_enabled(pkg.name),
+ running: (stat(pkg.run_file)?.size > 0),
+ outputFileExists: (stat(svc_data?.outputFile || dns_output.file)?.size > 0) || false,
+ outputCacheExists: (stat(svc_data?.outputCache || dns_output.cache)?.size > 0) || false,
+ outputGzipExists: gzip_path ? (stat(gzip_path)?.size > 0) || false : false,
+
+ // From procd ubus data (pre-computed at start/dl time)
+ status: svc_data?.status || '',
+ message: svc_data?.message || '',
+ stats: svc_data?.stats || '',
+ entries: svc_data?.entries || 0,
+ dns: svc_data?.dns || cfg.dns,
+ outputFile: svc_data?.outputFile || dns_output.file,
+ outputCache: svc_data?.outputCache || dns_output.cache,
+ outputGzip: gzip_path,
+ force_dns_active: svc_data?.force_dns_active || false,
+ force_dns_ports: svc_data?.force_dns_ports || [],
+ errors: svc_data?.errors || [],
+ warnings: svc_data?.warnings || [],
+
+ // Platform support (from env.detect, runs once per rpcd lifetime)
+ platform: {
+ ipset_installed: env.ipset_supported,
+ nft_installed: env.nft_installed,
+ dnsmasq_installed: env.dnsmasq_installed,
+ dnsmasq_ipset_support: env.check_dnsmasq_ipset(),
+ dnsmasq_nftset_support: env.check_dnsmasq_nftset(),
+ smartdns_installed: env.smartdns_installed,
+ smartdns_ipset_support: env.smartdns_installed && env.ipset_supported,
+ smartdns_nftset_support: env.smartdns_installed && env.nft_installed,
+ unbound_installed: env.unbound_installed,
+ leds: lsdir('/sys/class/leds') || [],
+ },
+
+ // File URL sizes (from procd data, pre-computed at start time)
+ file_url: svc_data?.file_url || [],
+ };
+ return result;
+}
+
+function get_init_list(name) {
+ name = name || pkg.name;
+ let result = {};
+ let enabled_val = (uci(pkg.name).get(pkg.name, 'config', 'enabled') ?? '0');
+ result[name] = { enabled: (enabled_val == '1') };
+ return result;
+}
+
+function get_platform_support(name) {
+ name = name || pkg.name;
+ env.detect();
+ let result = {};
+ result[name] = {
+ ipset_installed: env.ipset_supported,
+ nft_installed: env.nft_installed,
+ dnsmasq_installed: env.dnsmasq_installed,
+ dnsmasq_ipset_support: env.check_dnsmasq_ipset(),
+ dnsmasq_nftset_support: env.check_dnsmasq_nftset(),
+ smartdns_installed: env.smartdns_installed,
+ smartdns_ipset_support: env.smartdns_installed && env.ipset_supported,
+ smartdns_nftset_support: env.smartdns_installed && env.nft_installed,
+ unbound_installed: env.unbound_installed,
+ leds: length(lsdir('/sys/class/leds') || []) > 0,
+ };
+ return result;
+}
+
+function get_file_url_filesizes(name) {
+ name = name || pkg.name;
+ env.load('rpcd');
+
+ let files = [];
+ uci(pkg.name).foreach(pkg.name, 'file_url', (s) => {
+ let size = s.size;
+ if (!size && s.url) size = get_url_filesize(s.url);
+ push(files, { name: s.name || s.url, url: s.url, size: size || '' });
+ });
+
+ let result = {};
+ result[name] = { file_url: files };
+ return result;
+}
+
+// ── Module Init & Export ────────────────────────────────────────────
+
+function set_script_name(name) {
+ state.script_name = name;
+}
+
+export default {
+ init: function() {}, // backward compat (rpcd plugin may still call this)
+ set_script_name,
+ pkg,
+
+ // Core lifecycle
+ env,
+ start,
+ stop,
+ status_service,
+
+ // Config
+ load_dl_command,
+ adb_config_update,
+ adb_config_cache,
+
+ // Extra commands
+ allow,
+ check,
+ check_tld,
+ check_leading_dot,
+ check_lists,
+ killcache,
+ pause,
+ show_blocklist,
+ sizes,
+
+ // rpcd data
+ get_init_status,
+ get_init_list,
+ get_platform_support,
+ get_file_url_filesizes,
+
+ // init script helpers
+ get_network_trigger_info,
+ dl,
+ emit_procd_shell,
+ process_file_url,
+};
+
diff --git a/net/adblock-fast/files/lib/adblock-fast/cli.uc b/net/adblock-fast/files/lib/adblock-fast/cli.uc
new file mode 100644
index 0000000000..6c63145315
--- /dev/null
+++ b/net/adblock-fast/files/lib/adblock-fast/cli.uc
@@ -0,0 +1,95 @@
+'use strict';
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Copyright 2023-2026 MOSSDeF, Stan Grishin (stangri@melmac.ca).
+//
+// CLI dispatcher for adblock-fast.
+// Called from init script:
+// ucode -S -L /lib/adblock-fast /lib/adblock-fast/cli.uc -- [args...]
+
+import adb from 'adblock-fast';
+
+let action = shift(ARGV);
+if (action == '--') action = shift(ARGV);
+
+switch (action) {
+case 'start':
+ let start_result = adb.start(ARGV);
+ if (start_result)
+ print(adb.emit_procd_shell(start_result));
+ break;
+
+case 'stop':
+ let stop_result = adb.stop();
+ if (stop_result)
+ print(adb.emit_procd_shell(stop_result));
+ break;
+
+case 'status':
+ adb.status_service(ARGV[0]);
+ break;
+
+case 'allow':
+ adb.allow(join(' ', ARGV));
+ break;
+
+case 'check':
+ adb.check(join(' ', ARGV));
+ break;
+
+case 'check_tld':
+ adb.check_tld();
+ break;
+
+case 'check_leading_dot':
+ adb.check_leading_dot();
+ break;
+
+case 'check_lists':
+ adb.check_lists(join(' ', ARGV));
+ break;
+
+case 'dl':
+ let dl_result = adb.dl();
+ if (dl_result)
+ print(adb.emit_procd_shell(dl_result));
+ break;
+
+case 'killcache':
+ adb.killcache();
+ break;
+
+case 'pause':
+ adb.pause(ARGV[0]);
+ break;
+
+case 'show_blocklist':
+ adb.show_blocklist();
+ break;
+
+case 'sizes':
+ adb.sizes();
+ break;
+
+case 'version':
+ print(adb.pkg.version + '\n');
+ break;
+
+case 'get_wan_interfaces':
+ let info = adb.get_network_trigger_info();
+ if (info)
+ print(sprintf('%J', info) + '\n');
+ break;
+
+case 'adb_config_update':
+ adb.adb_config_update(ARGV[0]);
+ break;
+
+case 'load_environment':
+ let env_ok = adb.env.load(ARGV[0], ARGV[1]);
+ exit(env_ok ? 0 : 1);
+ break;
+
+default:
+ warn('Unknown action: ' + (action || '(none)') + '\n');
+ exit(1);
+}
diff --git a/net/adblock-fast/tests/01_pipeline/01_all_dns_modes b/net/adblock-fast/tests/01_pipeline/01_all_dns_modes
new file mode 100644
index 0000000000..66de03b00d
--- /dev/null
+++ b/net/adblock-fast/tests/01_pipeline/01_all_dns_modes
@@ -0,0 +1,193 @@
+Test that all 9 DNS modes produce valid output files containing
+domains from both the domains.txt and hosts.txt test data files.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { readfile, dirname, mkdir } from 'fs';
+let ti = adb._test_internals;
+
+let modes = [
+ 'dnsmasq.servers',
+ 'dnsmasq.conf',
+ 'dnsmasq.ipset',
+ 'dnsmasq.nftset',
+ 'dnsmasq.addnhosts',
+ 'smartdns.domainset',
+ 'smartdns.ipset',
+ 'smartdns.nftset',
+ 'unbound.adb_list',
+];
+
+// Known-good domains that MUST appear in every output
+let must_have = [
+ 'ad.doubleclick.test.example.com',
+ 'tracker.analytics.test.example.com',
+ 'common-shared-1.test.example.com',
+ 'common-shared-10.test.example.com',
+ 'adhost-zero-1.test.example.org',
+ 'adhost-loopback-1.test.example.org',
+ 'parent-dedup-1.test.example.com',
+];
+
+// Domains that MUST NOT appear (invalid entries or subdomain-deduped)
+let must_not_have = [
+ 'localhost',
+ 'nodot',
+ 'child.parent-dedup-1.test.example.com',
+ 'sub.child.parent-dedup-2.test.example.com',
+ 'deep.sub.parent-dedup-3.test.example.com',
+];
+
+let results = [];
+
+for (let mode in modes) {
+ // Reset module state for each mode
+ ti.env._config_loaded = false;
+ ti.env._loaded = false;
+ ti.env.dnsmasq_features = '';
+ ti.env._detected = false;
+ ti.env.dnsmasq_ubus = null;
+ ti.status_data.errors = [];
+ ti.status_data.warnings = [];
+ ti.status_data.status = '';
+ ti.status_data.message = '';
+ ti.status_data.stats = '';
+
+ // Load config from mock UCI
+ adb.env.load_config();
+
+ // Override DNS mode via set_cfg (load() reassigns cfg, so direct ref is stale)
+ ti.set_cfg('dns', mode);
+ ti.set_cfg('enabled', true);
+ ti.set_cfg('dnsmasq_sanity_check', false);
+ ti.set_cfg('dnsmasq_validity_check', false);
+ ti.set_cfg('heartbeat_domain', null);
+ ti.set_cfg('config_update_enabled', false);
+ ti.set_cfg('update_config_sizes', false);
+ ti.env.dns_set_output_values(mode);
+
+ // Collect file_url sections
+ ti.append_urls();
+
+ // Ensure output directory exists
+ let out_file = ti.dns_output.file;
+ let out_dir = dirname(out_file);
+ mkdir(out_dir);
+
+ // Run the download and processing pipeline
+ let ok = ti.download_lists();
+
+ if (!ok) {
+ push(results, sprintf('%s: FAIL (download_lists returned false)', mode));
+ if (length(ti.status_data.errors))
+ push(results, sprintf(' errors: %J', ti.status_data.errors));
+ continue;
+ }
+
+ // Read output
+ let content = readfile(out_file);
+ if (!content || !length(content)) {
+ push(results, sprintf('%s: FAIL (empty output file %s)', mode, out_file));
+ continue;
+ }
+
+ let lines = filter(split(content, '\n'), l => length(l) > 0);
+ let line_count = length(lines);
+
+ // Extract domains using the mode's parse_filter
+ let dm = ti.dns_modes[mode];
+ let domains = {};
+ let bad_format = 0;
+
+ for (let line in lines) {
+ let domain;
+ switch (mode) {
+ case 'dnsmasq.servers':
+ let m1 = match(line, /^server=\/([^\/]+)\/$/);
+ domain = m1 ? m1[1] : null;
+ break;
+ case 'dnsmasq.conf':
+ let m2 = match(line, /^local=\/([^\/]+)\/$/);
+ domain = m2 ? m2[1] : null;
+ break;
+ case 'dnsmasq.ipset':
+ let m3 = match(line, /^ipset=\/([^\/]+)\/adb$/);
+ domain = m3 ? m3[1] : null;
+ break;
+ case 'dnsmasq.nftset':
+ let m4 = match(line, /^nftset=\/([^\/]+)\/4#/);
+ domain = m4 ? m4[1] : null;
+ break;
+ case 'dnsmasq.addnhosts':
+ let m5 = match(line, /^127\.0\.0\.1 (.+)$/);
+ domain = m5 ? m5[1] : null;
+ break;
+ case 'smartdns.domainset':
+ case 'smartdns.ipset':
+ case 'smartdns.nftset':
+ domain = match(line, /^[a-zA-Z0-9._-]+$/) ? line : null;
+ break;
+ case 'unbound.adb_list':
+ let m6 = match(line, /^local-zone: "([^"]+)\." always_nxdomain$/);
+ domain = m6 ? m6[1] : null;
+ if (!domain && line == 'server:') domain = '__header__';
+ break;
+ }
+ if (domain && domain != '__header__')
+ domains[domain] = true;
+ else if (!domain)
+ bad_format++;
+ }
+
+ let domain_count = length(keys(domains));
+
+ // Check must_have domains
+ let missing = [];
+ for (let d in must_have) {
+ if (!domains[d])
+ push(missing, d);
+ }
+
+ // Check must_not_have domains
+ let unwanted = [];
+ for (let d in must_not_have) {
+ if (domains[d])
+ push(unwanted, d);
+ }
+
+ // dnsmasq.addnhosts doesn't do subdomain dedup (not in needs_optimization list)
+ let skip_dedup = (mode == 'dnsmasq.addnhosts');
+ if (skip_dedup) {
+ unwanted = filter(unwanted, d =>
+ d != 'child.parent-dedup-1.test.example.com' &&
+ d != 'sub.child.parent-dedup-2.test.example.com' &&
+ d != 'deep.sub.parent-dedup-3.test.example.com');
+ }
+
+ if (length(missing) == 0 && length(unwanted) == 0 && bad_format == 0 && domain_count > 100)
+ push(results, sprintf('%s: PASS (%d domains)', mode, domain_count));
+ else {
+ let detail = sprintf('%s: FAIL (%d domains, %d bad_format', mode, domain_count, bad_format);
+ if (length(missing))
+ detail += sprintf(', missing: %J', missing);
+ if (length(unwanted))
+ detail += sprintf(', unwanted: %J', unwanted);
+ detail += ')';
+ push(results, detail);
+ }
+}
+
+print(join('\n', results) + '\n');
+-- End --
+
+-- Expect stdout --
+dnsmasq.servers: PASS (162 domains)
+dnsmasq.conf: PASS (162 domains)
+dnsmasq.ipset: PASS (162 domains)
+dnsmasq.nftset: PASS (162 domains)
+dnsmasq.addnhosts: PASS (165 domains)
+smartdns.domainset: PASS (162 domains)
+smartdns.ipset: PASS (162 domains)
+smartdns.nftset: PASS (162 domains)
+unbound.adb_list: PASS (162 domains)
+-- End --
diff --git a/net/adblock-fast/tests/01_pipeline/02_input_format_detection b/net/adblock-fast/tests/01_pipeline/02_input_format_detection
new file mode 100644
index 0000000000..a210c726bc
--- /dev/null
+++ b/net/adblock-fast/tests/01_pipeline/02_input_format_detection
@@ -0,0 +1,42 @@
+Test that detect_file_type() correctly identifies all supported list formats.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { writefile, mkdir } from 'fs';
+let ti = adb._test_internals;
+
+adb.env.load_config();
+
+let test_dir = '' + TESTDIR + '/fmt_test';
+mkdir(test_dir);
+
+let tests = [
+ ['domains', 'example.com\nad.tracker.net\nmalware.bad.org\n'],
+ ['hosts', '0.0.0.0 example.com\n127.0.0.1 tracker.net\n0.0.0.0 malware.org\n'],
+ ['adblockplus', '[Adblock Plus]\n||example.com^\n||tracker.net^\n'],
+ ['dnsmasq', 'server=/example.com/\nserver=/tracker.net/\n'],
+];
+
+let results = [];
+
+for (let t in tests) {
+ let name = t[0];
+ let content = t[1];
+ let path = test_dir + '/' + name + '.txt';
+ writefile(path, content);
+ let detected = ti.detect_file_type(path);
+ if (detected == name)
+ push(results, sprintf('%s: PASS', name));
+ else
+ push(results, sprintf('%s: FAIL (detected as %s)', name, detected));
+}
+
+print(join('\n', results) + '\n');
+-- End --
+
+-- Expect stdout --
+domains: PASS
+hosts: PASS
+adblockplus: PASS
+dnsmasq: PASS
+-- End --
diff --git a/net/adblock-fast/tests/01_pipeline/03_subdomain_dedup b/net/adblock-fast/tests/01_pipeline/03_subdomain_dedup
new file mode 100644
index 0000000000..b0fde8790d
--- /dev/null
+++ b/net/adblock-fast/tests/01_pipeline/03_subdomain_dedup
@@ -0,0 +1,58 @@
+Test that subdomain dedup removes child domains when parent exists.
+Parent domains are in domains.txt, children are in hosts.txt.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { readfile } from 'fs';
+let ti = adb._test_internals;
+
+adb.env.load_config();
+ti.set_cfg('dns', 'dnsmasq.servers');
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.env.dns_set_output_values('dnsmasq.servers');
+ti.append_urls();
+
+let ok = ti.download_lists();
+if (!ok) {
+ print('download_lists failed\n');
+} else {
+ let content = readfile(ti.dns_output.file) || '';
+
+ // Parents must exist
+ let parents = [
+ 'parent-dedup-1.test.example.com',
+ 'parent-dedup-2.test.example.com',
+ 'parent-dedup-3.test.example.com',
+ ];
+ // Children must be removed
+ let children = [
+ 'child.parent-dedup-1.test.example.com',
+ 'sub.child.parent-dedup-2.test.example.com',
+ 'deep.sub.parent-dedup-3.test.example.com',
+ ];
+
+ let results = [];
+ for (let p in parents) {
+ let found = index(content, 'server=/' + p + '/') >= 0;
+ push(results, sprintf('parent %s: %s', p, found ? 'PRESENT' : 'MISSING'));
+ }
+ for (let c in children) {
+ let found = index(content, 'server=/' + c + '/') >= 0;
+ push(results, sprintf('child %s: %s', c, found ? 'PRESENT (BAD)' : 'REMOVED'));
+ }
+ print(join('\n', results) + '\n');
+}
+-- End --
+
+-- Expect stdout --
+parent parent-dedup-1.test.example.com: PRESENT
+parent parent-dedup-2.test.example.com: PRESENT
+parent parent-dedup-3.test.example.com: PRESENT
+child child.parent-dedup-1.test.example.com: REMOVED
+child sub.child.parent-dedup-2.test.example.com: REMOVED
+child deep.sub.parent-dedup-3.test.example.com: REMOVED
+-- End --
diff --git a/net/adblock-fast/tests/01_pipeline/04_allowed_domains b/net/adblock-fast/tests/01_pipeline/04_allowed_domains
new file mode 100644
index 0000000000..a54d03f7da
--- /dev/null
+++ b/net/adblock-fast/tests/01_pipeline/04_allowed_domains
@@ -0,0 +1,55 @@
+Test that allowed_domain config option removes domains and subdomains from output.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { readfile } from 'fs';
+let ti = adb._test_internals;
+
+adb.env.load_config();
+ti.set_cfg('dns', 'dnsmasq.servers');
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.set_cfg('allowed_domain', 'ad.doubleclick.test.example.com tracker.analytics.test.example.com');
+ti.env.dns_set_output_values('dnsmasq.servers');
+ti.append_urls();
+
+let ok = ti.download_lists();
+if (!ok) {
+ print('download_lists failed\n');
+} else {
+ let content = readfile(ti.dns_output.file) || '';
+
+ // These should be removed by allowed_domain
+ let allowed = [
+ 'ad.doubleclick.test.example.com',
+ 'tracker.analytics.test.example.com',
+ ];
+ // This should still be present
+ let blocked = [
+ 'pixel.tracking.test.example.com',
+ 'beacon.metrics.test.example.com',
+ ];
+
+ let results = [];
+ for (let a in allowed) {
+ // Check for block format "server=/domain/\n" — not allow format "server=/domain/#\n"
+ let found = index(content, 'server=/' + a + '/\n') >= 0;
+ push(results, sprintf('allowed %s: %s', a, found ? 'STILL PRESENT (BAD)' : 'REMOVED'));
+ }
+ for (let b in blocked) {
+ let found = index(content, 'server=/' + b + '/\n') >= 0;
+ push(results, sprintf('blocked %s: %s', b, found ? 'PRESENT' : 'MISSING'));
+ }
+ print(join('\n', results) + '\n');
+}
+-- End --
+
+-- Expect stdout --
+allowed ad.doubleclick.test.example.com: REMOVED
+allowed tracker.analytics.test.example.com: REMOVED
+blocked pixel.tracking.test.example.com: PRESENT
+blocked beacon.metrics.test.example.com: PRESENT
+-- End --
diff --git a/net/adblock-fast/tests/01_pipeline/05_canary_domains b/net/adblock-fast/tests/01_pipeline/05_canary_domains
new file mode 100644
index 0000000000..f1944d45ab
--- /dev/null
+++ b/net/adblock-fast/tests/01_pipeline/05_canary_domains
@@ -0,0 +1,45 @@
+Test that canary domains are injected when enabled.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { readfile } from 'fs';
+let ti = adb._test_internals;
+
+adb.env.load_config();
+ti.set_cfg('dns', 'dnsmasq.servers');
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.set_cfg('canary_domains_icloud', true);
+ti.set_cfg('canary_domains_mozilla', true);
+ti.env.dns_set_output_values('dnsmasq.servers');
+ti.append_urls();
+
+let ok = ti.download_lists();
+if (!ok) {
+ print('download_lists failed\n');
+} else {
+ let content = readfile(ti.dns_output.file) || '';
+
+ let canary_domains = [
+ 'mask.icloud.com',
+ 'mask-h2.icloud.com',
+ 'use-application-dns.net',
+ ];
+
+ let results = [];
+ for (let d in canary_domains) {
+ let found = index(content, 'server=/' + d + '/') >= 0;
+ push(results, sprintf('canary %s: %s', d, found ? 'PRESENT' : 'MISSING'));
+ }
+ print(join('\n', results) + '\n');
+}
+-- End --
+
+-- Expect stdout --
+canary mask.icloud.com: PRESENT
+canary mask-h2.icloud.com: PRESENT
+canary use-application-dns.net: PRESENT
+-- End --
diff --git a/net/adblock-fast/tests/01_pipeline/06_servers_mode_allow b/net/adblock-fast/tests/01_pipeline/06_servers_mode_allow
new file mode 100644
index 0000000000..561781a5c0
--- /dev/null
+++ b/net/adblock-fast/tests/01_pipeline/06_servers_mode_allow
@@ -0,0 +1,57 @@
+Test that dnsmasq.servers mode prepends explicit allow entries (server=/domain/#)
+when allowed_domain is set.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { readfile } from 'fs';
+let ti = adb._test_internals;
+
+adb.env.load_config();
+ti.set_cfg('dns', 'dnsmasq.servers');
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.set_cfg('allowed_domain', 'safe.example.com also-safe.example.com');
+ti.env.dns_set_output_values('dnsmasq.servers');
+ti.append_urls();
+
+let ok = ti.download_lists();
+if (!ok) {
+ print('download_lists failed\n');
+} else {
+ let content = readfile(ti.dns_output.file) || '';
+ let lines = filter(split(content, '\n'), l => length(l) > 0);
+
+ // In servers mode with allowed_domain, server=/domain/# entries are prepended
+ let allow_entries = filter(lines, l => match(l, /\/#$/));
+ let block_entries = filter(lines, l => match(l, /\/$/));
+
+ let results = [];
+ push(results, sprintf('allow_entries: %d', length(allow_entries)));
+ push(results, sprintf('block_entries: %d', length(block_entries)));
+
+ // Check specific allow entries
+ let has_safe = index(content, 'server=/safe.example.com/#') >= 0;
+ let has_also = index(content, 'server=/also-safe.example.com/#') >= 0;
+ push(results, sprintf('safe.example.com allow: %s', has_safe ? 'PRESENT' : 'MISSING'));
+ push(results, sprintf('also-safe.example.com allow: %s', has_also ? 'PRESENT' : 'MISSING'));
+
+ // Allow entries should be at the top
+ if (length(allow_entries) > 0 && length(lines) > 0) {
+ let first_is_allow = match(lines[0], /\/#$/);
+ push(results, sprintf('allow_entries_at_top: %s', first_is_allow ? 'YES' : 'NO'));
+ }
+
+ print(join('\n', results) + '\n');
+}
+-- End --
+
+-- Expect stdout --
+allow_entries: 2
+block_entries: 162
+safe.example.com allow: PRESENT
+also-safe.example.com allow: PRESENT
+allow_entries_at_top: YES
+-- End --
diff --git a/net/adblock-fast/tests/01_pipeline/07_ipv6_addnhosts b/net/adblock-fast/tests/01_pipeline/07_ipv6_addnhosts
new file mode 100644
index 0000000000..df36d90944
--- /dev/null
+++ b/net/adblock-fast/tests/01_pipeline/07_ipv6_addnhosts
@@ -0,0 +1,50 @@
+Test that dnsmasq.addnhosts with ipv6_enabled produces both
+127.0.0.1 and :: entries (dual-stack).
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { readfile } from 'fs';
+let ti = adb._test_internals;
+
+adb.env.load_config();
+ti.set_cfg('dns', 'dnsmasq.addnhosts');
+ti.set_cfg('ipv6_enabled', true);
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.env.dns_set_output_values('dnsmasq.addnhosts');
+ti.append_urls();
+
+let ok = ti.download_lists();
+if (!ok) {
+ print('download_lists failed\n');
+} else {
+ let content = readfile(ti.dns_output.file) || '';
+ let lines = filter(split(content, '\n'), l => length(l) > 0);
+
+ let ipv4_lines = filter(lines, l => match(l, /^127\.0\.0\.1 /));
+ let ipv6_lines = filter(lines, l => match(l, /^:: /));
+
+ let results = [];
+ push(results, sprintf('ipv4_count: %d', length(ipv4_lines)));
+ push(results, sprintf('ipv6_count: %d', length(ipv6_lines)));
+ push(results, sprintf('counts_match: %s', length(ipv4_lines) == length(ipv6_lines) ? 'YES' : 'NO'));
+
+ // Spot-check a specific domain appears in both
+ let test_domain = 'ad.doubleclick.test.example.com';
+ let has_ipv4 = index(content, '127.0.0.1 ' + test_domain) >= 0;
+ let has_ipv6 = index(content, ':: ' + test_domain) >= 0;
+ push(results, sprintf('dual_stack_%s: %s', test_domain, (has_ipv4 && has_ipv6) ? 'YES' : 'NO'));
+
+ print(join('\n', results) + '\n');
+}
+-- End --
+
+-- Expect stdout --
+ipv4_count: 165
+ipv6_count: 165
+counts_match: YES
+dual_stack_ad.doubleclick.test.example.com: YES
+-- End --
diff --git a/net/adblock-fast/tests/01_pipeline/08_ipv6_nftset b/net/adblock-fast/tests/01_pipeline/08_ipv6_nftset
new file mode 100644
index 0000000000..1c36d1786f
--- /dev/null
+++ b/net/adblock-fast/tests/01_pipeline/08_ipv6_nftset
@@ -0,0 +1,54 @@
+Test that dnsmasq.nftset with ipv6_enabled includes IPv6 set reference.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { readfile } from 'fs';
+let ti = adb._test_internals;
+
+adb.env.load_config();
+ti.set_cfg('dns', 'dnsmasq.nftset');
+ti.set_cfg('ipv6_enabled', true);
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.env.dns_set_output_values('dnsmasq.nftset');
+ti.append_urls();
+
+let ok = ti.download_lists();
+if (!ok) {
+ print('download_lists failed\n');
+} else {
+ let content = readfile(ti.dns_output.file) || '';
+ let lines = filter(split(content, '\n'), l => length(l) > 0);
+
+ // With IPv6 enabled, nftset format should include ,6#inet#fw4#adb6
+ let ipv6_lines = filter(lines, l => match(l, /6#inet#fw4#adb6/));
+ let ipv4_only = filter(lines, l => match(l, /4#inet#fw4#adb4/) && !match(l, /6#inet#fw4#adb6/));
+
+ let results = [];
+ push(results, sprintf('total_lines: %d', length(lines)));
+ push(results, sprintf('with_ipv6_set: %d', length(ipv6_lines)));
+ push(results, sprintf('ipv4_only: %d', length(ipv4_only)));
+
+ // All lines should have both IPv4 and IPv6 set references
+ push(results, sprintf('all_dual_stack: %s', (length(ipv6_lines) == length(lines)) ? 'YES' : 'NO'));
+
+ // Spot-check format
+ let test_domain = 'ad.doubleclick.test.example.com';
+ let expected = 'nftset=/' + test_domain + '/4#inet#fw4#adb4,6#inet#fw4#adb6';
+ let has_entry = index(content, expected) >= 0;
+ push(results, sprintf('correct_format: %s', has_entry ? 'YES' : 'NO'));
+
+ print(join('\n', results) + '\n');
+}
+-- End --
+
+-- Expect stdout --
+total_lines: 162
+with_ipv6_set: 162
+ipv4_only: 0
+all_dual_stack: YES
+correct_format: YES
+-- End --
diff --git a/net/adblock-fast/tests/01_pipeline/09_unbound_header b/net/adblock-fast/tests/01_pipeline/09_unbound_header
new file mode 100644
index 0000000000..dc73b1fff9
--- /dev/null
+++ b/net/adblock-fast/tests/01_pipeline/09_unbound_header
@@ -0,0 +1,48 @@
+Test that unbound.adb_list mode prepends "server:" header line.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { readfile } from 'fs';
+let ti = adb._test_internals;
+
+adb.env.load_config();
+ti.set_cfg('dns', 'unbound.adb_list');
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.env.dns_set_output_values('unbound.adb_list');
+ti.append_urls();
+
+let ok = ti.download_lists();
+if (!ok) {
+ print('download_lists failed\n');
+} else {
+ let content = readfile(ti.dns_output.file) || '';
+ let lines = filter(split(content, '\n'), l => length(l) > 0);
+
+ let results = [];
+
+ // First line should be "server:"
+ push(results, sprintf('first_line: %s', lines[0]));
+
+ // Rest should be local-zone entries
+ let lz_lines = filter(lines, l => match(l, /^local-zone: /));
+ push(results, sprintf('local_zone_entries: %d', length(lz_lines)));
+
+ // Spot-check format
+ let test_domain = 'ad.doubleclick.test.example.com';
+ let expected = 'local-zone: "' + test_domain + '." always_nxdomain';
+ let has_entry = index(content, expected) >= 0;
+ push(results, sprintf('correct_format: %s', has_entry ? 'YES' : 'NO'));
+
+ print(join('\n', results) + '\n');
+}
+-- End --
+
+-- Expect stdout --
+first_line: server:
+local_zone_entries: 162
+correct_format: YES
+-- End --
diff --git a/net/adblock-fast/tests/02_config/01_blocked_domain_injection b/net/adblock-fast/tests/02_config/01_blocked_domain_injection
new file mode 100644
index 0000000000..0caf3e069a
--- /dev/null
+++ b/net/adblock-fast/tests/02_config/01_blocked_domain_injection
@@ -0,0 +1,41 @@
+Test that cfg.blocked_domain entries are added to the output.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { readfile } from 'fs';
+let ti = adb._test_internals;
+
+adb.env.load_config();
+ti.set_cfg('dns', 'dnsmasq.servers');
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.set_cfg('blocked_domain', 'custom-block-1.example.net custom-block-2.example.net');
+ti.env.dns_set_output_values('dnsmasq.servers');
+ti.append_urls();
+
+let ok = ti.download_lists();
+if (!ok) {
+ print('download_lists failed\n');
+} else {
+ let content = readfile(ti.dns_output.file) || '';
+
+ let customs = [
+ 'custom-block-1.example.net',
+ 'custom-block-2.example.net',
+ ];
+ let results = [];
+ for (let d in customs) {
+ let found = index(content, 'server=/' + d + '/') >= 0;
+ push(results, sprintf('%s: %s', d, found ? 'PRESENT' : 'MISSING'));
+ }
+ print(join('\n', results) + '\n');
+}
+-- End --
+
+-- Expect stdout --
+custom-block-1.example.net: PRESENT
+custom-block-2.example.net: PRESENT
+-- End --
diff --git a/net/adblock-fast/tests/03_functional/01_check_domain b/net/adblock-fast/tests/03_functional/01_check_domain
new file mode 100644
index 0000000000..9b013eb82e
--- /dev/null
+++ b/net/adblock-fast/tests/03_functional/01_check_domain
@@ -0,0 +1,33 @@
+Test that check() correctly identifies blocked and unblocked domains.
+check() output goes to stderr via output.info/output.print when is_tty=true.
+
+-- Testcase --
+import adb from 'adblock-fast';
+let ti = adb._test_internals;
+
+// Build the blocklist
+adb.env.load_config();
+ti.set_cfg('dns', 'dnsmasq.servers');
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.env.dns_set_output_values('dnsmasq.servers');
+ti.append_urls();
+ti.download_lists();
+
+// Enable tty mode so output goes to stderr (captured by test framework)
+// Use verbosity=2 to get verbose output (which includes [PROC] prefix)
+ti.state.is_tty = true;
+ti.set_cfg('verbosity', 2);
+
+// Now test check()
+adb.check('ad.doubleclick.test.example.com this-domain-not-in-list.test.example.com');
+-- End --
+
+-- Expect stderr --
+[PROC] Found 1 match for 'ad.doubleclick.test.example.com' in 'TESTDIR/var_run/adblock-fast/dnsmasq.servers'.
+ad.doubleclick.test.example.com
+[PROC] The 'this-domain-not-in-list.test.example.com' is not found in current block-list ('TESTDIR/var_run/adblock-fast/dnsmasq.servers').
+-- End --
diff --git a/net/adblock-fast/tests/03_functional/02_show_blocklist b/net/adblock-fast/tests/03_functional/02_show_blocklist
new file mode 100644
index 0000000000..52f877a551
--- /dev/null
+++ b/net/adblock-fast/tests/03_functional/02_show_blocklist
@@ -0,0 +1,53 @@
+Test that show_blocklist() outputs parsed domains to stdout.
+
+-- Testcase --
+import adb from 'adblock-fast';
+import { popen } from 'fs';
+let ti = adb._test_internals;
+
+// Build the blocklist
+adb.env.load_config();
+ti.set_cfg('dns', 'dnsmasq.servers');
+ti.set_cfg('dnsmasq_sanity_check', false);
+ti.set_cfg('dnsmasq_validity_check', false);
+ti.set_cfg('heartbeat_domain', null);
+ti.set_cfg('config_update_enabled', false);
+ti.set_cfg('update_config_sizes', false);
+ti.env.dns_set_output_values('dnsmasq.servers');
+ti.append_urls();
+ti.download_lists();
+
+// show_blocklist() uses system() to run sed parse_filter on the output file,
+// which outputs plain domains to stdout. Count lines and check a few domains.
+let cmd = sprintf("sed '%s' %s", ti.dns_output.parse_filter, ti.dns_output.file);
+let p = popen(cmd, 'r');
+let out = p ? (p.read('all') || '') : '';
+if (p) p.close();
+
+let domains = filter(split(out, '\n'), l => length(l) > 0);
+let count = length(domains);
+
+let must_have = [
+ 'ad.doubleclick.test.example.com',
+ 'common-shared-1.test.example.com',
+ 'adhost-zero-1.test.example.org',
+];
+
+let domain_set = {};
+for (let d in domains) domain_set[d] = true;
+
+let results = [];
+push(results, sprintf('domain_count: %d', count));
+for (let d in must_have) {
+ push(results, sprintf('%s: %s', d, domain_set[d] ? 'PRESENT' : 'MISSING'));
+}
+
+print(join('\n', results) + '\n');
+-- End --
+
+-- Expect stdout --
+domain_count: 162
+ad.doubleclick.test.example.com: PRESENT
+common-shared-1.test.example.com: PRESENT
+adhost-zero-1.test.example.org: PRESENT
+-- End --
diff --git a/net/adblock-fast/tests/data/adblockplus.txt b/net/adblock-fast/tests/data/adblockplus.txt
new file mode 100644
index 0000000000..f2203da06e
--- /dev/null
+++ b/net/adblock-fast/tests/data/adblockplus.txt
@@ -0,0 +1,26 @@
+[Adblock Plus 2.0]
+! Title: Test ABP blocklist
+! Last modified: 2024-01-01
+! Homepage: https://test.example.com
+
+||abp-tracker-1.test.example.com^
+||abp-tracker-2.test.example.com^
+||abp-tracker-3.test.example.com^
+||abp-tracker-4.test.example.com^
+||abp-tracker-5.test.example.com^
+||abp-tracker-6.test.example.com^
+||abp-tracker-7.test.example.com^
+||abp-tracker-8.test.example.com^
+||abp-tracker-9.test.example.com^
+||abp-tracker-10.test.example.com^
+||abp-tracker-11.test.example.com^$third-party
+||abp-tracker-12.test.example.com^$third-party
+||abp-tracker-13.test.example.com^
+||abp-tracker-14.test.example.com^
+||abp-tracker-15.test.example.com^
+! Comment between entries
+||abp-tracker-16.test.example.com^
+||abp-tracker-17.test.example.com^
+||abp-tracker-18.test.example.com^
+||abp-tracker-19.test.example.com^
+||abp-tracker-20.test.example.com^
diff --git a/net/adblock-fast/tests/data/allowed.txt b/net/adblock-fast/tests/data/allowed.txt
new file mode 100644
index 0000000000..de12cea0bc
--- /dev/null
+++ b/net/adblock-fast/tests/data/allowed.txt
@@ -0,0 +1,6 @@
+# Allowed domains list (hosts format, used with action=allow)
+0.0.0.0 adhost-zero-1.test.example.org
+0.0.0.0 adhost-zero-2.test.example.org
+0.0.0.0 adhost-zero-3.test.example.org
+0.0.0.0 common-shared-1.test.example.com
+0.0.0.0 common-shared-2.test.example.com
diff --git a/net/adblock-fast/tests/data/dnsmasq_servers.txt b/net/adblock-fast/tests/data/dnsmasq_servers.txt
new file mode 100644
index 0000000000..26fb4e8694
--- /dev/null
+++ b/net/adblock-fast/tests/data/dnsmasq_servers.txt
@@ -0,0 +1,10 @@
+server=/dnsmasq-input-1.test.example.com/
+server=/dnsmasq-input-2.test.example.com/
+server=/dnsmasq-input-3.test.example.com/
+server=/dnsmasq-input-4.test.example.com/
+server=/dnsmasq-input-5.test.example.com/
+server=/dnsmasq-input-6.test.example.com/
+server=/dnsmasq-input-7.test.example.com/
+server=/dnsmasq-input-8.test.example.com/
+server=/dnsmasq-input-9.test.example.com/
+server=/dnsmasq-input-10.test.example.com/
diff --git a/net/adblock-fast/tests/data/domains.txt b/net/adblock-fast/tests/data/domains.txt
new file mode 100644
index 0000000000..27bef176bd
--- /dev/null
+++ b/net/adblock-fast/tests/data/domains.txt
@@ -0,0 +1,120 @@
+# Test domain blocklist for adblock-fast functional tests
+# Lines starting with # are comments and should be filtered out
+
+# -- Valid domains (85 unique) --
+ad.doubleclick.test.example.com
+ads.bigadnetwork.test.example.com
+tracker.analytics.test.example.com
+pixel.tracking.test.example.com
+beacon.metrics.test.example.com
+telemetry.data.test.example.com
+stats.counter.test.example.com
+banner.display.test.example.com
+popup.overlay.test.example.com
+interstitial.fullpage.test.example.com
+video.preroll.test.example.com
+native.sponsored.test.example.com
+widget.recommendation.test.example.com
+feed.promoted.test.example.com
+sidebar.adunit.test.example.com
+footer.adzone.test.example.com
+header.leaderboard.test.example.com
+skyscraper.tower.test.example.com
+rectangle.medium.test.example.com
+billboard.jumbo.test.example.com
+expandable.rich.test.example.com
+floating.sticky.test.example.com
+adhesion.anchor.test.example.com
+pushdown.slide.test.example.com
+catfish.bottom.test.example.com
+curtain.takeover.test.example.com
+skin.wrap.test.example.com
+roadblock.wall.test.example.com
+splash.welcome.test.example.com
+exit.intent.test.example.com
+retarget.remarket.test.example.com
+lookalike.audience.test.example.com
+segment.profile.test.example.com
+cookie.sync.test.example.com
+fingerprint.device.test.example.com
+supercookie.persist.test.example.com
+evercookie.track.test.example.com
+canvas.fp.test.example.com
+webgl.hash.test.example.com
+audio.context.test.example.com
+font.enum.test.example.com
+plugin.detect.test.example.com
+screen.res.test.example.com
+timezone.offset.test.example.com
+language.pref.test.example.com
+dnt.ignore.test.example.com
+referer.leak.test.example.com
+click.redirect.test.example.com
+impression.log.test.example.com
+conversion.pixel.test.example.com
+attribution.track.test.example.com
+viewability.measure.test.example.com
+brand.safety.test.example.com
+fraud.detect.test.example.com
+bot.filter.test.example.com
+programmatic.bid.test.example.com
+realtime.auction.test.example.com
+demand.side.test.example.com
+supply.platform.test.example.com
+exchange.market.test.example.com
+mediation.waterfall.test.example.com
+prebid.header.test.example.com
+openrtb.proto.test.example.com
+vast.vpaid.test.example.com
+mraid.sdk.test.example.com
+gdpr.consent.test.example.com
+ccpa.optout.test.example.com
+tcf.vendor.test.example.com
+cmp.dialog.test.example.com
+ab.testing.test.example.com
+multivariate.experiment.test.example.com
+heatmap.session.test.example.com
+scroll.depth.test.example.com
+funnel.analysis.test.example.com
+cohort.study.test.example.com
+survey.feedback.test.example.com
+nps.score.test.example.com
+crm.integration.test.example.com
+cdp.unify.test.example.com
+dmp.segment.test.example.com
+tag.manager.test.example.com
+container.script.test.example.com
+
+# -- Domains shared with hosts.txt (10 overlapping) --
+common-shared-1.test.example.com
+common-shared-2.test.example.com
+common-shared-3.test.example.com
+common-shared-4.test.example.com
+common-shared-5.test.example.com
+common-shared-6.test.example.com
+common-shared-7.test.example.com
+common-shared-8.test.example.com
+common-shared-9.test.example.com
+common-shared-10.test.example.com
+
+# -- Subdomain dedup test pairs (parent in this file) --
+parent-dedup-1.test.example.com
+parent-dedup-2.test.example.com
+parent-dedup-3.test.example.com
+
+# -- Invalid entries (should be filtered out) --
+# Comment only (filtered by /^#/d)
+192.168.1.1
+10.0.0.1
+127.0.0.1
+0.0.0.0
+255.255.255.255
+-starts-with-dash.test.example.com
+.starts-with-dot.test.example.com
+ends-with-dot.test.example.com.
+has..double.dots.test.example.com
+has spaces in it.test.example.com
+has@symbol.test.example.com
+has!bang.test.example.com
+nodot
+just a random sentence
diff --git a/net/adblock-fast/tests/data/hosts.txt b/net/adblock-fast/tests/data/hosts.txt
new file mode 100644
index 0000000000..fab876b2b9
--- /dev/null
+++ b/net/adblock-fast/tests/data/hosts.txt
@@ -0,0 +1,108 @@
+# Test hosts blocklist for adblock-fast functional tests
+# Title: StevenBlack Unified hosts (adware + malware)
+#
+# This block between "# Title: StevenBlack" and "# Custom host records"
+# is specifically removed by adblock-fast (lines 1589-1590).
+# Entries here should NOT appear in the output.
+# stevenblack-should-not-appear.test.example.com
+0.0.0.0 stevenblack-entry-hidden.test.example.com
+# Custom host records are listed here
+
+# -- Standard localhost entries (should be filtered) --
+127.0.0.1 localhost
+127.0.0.1 localhost.localdomain
+127.0.0.1 local
+0.0.0.0 0.0.0.0
+::1 localhost
+
+# -- Valid host entries with 0.0.0.0 prefix (40 unique) --
+0.0.0.0 adhost-zero-1.test.example.org
+0.0.0.0 adhost-zero-2.test.example.org
+0.0.0.0 adhost-zero-3.test.example.org
+0.0.0.0 adhost-zero-4.test.example.org
+0.0.0.0 adhost-zero-5.test.example.org
+0.0.0.0 adhost-zero-6.test.example.org
+0.0.0.0 adhost-zero-7.test.example.org
+0.0.0.0 adhost-zero-8.test.example.org
+0.0.0.0 adhost-zero-9.test.example.org
+0.0.0.0 adhost-zero-10.test.example.org
+0.0.0.0 adhost-zero-11.test.example.org
+0.0.0.0 adhost-zero-12.test.example.org
+0.0.0.0 adhost-zero-13.test.example.org
+0.0.0.0 adhost-zero-14.test.example.org
+0.0.0.0 adhost-zero-15.test.example.org
+0.0.0.0 adhost-zero-16.test.example.org
+0.0.0.0 adhost-zero-17.test.example.org
+0.0.0.0 adhost-zero-18.test.example.org
+0.0.0.0 adhost-zero-19.test.example.org
+0.0.0.0 adhost-zero-20.test.example.org
+0.0.0.0 adhost-zero-21.test.example.org
+0.0.0.0 adhost-zero-22.test.example.org
+0.0.0.0 adhost-zero-23.test.example.org
+0.0.0.0 adhost-zero-24.test.example.org
+0.0.0.0 adhost-zero-25.test.example.org
+0.0.0.0 adhost-zero-26.test.example.org
+0.0.0.0 adhost-zero-27.test.example.org
+0.0.0.0 adhost-zero-28.test.example.org
+0.0.0.0 adhost-zero-29.test.example.org
+0.0.0.0 adhost-zero-30.test.example.org
+0.0.0.0 adhost-zero-31.test.example.org
+0.0.0.0 adhost-zero-32.test.example.org
+0.0.0.0 adhost-zero-33.test.example.org
+0.0.0.0 adhost-zero-34.test.example.org
+0.0.0.0 adhost-zero-35.test.example.org
+0.0.0.0 adhost-zero-36.test.example.org
+0.0.0.0 adhost-zero-37.test.example.org
+0.0.0.0 adhost-zero-38.test.example.org
+0.0.0.0 adhost-zero-39.test.example.org
+0.0.0.0 adhost-zero-40.test.example.org
+
+# -- Valid host entries with 127.0.0.1 prefix (25 unique) --
+127.0.0.1 adhost-loopback-1.test.example.org
+127.0.0.1 adhost-loopback-2.test.example.org
+127.0.0.1 adhost-loopback-3.test.example.org
+127.0.0.1 adhost-loopback-4.test.example.org
+127.0.0.1 adhost-loopback-5.test.example.org
+127.0.0.1 adhost-loopback-6.test.example.org
+127.0.0.1 adhost-loopback-7.test.example.org
+127.0.0.1 adhost-loopback-8.test.example.org
+127.0.0.1 adhost-loopback-9.test.example.org
+127.0.0.1 adhost-loopback-10.test.example.org
+127.0.0.1 adhost-loopback-11.test.example.org
+127.0.0.1 adhost-loopback-12.test.example.org
+127.0.0.1 adhost-loopback-13.test.example.org
+127.0.0.1 adhost-loopback-14.test.example.org
+127.0.0.1 adhost-loopback-15.test.example.org
+127.0.0.1 adhost-loopback-16.test.example.org
+127.0.0.1 adhost-loopback-17.test.example.org
+127.0.0.1 adhost-loopback-18.test.example.org
+127.0.0.1 adhost-loopback-19.test.example.org
+127.0.0.1 adhost-loopback-20.test.example.org
+127.0.0.1 adhost-loopback-21.test.example.org
+127.0.0.1 adhost-loopback-22.test.example.org
+127.0.0.1 adhost-loopback-23.test.example.org
+127.0.0.1 adhost-loopback-24.test.example.org
+127.0.0.1 adhost-loopback-25.test.example.org
+
+# -- Domains shared with domains.txt (10 overlapping) --
+0.0.0.0 common-shared-1.test.example.com
+0.0.0.0 common-shared-2.test.example.com
+0.0.0.0 common-shared-3.test.example.com
+0.0.0.0 common-shared-4.test.example.com
+0.0.0.0 common-shared-5.test.example.com
+127.0.0.1 common-shared-6.test.example.com
+127.0.0.1 common-shared-7.test.example.com
+127.0.0.1 common-shared-8.test.example.com
+127.0.0.1 common-shared-9.test.example.com
+127.0.0.1 common-shared-10.test.example.com
+
+# -- Subdomain dedup test (children that should be removed when parent exists) --
+0.0.0.0 child.parent-dedup-1.test.example.com
+0.0.0.0 sub.child.parent-dedup-2.test.example.com
+0.0.0.0 deep.sub.parent-dedup-3.test.example.com
+
+# -- Invalid entries --
+some random text without IP prefix
+0.0.0.0
+127.0.0.1
+
diff --git a/net/adblock-fast/tests/lib/mocklib.uc b/net/adblock-fast/tests/lib/mocklib.uc
new file mode 100644
index 0000000000..fd2bbf0da0
--- /dev/null
+++ b/net/adblock-fast/tests/lib/mocklib.uc
@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Hybrid mocklib for adblock-fast functional tests.
+//
+// Key difference from mwan4's mocklib:
+// - Does NOT mock the 'fs' module (real filesystem operations)
+// - Selectively overrides system() to block service management
+// while passing through data-processing commands (sed/sort/grep/awk)
+// - Mocks only 'uci' and 'ubus'
+
+'use strict';
+
+if (!exists(global, 'REQUIRE_SEARCH_PATH'))
+ global.REQUIRE_SEARCH_PATH = [];
+
+if (!exists(global, 'MOCK_SEARCH_PATH'))
+ global.MOCK_SEARCH_PATH = [];
+
+if (!exists(global, 'TRACE_CALLS'))
+ global.TRACE_CALLS = null;
+
+let _fs = require("fs");
+
+// Force reloading uci and ubus modules so our mocks intercept them.
+// Do NOT delete fs -- we want the REAL filesystem module.
+delete global.modules.uci;
+delete global.modules.ubus;
+
+let _log = (level, fmt, ...args) => {
+ let color, prefix;
+
+ switch (level) {
+ case 'info':
+ color = 34;
+ prefix = '!';
+ break;
+ case 'warn':
+ color = 33;
+ prefix = 'W';
+ break;
+ case 'error':
+ color = 31;
+ prefix = 'E';
+ break;
+ default:
+ color = 0;
+ prefix = 'I';
+ }
+
+ let f = sprintf("\u001b[%d;1m[%s] %s\u001b[0m", color, prefix, fmt);
+ warn(replace(sprintf(f, ...args), "\n", "\n "), "\n");
+};
+
+let format_json = (data) => {
+ let rv;
+
+ let format_value = (value) => {
+ switch (type(value)) {
+ case "object":
+ return sprintf("{ /* %d keys */ }", length(value));
+ case "array":
+ return sprintf("[ /* %d items */ ]", length(value));
+ case "string":
+ if (length(value) > 64)
+ value = substr(value, 0, 64) + "...";
+ return sprintf("%J", value);
+ default:
+ return sprintf("%J", value);
+ }
+ };
+
+ switch (type(data)) {
+ case "object":
+ rv = "{";
+ let k = sort(keys(data));
+ for (let i, n in k)
+ rv += sprintf("%s %J: %s", i ? "," : "", n, format_value(data[n]));
+ rv += " }";
+ break;
+ case "array":
+ rv = "[";
+ for (let i, v in data)
+ rv += (i ? "," : "") + " " + format_value(v);
+ rv += " ]";
+ break;
+ default:
+ rv = format_value(data);
+ }
+
+ return rv;
+};
+
+let read_data_file = (path) => {
+ for (let dir in MOCK_SEARCH_PATH) {
+ let fd = _fs.open(dir + '/' + path, "r");
+
+ if (fd) {
+ let data = fd.read("all");
+ fd.close();
+ return data;
+ }
+ }
+
+ return null;
+};
+
+let read_json_file = (path) => {
+ let data = read_data_file(path);
+
+ if (data != null) {
+ try {
+ return json(data);
+ }
+ catch (e) {
+ _log('error', "Unable to parse JSON data in %s: %s", path, e);
+ return NaN;
+ }
+ }
+
+ return null;
+};
+
+let trace_call = (ns, func, args) => {
+ let msg = "[call] " +
+ (ns ? ns + "." : "") +
+ func;
+
+ for (let k, v in args) {
+ msg += ' ' + k + ' <';
+
+ switch (type(v)) {
+ case "array":
+ case "object":
+ msg += format_json(v);
+ break;
+ default:
+ msg += v;
+ }
+
+ msg += '>';
+ }
+
+ switch (TRACE_CALLS) {
+ case '1':
+ case 'stdout':
+ _fs.stdout.write(msg + "\n");
+ break;
+ case 'stderr':
+ _fs.stderr.write(msg + "\n");
+ break;
+ }
+};
+
+// Prepend mocklib/ to REQUIRE_SEARCH_PATH so mock uci/ubus are found
+for (let pattern in REQUIRE_SEARCH_PATH) {
+ if (!match(pattern, /\*\.uc$/))
+ continue;
+
+ let path = replace(pattern, /\*/, 'mocklib'),
+ s = _fs.stat(path);
+
+ if (!s || s.type != 'file')
+ continue;
+
+ if (!length(global.MOCK_SEARCH_PATH))
+ global.MOCK_SEARCH_PATH = [ replace(path, /mocklib\.uc$/, '../mocks') ];
+
+ unshift(REQUIRE_SEARCH_PATH, replace(path, /mocklib\.uc$/, 'mocklib/*.uc'));
+ break;
+}
+
+if (!length(global.MOCK_SEARCH_PATH))
+ global.MOCK_SEARCH_PATH = [ './mocks' ];
+
+// Register global mocklib namespace
+global.mocklib = {
+ require: function(module) {
+ let path, res, ex;
+
+ if (type(REQUIRE_SEARCH_PATH) == "array" && index(REQUIRE_SEARCH_PATH[0], 'mocklib/*.uc') != -1)
+ path = shift(REQUIRE_SEARCH_PATH);
+
+ try {
+ res = require(module);
+ }
+ catch (e) {
+ ex = e;
+ }
+
+ if (path)
+ unshift(REQUIRE_SEARCH_PATH, path);
+
+ if (ex)
+ die(ex);
+
+ return res;
+ },
+
+ I: (...args) => _log('info', ...args),
+ N: (...args) => _log('notice', ...args),
+ W: (...args) => _log('warn', ...args),
+ E: (...args) => _log('error', ...args),
+
+ format_json,
+ read_data_file,
+ read_json_file,
+ trace_call,
+};
+
+// Selectively override system() -- block service management, pass through data processing
+global.system = ((_orig_system) => function(argv, timeout) {
+ let cmd = '' + argv;
+
+ // Block commands that interact with system services or would hang
+ if (match(cmd, /^\/etc\/init\.d\//) ||
+ match(cmd, /\/usr\/bin\/logger\b/) ||
+ match(cmd, /^logger\b/) ||
+ match(cmd, /^sleep\b/) ||
+ match(cmd, /^resolveip\b/) ||
+ match(cmd, /^dnsmasq\s+--test/) ||
+ match(cmd, /^ipset\b/) ||
+ match(cmd, /^nft\b/) ||
+ match(cmd, /^chmod\b/) ||
+ match(cmd, /^chown\b/)) {
+ trace_call(null, "system[blocked]", { command: cmd });
+ return 0;
+ }
+
+ // Pass through real commands for data processing
+ return _orig_system(argv, timeout);
+})(global.system);
+
+// Override time() to return fixed value for reproducible tests
+global.time = function() {
+ return 1615382640;
+};
+
+// Override getenv -- return null to prevent env interference
+global.getenv = function(key) {
+ return null;
+};
+
+return global.mocklib;
diff --git a/net/adblock-fast/tests/lib/mocklib/ubus.uc b/net/adblock-fast/tests/lib/mocklib/ubus.uc
new file mode 100644
index 0000000000..ee6d487884
--- /dev/null
+++ b/net/adblock-fast/tests/lib/mocklib/ubus.uc
@@ -0,0 +1,69 @@
+// UBus mock for adblock-fast tests.
+// Reused from mwan4's mock with no changes.
+
+let mocklib = global.mocklib; // ucode-lsp disable
+
+return {
+ connect: function() {
+ let self = this;
+
+ return {
+ call: (object, method, args) => {
+ let signature = [ object + "~" + method ];
+
+ if (type(args) == "object") {
+ for (let i, k in sort(keys(args))) {
+ switch (type(args[k])) {
+ case "string":
+ case "double":
+ case "bool":
+ case "int":
+ push(signature, k + "-" + replace(args[k], /[^A-Za-z0-9_-]+/g, "_"));
+ break;
+
+ default:
+ push(signature, type(args[k]));
+ }
+ }
+ }
+
+ let candidates = [];
+
+ for (let i = length(signature); i > 0; i--) {
+ let path = sprintf("ubus/%s.json", join("~", signature)),
+ mock = mocklib.read_json_file(path);
+
+ if (mock != mock) {
+ self._error = "Invalid argument";
+
+ return null;
+ }
+ else if (mock) {
+ mocklib.trace_call("ctx", "call", { object, method, args });
+
+ return mock;
+ }
+
+ push(candidates, path);
+ pop(signature);
+ }
+
+ // Return null silently for unmatched calls (non-critical in tests)
+ self._error = "Method not found";
+
+ return null;
+ },
+
+ disconnect: () => null,
+
+ error: () => self.error()
+ };
+ },
+
+ error: function() {
+ let e = this._error;
+ delete this._error;
+
+ return e;
+ }
+};
diff --git a/net/adblock-fast/tests/lib/mocklib/uci.uc b/net/adblock-fast/tests/lib/mocklib/uci.uc
new file mode 100644
index 0000000000..501ca1c92d
--- /dev/null
+++ b/net/adblock-fast/tests/lib/mocklib/uci.uc
@@ -0,0 +1,213 @@
+// UCI mock for adblock-fast tests.
+// Based on mwan4's UCI mock, extended with set/save/commit/changes/delete.
+
+let mocklib = global.mocklib; // ucode-lsp disable
+
+let byte = (str, off) => { // ucode-lsp disable
+ let v = ord(str, off);
+ return length(v) ? v[0] : v;
+};
+
+let hash = (s) => { // ucode-lsp disable
+ let h = 7;
+
+ for (let i = 0; i < length(s); i++)
+ h = h * 31 + byte(s, i);
+
+ return h;
+};
+
+let id = (config, t, n) => { // ucode-lsp disable
+ while (true) {
+ let id = sprintf('cfg%08x', hash(t + n));
+
+ if (!exists(config, id))
+ return id;
+
+ n++;
+ }
+};
+
+let fixup_config = (config) => { // ucode-lsp disable
+ let rv = {};
+ let n_section = 0;
+
+ for (let stype in config) {
+ switch (type(config[stype])) {
+ case 'object':
+ config[stype] = [ config[stype] ];
+ /* fall through */
+
+ case 'array':
+ for (let idx, sobj in config[stype]) {
+ let sid, anon;
+
+ if (exists(sobj, '.name') && !exists(rv, sobj['.name'])) {
+ sid = sobj['.name'];
+ anon = false;
+ }
+ else {
+ sid = id(rv, stype, idx);
+ anon = true;
+ }
+
+ rv[sid] = {
+ '.index': n_section++,
+ ...sobj,
+ '.name': sid,
+ '.type': stype,
+ '.anonymous': anon
+ };
+ }
+
+ break;
+ }
+ }
+
+ for (let n, sid in sort(keys(rv), (a, b) => rv[a]['.index'] - rv[b]['.index']))
+ rv[sid]['.index'] = n;
+
+ return rv;
+};
+
+return {
+ cursor: () => ({
+ _configs: {},
+ _dirty: {},
+
+ load: function(file) {
+ let basename = replace(file, /^.+\//, ''),
+ path = sprintf("uci/%s.json", basename),
+ mock = mocklib.read_json_file(path);
+
+ if (!mock || mock != mock) {
+ mocklib.I("No configuration fixture defined for uci package %s.", file);
+ mocklib.I("Provide a mock configuration through the following JSON file:\n%s\n", path);
+
+ return null;
+ }
+
+ this._configs[basename] = fixup_config(mock);
+ },
+
+ _get_section: function(config, section) {
+ if (!exists(this._configs, config)) {
+ this.load(config);
+
+ if (!exists(this._configs, config))
+ return null;
+ }
+
+ let cfg = this._configs[config],
+ extended = match(section, "^@([A-Za-z0-9_-]+)\\[(-?[0-9]+)\\]$");
+
+ if (extended) {
+ let stype = extended[1],
+ sindex = +extended[2];
+
+ let sids = sort(
+ filter(keys(cfg), sid => cfg[sid]['.type'] == stype),
+ (a, b) => cfg[a]['.index'] - cfg[b]['.index']
+ );
+
+ if (sindex < 0)
+ sindex = sids.length + sindex;
+
+ return cfg[sids[sindex]];
+ }
+
+ return cfg[section];
+ },
+
+ get: function(config, section, option) {
+ let sobj = this._get_section(config, section);
+
+ if (option && index(option, ".") == 0)
+ return null;
+ else if (sobj && option)
+ return sobj[option];
+ else if (sobj)
+ return sobj[".type"];
+ },
+
+ get_all: function(config, section) {
+ return section ? this._get_section(config, section) : this._configs[config];
+ },
+
+ foreach: function(config, stype, cb) {
+ let rv = false;
+
+ if (!exists(this._configs, config))
+ this.load(config);
+
+ if (exists(this._configs, config)) {
+ let cfg = this._configs[config],
+ sids = sort(keys(cfg), (a, b) => cfg[a]['.index'] - cfg[b]['.index']);
+
+ for (let i, sid in sids) {
+ if (stype == null || cfg[sid]['.type'] == stype) {
+ if (cb({ ...(cfg[sid]) }) === false)
+ break;
+
+ rv = true;
+ }
+ }
+ }
+
+ return rv;
+ },
+
+ // -- Extensions for adblock-fast --
+
+ set: function(config, section, option, value) {
+ let sobj = this._get_section(config, section);
+ if (sobj) {
+ sobj[option] = value;
+ this._dirty[config] = true;
+ }
+ },
+
+ save: function(config) {
+ return true;
+ },
+
+ commit: function(config) {
+ delete this._dirty[config];
+ return true;
+ },
+
+ changes: function(config) {
+ return this._dirty[config] ? [['set']] : [];
+ },
+
+ revert: function(config) {
+ delete this._dirty[config];
+ },
+
+ delete: function(config, section, option) {
+ if (option) {
+ let sobj = this._get_section(config, section);
+ if (sobj) delete sobj[option];
+ } else if (section) {
+ if (exists(this._configs, config))
+ delete this._configs[config][section];
+ }
+ },
+
+ list_add: function(config, section, option, value) {
+ let sobj = this._get_section(config, section);
+ if (!sobj) return;
+ if (type(sobj[option]) != 'array')
+ sobj[option] = sobj[option] ? [sobj[option]] : [];
+ push(sobj[option], value);
+ this._dirty[config] = true;
+ },
+
+ list_remove: function(config, section, option, value) {
+ let sobj = this._get_section(config, section);
+ if (!sobj || type(sobj[option]) != 'array') return;
+ sobj[option] = filter(sobj[option], v => v != value);
+ this._dirty[config] = true;
+ },
+ })
+};
diff --git a/net/adblock-fast/tests/mocks/ubus/network.interface.wan~status.json b/net/adblock-fast/tests/mocks/ubus/network.interface.wan~status.json
new file mode 100644
index 0000000000..2ae5bcc60b
--- /dev/null
+++ b/net/adblock-fast/tests/mocks/ubus/network.interface.wan~status.json
@@ -0,0 +1,23 @@
+{
+ "up": true,
+ "pending": false,
+ "available": true,
+ "autostart": true,
+ "device": "eth0",
+ "l3_device": "eth0",
+ "proto": "dhcp",
+ "ipv4-address": [
+ {
+ "address": "192.168.1.100",
+ "mask": 24
+ }
+ ],
+ "route": [
+ {
+ "target": "0.0.0.0",
+ "mask": 0,
+ "nexthop": "192.168.1.1",
+ "source": "192.168.1.100/24"
+ }
+ ]
+}
diff --git a/net/adblock-fast/tests/mocks/ubus/network.interface~dump.json b/net/adblock-fast/tests/mocks/ubus/network.interface~dump.json
new file mode 100644
index 0000000000..2d9b893b1f
--- /dev/null
+++ b/net/adblock-fast/tests/mocks/ubus/network.interface~dump.json
@@ -0,0 +1,24 @@
+{
+ "interface": [
+ {
+ "interface": "loopback",
+ "up": true,
+ "device": "lo",
+ "l3_device": "lo",
+ "route": []
+ },
+ {
+ "interface": "wan",
+ "up": true,
+ "device": "eth0",
+ "l3_device": "eth0",
+ "route": [
+ {
+ "target": "0.0.0.0",
+ "mask": 0,
+ "nexthop": "192.168.1.1"
+ }
+ ]
+ }
+ ]
+}
diff --git a/net/adblock-fast/tests/mocks/ubus/service~list~name-dnsmasq.json b/net/adblock-fast/tests/mocks/ubus/service~list~name-dnsmasq.json
new file mode 100644
index 0000000000..212fb2e526
--- /dev/null
+++ b/net/adblock-fast/tests/mocks/ubus/service~list~name-dnsmasq.json
@@ -0,0 +1,11 @@
+{
+ "dnsmasq": {
+ "instances": {
+ "cfg01411c": {
+ "running": true,
+ "pid": 1234,
+ "command": [ "/usr/sbin/dnsmasq", "-C", "/var/etc/dnsmasq.conf.cfg01411c", "-k", "-x", "/var/run/dnsmasq/dnsmasq.cfg01411c.pid" ]
+ }
+ }
+ }
+}
diff --git a/net/adblock-fast/tests/mocks/ubus/system~info.json b/net/adblock-fast/tests/mocks/ubus/system~info.json
new file mode 100644
index 0000000000..c61c5b7471
--- /dev/null
+++ b/net/adblock-fast/tests/mocks/ubus/system~info.json
@@ -0,0 +1,17 @@
+{
+ "memory": {
+ "total": 536870912,
+ "available": 268435456,
+ "free": 134217728,
+ "cached": 67108864,
+ "buffered": 33554432,
+ "shared": 4194304
+ },
+ "swap": {
+ "total": 0,
+ "free": 0
+ },
+ "uptime": 86400,
+ "localtime": 1615382640,
+ "load": [ 0, 0, 0 ]
+}
diff --git a/net/adblock-fast/tests/mocks/uci/adblock-fast.json b/net/adblock-fast/tests/mocks/uci/adblock-fast.json
new file mode 100644
index 0000000000..e30ea2036e
--- /dev/null
+++ b/net/adblock-fast/tests/mocks/uci/adblock-fast.json
@@ -0,0 +1,44 @@
+{
+ "config": [
+ {
+ ".name": "config",
+ ".type": "config",
+ "enabled": "1",
+ "dns": "dnsmasq.servers",
+ "verbosity": "2",
+ "force_dns": "0",
+ "compressed_cache": "0",
+ "config_update_enabled": "0",
+ "ipv6_enabled": "0",
+ "canary_domains_icloud": "0",
+ "canary_domains_mozilla": "0",
+ "dnsmasq_sanity_check": "0",
+ "dnsmasq_validity_check": "0",
+ "parallel_downloads": "0",
+ "allow_non_ascii": "0",
+ "update_config_sizes": "0",
+ "heartbeat_domain": "-",
+ "download_timeout": "10",
+ "curl_retry": "1",
+ "compressed_cache_dir": "TESTDIR/cache"
+ }
+ ],
+ "file_url": [
+ {
+ ".name": "blocked_domains",
+ ".type": "file_url",
+ "enabled": "1",
+ "url": "file://TESTDIR/data/domains.txt",
+ "action": "block",
+ "name": "Test Domains"
+ },
+ {
+ ".name": "blocked_hosts",
+ ".type": "file_url",
+ "enabled": "1",
+ "url": "file://TESTDIR/data/hosts.txt",
+ "action": "block",
+ "name": "Test Hosts"
+ }
+ ]
+}
diff --git a/net/adblock-fast/tests/mocks/uci/dhcp.json b/net/adblock-fast/tests/mocks/uci/dhcp.json
new file mode 100644
index 0000000000..d800ffad66
--- /dev/null
+++ b/net/adblock-fast/tests/mocks/uci/dhcp.json
@@ -0,0 +1,23 @@
+{
+ "dnsmasq": [
+ {
+ ".name": "cfg01411c",
+ ".type": "dnsmasq",
+ ".anonymous": true,
+ "domainneeded": "1",
+ "localise_queries": "1",
+ "rebind_protection": "1",
+ "rebind_localhost": "1",
+ "local": "/lan/",
+ "domain": "lan",
+ "expandhosts": "1",
+ "cachesize": "1000",
+ "authoritative": "1",
+ "readethers": "1",
+ "leasefile": "/tmp/dhcp.leases",
+ "resolvfile": "/tmp/resolv.conf.d/resolv.conf.auto",
+ "localservice": "1",
+ "ednspacket_max": "1232"
+ }
+ ]
+}
diff --git a/net/adblock-fast/tests/mocks/uci/network.json b/net/adblock-fast/tests/mocks/uci/network.json
new file mode 100644
index 0000000000..5b33d2007f
--- /dev/null
+++ b/net/adblock-fast/tests/mocks/uci/network.json
@@ -0,0 +1,18 @@
+{
+ "interface": [
+ {
+ ".name": "loopback",
+ ".type": "interface",
+ "device": "lo",
+ "proto": "static",
+ "ipaddr": "127.0.0.1",
+ "netmask": "255.0.0.0"
+ },
+ {
+ ".name": "wan",
+ ".type": "interface",
+ "device": "eth0",
+ "proto": "dhcp"
+ }
+ ]
+}
diff --git a/net/adblock-fast/tests/mocks/uci/smartdns.json b/net/adblock-fast/tests/mocks/uci/smartdns.json
new file mode 100644
index 0000000000..a4885b609a
--- /dev/null
+++ b/net/adblock-fast/tests/mocks/uci/smartdns.json
@@ -0,0 +1,9 @@
+{
+ "smartdns": [
+ {
+ ".name": "smartdns",
+ ".type": "smartdns",
+ "enabled": "0"
+ }
+ ]
+}
diff --git a/net/adblock-fast/tests/mocks/uci/unbound.json b/net/adblock-fast/tests/mocks/uci/unbound.json
new file mode 100644
index 0000000000..46b13b58b5
--- /dev/null
+++ b/net/adblock-fast/tests/mocks/uci/unbound.json
@@ -0,0 +1,9 @@
+{
+ "unbound": [
+ {
+ ".name": "unbound",
+ ".type": "unbound",
+ "enabled": "0"
+ }
+ ]
+}
diff --git a/net/adblock-fast/tests/run_tests.sh b/net/adblock-fast/tests/run_tests.sh
new file mode 100644
index 0000000000..90eb12b57b
--- /dev/null
+++ b/net/adblock-fast/tests/run_tests.sh
@@ -0,0 +1,381 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# Functional test runner for adblock-fast.
+#
+# Adapts the mwan4 mock-and-expect pattern for adblock-fast:
+# - Patches ES module imports to require() calls
+# - Redirects hardcoded paths to a temp directory
+# - Exports internal functions for test access
+# - Uses real shell commands (sed/sort/grep/awk) with mock UCI/UBus
+#
+# Usage: cd source.openwrt.melmac.ca/adblock-fast && bash tests/run_tests.sh [test_file...]
+
+set -o pipefail
+
+line='........................................'
+
+# ── Temp directories ─────────────────────────────────────────────────
+
+TESTDIR="/tmp/adb_test.$$"
+patch_dir="/tmp/adb_test_modules.$$"
+stub_dir="$TESTDIR/stubs"
+
+mkdir -p "$TESTDIR"/{var_run/adblock-fast,var,shm,var_lib_unbound,etc,cache,tmp}
+mkdir -p "$patch_dir"
+mkdir -p "$stub_dir"
+
+trap "rm -rf '$TESTDIR' '$patch_dir'" EXIT
+
+# ── Copy test data ───────────────────────────────────────────────────
+
+cp -r ./tests/data "$TESTDIR/data"
+
+# ── Prepare resolved mock fixtures (replace TESTDIR placeholder) ─────
+
+mkdir -p "$TESTDIR/mocks_resolved/uci" "$TESTDIR/mocks_resolved/ubus"
+for f in ./tests/mocks/uci/*.json; do
+ sed "s|TESTDIR|$TESTDIR|g" "$f" > "$TESTDIR/mocks_resolved/uci/$(basename "$f")"
+done
+for f in ./tests/mocks/ubus/*.json; do
+ cp "$f" "$TESTDIR/mocks_resolved/ubus/$(basename "$f")"
+done
+
+# ── Create resolver stubs ───────────────────────────────────────────
+
+cat > "$stub_dir/dnsmasq" << 'STUB'
+#!/bin/sh
+case "$1" in
+ --version)
+ echo "Dnsmasq version 2.89"
+ echo "Compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset nftset auth no-cryptohash no-DNSSEC loop-detect inotify dumpfile"
+ ;;
+ --test)
+ echo "dnsmasq: syntax check OK."
+ exit 0
+ ;;
+esac
+STUB
+chmod +x "$stub_dir/dnsmasq"
+
+for cmd in smartdns unbound; do
+ printf '#!/bin/sh\nexit 0\n' > "$stub_dir/$cmd"
+ chmod +x "$stub_dir/$cmd"
+done
+
+# Create ipset/nft stubs
+for cmd in ipset nft; do
+ printf '#!/bin/sh\nexit 0\n' > "$stub_dir/$cmd"
+ chmod +x "$stub_dir/$cmd"
+done
+
+# Create resolveip stub
+cat > "$stub_dir/resolveip" << 'STUB'
+#!/bin/sh
+echo "127.0.0.1"
+exit 0
+STUB
+chmod +x "$stub_dir/resolveip"
+
+# ── Patch adblock-fast.uc ───────────────────────────────────────────
+
+# The sed pipeline:
+# 1. Convert ES module imports to require() calls
+# 2. Redirect hardcoded paths to TESTDIR
+# 3. Extend is_present() search paths with stub_dir
+# 4. Export internal test helpers
+
+sed \
+ -e "s|import { readfile, writefile, popen, stat, unlink, rename, open, glob, mkdir, mkstemp, symlink, chmod, chown, realpath, lsdir, access, dirname } from 'fs';|let _fs = require('fs'), readfile = _fs.readfile, writefile = _fs.writefile, popen = _fs.popen, stat = _fs.stat, unlink = _fs.unlink, rename = _fs.rename, open = _fs.open, glob = _fs.glob, mkdir = _fs.mkdir, mkstemp = _fs.mkstemp, symlink = _fs.symlink, chmod = _fs.chmod, chown = _fs.chown, realpath = _fs.realpath, lsdir = _fs.lsdir, access = _fs.access, dirname = _fs.dirname;|" \
+ -e "s|import { cursor } from 'uci';|let _uci = require('uci'), cursor = _uci.cursor;|" \
+ -e "s|import { connect } from 'ubus';|let _ubus = require('ubus'), connect = _ubus.connect;|" \
+ -e "s|dnsmasq_file: '/var/run/adblock-fast/adblock-fast.dnsmasq'|dnsmasq_file: '${TESTDIR}/var_run/adblock-fast/adblock-fast.dnsmasq'|" \
+ -e "s|config_file: '/etc/config/adblock-fast'|config_file: '${TESTDIR}/etc/adblock-fast'|" \
+ -e "s|run_file: '/dev/shm/adblock-fast'|run_file: '${TESTDIR}/shm/adblock-fast'|" \
+ -e "s|status_file: '/dev/shm/adblock-fast.status.json'|status_file: '${TESTDIR}/shm/adblock-fast.status.json'|" \
+ -e "s|'/var/run/' + pkg.name|'${TESTDIR}/var_run/' + pkg.name|g" \
+ -e "s|'/var/lib/unbound/adb_list.' + pkg.name|'${TESTDIR}/var_run/' + pkg.name + '/adb_list.' + pkg.name|g" \
+ -e "s|'/var/' + pkg.name|'${TESTDIR}/var/' + pkg.name|g" \
+ -e "s|for (let dir in \['/usr/sbin', '/usr/bin', '/sbin', '/bin'\])|for (let dir in ['${stub_dir}', '/usr/sbin', '/usr/bin', '/sbin', '/bin'])|" \
+ -e "s|stat('/etc/config/dhcp')|stat('${TESTDIR}/etc/dhcp')|g" \
+ -e "s|stat('/etc/config/smartdns')|stat('${TESTDIR}/etc/smartdns')|g" \
+ ./files/lib/adblock-fast/adblock-fast.uc > "$patch_dir/adblock-fast.uc"
+
+# Append test-helper exports to the patched module.
+# We add a _test_internals object that gives tests access to module-private state.
+# NOTE: cfg is accessed via get_cfg()/set_cfg() because env.load_config()
+# reassigns cfg, which would make a direct reference stale.
+sed -i '/^export default {/,/^};/{
+ /process_file_url,/a\
+\t// Test helpers (injected by test runner)\
+\t_test_internals: {\
+\t\tdownload_lists: download_lists,\
+\t\tdetect_file_type: detect_file_type,\
+\t\tdns_modes: dns_modes,\
+\t\tget_cfg: function() { return cfg; },\
+\t\tset_cfg: function(k, v) { cfg[k] = v; },\
+\t\tstate: state,\
+\t\tenv: env,\
+\t\tdns_output: dns_output,\
+\t\tstatus_data: status_data,\
+\t\tlist_formats: list_formats,\
+\t\ttmp: tmp,\
+\t\tappend_urls: append_urls,\
+\t\tcount_lines: count_lines,\
+\t\tcount_blocked_domains: count_blocked_domains,\
+\t},
+}' "$patch_dir/adblock-fast.uc"
+
+# Patch cli.uc too (for tests that exercise the CLI path)
+sed \
+ -e "s|import adb from 'adblock-fast';|let adb = require('adblock-fast');|" \
+ ./files/lib/adblock-fast/cli.uc > "$patch_dir/cli.uc"
+
+# ── Set up environment ───────────────────────────────────────────────
+
+export TMPDIR="$TESTDIR/tmp"
+export PATH="$stub_dir:$PATH"
+
+# ucode invocation: patched module first, then mocklib, then original source
+ucode="ucode -S -L$patch_dir -L./tests/lib -L./files/lib/adblock-fast"
+
+# ── Test framework (adapted from mwan4) ──────────────────────────────
+
+extract_sections() {
+ local file=$1
+ local dir=$2
+ local count=0
+ local tag line outfile
+
+ while IFS= read -r line; do
+ case "$line" in
+ "-- Testcase --")
+ tag="test"
+ count=$((count + 1))
+ outfile=$(printf "%s/%03d.in" "$dir" $count)
+ printf "" > "$outfile"
+ ;;
+ "-- Environment --")
+ tag="env"
+ count=$((count + 1))
+ outfile=$(printf "%s/%03d.env" "$dir" $count)
+ printf "" > "$outfile"
+ ;;
+ "-- Expect stdout --"|"-- Expect stderr --"|"-- Expect exitcode --")
+ tag="${line#-- Expect }"
+ tag="${tag% --}"
+ count=$((count + 1))
+ outfile=$(printf "%s/%03d.%s" "$dir" $count "$tag")
+ printf "" > "$outfile"
+ ;;
+ "-- File "*" --")
+ tag="file"
+ outfile="${line#-- File }"
+ outfile="$(echo "${outfile% --}" | xargs)"
+ outfile="$dir/files$(readlink -m "/${outfile:-file}")"
+ mkdir -p "$(dirname "$outfile")"
+ printf "" > "$outfile"
+ ;;
+ "-- End --")
+ tag=""
+ outfile=""
+ ;;
+ *)
+ if [ -n "$tag" ]; then
+ printf "%s\\n" "$line" >> "$outfile"
+ fi
+ ;;
+ esac
+ done < "$file"
+
+ # Post-process: replace TESTDIR placeholder in extracted files
+ # - files/ directory (mock data)
+ # - expect sections (.stdout, .stderr) so tests can reference TESTDIR paths
+ # NOTE: Do NOT substitute in .in files — those use TESTDIR as a ucode global variable
+ find "$dir/files" -type f 2>/dev/null | while read -r f; do
+ sed -i "s|TESTDIR|$TESTDIR|g" "$f"
+ done
+ for f in "$dir"/*.stdout "$dir"/*.stderr; do
+ [ -f "$f" ] && sed -i "s|TESTDIR|$TESTDIR|g" "$f"
+ done
+
+ return $(ls -l "$dir/"*.in 2>/dev/null | wc -l)
+}
+
+run_testcase() {
+ local num=$1
+ local dir=$2
+ local in=$3
+ local env=$4
+ local out=$5
+ local err=$6
+ local code=$7
+ local fail=0
+
+ # Clean test state between runs
+ rm -rf "$TESTDIR"/var_run/adblock-fast/*
+ rm -f "$TESTDIR"/var/adblock-fast.*
+ rm -f "$TESTDIR"/shm/adblock-fast*
+ rm -f "$TESTDIR"/var_lib_unbound/*
+ rm -f "$TESTDIR"/tmp/adblock-fast*
+ mkdir -p "$TESTDIR"/var_run/adblock-fast
+
+ $ucode \
+ -D MOCK_SEARCH_PATH='["'"$dir"'/files", "'"$TESTDIR"'/mocks_resolved", "./tests/mocks"]' \
+ -D TESTDIR='"'"$TESTDIR"'"' \
+ ${env:+-F "$env"} \
+ -l mocklib \
+ - <"$in" >"$dir/res.out" 2>"$dir/res.err"
+
+ printf "%d\n" $? > "$dir/res.code"
+
+ touch "$dir/empty"
+
+ if ! cmp -s "$dir/res.err" "${err:-$dir/empty}"; then
+ [ $fail = 0 ] && printf "!\n"
+ printf "Testcase #%d: Expected stderr did not match:\n" $num
+ diff -u --color=always --label="Expected stderr" --label="Resulting stderr" "${err:-$dir/empty}" "$dir/res.err"
+ printf -- "---\n"
+ fail=1
+ fi
+
+ if ! cmp -s "$dir/res.out" "${out:-$dir/empty}"; then
+ [ $fail = 0 ] && printf "!\n"
+ printf "Testcase #%d: Expected stdout did not match:\n" $num
+ diff -u --color=always --label="Expected stdout" --label="Resulting stdout" "${out:-$dir/empty}" "$dir/res.out"
+ printf -- "---\n"
+ fail=1
+ fi
+
+ if [ -n "$code" ] && ! cmp -s "$dir/res.code" "$code"; then
+ [ $fail = 0 ] && printf "!\n"
+ printf "Testcase #%d: Expected exit code did not match:\n" $num
+ diff -u --color=always --label="Expected code" --label="Resulting code" "$code" "$dir/res.code"
+ printf -- "---\n"
+ fail=1
+ fi
+
+ return $fail
+}
+
+run_test() {
+ local file=$1
+ local name=${file##*/}
+ local res ecode eout eerr ein eenv tests
+ local testcase_first=0 failed=0 count=0
+
+ printf "%s %s " "$name" "${line:${#name}}"
+
+ mkdir "/tmp/test.$$"
+
+ extract_sections "$file" "/tmp/test.$$"
+ tests=$?
+
+ [ -f "/tmp/test.$$/001.in" ] && testcase_first=1
+
+ for res in "/tmp/test.$$/"[0-9]*; do
+ case "$res" in
+ *.in)
+ count=$((count + 1))
+
+ if [ $testcase_first = 1 ]; then
+ # Flush previous test
+ if [ -n "$ein" ]; then
+ run_testcase $count "/tmp/test.$$" "$ein" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+
+ eout=""
+ eerr=""
+ ecode=""
+ eenv=""
+ fi
+
+ ein=$res
+ else
+ run_testcase $count "/tmp/test.$$" "$res" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+
+ eout=""
+ eerr=""
+ ecode=""
+ eenv=""
+ fi
+
+ ;;
+ *.env) eenv=$res ;;
+ *.stdout) eout=$res ;;
+ *.stderr) eerr=$res ;;
+ *.exitcode) ecode=$res ;;
+ esac
+ done
+
+ # Flush last test
+ if [ $testcase_first = 1 ] && [ -n "$ein" ]; then
+ run_testcase $count "/tmp/test.$$" "$ein" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+ fi
+
+ rm -r "/tmp/test.$$"
+
+ if [ $failed = 0 ]; then
+ printf "OK\n"
+ else
+ printf "%s %s FAILED (%d/%d)\n" "$name" "${line:${#name}}" $failed $tests
+ fi
+
+ return $failed
+}
+
+
+n_tests=0
+n_fails=0
+
+select_tests="$@"
+
+use_test() {
+ local input="$(readlink -f "$1")"
+ local test
+
+ [ -f "$input" ] || return 1
+ [ -n "$select_tests" ] || return 0
+
+ for test in $select_tests; do
+ test="$(readlink -f "$test")"
+
+ [ "$test" != "$input" ] || return 0
+ done
+
+ return 1
+}
+
+for catdir in tests/[0-9][0-9]_*; do
+ [ -d "$catdir" ] || continue
+
+ printf "\n##\n## Running %s tests\n##\n\n" "${catdir##*/[0-9][0-9]_}"
+
+ for testfile in "$catdir/"[0-9][0-9]_*; do
+ use_test "$testfile" || continue
+
+ n_tests=$((n_tests + 1))
+ run_test "$testfile" || n_fails=$((n_fails + 1))
+ done
+done
+
+# ── Shell script syntax checks ──────────────────────────────────────
+
+printf "\n##\n## Checking shell script syntax\n##\n\n"
+for shellscript in \
+ files/etc/init.d/* \
+ files/etc/uci-defaults/*; do
+ [ -f "$shellscript" ] || continue
+ head -1 "$shellscript" | grep -q '^#!/bin/sh' || continue
+ name="${shellscript#files/}"
+ n_tests=$((n_tests + 1))
+ printf "%s %s " "$name" "${line:${#name}}"
+ if sh -n "$shellscript" 2>/dev/null; then
+ printf "OK\n"
+ else
+ printf "FAIL\n"
+ sh -n "$shellscript"
+ n_fails=$((n_fails + 1))
+ fi
+done
+
+printf "\nRan %d tests, %d okay, %d failures\n" $n_tests $((n_tests - n_fails)) $n_fails
+exit $n_fails