Compare commits
255 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f15b664fc | |||
| 49f287439e | |||
| 150cd119c7 | |||
| e2384e143c | |||
| 52c59704de | |||
| 0b828f9b96 | |||
| 51a3e5c0b4 | |||
| c9c343cde2 | |||
| c563a24348 | |||
| 808852c547 | |||
| 035055ef0a | |||
| 508ba44576 | |||
| 999b761ed0 | |||
| 129f94dccd | |||
| aaee3c5cbe | |||
| dddbf3b370 | |||
| d29f47fc9b | |||
| e6e1795d08 | |||
| fd29cf3402 | |||
| 56ad5f74e9 | |||
| 49bf55021f | |||
| 0bec5b04b0 | |||
| d36e7e27ff | |||
| b0b6866c51 | |||
| 9098cd1161 | |||
| 8365adf435 | |||
| 9d9859248e | |||
| f7e9f4d631 | |||
| f2000aa1da | |||
| 82de3e3090 | |||
| 41a4c6362a | |||
| aede9f6e45 | |||
| 1eeed89174 | |||
| c1c5f7a7c7 | |||
| 4ed646973e | |||
| 9295895e3a | |||
| 7b9d4cb9e3 | |||
| 1fecd8eb6c | |||
| accf60b82f | |||
| f6af9d9ffb | |||
| 78b38a4eba | |||
| ec031b1f19 | |||
| 8553723e04 | |||
| 38445114e0 | |||
| a21c569e9f | |||
| 0552d75aa1 | |||
| e47a8232d8 | |||
| f818cdd963 | |||
| 28ce4d5164 | |||
| c2131cb757 | |||
| b23578dc7c | |||
| a89ad95901 | |||
| 5618c465a2 | |||
| de08978a80 | |||
| 9268c0c4bc | |||
| 5c501ac9a6 | |||
| 35450bf407 | |||
| f93c9797ea | |||
| bba6d2f919 | |||
| fa51e3f1d1 | |||
| 04a8c2ff5a | |||
| 4e516d6769 | |||
| fab7af6f38 | |||
| 3ae9fb538d | |||
| 78eaab8b5b | |||
| 20f8abdf04 | |||
| 2582ddd6f6 | |||
| 9556901a33 | |||
| ed9b4c85ed | |||
| fc452753a7 | |||
| d775682ef4 | |||
| ae339a000e | |||
| a80cf6a0dc | |||
| 727992f5d2 | |||
| 2a22c0f2d6 | |||
| b3f5635f4e | |||
| 946524aa8b | |||
| 1450538846 | |||
| 5a08c67f33 | |||
| 1e9c806614 | |||
| ccd8cfe478 | |||
| cb051f695d | |||
| 7a24f18eb7 | |||
| 83c95dc26d | |||
| e0bc5e12b3 | |||
| c4263da231 | |||
| 1eb3fd4de0 | |||
| 73be704b01 | |||
| 2699c613bd | |||
| 74e983ea6f | |||
| 48552d2663 | |||
| 501e412b84 | |||
| 77a26e4cd2 | |||
| 05d750539b | |||
| 7323a00612 | |||
| a6912ca7a2 | |||
| b256acc372 | |||
| 7e093575a4 | |||
| 740ffd68b6 | |||
| f67e1d8fc4 | |||
| 33af8845b6 | |||
| 154774ada2 | |||
| 3bddab8a9e | |||
| f9239dae75 | |||
| 642b627d27 | |||
| 38accad27d | |||
| bf58159d99 | |||
| efd4b28a0d | |||
| ae565db371 | |||
| e199ed0d6c | |||
| ba1d0c05be | |||
| 12503ae51d | |||
| ae7fb7323f | |||
| 5ae9eec555 | |||
| 6528a581de | |||
| e11224f394 | |||
| 5971c197bd | |||
| ecbab37e0e | |||
| 4eec53d6d3 | |||
| 8a916beb38 | |||
| e4ac48bc75 | |||
| d06cff2a36 | |||
| de14b76b4d | |||
| af78fa9a1c | |||
| 7ef12d93a7 | |||
| 8259145f85 | |||
| 034a1a12f7 | |||
| 9bc7e58487 | |||
| 27265fc222 | |||
| 63e67a5e13 | |||
| bde984625c | |||
| 1ded24f0e0 | |||
| 1fd0c56f08 | |||
| e59dbe6364 | |||
| 4d63a3e9bd | |||
| 9946d8f989 | |||
| 15b6cf5412 | |||
| 851bd8102d | |||
| 0d7a585bf7 | |||
| 663bb02c68 | |||
| b491b9c371 | |||
| 707f292e4e | |||
| aa41bd7d6c | |||
| ec5be76d06 | |||
| 692b0c6519 | |||
| bb836b8fdc | |||
| 8eda4641ca | |||
| 60e13ddbf6 | |||
| 0dcb285b67 | |||
| 6838dc3a74 | |||
| 46f4be6087 | |||
| 2fa2f58aad | |||
| 1eddb2c86d | |||
| ddccd6da4c | |||
| aa0b288436 | |||
| 71568d1899 | |||
| 06a783f50a | |||
| 465c22f769 | |||
| b7e5638681 | |||
| 66b4aedd08 | |||
| 105eca7adc | |||
| e3801308cb | |||
| 05d0429c50 | |||
| fa9a4921a8 | |||
| c827a00307 | |||
| dcfa9473e9 | |||
| 782dd2ea4e | |||
| c9267ba634 | |||
| 8d26a3c536 | |||
| 7631844fbe | |||
| 046b540e53 | |||
| f6faffa4c1 | |||
| 290bd192a0 | |||
| bf86731879 | |||
| 4e386e85e4 | |||
| 046d1413ec | |||
| e1b258353c | |||
| cdd132d7d0 | |||
| 679d63294d | |||
| d01d46fde8 | |||
| a3bc306b6e | |||
| 3e1772ccd0 | |||
| b946cbc0f3 | |||
| 8590a8804a | |||
| a117e3ae3e | |||
| ef2012330f | |||
| 6bb25782c1 | |||
| 1dac181803 | |||
| d556729705 | |||
| b1ae13652c | |||
| 203d6b4596 | |||
| 2676ee0169 | |||
| c60c29b93c | |||
| bdb2411ebc | |||
| 923d039a78 | |||
| abb0312d38 | |||
| f1b8de7312 | |||
| cc122d7463 | |||
| 39a067cb96 | |||
| c5e8b05e8b | |||
| 4b7094d652 | |||
| 0f03ffc920 | |||
| 1502bd42d3 | |||
| 290f83d5ef | |||
| e8d68396ca | |||
| 8e7bfb15ed | |||
| 95456ec956 | |||
| 7485474c4c | |||
| 1d63509b92 | |||
| f3e32ab737 | |||
| 59b9a6e5d2 | |||
| 3136fe0e2c | |||
| a1070d2b29 | |||
| 5ee4d392b5 | |||
| c17e4a27a2 | |||
| 8409b7e929 | |||
| 651ffa0c51 | |||
| 4404bb2b7d | |||
| e66cf5264a | |||
| af58bfcb00 | |||
| 2f7e437202 | |||
| 62573e2ad7 | |||
| a5f9dc4821 | |||
| 7827147bc8 | |||
| a473dfe4f8 | |||
| fb6a7f6007 | |||
| f7b04b0b5d | |||
| c88c660b51 | |||
| ec2d67ea00 | |||
| 9840e94c84 | |||
| 85a19da487 | |||
| b5447add1e | |||
| 879e9816aa | |||
| 3269bb476a | |||
| b3515c937e | |||
| 7e9ee913c1 | |||
| d0ec532bd3 | |||
| ff6a293660 | |||
| c5c536318f | |||
| 6e36c72f96 | |||
| f9dcfc1b9d | |||
| 33edfd3587 | |||
| aa0b6e0c60 | |||
| e992030569 | |||
| 18a21064b2 | |||
| f6a5dfead4 | |||
| 0f4b1c5c1c | |||
| 3496adca86 | |||
| 0a55a284d5 | |||
| 02c31c89f6 | |||
| 95a4419b20 | |||
| 6c9fc8bcb1 | |||
| 1a43ad6e39 | |||
| a62f7fb988 | |||
| 636aa98b79 |
@@ -19,6 +19,7 @@ DerivedData
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xcscheme
|
||||
|
||||
## Other
|
||||
*.xccheckout
|
||||
@@ -38,3 +39,6 @@ fastlane/screenshots
|
||||
fastlane/test_output
|
||||
Preview.html
|
||||
output
|
||||
|
||||
# Wireguard specific
|
||||
WireGuard/WireGuard/Config/Developer.xcconfig
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
disabled_rules:
|
||||
- line_length
|
||||
- trailing_comma
|
||||
excluded:
|
||||
- Pods
|
||||
file_length:
|
||||
warning: 500
|
||||
type_name:
|
||||
min_length: 2 # only warning
|
||||
max_length: # warning and error
|
||||
warning: 50
|
||||
error: 60
|
||||
@@ -1,338 +1,19 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
Preamble
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
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
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the 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 a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE 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.
|
||||
|
||||
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
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 2
|
||||
as published by the Free Software Foundation.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
|
||||
# ***DO NOT USE: CODEBASE HORRIBLY BROKEN***
|
||||
|
||||
----
|
||||
|
||||
# [WireGuard](https://www.wireguard.com/) for iOS
|
||||
|
||||
## Building
|
||||
@@ -28,6 +23,12 @@ $ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/
|
||||
$ vim WireGuard/WireGuard/Config/Developer.xcconfig
|
||||
```
|
||||
|
||||
- Install swiftlint:
|
||||
|
||||
```
|
||||
$ brew install swiftlint
|
||||
```
|
||||
|
||||
- Open project in XCode:
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
disabled_rules:
|
||||
- line_length
|
||||
- trailing_whitespace
|
||||
- todo
|
||||
opt_in_rules:
|
||||
- empty_count
|
||||
- empty_string
|
||||
- implicitly_unwrapped_optional
|
||||
- legacy_random
|
||||
- let_var_whitespace
|
||||
- literal_expression_end_indentation
|
||||
- override_in_extension
|
||||
- redundant_type_annotation
|
||||
- toggle_bool
|
||||
- unneeded_parentheses_in_closure_argument
|
||||
- unused_import
|
||||
# - trailing_closure
|
||||
file_length:
|
||||
warning: 500
|
||||
cyclomatic_complexity:
|
||||
warning: 10
|
||||
error: 25
|
||||
function_body_length:
|
||||
warning: 45
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension FileManager {
|
||||
private static var sharedFolderURL: URL? {
|
||||
guard let appGroupId = Bundle.main.object(forInfoDictionaryKey: "com.wireguard.ios.app_group_id") as? String else {
|
||||
os_log("Cannot obtain app group ID from bundle", log: OSLog.default, type: .error)
|
||||
return nil
|
||||
}
|
||||
guard let sharedFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
|
||||
wg_log(.error, message: "Cannot obtain shared folder URL")
|
||||
return nil
|
||||
}
|
||||
return sharedFolderURL
|
||||
}
|
||||
|
||||
static var networkExtensionLogFileURL: URL? {
|
||||
return sharedFolderURL?.appendingPathComponent("tunnel-log.bin")
|
||||
}
|
||||
|
||||
static var networkExtensionLastErrorFileURL: URL? {
|
||||
return sharedFolderURL?.appendingPathComponent("last-error.txt")
|
||||
}
|
||||
|
||||
static var appLogFileURL: URL? {
|
||||
guard let documentDirURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
||||
wg_log(.error, message: "Cannot obtain app documents folder URL")
|
||||
return nil
|
||||
}
|
||||
return documentDirURL.appendingPathComponent("app-log.bin")
|
||||
}
|
||||
|
||||
static func deleteFile(at url: URL) -> Bool {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
public class Logger {
|
||||
enum LoggerError: Error {
|
||||
case openFailure
|
||||
}
|
||||
|
||||
static var global: Logger?
|
||||
|
||||
var log: OpaquePointer
|
||||
|
||||
init(withFilePath filePath: String) throws {
|
||||
guard let log = open_log(filePath) else { throw LoggerError.openFailure }
|
||||
self.log = log
|
||||
}
|
||||
|
||||
deinit {
|
||||
close_log(self.log)
|
||||
}
|
||||
|
||||
func log(message: String) {
|
||||
write_msg_to_log(log, message.trimmingCharacters(in: .newlines))
|
||||
}
|
||||
|
||||
func writeLog(called ourTag: String, mergedWith otherLogFile: String, called otherTag: String, to targetFile: String) -> Bool {
|
||||
guard let other = open_log(otherLogFile) else { return false }
|
||||
let ret = write_logs_to_file(targetFile, log, ourTag, other, otherTag)
|
||||
close_log(other)
|
||||
return ret == 0
|
||||
}
|
||||
|
||||
static func configureGlobal(withFilePath filePath: String?) {
|
||||
if Logger.global != nil {
|
||||
return
|
||||
}
|
||||
guard let filePath = filePath else {
|
||||
os_log("Unable to determine log destination path. Log will not be saved to file.", log: OSLog.default, type: .error)
|
||||
return
|
||||
}
|
||||
do {
|
||||
try Logger.global = Logger(withFilePath: filePath)
|
||||
} catch {
|
||||
os_log("Unable to open log file for writing. Log will not be saved to file.", log: OSLog.default, type: .error)
|
||||
return
|
||||
}
|
||||
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
|
||||
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
appVersion += " (\(appBuild))"
|
||||
}
|
||||
let goBackendVersion = WIREGUARD_GO_VERSION
|
||||
Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func wg_log(_ type: OSLogType, staticMessage msg: StaticString) {
|
||||
os_log(msg, log: OSLog.default, type: type)
|
||||
Logger.global?.log(message: "\(msg)")
|
||||
}
|
||||
|
||||
func wg_log(_ type: OSLogType, message msg: String) {
|
||||
os_log("%{public}s", log: OSLog.default, type: type, msg)
|
||||
Logger.global?.log(message: msg)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/mman.h>
|
||||
#include "ringlogger.h"
|
||||
|
||||
enum {
|
||||
MAX_LOG_LINE_LENGTH = 512,
|
||||
MAX_LINES = 1024,
|
||||
MAGIC = 0xbeefbabeU
|
||||
};
|
||||
|
||||
struct log_line {
|
||||
struct timeval tv;
|
||||
char line[MAX_LOG_LINE_LENGTH];
|
||||
};
|
||||
|
||||
struct log {
|
||||
struct { uint32_t first, len; } header;
|
||||
struct log_line lines[MAX_LINES];
|
||||
uint32_t magic;
|
||||
};
|
||||
|
||||
void write_msg_to_log(struct log *log, const char *msg)
|
||||
{
|
||||
struct log_line *line = &log->lines[(log->header.first + log->header.len) % MAX_LINES];
|
||||
|
||||
if (log->header.len == MAX_LINES)
|
||||
log->header.first = (log->header.first + 1) % MAX_LINES;
|
||||
else
|
||||
++log->header.len;
|
||||
|
||||
gettimeofday(&line->tv, NULL);
|
||||
strncpy(line->line, msg, MAX_LOG_LINE_LENGTH - 1);
|
||||
line->line[MAX_LOG_LINE_LENGTH - 1] = '\0';
|
||||
|
||||
msync(&log->header, sizeof(log->header), MS_ASYNC);
|
||||
msync(line, sizeof(*line), MS_ASYNC);
|
||||
}
|
||||
|
||||
static bool first_before_second(const struct log_line *line1, const struct log_line *line2)
|
||||
{
|
||||
if (line1->tv.tv_sec <= line2->tv.tv_sec)
|
||||
return true;
|
||||
if (line1->tv.tv_sec == line2->tv.tv_sec)
|
||||
return line1->tv.tv_usec <= line2->tv.tv_usec;
|
||||
return false;
|
||||
}
|
||||
|
||||
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2)
|
||||
{
|
||||
uint32_t i1, i2, len1 = log1->header.len, len2 = log2->header.len;
|
||||
FILE *file;
|
||||
int ret;
|
||||
|
||||
if (len1 > MAX_LINES)
|
||||
len1 = MAX_LINES;
|
||||
if (len2 > MAX_LINES)
|
||||
len2 = MAX_LINES;
|
||||
|
||||
file = fopen(file_name, "w");
|
||||
if (!file)
|
||||
return -errno;
|
||||
|
||||
for (i1 = 0, i2 = 0;;) {
|
||||
struct tm tm;
|
||||
char buf[MAX_LOG_LINE_LENGTH];
|
||||
const struct log_line *line1 = &log1->lines[(log1->header.first + i1) % MAX_LINES];
|
||||
const struct log_line *line2 = &log2->lines[(log2->header.first + i2) % MAX_LINES];
|
||||
const struct log_line *line;
|
||||
const char *tag;
|
||||
|
||||
if (i1 < len1 && (i2 >= len2 || first_before_second(line1, line2))) {
|
||||
line = line1;
|
||||
tag = tag1;
|
||||
++i1;
|
||||
} else if (i2 < len2 && (i1 >= len1 || first_before_second(line2, line1))) {
|
||||
line = line2;
|
||||
tag = tag2;
|
||||
++i2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
memcpy(buf, line->line, MAX_LOG_LINE_LENGTH);
|
||||
buf[MAX_LOG_LINE_LENGTH - 1] = '\0';
|
||||
if (!localtime_r(&line->tv.tv_sec, &tm))
|
||||
goto err;
|
||||
if (fprintf(file, "%04d-%02d-%02d %02d:%02d:%02d.%06d: [%s] %s\n",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec, line->tv.tv_usec,
|
||||
tag, buf) < 0)
|
||||
goto err;
|
||||
}
|
||||
errno = 0;
|
||||
|
||||
err:
|
||||
ret = -errno;
|
||||
fclose(file);
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct log *open_log(const char *file_name)
|
||||
{
|
||||
int fd;
|
||||
struct log *log;
|
||||
|
||||
fd = open(file_name, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
|
||||
if (fd < 0)
|
||||
return NULL;
|
||||
if (ftruncate(fd, sizeof(*log)))
|
||||
goto err;
|
||||
log = mmap(NULL, sizeof(*log), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (log == MAP_FAILED)
|
||||
goto err;
|
||||
close(fd);
|
||||
|
||||
if (log->magic != MAGIC) {
|
||||
memset(log, 0, sizeof(*log));
|
||||
log->magic = MAGIC;
|
||||
msync(log, sizeof(*log), MS_ASYNC);
|
||||
}
|
||||
|
||||
return log;
|
||||
|
||||
err:
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void close_log(struct log *log)
|
||||
{
|
||||
munmap(log, sizeof(*log));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef RINGLOGGER_H
|
||||
#define RINGLOGGER_H
|
||||
|
||||
struct log;
|
||||
void write_msg_to_log(struct log *log, const char *msg);
|
||||
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2);
|
||||
struct log *open_log(const char *file_name);
|
||||
void close_log(struct log *log);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
struct DNSServer {
|
||||
let address: IPAddress
|
||||
|
||||
init(address: IPAddress) {
|
||||
self.address = address
|
||||
}
|
||||
}
|
||||
|
||||
extension DNSServer: Equatable {
|
||||
static func == (lhs: DNSServer, rhs: DNSServer) -> Bool {
|
||||
return lhs.address.rawValue == rhs.address.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension DNSServer {
|
||||
var stringRepresentation: String {
|
||||
return "\(address)"
|
||||
}
|
||||
|
||||
init?(from addressString: String) {
|
||||
if let addr = IPv4Address(addressString) {
|
||||
address = addr
|
||||
} else if let addr = IPv6Address(addressString) {
|
||||
address = addr
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,56 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
struct Endpoint {
|
||||
let host: NWEndpoint.Host
|
||||
let port: NWEndpoint.Port
|
||||
|
||||
init(host: NWEndpoint.Host, port: NWEndpoint.Port) {
|
||||
self.host = host
|
||||
self.port = port
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Converting to and from String
|
||||
// For use in the UI
|
||||
extension Endpoint: Equatable {
|
||||
static func == (lhs: Endpoint, rhs: Endpoint) -> Bool {
|
||||
return lhs.host == rhs.host && lhs.port == rhs.port
|
||||
}
|
||||
}
|
||||
|
||||
extension Endpoint: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(host)
|
||||
hasher.combine(port)
|
||||
}
|
||||
}
|
||||
|
||||
extension Endpoint {
|
||||
var stringRepresentation: String {
|
||||
switch host {
|
||||
case .name(let hostname, _):
|
||||
return "\(hostname):\(port)"
|
||||
case .ipv4(let address):
|
||||
return "\(address):\(port)"
|
||||
case .ipv6(let address):
|
||||
return "[\(address)]:\(port)"
|
||||
}
|
||||
}
|
||||
|
||||
init?(from string: String) {
|
||||
// Separation of host and port is based on 'parse_endpoint' function in
|
||||
// https://git.zx2c4.com/WireGuard/tree/src/tools/config.c
|
||||
guard (!string.isEmpty) else { return nil }
|
||||
guard !string.isEmpty else { return nil }
|
||||
let startOfPort: String.Index
|
||||
let hostString: String
|
||||
if (string.first! == "[") {
|
||||
if string.first! == "[" {
|
||||
// Look for IPv6-style endpoint, like [::1]:80
|
||||
let startOfHost = string.index(after: string.startIndex)
|
||||
guard let endOfHost = string.dropFirst().firstIndex(of: "]") else { return nil }
|
||||
let afterEndOfHost = string.index(after: endOfHost)
|
||||
guard (string[afterEndOfHost] == ":") else { return nil }
|
||||
guard string[afterEndOfHost] == ":" else { return nil }
|
||||
startOfPort = string.index(after: afterEndOfHost)
|
||||
hostString = String(string[startOfHost ..< endOfHost])
|
||||
} else {
|
||||
@@ -35,43 +60,35 @@ extension Endpoint {
|
||||
hostString = String(string[string.startIndex ..< endOfHost])
|
||||
}
|
||||
guard let endpointPort = NWEndpoint.Port(String(string[startOfPort ..< string.endIndex])) else { return nil }
|
||||
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { (c) -> Bool in
|
||||
return !CharacterSet.urlHostAllowed.contains(c)
|
||||
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
|
||||
return !CharacterSet.urlHostAllowed.contains(char)
|
||||
}
|
||||
guard (invalidCharacterIndex == nil) else { return nil }
|
||||
guard invalidCharacterIndex == nil else { return nil }
|
||||
host = NWEndpoint.Host(hostString)
|
||||
port = endpointPort
|
||||
}
|
||||
func stringRepresentation() -> String {
|
||||
switch (host) {
|
||||
}
|
||||
|
||||
extension Endpoint {
|
||||
func hasHostAsIPAddress() -> Bool {
|
||||
switch host {
|
||||
case .name:
|
||||
return false
|
||||
case .ipv4:
|
||||
return true
|
||||
case .ipv6:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func hostname() -> String? {
|
||||
switch host {
|
||||
case .name(let hostname, _):
|
||||
return "\(hostname):\(port)"
|
||||
case .ipv4(let address):
|
||||
return "\(address):\(port)"
|
||||
case .ipv6(let address):
|
||||
return "[\(address)]:\(port)"
|
||||
return hostname
|
||||
case .ipv4:
|
||||
return nil
|
||||
case .ipv6:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
// For serializing to disk
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
extension Endpoint: Codable {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(self.stringRepresentation())
|
||||
}
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let endpointString = try container.decode(String.self)
|
||||
guard let endpoint = Endpoint(from: endpointString) else {
|
||||
throw DecodingError.invalidData
|
||||
}
|
||||
self = endpoint
|
||||
}
|
||||
enum DecodingError: Error {
|
||||
case invalidData
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
struct IPAddressRange {
|
||||
let address: IPAddress
|
||||
var networkPrefixLength: UInt8
|
||||
|
||||
init(address: IPAddress, networkPrefixLength: UInt8) {
|
||||
self.address = address
|
||||
self.networkPrefixLength = networkPrefixLength
|
||||
}
|
||||
}
|
||||
|
||||
extension IPAddressRange: Equatable {
|
||||
static func == (lhs: IPAddressRange, rhs: IPAddressRange) -> Bool {
|
||||
return lhs.address.rawValue == rhs.address.rawValue && lhs.networkPrefixLength == rhs.networkPrefixLength
|
||||
}
|
||||
}
|
||||
|
||||
extension IPAddressRange: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(address.rawValue)
|
||||
hasher.combine(networkPrefixLength)
|
||||
}
|
||||
}
|
||||
|
||||
extension IPAddressRange {
|
||||
var stringRepresentation: String {
|
||||
return "\(address)/\(networkPrefixLength)"
|
||||
}
|
||||
|
||||
init?(from string: String) {
|
||||
guard let parsed = IPAddressRange.parseAddressString(string) else { return nil }
|
||||
address = parsed.0
|
||||
networkPrefixLength = parsed.1
|
||||
}
|
||||
|
||||
private static func parseAddressString(_ string: String) -> (IPAddress, UInt8)? {
|
||||
let endOfIPAddress = string.lastIndex(of: "/") ?? string.endIndex
|
||||
let addressString = String(string[string.startIndex ..< endOfIPAddress])
|
||||
let address: IPAddress
|
||||
if let addr = IPv4Address(addressString) {
|
||||
address = addr
|
||||
} else if let addr = IPv6Address(addressString) {
|
||||
address = addr
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let maxNetworkPrefixLength: UInt8 = address is IPv4Address ? 32 : 128
|
||||
var networkPrefixLength: UInt8
|
||||
if endOfIPAddress < string.endIndex { // "/" was located
|
||||
let indexOfNetworkPrefixLength = string.index(after: endOfIPAddress)
|
||||
guard indexOfNetworkPrefixLength < string.endIndex else { return nil }
|
||||
let networkPrefixLengthSubstring = string[indexOfNetworkPrefixLength ..< string.endIndex]
|
||||
guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil }
|
||||
networkPrefixLength = min(npl, maxNetworkPrefixLength)
|
||||
} else {
|
||||
networkPrefixLength = maxNetworkPrefixLength
|
||||
}
|
||||
|
||||
return (address, networkPrefixLength)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
struct InterfaceConfiguration {
|
||||
var privateKey: Data
|
||||
var addresses = [IPAddressRange]()
|
||||
var listenPort: UInt16?
|
||||
var mtu: UInt16?
|
||||
var dns = [DNSServer]()
|
||||
|
||||
init(privateKey: Data) {
|
||||
if privateKey.count != TunnelConfiguration.keyLength {
|
||||
fatalError("Invalid private key")
|
||||
}
|
||||
self.privateKey = privateKey
|
||||
}
|
||||
}
|
||||
|
||||
extension InterfaceConfiguration: Equatable {
|
||||
static func == (lhs: InterfaceConfiguration, rhs: InterfaceConfiguration) -> Bool {
|
||||
let lhsAddresses = lhs.addresses.filter { $0.address is IPv4Address } + lhs.addresses.filter { $0.address is IPv6Address }
|
||||
let rhsAddresses = rhs.addresses.filter { $0.address is IPv4Address } + rhs.addresses.filter { $0.address is IPv6Address }
|
||||
|
||||
return lhs.privateKey == rhs.privateKey &&
|
||||
lhsAddresses == rhsAddresses &&
|
||||
lhs.listenPort == rhs.listenPort &&
|
||||
lhs.mtu == rhs.mtu &&
|
||||
lhs.dns == rhs.dns
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import NetworkExtension
|
||||
|
||||
protocol LegacyModel: Decodable {
|
||||
associatedtype Model
|
||||
|
||||
var migrated: Model { get }
|
||||
}
|
||||
|
||||
struct LegacyDNSServer: LegacyModel {
|
||||
let address: IPAddress
|
||||
|
||||
var migrated: DNSServer {
|
||||
return DNSServer(address: address)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
var data = try container.decode(Data.self)
|
||||
let ipAddressFromData: IPAddress? = {
|
||||
switch data.count {
|
||||
case 4: return IPv4Address(data)
|
||||
case 16: return IPv6Address(data)
|
||||
default: return nil
|
||||
}
|
||||
}()
|
||||
guard let ipAddress = ipAddressFromData else {
|
||||
throw DecodingError.invalidData
|
||||
}
|
||||
address = ipAddress
|
||||
}
|
||||
|
||||
enum DecodingError: Error {
|
||||
case invalidData
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == LegacyDNSServer {
|
||||
var migrated: [DNSServer] {
|
||||
return map { $0.migrated }
|
||||
}
|
||||
}
|
||||
|
||||
struct LegacyEndpoint: LegacyModel {
|
||||
let host: Network.NWEndpoint.Host
|
||||
let port: Network.NWEndpoint.Port
|
||||
|
||||
var migrated: Endpoint {
|
||||
return Endpoint(host: host, port: port)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let endpointString = try container.decode(String.self)
|
||||
guard !endpointString.isEmpty else { throw DecodingError.invalidData }
|
||||
let startOfPort: String.Index
|
||||
let hostString: String
|
||||
if endpointString.first! == "[" {
|
||||
// Look for IPv6-style endpoint, like [::1]:80
|
||||
let startOfHost = endpointString.index(after: endpointString.startIndex)
|
||||
guard let endOfHost = endpointString.dropFirst().firstIndex(of: "]") else { throw DecodingError.invalidData }
|
||||
let afterEndOfHost = endpointString.index(after: endOfHost)
|
||||
guard endpointString[afterEndOfHost] == ":" else { throw DecodingError.invalidData }
|
||||
startOfPort = endpointString.index(after: afterEndOfHost)
|
||||
hostString = String(endpointString[startOfHost ..< endOfHost])
|
||||
} else {
|
||||
// Look for an IPv4-style endpoint, like 127.0.0.1:80
|
||||
guard let endOfHost = endpointString.firstIndex(of: ":") else { throw DecodingError.invalidData }
|
||||
startOfPort = endpointString.index(after: endOfHost)
|
||||
hostString = String(endpointString[endpointString.startIndex ..< endOfHost])
|
||||
}
|
||||
guard let endpointPort = NWEndpoint.Port(String(endpointString[startOfPort ..< endpointString.endIndex])) else { throw DecodingError.invalidData }
|
||||
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
|
||||
return !CharacterSet.urlHostAllowed.contains(char)
|
||||
}
|
||||
guard invalidCharacterIndex == nil else { throw DecodingError.invalidData }
|
||||
host = NWEndpoint.Host(hostString)
|
||||
port = endpointPort
|
||||
}
|
||||
|
||||
enum DecodingError: Error {
|
||||
case invalidData
|
||||
}
|
||||
}
|
||||
|
||||
struct LegacyInterfaceConfiguration: LegacyModel {
|
||||
let name: String
|
||||
let privateKey: Data
|
||||
let addresses: [LegacyIPAddressRange]
|
||||
let listenPort: UInt16?
|
||||
let mtu: UInt16?
|
||||
let dns: [LegacyDNSServer]
|
||||
|
||||
var migrated: InterfaceConfiguration {
|
||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||
interface.addresses = addresses.migrated
|
||||
interface.listenPort = listenPort
|
||||
interface.mtu = mtu
|
||||
interface.dns = dns.migrated
|
||||
return interface
|
||||
}
|
||||
}
|
||||
|
||||
struct LegacyIPAddressRange: LegacyModel {
|
||||
let address: IPAddress
|
||||
let networkPrefixLength: UInt8
|
||||
|
||||
var migrated: IPAddressRange {
|
||||
return IPAddressRange(address: address, networkPrefixLength: networkPrefixLength)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
var data = try container.decode(Data.self)
|
||||
networkPrefixLength = data.removeLast()
|
||||
let ipAddressFromData: IPAddress? = {
|
||||
switch data.count {
|
||||
case 4: return IPv4Address(data)
|
||||
case 16: return IPv6Address(data)
|
||||
default: return nil
|
||||
}
|
||||
}()
|
||||
guard let ipAddress = ipAddressFromData else { throw DecodingError.invalidData }
|
||||
address = ipAddress
|
||||
}
|
||||
|
||||
enum DecodingError: Error {
|
||||
case invalidData
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == LegacyIPAddressRange {
|
||||
var migrated: [IPAddressRange] {
|
||||
return map { $0.migrated }
|
||||
}
|
||||
}
|
||||
|
||||
struct LegacyPeerConfiguration: LegacyModel {
|
||||
let publicKey: Data
|
||||
let preSharedKey: Data?
|
||||
let allowedIPs: [LegacyIPAddressRange]
|
||||
let endpoint: LegacyEndpoint?
|
||||
let persistentKeepAlive: UInt16?
|
||||
|
||||
var migrated: PeerConfiguration {
|
||||
var configuration = PeerConfiguration(publicKey: publicKey)
|
||||
configuration.preSharedKey = preSharedKey
|
||||
configuration.allowedIPs = allowedIPs.migrated
|
||||
configuration.endpoint = endpoint?.migrated
|
||||
configuration.persistentKeepAlive = persistentKeepAlive
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == LegacyPeerConfiguration {
|
||||
var migrated: [PeerConfiguration] {
|
||||
return map { $0.migrated }
|
||||
}
|
||||
}
|
||||
|
||||
final class LegacyTunnelConfiguration: LegacyModel {
|
||||
let interface: LegacyInterfaceConfiguration
|
||||
let peers: [LegacyPeerConfiguration]
|
||||
|
||||
var migrated: TunnelConfiguration {
|
||||
return TunnelConfiguration(name: interface.name, interface: interface.migrated, peers: peers.migrated)
|
||||
}
|
||||
}
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
|
||||
@discardableResult
|
||||
func migrateConfigurationIfNeeded() -> Bool {
|
||||
guard let configurationVersion = providerConfiguration?["tunnelConfigurationVersion"] as? Int else { return false }
|
||||
if configurationVersion == 1 {
|
||||
migrateFromConfigurationV1()
|
||||
} else {
|
||||
fatalError("No migration from configuration version \(configurationVersion) exists.")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func migrateFromConfigurationV1() {
|
||||
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return }
|
||||
guard let configuration = try? JSONDecoder().decode(LegacyTunnelConfiguration.self, from: serializedTunnelConfiguration) else { return }
|
||||
providerConfiguration = [Keys.wgQuickConfig.rawValue: configuration.migrated.asWgQuickConfig()]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
enum PacketTunnelProviderError: String, Error {
|
||||
case savedProtocolConfigurationIsInvalid
|
||||
case dnsResolutionFailure
|
||||
case couldNotStartBackend
|
||||
case couldNotDetermineFileDescriptor
|
||||
case couldNotSetNetworkSettings
|
||||
}
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
|
||||
enum Keys: String {
|
||||
case wgQuickConfig = "WgQuickConfig"
|
||||
}
|
||||
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration) {
|
||||
self.init()
|
||||
|
||||
let appId = Bundle.main.bundleIdentifier!
|
||||
providerBundleIdentifier = "\(appId).network-extension"
|
||||
providerConfiguration = [Keys.wgQuickConfig.rawValue: tunnelConfiguration.asWgQuickConfig()]
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
|
||||
if endpoints.count == 1 {
|
||||
serverAddress = endpoints[0].stringRepresentation
|
||||
} else if endpoints.isEmpty {
|
||||
serverAddress = "Unspecified"
|
||||
} else {
|
||||
serverAddress = "Multiple endpoints"
|
||||
}
|
||||
}
|
||||
|
||||
func asTunnelConfiguration(called name: String? = nil) -> TunnelConfiguration? {
|
||||
migrateConfigurationIfNeeded()
|
||||
guard let serializedConfig = providerConfiguration?[Keys.wgQuickConfig.rawValue] as? String else { return nil }
|
||||
return try? TunnelConfiguration(fromWgQuickConfig: serializedConfig, called: name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PeerConfiguration {
|
||||
var publicKey: Data
|
||||
var preSharedKey: Data? {
|
||||
didSet(value) {
|
||||
if let value = value {
|
||||
if value.count != TunnelConfiguration.keyLength {
|
||||
fatalError("Invalid preshared key")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var allowedIPs = [IPAddressRange]()
|
||||
var endpoint: Endpoint?
|
||||
var persistentKeepAlive: UInt16?
|
||||
|
||||
init(publicKey: Data) {
|
||||
self.publicKey = publicKey
|
||||
if publicKey.count != TunnelConfiguration.keyLength {
|
||||
fatalError("Invalid public key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PeerConfiguration: Equatable {
|
||||
static func == (lhs: PeerConfiguration, rhs: PeerConfiguration) -> Bool {
|
||||
return lhs.publicKey == rhs.publicKey &&
|
||||
lhs.preSharedKey == rhs.preSharedKey &&
|
||||
Set(lhs.allowedIPs) == Set(rhs.allowedIPs) &&
|
||||
lhs.endpoint == rhs.endpoint &&
|
||||
lhs.persistentKeepAlive == rhs.persistentKeepAlive
|
||||
}
|
||||
}
|
||||
|
||||
extension PeerConfiguration: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(publicKey)
|
||||
hasher.combine(preSharedKey)
|
||||
hasher.combine(Set(allowedIPs))
|
||||
hasher.combine(endpoint)
|
||||
hasher.combine(persistentKeepAlive)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
|
||||
return split(separator: separator)
|
||||
.map {
|
||||
if let charSet = trimmingCharacters {
|
||||
return $0.trimmingCharacters(in: charSet)
|
||||
} else {
|
||||
return String($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Optional where Wrapped == String {
|
||||
|
||||
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
|
||||
switch self {
|
||||
case .none:
|
||||
return []
|
||||
case .some(let wrapped):
|
||||
return wrapped.splitToArray(separator: separator, trimmingCharacters: trimmingCharacters)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension TunnelConfiguration {
|
||||
|
||||
enum ParserState {
|
||||
case inInterfaceSection
|
||||
case inPeerSection
|
||||
case notInASection
|
||||
}
|
||||
|
||||
enum ParseError: Error {
|
||||
case invalidLine(_ line: String.SubSequence)
|
||||
case noInterface
|
||||
case invalidInterface
|
||||
case multipleInterfaces
|
||||
case multiplePeersWithSamePublicKey
|
||||
case invalidPeer
|
||||
}
|
||||
|
||||
//swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||
convenience init(fromWgQuickConfig wgQuickConfig: String, called name: String? = nil) throws {
|
||||
var interfaceConfiguration: InterfaceConfiguration?
|
||||
var peerConfigurations = [PeerConfiguration]()
|
||||
|
||||
let lines = wgQuickConfig.split(separator: "\n")
|
||||
|
||||
var parserState = ParserState.notInASection
|
||||
var attributes = [String: String]()
|
||||
|
||||
for (lineIndex, line) in lines.enumerated() {
|
||||
var trimmedLine: String
|
||||
if let commentRange = line.range(of: "#") {
|
||||
trimmedLine = String(line[..<commentRange.lowerBound])
|
||||
} else {
|
||||
trimmedLine = String(line)
|
||||
}
|
||||
|
||||
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
guard !trimmedLine.isEmpty else { continue }
|
||||
let lowercasedLine = line.lowercased()
|
||||
|
||||
if let equalsIndex = line.firstIndex(of: "=") {
|
||||
// Line contains an attribute
|
||||
let key = line[..<equalsIndex].trimmingCharacters(in: .whitespaces).lowercased()
|
||||
let value = line[line.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespaces)
|
||||
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
|
||||
if let presentValue = attributes[key], keysWithMultipleEntriesAllowed.contains(key) {
|
||||
attributes[key] = presentValue + "," + value
|
||||
} else {
|
||||
attributes[key] = value
|
||||
}
|
||||
} else if lowercasedLine != "[interface]" && lowercasedLine != "[peer]" {
|
||||
throw ParseError.invalidLine(line)
|
||||
}
|
||||
|
||||
let isLastLine = lineIndex == lines.count - 1
|
||||
|
||||
if isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]" {
|
||||
// Previous section has ended; process the attributes collected so far
|
||||
if parserState == .inInterfaceSection {
|
||||
guard let interface = TunnelConfiguration.collate(interfaceAttributes: attributes) else { throw ParseError.invalidInterface }
|
||||
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
|
||||
interfaceConfiguration = interface
|
||||
} else if parserState == .inPeerSection {
|
||||
guard let peer = TunnelConfiguration.collate(peerAttributes: attributes) else { throw ParseError.invalidPeer }
|
||||
peerConfigurations.append(peer)
|
||||
}
|
||||
}
|
||||
|
||||
if lowercasedLine == "[interface]" {
|
||||
parserState = .inInterfaceSection
|
||||
attributes.removeAll()
|
||||
} else if lowercasedLine == "[peer]" {
|
||||
parserState = .inPeerSection
|
||||
attributes.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||
throw ParseError.multiplePeersWithSamePublicKey
|
||||
}
|
||||
|
||||
if let interfaceConfiguration = interfaceConfiguration {
|
||||
self.init(name: name, interface: interfaceConfiguration, peers: peerConfigurations)
|
||||
} else {
|
||||
throw ParseError.noInterface
|
||||
}
|
||||
}
|
||||
|
||||
func asWgQuickConfig() -> String {
|
||||
var output = "[Interface]\n"
|
||||
output.append("PrivateKey = \(interface.privateKey.base64EncodedString())\n")
|
||||
if let listenPort = interface.listenPort {
|
||||
output.append("ListenPort = \(listenPort)\n")
|
||||
}
|
||||
if !interface.addresses.isEmpty {
|
||||
let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
output.append("Address = \(addressString)\n")
|
||||
}
|
||||
if !interface.dns.isEmpty {
|
||||
let dnsString = interface.dns.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
output.append("DNS = \(dnsString)\n")
|
||||
}
|
||||
if let mtu = interface.mtu {
|
||||
output.append("MTU = \(mtu)\n")
|
||||
}
|
||||
|
||||
for peer in peers {
|
||||
output.append("\n[Peer]\n")
|
||||
output.append("PublicKey = \(peer.publicKey.base64EncodedString())\n")
|
||||
if let preSharedKey = peer.preSharedKey {
|
||||
output.append("PresharedKey = \(preSharedKey.base64EncodedString())\n")
|
||||
}
|
||||
if !peer.allowedIPs.isEmpty {
|
||||
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
output.append("AllowedIPs = \(allowedIPsString)\n")
|
||||
}
|
||||
if let endpoint = peer.endpoint {
|
||||
output.append("Endpoint = \(endpoint.stringRepresentation)\n")
|
||||
}
|
||||
if let persistentKeepAlive = peer.persistentKeepAlive {
|
||||
output.append("PersistentKeepalive = \(persistentKeepAlive)\n")
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity
|
||||
private static func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? {
|
||||
// required wg fields
|
||||
guard let privateKeyString = attributes["privatekey"] else { return nil }
|
||||
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { return nil }
|
||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||
// other wg fields
|
||||
if let listenPortString = attributes["listenport"] {
|
||||
guard let listenPort = UInt16(listenPortString) else { return nil }
|
||||
interface.listenPort = listenPort
|
||||
}
|
||||
// wg-quick fields
|
||||
if let addressesString = attributes["address"] {
|
||||
var addresses = [IPAddressRange]()
|
||||
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespaces) {
|
||||
guard let address = IPAddressRange(from: addressString) else { return nil }
|
||||
addresses.append(address)
|
||||
}
|
||||
interface.addresses = addresses
|
||||
}
|
||||
if let dnsString = attributes["dns"] {
|
||||
var dnsServers = [DNSServer]()
|
||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespaces) {
|
||||
guard let dnsServer = DNSServer(from: dnsServerString) else { return nil }
|
||||
dnsServers.append(dnsServer)
|
||||
}
|
||||
interface.dns = dnsServers
|
||||
}
|
||||
if let mtuString = attributes["mtu"] {
|
||||
guard let mtu = UInt16(mtuString) else { return nil }
|
||||
interface.mtu = mtu
|
||||
}
|
||||
return interface
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity
|
||||
private static func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
|
||||
// required wg fields
|
||||
guard let publicKeyString = attributes["publickey"] else { return nil }
|
||||
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else { return nil }
|
||||
var peer = PeerConfiguration(publicKey: publicKey)
|
||||
// wg fields
|
||||
if let preSharedKeyString = attributes["presharedkey"] {
|
||||
guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else { return nil }
|
||||
peer.preSharedKey = preSharedKey
|
||||
}
|
||||
if let allowedIPsString = attributes["allowedips"] {
|
||||
var allowedIPs = [IPAddressRange]()
|
||||
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
guard let allowedIP = IPAddressRange(from: allowedIPString) else { return nil }
|
||||
allowedIPs.append(allowedIP)
|
||||
}
|
||||
peer.allowedIPs = allowedIPs
|
||||
}
|
||||
if let endpointString = attributes["endpoint"] {
|
||||
guard let endpoint = Endpoint(from: endpointString) else { return nil }
|
||||
peer.endpoint = endpoint
|
||||
}
|
||||
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
|
||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
|
||||
peer.persistentKeepAlive = persistentKeepAlive
|
||||
}
|
||||
return peer
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
final class TunnelConfiguration {
|
||||
var name: String?
|
||||
var interface: InterfaceConfiguration
|
||||
let peers: [PeerConfiguration]
|
||||
|
||||
static let keyLength = 32
|
||||
|
||||
init(name: String?, interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
|
||||
self.interface = interface
|
||||
self.peers = peers
|
||||
self.name = name
|
||||
|
||||
let peerPublicKeysArray = peers.map { $0.publicKey }
|
||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||
fatalError("Two or more peers cannot have the same public key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelConfiguration: Equatable {
|
||||
static func == (lhs: TunnelConfiguration, rhs: TunnelConfiguration) -> Bool {
|
||||
return lhs.name == rhs.name &&
|
||||
lhs.interface == rhs.interface &&
|
||||
Set(lhs.peers) == Set(rhs.peers)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PacketTunnelOptionKey: String {
|
||||
|
||||
case interfaceName, wireguardSettings, remoteAddress, dnsServers, mtu,
|
||||
|
||||
// IPv4 settings
|
||||
ipv4Addresses, ipv4SubnetMasks,
|
||||
ipv4IncludedRouteAddresses, ipv4IncludedRouteSubnetMasks,
|
||||
ipv4ExcludedRouteAddresses, ipv4ExcludedRouteSubnetMasks,
|
||||
|
||||
// IPv6 settings
|
||||
ipv6Addresses, ipv6NetworkPrefixLengths,
|
||||
ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths,
|
||||
ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths
|
||||
}
|
||||
|
||||
extension Dictionary where Key == String {
|
||||
subscript(key: PacketTunnelOptionKey) -> Value? {
|
||||
get {
|
||||
return self[key.rawValue]
|
||||
}
|
||||
set(value) {
|
||||
self[key.rawValue] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,32 +7,51 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; };
|
||||
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
|
||||
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */; };
|
||||
5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */; };
|
||||
5F45419021C2D53800994C13 /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418F21C2D53800994C13 /* SwitchCell.swift */; };
|
||||
5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419121C2D55800994C13 /* CheckmarkCell.swift */; };
|
||||
5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419721C2D60500994C13 /* KeyValueCell.swift */; };
|
||||
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419F21C2D6B700994C13 /* TunnelListCell.swift */; };
|
||||
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */; };
|
||||
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A521C4449E00994C13 /* ButtonCell.swift */; };
|
||||
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A821C451D100994C13 /* TunnelStatus.swift */; };
|
||||
5F4541B221CBFAEE00994C13 /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */; };
|
||||
5F9696AA21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */; };
|
||||
5F9696AB21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */; };
|
||||
5F9696AE21CD6F72008063FE /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */; };
|
||||
5F9696B021CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */; };
|
||||
5F9696B121CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */; };
|
||||
5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */; };
|
||||
5FF7B96321CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */; };
|
||||
5FF7B96521CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */; };
|
||||
5FF7B96621CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */; };
|
||||
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
|
||||
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
|
||||
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; };
|
||||
6F5D0C22218352EF000F85AD /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
6F5D0C452183BCDA000F85AD /* PacketTunnelOptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */; };
|
||||
6F5D0C462183C0B4000F85AD /* PacketTunnelOptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */; };
|
||||
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */; };
|
||||
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1E821B932F700483816 /* WireGuardAppError.swift */; };
|
||||
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */; };
|
||||
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; };
|
||||
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
|
||||
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */; };
|
||||
6F6899A62180447E0012E523 /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A52180447E0012E523 /* x25519.c */; };
|
||||
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A7218044FC0012E523 /* Curve25519.swift */; };
|
||||
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */; };
|
||||
6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
|
||||
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; };
|
||||
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; };
|
||||
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */; };
|
||||
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; };
|
||||
6F7774E82172020C006A79B3 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */; };
|
||||
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
|
||||
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; };
|
||||
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; };
|
||||
6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; };
|
||||
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; };
|
||||
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; };
|
||||
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; };
|
||||
6F919EDB218C65C50023B400 /* wireguard_doc_logo_64x64.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */; };
|
||||
6F919EDC218C65C50023B400 /* wireguard_doc_logo_320x320.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */; };
|
||||
6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1017821C57DE600766195 /* MockTunnels.swift */; };
|
||||
6FDEF7E421846C1A00D8FBF6 /* libwg-go.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */; };
|
||||
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */; };
|
||||
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7F621863B6100D8FBF6 /* unzip.c */; };
|
||||
@@ -40,9 +59,26 @@
|
||||
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7FF21863C0100D8FBF6 /* ioapi.c */; };
|
||||
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; };
|
||||
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; };
|
||||
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; };
|
||||
6FE1765621C90BBE002690EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6FE1765421C90BBE002690EA /* Localizable.strings */; };
|
||||
6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE1765921C90E87002690EA /* LocalizationHelper.swift */; };
|
||||
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; };
|
||||
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FE219C60290028284D /* ZipExporter.swift */; };
|
||||
6FF3527021C240160008484E /* ringlogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526C21C23F960008484E /* ringlogger.c */; };
|
||||
6FF3527121C240160008484E /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526E21C23FA10008484E /* Logger.swift */; };
|
||||
6FF3527221C2616C0008484E /* ringlogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526C21C23F960008484E /* ringlogger.c */; };
|
||||
6FF3527321C2616C0008484E /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526E21C23FA10008484E /* Logger.swift */; };
|
||||
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; };
|
||||
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; };
|
||||
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */; };
|
||||
6FFA5D8E2194370D0001E2F7 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */; };
|
||||
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
|
||||
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
|
||||
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
|
||||
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
|
||||
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
|
||||
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
|
||||
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */; };
|
||||
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -77,15 +113,30 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; };
|
||||
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Reuse.swift"; sourceTree = "<group>"; };
|
||||
5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditKeyValueCell.swift; sourceTree = "<group>"; };
|
||||
5F45418F21C2D53800994C13 /* SwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchCell.swift; sourceTree = "<group>"; };
|
||||
5F45419121C2D55800994C13 /* CheckmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkCell.swift; sourceTree = "<group>"; };
|
||||
5F45419721C2D60500994C13 /* KeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueCell.swift; sourceTree = "<group>"; };
|
||||
5F45419F21C2D6B700994C13 /* TunnelListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListCell.swift; sourceTree = "<group>"; };
|
||||
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedTextButton.swift; sourceTree = "<group>"; };
|
||||
5F4541A521C4449E00994C13 /* ButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCell.swift; sourceTree = "<group>"; };
|
||||
5F4541A821C451D100994C13 /* TunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatus.swift; sourceTree = "<group>"; };
|
||||
5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ArrayConversion.swift"; sourceTree = "<group>"; };
|
||||
5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyConfigMigration.swift; sourceTree = "<group>"; };
|
||||
5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+WgQuickConfig.swift"; sourceTree = "<group>"; };
|
||||
5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceConfiguration.swift; sourceTree = "<group>"; };
|
||||
5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerConfiguration.swift; sourceTree = "<group>"; };
|
||||
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = "<group>"; };
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
|
||||
6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
||||
6F5D0C1E218352EF000F85AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuardNetworkExtension.entitlements; sourceTree = "<group>"; };
|
||||
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WireGuardNetworkExtension-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptionKey.swift; sourceTree = "<group>"; };
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptionsGenerator.swift; sourceTree = "<group>"; };
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; };
|
||||
6F61F1E821B932F700483816 /* WireGuardAppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardAppError.swift; sourceTree = "<group>"; };
|
||||
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardResult.swift; sourceTree = "<group>"; };
|
||||
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewModel.swift; sourceTree = "<group>"; };
|
||||
6F628C3E217F3413003482A3 /* DNSServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSServer.swift; sourceTree = "<group>"; };
|
||||
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -93,20 +144,21 @@
|
||||
6F6899A42180447E0012E523 /* x25519.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = x25519.h; sourceTree = "<group>"; };
|
||||
6F6899A52180447E0012E523 /* x25519.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = x25519.c; sourceTree = "<group>"; };
|
||||
6F6899A7218044FC0012E523 /* Curve25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Curve25519.swift; sourceTree = "<group>"; };
|
||||
6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileParser.swift; sourceTree = "<group>"; };
|
||||
6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
|
||||
6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
||||
6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = "<group>"; };
|
||||
6F7774E72172020C006A79B3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConfiguration.swift; sourceTree = "<group>"; };
|
||||
6F7774E9217229DB006A79B3 /* IPAddressRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAddressRange.swift; sourceTree = "<group>"; };
|
||||
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = "<group>"; };
|
||||
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; };
|
||||
6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; };
|
||||
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; };
|
||||
6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; };
|
||||
6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; };
|
||||
6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_64x64.png; sourceTree = "<group>"; };
|
||||
6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_320x320.png; sourceTree = "<group>"; };
|
||||
6FB1017821C57DE600766195 /* MockTunnels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnels.swift; sourceTree = "<group>"; };
|
||||
6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libwg-go.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = "<group>"; };
|
||||
6FDEF7F621863B6100D8FBF6 /* unzip.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = unzip.c; sourceTree = "<group>"; };
|
||||
@@ -117,7 +169,13 @@
|
||||
6FDEF7FF21863C0100D8FBF6 /* ioapi.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ioapi.c; sourceTree = "<group>"; };
|
||||
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = "<group>"; };
|
||||
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; };
|
||||
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = "<group>"; };
|
||||
6FE1765521C90BBE002690EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = WireGuard/Base.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
6FE1765921C90E87002690EA /* LocalizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationHelper.swift; sourceTree = "<group>"; };
|
||||
6FE254FA219C10800028284D /* ZipImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipImporter.swift; sourceTree = "<group>"; };
|
||||
6FE254FE219C60290028284D /* ZipExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipExporter.swift; sourceTree = "<group>"; };
|
||||
6FF3526B21C23F960008484E /* ringlogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ringlogger.h; sourceTree = "<group>"; };
|
||||
6FF3526C21C23F960008484E /* ringlogger.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ringlogger.c; sourceTree = "<group>"; };
|
||||
6FF3526E21C23FA10008484E /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
6FF4AC14211EC46F002C96EB /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
@@ -125,6 +183,9 @@
|
||||
6FF4AC2B211EC776002C96EB /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Config.xcconfig; path = Config/Config.xcconfig; sourceTree = "<group>"; };
|
||||
6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
|
||||
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; };
|
||||
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; };
|
||||
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = "<group>"; };
|
||||
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandSetting.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -146,13 +207,43 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
5F4541A721C44F5B00994C13 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5F45419F21C2D6B700994C13 /* TunnelListCell.swift */,
|
||||
5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */,
|
||||
5F45418F21C2D53800994C13 /* SwitchCell.swift */,
|
||||
5F45419721C2D60500994C13 /* KeyValueCell.swift */,
|
||||
5F4541A521C4449E00994C13 /* ButtonCell.swift */,
|
||||
5F45419121C2D55800994C13 /* CheckmarkCell.swift */,
|
||||
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5F4541AC21C4720B00994C13 /* ViewController */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
|
||||
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
|
||||
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
|
||||
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
|
||||
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
|
||||
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
|
||||
);
|
||||
path = ViewController;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */,
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */,
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */,
|
||||
6F5D0C1E218352EF000F85AD /* Info.plist */,
|
||||
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */,
|
||||
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */,
|
||||
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */,
|
||||
);
|
||||
path = WireGuardNetworkExtension;
|
||||
sourceTree = "<group>";
|
||||
@@ -160,7 +251,9 @@
|
||||
6F5D0C432183B4A4000F85AD /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */,
|
||||
6FF3526A21C23F720008484E /* Logging */,
|
||||
6F7774E6217201E0006A79B3 /* Model */,
|
||||
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
@@ -175,15 +268,6 @@
|
||||
path = Crypto;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6F6899AA218099D00012E523 /* ConfigFile */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */,
|
||||
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */,
|
||||
);
|
||||
path = ConfigFile;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6F7774DD217181B1006A79B3 /* UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -196,15 +280,11 @@
|
||||
6F7774DE217181B1006A79B3 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */,
|
||||
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
|
||||
5F4541AC21C4720B00994C13 /* ViewController */,
|
||||
5F4541A721C44F5B00994C13 /* View */,
|
||||
6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
|
||||
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
|
||||
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
|
||||
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
|
||||
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
|
||||
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
|
||||
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */,
|
||||
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */,
|
||||
);
|
||||
path = iOS;
|
||||
sourceTree = "<group>";
|
||||
@@ -212,22 +292,30 @@
|
||||
6F7774E6217201E0006A79B3 /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F7774E72172020C006A79B3 /* Configuration.swift */,
|
||||
5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */,
|
||||
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
|
||||
5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */,
|
||||
5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */,
|
||||
6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */,
|
||||
6F7774E9217229DB006A79B3 /* IPAddressRange.swift */,
|
||||
6F693A552179E556008551C1 /* Endpoint.swift */,
|
||||
6F628C3E217F3413003482A3 /* DNSServer.swift */,
|
||||
5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */,
|
||||
5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6F7774ED21722D0C006A79B3 /* VPN */ = {
|
||||
6F7774ED21722D0C006A79B3 /* Tunnel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */,
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */,
|
||||
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
|
||||
5F4541A821C451D100994C13 /* TunnelStatus.swift */,
|
||||
6FB1017821C57DE600766195 /* MockTunnels.swift */,
|
||||
6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */,
|
||||
);
|
||||
path = VPN;
|
||||
path = Tunnel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6F919ED3218C65C50023B400 /* Resources */ = {
|
||||
@@ -252,6 +340,8 @@
|
||||
6FDEF7E72186320E00D8FBF6 /* ZipArchive */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6FE254FA219C10800028284D /* ZipImporter.swift */,
|
||||
6FE254FE219C60290028284D /* ZipExporter.swift */,
|
||||
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */,
|
||||
6FDEF7F421863B6100D8FBF6 /* 3rdparty */,
|
||||
);
|
||||
@@ -279,9 +369,20 @@
|
||||
path = minizip;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6FF3526A21C23F720008484E /* Logging */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6FF3526C21C23F960008484E /* ringlogger.c */,
|
||||
6FF3526B21C23F960008484E /* ringlogger.h */,
|
||||
6FF3526E21C23FA10008484E /* Logger.swift */,
|
||||
);
|
||||
path = Logging;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6FF4AC0B211EC46F002C96EB = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6FE1765421C90BBE002690EA /* Localizable.strings */,
|
||||
6F5D0C432183B4A4000F85AD /* Shared */,
|
||||
6FF4AC16211EC46F002C96EB /* WireGuard */,
|
||||
6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */,
|
||||
@@ -304,11 +405,12 @@
|
||||
children = (
|
||||
6F919ED3218C65C50023B400 /* Resources */,
|
||||
6F6899A32180445A0012E523 /* Crypto */,
|
||||
6F6899AA218099D00012E523 /* ConfigFile */,
|
||||
6F7774E6217201E0006A79B3 /* Model */,
|
||||
6F7774DD217181B1006A79B3 /* UI */,
|
||||
6F7774ED21722D0C006A79B3 /* VPN */,
|
||||
6F7774ED21722D0C006A79B3 /* Tunnel */,
|
||||
6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
|
||||
6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
|
||||
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
|
||||
6FE1765921C90E87002690EA /* LocalizationHelper.swift */,
|
||||
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */,
|
||||
6FF4AC1E211EC472002C96EB /* Assets.xcassets */,
|
||||
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */,
|
||||
@@ -352,6 +454,8 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */;
|
||||
buildPhases = (
|
||||
5F45417B21C0906F00994C13 /* Swiftlint */,
|
||||
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */,
|
||||
6F5D0C16218352EF000F85AD /* Sources */,
|
||||
6F5D0C17218352EF000F85AD /* Frameworks */,
|
||||
6F5D0C18218352EF000F85AD /* Resources */,
|
||||
@@ -370,6 +474,8 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6FF4AC26211EC472002C96EB /* Build configuration list for PBXNativeTarget "WireGuard" */;
|
||||
buildPhases = (
|
||||
5F784E5721CDF6DD00B8D9A0 /* Strip Trailing Whitespace */,
|
||||
5F45417A21C0902400994C13 /* Swiftlint */,
|
||||
6B87860E2189532500C099FB /* Extract wireguard-go Version */,
|
||||
6FF4AC10211EC46F002C96EB /* Sources */,
|
||||
6FF4AC11211EC46F002C96EB /* Frameworks */,
|
||||
@@ -393,7 +499,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1000;
|
||||
LastUpgradeCheck = 0940;
|
||||
LastUpgradeCheck = 1010;
|
||||
ORGANIZATIONNAME = "WireGuard LLC";
|
||||
TargetAttributes = {
|
||||
6F5D0C19218352EF000F85AD = {
|
||||
@@ -407,6 +513,9 @@
|
||||
CreatedOnToolsVersion = 9.4.1;
|
||||
LastSwiftMigration = 1000;
|
||||
SystemCapabilities = {
|
||||
com.apple.ApplicationGroups.iOS = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.NetworkExtensions.iOS = {
|
||||
enabled = 1;
|
||||
};
|
||||
@@ -447,6 +556,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */,
|
||||
6FE1765621C90BBE002690EA /* Localizable.strings in Resources */,
|
||||
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */,
|
||||
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */,
|
||||
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */,
|
||||
@@ -458,6 +568,60 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
5F45417A21C0902400994C13 /* Swiftlint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = Swiftlint;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||
};
|
||||
5F45417B21C0906F00994C13 /* Swiftlint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = Swiftlint;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||
};
|
||||
5F784E5721CDF6DD00B8D9A0 /* Strip Trailing Whitespace */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Strip Trailing Whitespace";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "find . -name '*.swift' -exec sed -i '' -E 's/[[:space:]]+$//g' {} +\n";
|
||||
};
|
||||
6B87860E2189532500C099FB /* Extract wireguard-go Version */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -477,6 +641,25 @@
|
||||
shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Extract wireguard-go Version";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -484,7 +667,22 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6F5D0C462183C0B4000F85AD /* PacketTunnelOptionKey.swift in Sources */,
|
||||
6FF3527021C240160008484E /* ringlogger.c in Sources */,
|
||||
6FF3527121C240160008484E /* Logger.swift in Sources */,
|
||||
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */,
|
||||
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */,
|
||||
5F9696B121CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */,
|
||||
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
|
||||
6FFA5D8E2194370D0001E2F7 /* TunnelConfiguration.swift in Sources */,
|
||||
5FF7B96621CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */,
|
||||
5F9696AE21CD6F72008063FE /* String+ArrayConversion.swift in Sources */,
|
||||
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */,
|
||||
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */,
|
||||
5FF7B96321CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */,
|
||||
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */,
|
||||
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */,
|
||||
5F9696AB21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */,
|
||||
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */,
|
||||
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -493,32 +691,52 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */,
|
||||
6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */,
|
||||
6FF3527221C2616C0008484E /* ringlogger.c in Sources */,
|
||||
6FF3527321C2616C0008484E /* Logger.swift in Sources */,
|
||||
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */,
|
||||
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
|
||||
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */,
|
||||
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */,
|
||||
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
|
||||
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */,
|
||||
5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */,
|
||||
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
|
||||
6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
|
||||
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
|
||||
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
|
||||
5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */,
|
||||
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */,
|
||||
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
|
||||
6F6899A62180447E0012E523 /* x25519.c in Sources */,
|
||||
6F5D0C452183BCDA000F85AD /* PacketTunnelOptionKey.swift in Sources */,
|
||||
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
|
||||
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */,
|
||||
6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */,
|
||||
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
|
||||
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
|
||||
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
|
||||
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */,
|
||||
5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */,
|
||||
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
|
||||
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
|
||||
5F9696AA21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */,
|
||||
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
|
||||
5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */,
|
||||
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
|
||||
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
|
||||
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
|
||||
6F7774E82172020C006A79B3 /* TunnelConfiguration.swift in Sources */,
|
||||
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
|
||||
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
|
||||
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */,
|
||||
5F9696B021CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */,
|
||||
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
|
||||
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */,
|
||||
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
|
||||
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
|
||||
5F45419021C2D53800994C13 /* SwitchCell.swift in Sources */,
|
||||
6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */,
|
||||
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
|
||||
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */,
|
||||
5FF7B96521CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */,
|
||||
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
|
||||
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
|
||||
5F4541B221CBFAEE00994C13 /* String+ArrayConversion.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -538,6 +756,14 @@
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
6FE1765421C90BBE002690EA /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
6FE1765521C90BBE002690EA /* Base */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
@@ -646,7 +872,9 @@
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
@@ -706,7 +934,9 @@
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
@@ -747,7 +977,6 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
"OTHER_CFLAGS[arch=*]" = "-DNOCRYPT";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "WireGuard/WireGuard-Bridging-Header.h";
|
||||
@@ -767,7 +996,6 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
"OTHER_CFLAGS[arch=*]" = "-DNOCRYPT";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "WireGuard/WireGuard-Bridging-Header.h";
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
<dict>
|
||||
<key>FILEHEADER</key>
|
||||
<string> SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.</string>
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dyO-pm-zxZ">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dyO-pm-zxZ">
|
||||
<device id="ipad9_7" orientation="landscape">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -23,7 +23,7 @@
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="XJD-RE-ATd" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
<!--WireGuard-->
|
||||
<!--Table View Controller-->
|
||||
<scene sceneID="pXL-Um-fWR">
|
||||
<objects>
|
||||
<tableViewController clearsSelectionOnViewWillAppear="NO" id="9s0-Gz-xEQ" sceneMemberID="viewController">
|
||||
@@ -46,10 +46,7 @@
|
||||
<outlet property="delegate" destination="9s0-Gz-xEQ" id="Frz-TZ-bD1"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="WireGuard" id="0ZE-eq-OPY">
|
||||
<barButtonItem key="leftBarButtonItem" title="Settings" id="a9Y-E3-hac"/>
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="add" id="ZSm-4E-cJx"/>
|
||||
</navigationItem>
|
||||
<navigationItem key="navigationItem" id="0ZE-eq-OPY"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="YJ1-t3-sLe" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
// Generic alert action names
|
||||
|
||||
"actionOK" = "OK";
|
||||
"actionCancel" = "Cancel";
|
||||
"actionSave" = "Save";
|
||||
|
||||
// Tunnels list UI
|
||||
|
||||
"tunnelsListTitle" = "WireGuard";
|
||||
"tunnelsListSettingsButtonTitle" = "Settings";
|
||||
"tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel";
|
||||
"tunnelsListSwipeDeleteButtonTitle" = "Delete";
|
||||
|
||||
// Tunnels list menu
|
||||
|
||||
"addTunnelMenuHeader" = "Add a new WireGuard tunnel";
|
||||
"addTunnelMenuImportFile" = "Create from file or archive";
|
||||
"addTunnelMenuQRCode" = "Create from QR code";
|
||||
"addTunnelMenuFromScratch" = "Create from scratch";
|
||||
|
||||
// Tunnels list alerts
|
||||
|
||||
"alertImportedFromZipTitle (%d)" = "Created %d tunnels";
|
||||
"alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive";
|
||||
|
||||
"alertUnableToImportTitle" = "Unable to import tunnel";
|
||||
"alertUnableToImportMessage" = "An error occured when importing the tunnel configuration.";
|
||||
|
||||
// Tunnel detail and edit UI
|
||||
|
||||
"newTunnelViewTitle" = "New configuration";
|
||||
"editTunnelViewTitle" = "Edit configuration";
|
||||
|
||||
"tunnelSectionTitleStatus" = "Status";
|
||||
|
||||
"tunnelStatusInactive" = "Inactive";
|
||||
"tunnelStatusActivating" = "Activating";
|
||||
"tunnelStatusActive" = "Active";
|
||||
"tunnelStatusDeactivating" = "Deactivating";
|
||||
"tunnelStatusReasserting" = "Reactivating";
|
||||
"tunnelStatusRestarting" = "Restarting";
|
||||
"tunnelStatusWaiting" = "Waiting";
|
||||
|
||||
"tunnelSectionTitleInterface" = "Interface";
|
||||
|
||||
"tunnelInterfaceName" = "Name";
|
||||
"tunnelInterfacePrivateKey" = "Private key";
|
||||
"tunnelInterfacePublicKey" = "Public key";
|
||||
"tunnelInterfaceGenerateKeypair" = "Generate keypair";
|
||||
"tunnelInterfaceAddresses" = "Addresses";
|
||||
"tunnelInterfaceListenPort" = "Listen port";
|
||||
"tunnelInterfaceMTU" = "MTU";
|
||||
"tunnelInterfaceDNS" = "DNS servers";
|
||||
|
||||
"tunnelSectionTitlePeer" = "Peer";
|
||||
|
||||
"tunnelPeerPublicKey" = "Public key";
|
||||
"tunnelPeerPreSharedKey" = "Preshared key";
|
||||
"tunnelPeerEndpoint" = "Endpoint";
|
||||
"tunnelPeerPersistentKeepalive" = "Persistent keepalive";
|
||||
"tunnelPeerAllowedIPs" = "Allowed IPs";
|
||||
"tunnelPeerExcludePrivateIPs" = "Exclude private IPs";
|
||||
|
||||
"tunnelSectionTitleOnDemand" = "On-Demand Activation";
|
||||
|
||||
"tunnelOnDemandKey" = "Activate on demand";
|
||||
"tunnelOnDemandOptionOff" = "Off";
|
||||
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
|
||||
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
|
||||
"tunnelOnDemandOptionCellularOnly" = "Cellular only";
|
||||
|
||||
"addPeerButtonTitle" = "Add peer";
|
||||
|
||||
"deletePeerButtonTitle" = "Delete peer";
|
||||
"deletePeerConfirmationAlertButtonTitle" = "Delete";
|
||||
"deletePeerConfirmationAlertMessage" = "Delete this peer?";
|
||||
|
||||
"deleteTunnelButtonTitle" = "Delete tunnel";
|
||||
"deleteTunnelConfirmationAlertButtonTitle" = "Delete";
|
||||
"deleteTunnelConfirmationAlertMessage" = "Delete this tunnel?";
|
||||
|
||||
"tunnelEditPlaceholderTextRequired" = "Required";
|
||||
"tunnelEditPlaceholderTextOptional" = "Optional";
|
||||
"tunnelEditPlaceholderTextAutomatic" = "Automatic";
|
||||
"tunnelEditPlaceholderTextStronglyRecommended" = "Strongly recommended";
|
||||
"tunnelEditPlaceholderTextOff" = "Off";
|
||||
|
||||
// Error alerts while creating / editing a tunnel configuration
|
||||
|
||||
/* Alert title for error in the interface data */
|
||||
"alertInvalidInterfaceTitle" = "Invalid interface";
|
||||
|
||||
/* Any one of the following alert messages can go with the above title */
|
||||
"alertInvalidInterfaceMessageNameRequired" = "Interface name is required";
|
||||
"alertInvalidInterfaceMessagePrivateKeyRequired" = "Interface's private key is required";
|
||||
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interface's private key must be a 32-byte key in base64 encoding";
|
||||
"alertInvalidInterfaceMessageAddressInvalid" = "Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation";
|
||||
"alertInvalidInterfaceMessageListenPortInvalid" = "Interface's listen port must be between 0 and 65535, or unspecified";
|
||||
"alertInvalidInterfaceMessageMTUInvalid" = "Interface's MTU must be between 576 and 65535, or unspecified";
|
||||
"alertInvalidInterfaceMessageDNSInvalid" = "Interface's DNS servers must be a list of comma-separated IP addresses";
|
||||
|
||||
/* Alert title for error in the peer data */
|
||||
"alertInvalidPeerTitle" = "Invalid peer";
|
||||
|
||||
/* Any one of the following alert messages can go with the above title */
|
||||
"alertInvalidPeerMessagePublicKeyRequired" = "Peer's public key is required";
|
||||
"alertInvalidPeerMessagePublicKeyInvalid" = "Peer's public key must be a 32-byte key in base64 encoding";
|
||||
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peer's preshared key must be a 32-byte key in base64 encoding";
|
||||
"alertInvalidPeerMessageAllowedIPsInvalid" = "Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation";
|
||||
"alertInvalidPeerMessageEndpointInvalid" = "Peer's endpoint must be of the form 'host:port' or '[host]:port'";
|
||||
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peer's persistent keepalive must be between 0 to 65535, or unspecified";
|
||||
"alertInvalidPeerMessagePublicKeyDuplicated" = "Two or more peers cannot have the same public key";
|
||||
|
||||
// Scanning QR code UI
|
||||
|
||||
"scanQRCodeViewTitle" = "Scan QR code";
|
||||
"scanQRCodeTipText" = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`";
|
||||
|
||||
// Scanning QR code alerts
|
||||
|
||||
"alertScanQRCodeCameraUnsupportedTitle" = "Camera Unsupported";
|
||||
"alertScanQRCodeCameraUnsupportedMessage" = "This device is not able to scan QR codes";
|
||||
|
||||
"alertScanQRCodeInvalidQRCodeTitle" = "Invalid QR Code";
|
||||
"alertScanQRCodeInvalidQRCodeMessage" = "The scanned QR code is not a valid WireGuard configuration";
|
||||
|
||||
"alertScanQRCodeUnreadableQRCodeTitle" = "Invalid Code";
|
||||
"alertScanQRCodeUnreadableQRCodeMessage" = "The scanned code could not be read";
|
||||
|
||||
"alertScanQRCodeNamePromptTitle" = "Please name the scanned tunnel";
|
||||
|
||||
// Settings UI
|
||||
|
||||
"settingsViewTitle" = "Settings";
|
||||
|
||||
"settingsSectionTitleAbout" = "About";
|
||||
"settingsVersionKeyWireGuardForIOS" = "WireGuard for iOS";
|
||||
"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
|
||||
|
||||
"settingsSectionTitleExportConfigurations" = "Export configurations";
|
||||
"settingsExportZipButtonTitle" = "Export zip archive";
|
||||
|
||||
"settingsSectionTitleTunnelLog" = "Tunnel log";
|
||||
"settingsExportLogFileButtonTitle" = "Export log file";
|
||||
|
||||
// Settings alerts
|
||||
|
||||
"alertUnableToRemovePreviousLogTitle" = "Log export failed";
|
||||
"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
|
||||
|
||||
"alertUnableToFindExtensionLogPathTitle" = "Log export failed";
|
||||
"alertUnableToFindExtensionLogPathMessage" = "Unable to determine extension log path";
|
||||
|
||||
"alertUnableToWriteLogTitle" = "Log export failed";
|
||||
"alertUnableToWriteLogMessage" = "Unable to write logs to file";
|
||||
|
||||
// Zip import / export error alerts
|
||||
|
||||
"alertCantOpenInputZipFileTitle" = "Unable to read zip archive";
|
||||
"alertCantOpenInputZipFileMessage" = "The zip archive could not be read.";
|
||||
|
||||
"alertCantOpenOutputZipFileForWritingTitle" = "Unable to create zip archive";
|
||||
"alertCantOpenOutputZipFileForWritingMessage" = "Could not open zip file for writing.";
|
||||
|
||||
"alertBadArchiveTitle" = "Unable to read zip archive";
|
||||
"alertBadArchiveMessage" = "Bad or corrupt zip archive.";
|
||||
|
||||
"alertNoTunnelsToExportTitle" = "Nothing to export";
|
||||
"alertNoTunnelsToExportMessage" = "There are no tunnels to export";
|
||||
|
||||
"alertNoTunnelsInImportedZipArchiveTitle" = "No tunnels in zip archive";
|
||||
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
|
||||
|
||||
// Tunnel management error alerts
|
||||
|
||||
"alertTunnelActivationFailureTitle" = "Activation failure";
|
||||
"alertTunnelActivationFailureMessage" = "The tunnel could not be activated. Please ensure that you are connected to the Internet.";
|
||||
"alertTunnelActivationSavedConfigFailureMessage" = "Unable to retrieve tunnel information from the saved configuration.";
|
||||
"alertTunnelActivationBackendFailureMessage" = "Unable to turn on Go backend library.";
|
||||
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
|
||||
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
|
||||
|
||||
"alertTunnelActivationFailureOnDemandAddendum" = " This tunnel has Activate On Demand enabled, so this tunnel might be re-activated automatically by the OS. You may turn off Activate On Demand in this app by editing the tunnel configuration.";
|
||||
|
||||
"alertTunnelDNSFailureTitle" = "DNS resolution failure";
|
||||
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
|
||||
|
||||
"alertTunnelNameEmptyTitle" = "No name provided";
|
||||
"alertTunnelNameEmptyMessage" = "Cannot create tunnel with an empty name";
|
||||
|
||||
"alertTunnelAlreadyExistsWithThatNameTitle" = "Name already exists";
|
||||
"alertTunnelAlreadyExistsWithThatNameMessage" = "A tunnel with that name already exists";
|
||||
|
||||
"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Activation in progress";
|
||||
"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "The tunnel is already active or in the process of being activated";
|
||||
|
||||
// Tunnel management error alerts on system error
|
||||
|
||||
/* The alert message that goes with the following titles would be
|
||||
one of the alertSystemErrorMessage* listed further down */
|
||||
"alertSystemErrorOnListingTunnelsTitle" = "Unable to list tunnels";
|
||||
"alertSystemErrorOnAddTunnelTitle" = "Unable to create tunnel";
|
||||
"alertSystemErrorOnModifyTunnelTitle" = "Unable to modify tunnel";
|
||||
"alertSystemErrorOnRemoveTunnelTitle" = "Unable to remove tunnel";
|
||||
|
||||
/* The alert message for this alert shall include
|
||||
one of the alertSystemErrorMessage* listed further down */
|
||||
"alertTunnelActivationSystemErrorTitle" = "Activation failure";
|
||||
"alertTunnelActivationSystemErrorMessage (%@)" = "The tunnel could not be activated. %@";
|
||||
|
||||
/* alertSystemErrorMessage* messages */
|
||||
"alertSystemErrorMessageTunnelConfigurationInvalid" = "The configuration is invalid.";
|
||||
"alertSystemErrorMessageTunnelConfigurationDisabled" = "The configuration is disabled.";
|
||||
"alertSystemErrorMessageTunnelConnectionFailed" = "The connection failed.";
|
||||
"alertSystemErrorMessageTunnelConfigurationStale" = "The configuration is stale.";
|
||||
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
|
||||
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error.";
|
||||
@@ -1,2 +1,2 @@
|
||||
VERSION_NAME = 0.0.20181104
|
||||
VERSION_ID = 2
|
||||
VERSION_NAME = 0.0.20190107
|
||||
VERSION_ID = 1
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
class WgQuickConfigFileParser {
|
||||
|
||||
enum ParserState {
|
||||
case inInterfaceSection
|
||||
case inPeerSection
|
||||
case notInASection
|
||||
}
|
||||
|
||||
enum ParseError: Error {
|
||||
case invalidLine(_ line: String.SubSequence)
|
||||
case noInterface
|
||||
case invalidInterface
|
||||
case multipleInterfaces
|
||||
case invalidPeer
|
||||
}
|
||||
|
||||
static func parse(_ text: String, name: String) throws -> TunnelConfiguration {
|
||||
|
||||
assert(!name.isEmpty)
|
||||
|
||||
func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? {
|
||||
// required wg fields
|
||||
guard let privateKeyString = attributes["PrivateKey"] else { return nil }
|
||||
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else { return nil }
|
||||
var interface = InterfaceConfiguration(name: name, privateKey: privateKey)
|
||||
// other wg fields
|
||||
if let listenPortString = attributes["ListenPort"] {
|
||||
guard let listenPort = UInt16(listenPortString) else { return nil }
|
||||
interface.listenPort = listenPort
|
||||
}
|
||||
// wg-quick fields
|
||||
if let addressesString = attributes["Address"] {
|
||||
var addresses: [IPAddressRange] = []
|
||||
for addressString in addressesString.split(separator: ",") {
|
||||
let trimmedString = addressString.trimmingCharacters(in: .whitespaces)
|
||||
guard let address = IPAddressRange(from: trimmedString) else { return nil }
|
||||
addresses.append(address)
|
||||
}
|
||||
interface.addresses = addresses
|
||||
}
|
||||
if let dnsString = attributes["DNS"] {
|
||||
var dnsServers: [DNSServer] = []
|
||||
for dnsServerString in dnsString.split(separator: ",") {
|
||||
let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces)
|
||||
guard let dnsServer = DNSServer(from: trimmedString) else { return nil }
|
||||
dnsServers.append(dnsServer)
|
||||
}
|
||||
interface.dns = dnsServers
|
||||
}
|
||||
if let mtuString = attributes["MTU"] {
|
||||
guard let mtu = UInt16(mtuString) else { return nil }
|
||||
interface.mtu = mtu
|
||||
}
|
||||
return interface
|
||||
}
|
||||
|
||||
func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
|
||||
// required wg fields
|
||||
guard let publicKeyString = attributes["PublicKey"] else { return nil }
|
||||
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else { return nil }
|
||||
var peer = PeerConfiguration(publicKey: publicKey)
|
||||
// wg fields
|
||||
if let preSharedKeyString = attributes["PreSharedKey"] {
|
||||
guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 else { return nil }
|
||||
peer.preSharedKey = preSharedKey
|
||||
}
|
||||
if let allowedIPsString = attributes["AllowedIPs"] {
|
||||
var allowedIPs: [IPAddressRange] = []
|
||||
for allowedIPString in allowedIPsString.split(separator: ",") {
|
||||
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
guard let allowedIP = IPAddressRange(from: trimmedString) else { return nil }
|
||||
allowedIPs.append(allowedIP)
|
||||
}
|
||||
peer.allowedIPs = allowedIPs
|
||||
}
|
||||
if let endpointString = attributes["Endpoint"] {
|
||||
guard let endpoint = Endpoint(from: endpointString) else { return nil }
|
||||
peer.endpoint = endpoint
|
||||
}
|
||||
if let persistentKeepAliveString = attributes["PersistentKeepalive"] {
|
||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
|
||||
peer.persistentKeepAlive = persistentKeepAlive
|
||||
}
|
||||
return peer
|
||||
}
|
||||
|
||||
var interfaceConfiguration: InterfaceConfiguration?
|
||||
var peerConfigurations: [PeerConfiguration] = []
|
||||
|
||||
let lines = text.split(separator: "\n")
|
||||
|
||||
var parserState: ParserState = .notInASection
|
||||
var attributes: [String: String] = [:]
|
||||
|
||||
for (lineIndex, line) in lines.enumerated() {
|
||||
var trimmedLine: String
|
||||
if let commentRange = line.range(of: "#") {
|
||||
trimmedLine = String(line[..<commentRange.lowerBound])
|
||||
} else {
|
||||
trimmedLine = String(line)
|
||||
}
|
||||
|
||||
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
guard trimmedLine.count > 0 else { continue }
|
||||
let lowercasedLine = line.lowercased()
|
||||
|
||||
if let equalsIndex = line.firstIndex(of: "=") {
|
||||
// Line contains an attribute
|
||||
let key = line[..<equalsIndex].trimmingCharacters(in: .whitespaces)
|
||||
let value = line[line.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespaces)
|
||||
let keysWithMultipleEntriesAllowed: Set<String> = ["Address", "AllowedIPs", "DNS"]
|
||||
if let presentValue = attributes[key], keysWithMultipleEntriesAllowed.contains(key) {
|
||||
attributes[key] = presentValue + "," + value
|
||||
} else {
|
||||
attributes[key] = value
|
||||
}
|
||||
} else {
|
||||
if (lowercasedLine != "[interface]" && lowercasedLine != "[peer]") {
|
||||
throw ParseError.invalidLine(line)
|
||||
}
|
||||
}
|
||||
|
||||
let isLastLine: Bool = (lineIndex == lines.count - 1)
|
||||
|
||||
if (isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]") {
|
||||
// Previous section has ended; process the attributes collected so far
|
||||
if (parserState == .inInterfaceSection) {
|
||||
guard let interface = collate(interfaceAttributes: attributes) else { throw ParseError.invalidInterface }
|
||||
guard (interfaceConfiguration == nil) else { throw ParseError.multipleInterfaces }
|
||||
interfaceConfiguration = interface
|
||||
} else if (parserState == .inPeerSection) {
|
||||
guard let peer = collate(peerAttributes: attributes) else { throw ParseError.invalidPeer }
|
||||
peerConfigurations.append(peer)
|
||||
}
|
||||
}
|
||||
|
||||
if (lowercasedLine == "[interface]") {
|
||||
parserState = .inInterfaceSection
|
||||
attributes.removeAll()
|
||||
} else if (lowercasedLine == "[peer]") {
|
||||
parserState = .inPeerSection
|
||||
attributes.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
if let interfaceConfiguration = interfaceConfiguration {
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration)
|
||||
tunnelConfiguration.peers = peerConfigurations
|
||||
return tunnelConfiguration
|
||||
} else {
|
||||
throw ParseError.noInterface
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class WgQuickConfigFileWriter {
|
||||
static func writeConfigFile(from tc: TunnelConfiguration) -> Data? {
|
||||
let interface = tc.interface
|
||||
var output = "[Interface]\n"
|
||||
output.append("PrivateKey = \(interface.privateKey.base64EncodedString())\n")
|
||||
if let listenPort = interface.listenPort {
|
||||
output.append("ListenPort = \(listenPort)\n")
|
||||
}
|
||||
if (!interface.addresses.isEmpty) {
|
||||
let addressString = interface.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
|
||||
output.append("Address = \(addressString)\n")
|
||||
}
|
||||
if (!interface.dns.isEmpty) {
|
||||
let dnsString = interface.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
|
||||
output.append("DNS = \(dnsString)\n")
|
||||
}
|
||||
if let mtu = interface.mtu {
|
||||
output.append("MTU = \(mtu)\n")
|
||||
}
|
||||
|
||||
for peer in tc.peers {
|
||||
output.append("\n[Peer]\n")
|
||||
output.append("PublicKey = \(peer.publicKey.base64EncodedString())\n")
|
||||
if let preSharedKey = peer.preSharedKey {
|
||||
output.append("PresharedKey = \(preSharedKey.base64EncodedString())\n")
|
||||
}
|
||||
if (!peer.allowedIPs.isEmpty) {
|
||||
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
|
||||
output.append("AllowedIPs = \(allowedIPsString)\n")
|
||||
}
|
||||
if let endpoint = peer.endpoint {
|
||||
output.append("Endpoint = \(endpoint.stringRepresentation())\n")
|
||||
}
|
||||
if let persistentKeepAlive = peer.persistentKeepAlive {
|
||||
output.append("PersistentKeepalive = \(persistentKeepAlive)\n")
|
||||
}
|
||||
}
|
||||
|
||||
return output.data(using: .utf8)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,36 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
struct Curve25519 {
|
||||
|
||||
static let keyLength: Int = 32
|
||||
|
||||
static func generatePrivateKey() -> Data {
|
||||
var privateKey = Data(repeating: 0, count: 32)
|
||||
privateKey.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in
|
||||
var privateKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
|
||||
privateKey.withUnsafeMutableBytes { bytes in
|
||||
curve25519_generate_private_key(bytes)
|
||||
}
|
||||
assert(privateKey.count == 32)
|
||||
assert(privateKey.count == TunnelConfiguration.keyLength)
|
||||
return privateKey
|
||||
}
|
||||
|
||||
static func generatePublicKey(fromPrivateKey privateKey: Data) -> Data {
|
||||
assert(privateKey.count == 32)
|
||||
var publicKey = Data(repeating: 0, count: 32)
|
||||
privateKey.withUnsafeBytes { (privateKeyBytes: UnsafePointer<UInt8>) in
|
||||
publicKey.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in
|
||||
assert(privateKey.count == TunnelConfiguration.keyLength)
|
||||
var publicKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
|
||||
privateKey.withUnsafeBytes { privateKeyBytes in
|
||||
publicKey.withUnsafeMutableBytes { bytes in
|
||||
curve25519_derive_public_key(bytes, privateKeyBytes)
|
||||
}
|
||||
}
|
||||
assert(publicKey.count == 32)
|
||||
assert(publicKey.count == TunnelConfiguration.keyLength)
|
||||
return publicKey
|
||||
}
|
||||
}
|
||||
|
||||
extension InterfaceConfiguration {
|
||||
var publicKey: Data {
|
||||
return Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0+
|
||||
*
|
||||
* Copyright (C) 2015-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*
|
||||
* Curve25519 ECDH functions, based on TweetNaCl but cleaned up.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
@@ -41,6 +43,20 @@
|
||||
<string>com.pkware.zip-archive</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Text file</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.text</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
@@ -106,5 +122,7 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
func tr(_ key: String) -> String {
|
||||
return NSLocalizedString(key, comment: "")
|
||||
}
|
||||
|
||||
func tr(format: String, _ arguments: CVarArg...) -> String {
|
||||
return String(format: NSLocalizedString(format, comment: ""), arguments: arguments)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
class TunnelConfiguration: Codable {
|
||||
var interface: InterfaceConfiguration
|
||||
var peers: [PeerConfiguration] = []
|
||||
init(interface: InterfaceConfiguration) {
|
||||
self.interface = interface
|
||||
}
|
||||
}
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
struct InterfaceConfiguration: Codable {
|
||||
var name: String
|
||||
var privateKey: Data
|
||||
var addresses: [IPAddressRange] = []
|
||||
var listenPort: UInt16?
|
||||
var mtu: UInt16?
|
||||
var dns: [DNSServer] = []
|
||||
|
||||
var publicKey: Data {
|
||||
return Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
||||
}
|
||||
|
||||
init(name: String, privateKey: Data) {
|
||||
self.name = name
|
||||
self.privateKey = privateKey
|
||||
if (name.isEmpty) { fatalError("Empty name") }
|
||||
if (privateKey.count != 32) { fatalError("Invalid private key") }
|
||||
}
|
||||
}
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
struct PeerConfiguration: Codable {
|
||||
var publicKey: Data
|
||||
var preSharedKey: Data? {
|
||||
didSet(value) {
|
||||
if let value = value {
|
||||
if (value.count != 32) { fatalError("Invalid preshared key") }
|
||||
}
|
||||
}
|
||||
}
|
||||
var allowedIPs: [IPAddressRange] = []
|
||||
var endpoint: Endpoint?
|
||||
var persistentKeepAlive: UInt16?
|
||||
|
||||
init(publicKey: Data) {
|
||||
self.publicKey = publicKey
|
||||
if (publicKey.count != 32) { fatalError("Invalid public key") }
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
struct DNSServer {
|
||||
let address: IPAddress
|
||||
}
|
||||
|
||||
// MARK: Converting to and from String
|
||||
// For use in the UI
|
||||
|
||||
extension DNSServer {
|
||||
init?(from addressString: String) {
|
||||
if let addr = IPv4Address(addressString) {
|
||||
address = addr
|
||||
} else if let addr = IPv6Address(addressString) {
|
||||
address = addr
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func stringRepresentation() -> String {
|
||||
return "\(address)"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
// For serializing to disk
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
extension DNSServer: Codable {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(address.rawValue)
|
||||
}
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
var data = try container.decode(Data.self)
|
||||
let ipAddressFromData: IPAddress? = {
|
||||
switch (data.count) {
|
||||
case 4: return IPv4Address(data)
|
||||
case 16: return IPv6Address(data)
|
||||
default: return nil
|
||||
}
|
||||
}()
|
||||
guard let ipAddress = ipAddressFromData else {
|
||||
throw DecodingError.invalidData
|
||||
}
|
||||
address = ipAddress
|
||||
}
|
||||
enum DecodingError: Error {
|
||||
case invalidData
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
struct IPAddressRange {
|
||||
let address: IPAddress
|
||||
var networkPrefixLength: UInt8
|
||||
}
|
||||
|
||||
// MARK: Converting to and from String
|
||||
// For use in the UI
|
||||
|
||||
extension IPAddressRange {
|
||||
init?(from string: String) {
|
||||
let endOfIPAddress = string.lastIndex(of: "/") ?? string.endIndex
|
||||
let addressString = String(string[string.startIndex ..< endOfIPAddress])
|
||||
let address: IPAddress
|
||||
if let addr = IPv4Address(addressString) {
|
||||
address = addr
|
||||
} else if let addr = IPv6Address(addressString) {
|
||||
address = addr
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
let maxNetworkPrefixLength: UInt8 = (address is IPv4Address) ? 32 : 128
|
||||
var networkPrefixLength: UInt8
|
||||
if (endOfIPAddress < string.endIndex) { // "/" was located
|
||||
let indexOfNetworkPrefixLength = string.index(after: endOfIPAddress)
|
||||
guard (indexOfNetworkPrefixLength < string.endIndex) else { return nil }
|
||||
let networkPrefixLengthSubstring = string[indexOfNetworkPrefixLength ..< string.endIndex]
|
||||
guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil }
|
||||
networkPrefixLength = min(npl, maxNetworkPrefixLength)
|
||||
} else {
|
||||
networkPrefixLength = maxNetworkPrefixLength
|
||||
}
|
||||
self.address = address
|
||||
self.networkPrefixLength = networkPrefixLength
|
||||
}
|
||||
func stringRepresentation() -> String {
|
||||
return "\(address)/\(networkPrefixLength)"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
// For serializing to disk
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
extension IPAddressRange: Codable {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
let addressDataLength: Int
|
||||
if address is IPv4Address {
|
||||
addressDataLength = 4
|
||||
} else if address is IPv6Address {
|
||||
addressDataLength = 16
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
var data = Data(capacity: addressDataLength + 1)
|
||||
data.append(address.rawValue)
|
||||
data.append(networkPrefixLength)
|
||||
try container.encode(data)
|
||||
}
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
var data = try container.decode(Data.self)
|
||||
networkPrefixLength = data.removeLast()
|
||||
let ipAddressFromData: IPAddress? = {
|
||||
switch (data.count) {
|
||||
case 4: return IPv4Address(data)
|
||||
case 16: return IPv6Address(data)
|
||||
default: return nil
|
||||
}
|
||||
}()
|
||||
guard let ipAddress = ipAddressFromData else {
|
||||
throw DecodingError.invalidData
|
||||
}
|
||||
address = ipAddress
|
||||
}
|
||||
enum DecodingError: Error {
|
||||
case invalidData
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
struct ActivateOnDemandSetting {
|
||||
var isActivateOnDemandEnabled: Bool
|
||||
var activateOnDemandOption: ActivateOnDemandOption
|
||||
}
|
||||
|
||||
enum ActivateOnDemandOption {
|
||||
case none // Valid only when isActivateOnDemandEnabled is false
|
||||
case useOnDemandOverWiFiOrCellular
|
||||
case useOnDemandOverWiFiOnly
|
||||
case useOnDemandOverCellularOnly
|
||||
}
|
||||
|
||||
extension ActivateOnDemandSetting {
|
||||
func apply(on tunnelProviderManager: NETunnelProviderManager) {
|
||||
tunnelProviderManager.isOnDemandEnabled = isActivateOnDemandEnabled
|
||||
let rules: [NEOnDemandRule]?
|
||||
let connectRule = NEOnDemandRuleConnect()
|
||||
let disconnectRule = NEOnDemandRuleDisconnect()
|
||||
switch activateOnDemandOption {
|
||||
case .none:
|
||||
rules = nil
|
||||
case .useOnDemandOverWiFiOrCellular:
|
||||
rules = [connectRule]
|
||||
case .useOnDemandOverWiFiOnly:
|
||||
connectRule.interfaceTypeMatch = .wiFi
|
||||
disconnectRule.interfaceTypeMatch = .cellular
|
||||
rules = [connectRule, disconnectRule]
|
||||
case .useOnDemandOverCellularOnly:
|
||||
connectRule.interfaceTypeMatch = .cellular
|
||||
disconnectRule.interfaceTypeMatch = .wiFi
|
||||
rules = [connectRule, disconnectRule]
|
||||
}
|
||||
tunnelProviderManager.onDemandRules = rules
|
||||
}
|
||||
|
||||
init(from tunnelProviderManager: NETunnelProviderManager) {
|
||||
let rules = tunnelProviderManager.onDemandRules ?? []
|
||||
let activateOnDemandOption: ActivateOnDemandOption
|
||||
switch rules.count {
|
||||
case 0:
|
||||
activateOnDemandOption = .none
|
||||
case 1:
|
||||
let rule = rules[0]
|
||||
precondition(rule.action == .connect)
|
||||
activateOnDemandOption = .useOnDemandOverWiFiOrCellular
|
||||
case 2:
|
||||
let connectRule = rules.first(where: { $0.action == .connect })!
|
||||
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
|
||||
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular {
|
||||
activateOnDemandOption = .useOnDemandOverWiFiOnly
|
||||
} else if connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi {
|
||||
activateOnDemandOption = .useOnDemandOverCellularOnly
|
||||
} else {
|
||||
fatalError("Unexpected onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
default:
|
||||
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
self.activateOnDemandOption = activateOnDemandOption
|
||||
if activateOnDemandOption == .none {
|
||||
isActivateOnDemandEnabled = false
|
||||
} else {
|
||||
isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActivateOnDemandSetting {
|
||||
static var defaultSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: false, activateOnDemandOption: .none)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
// Creates mock tunnels for the iOS Simulator.
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
class MockTunnels {
|
||||
static let tunnelNames = [
|
||||
"demo",
|
||||
"edgesecurity",
|
||||
"home",
|
||||
"office",
|
||||
"infra-fr",
|
||||
"infra-us",
|
||||
"krantz",
|
||||
"metheny",
|
||||
"frisell"
|
||||
]
|
||||
static let address = "192.168.%d.%d/32"
|
||||
static let dnsServers = ["8.8.8.8", "8.8.4.4"]
|
||||
static let endpoint = "demo.wireguard.com:51820"
|
||||
static let allowedIPs = "0.0.0.0/0"
|
||||
|
||||
static func createMockTunnels() -> [NETunnelProviderManager] {
|
||||
return tunnelNames.map { tunnelName -> NETunnelProviderManager in
|
||||
|
||||
var interface = InterfaceConfiguration(privateKey: Curve25519.generatePrivateKey())
|
||||
interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!]
|
||||
interface.dns = dnsServers.map { DNSServer(from: $0)! }
|
||||
|
||||
var peer = PeerConfiguration(publicKey: Curve25519.generatePublicKey(fromPrivateKey: Curve25519.generatePrivateKey()))
|
||||
peer.endpoint = Endpoint(from: endpoint)
|
||||
peer.allowedIPs = [IPAddressRange(from: allowedIPs)!]
|
||||
|
||||
let tunnelConfiguration = TunnelConfiguration(name: tunnelName, interface: interface, peers: [peer])
|
||||
|
||||
let tunnelProviderManager = NETunnelProviderManager()
|
||||
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
|
||||
tunnelProviderManager.localizedDescription = tunnelConfiguration.name
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
return tunnelProviderManager
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,107 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
enum TunnelsManagerError: WireGuardAppError {
|
||||
case tunnelNameEmpty
|
||||
case tunnelAlreadyExistsWithThatName
|
||||
case systemErrorOnListingTunnels(systemError: Error)
|
||||
case systemErrorOnAddTunnel(systemError: Error)
|
||||
case systemErrorOnModifyTunnel(systemError: Error)
|
||||
case systemErrorOnRemoveTunnel(systemError: Error)
|
||||
|
||||
var alertText: AlertText {
|
||||
switch self {
|
||||
case .tunnelNameEmpty:
|
||||
return (tr("alertTunnelNameEmptyTitle"), tr("alertTunnelNameEmptyMessage"))
|
||||
case .tunnelAlreadyExistsWithThatName:
|
||||
return (tr("alertTunnelAlreadyExistsWithThatNameTitle"), tr("alertTunnelAlreadyExistsWithThatNameMessage"))
|
||||
case .systemErrorOnListingTunnels(let systemError):
|
||||
return (tr("alertSystemErrorOnListingTunnelsTitle"), systemError.localizedUIString)
|
||||
case .systemErrorOnAddTunnel(let systemError):
|
||||
return (tr("alertSystemErrorOnAddTunnelTitle"), systemError.localizedUIString)
|
||||
case .systemErrorOnModifyTunnel(let systemError):
|
||||
return (tr("alertSystemErrorOnModifyTunnelTitle"), systemError.localizedUIString)
|
||||
case .systemErrorOnRemoveTunnel(let systemError):
|
||||
return (tr("alertSystemErrorOnRemoveTunnelTitle"), systemError.localizedUIString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TunnelsManagerActivationAttemptError: WireGuardAppError {
|
||||
case tunnelIsNotInactive
|
||||
case failedWhileStarting(systemError: Error) // startTunnel() throwed
|
||||
case failedWhileSaving(systemError: Error) // save config after re-enabling throwed
|
||||
case failedWhileLoading(systemError: Error) // reloading config throwed
|
||||
case failedBecauseOfTooManyErrors(lastSystemError: Error) // recursion limit reached
|
||||
|
||||
var alertText: AlertText {
|
||||
switch self {
|
||||
case .tunnelIsNotInactive:
|
||||
return (tr("alertTunnelActivationErrorTunnelIsNotInactiveTitle"), tr("alertTunnelActivationErrorTunnelIsNotInactiveMessage"))
|
||||
case .failedWhileStarting(let systemError),
|
||||
.failedWhileSaving(let systemError),
|
||||
.failedWhileLoading(let systemError),
|
||||
.failedBecauseOfTooManyErrors(let systemError):
|
||||
return (tr("alertTunnelActivationSystemErrorTitle"),
|
||||
tr(format: "alertTunnelActivationSystemErrorMessage (%@)", systemError.localizedUIString))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TunnelsManagerActivationError: WireGuardAppError {
|
||||
case activationFailed(wasOnDemandEnabled: Bool)
|
||||
case activationFailedWithExtensionError(title: String, message: String, wasOnDemandEnabled: Bool)
|
||||
|
||||
var alertText: AlertText {
|
||||
switch self {
|
||||
case .activationFailed(let wasOnDemandEnabled):
|
||||
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage") + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
|
||||
case .activationFailedWithExtensionError(let title, let message, let wasOnDemandEnabled):
|
||||
return (title, message + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PacketTunnelProviderError: WireGuardAppError {
|
||||
var alertText: AlertText {
|
||||
switch self {
|
||||
case .savedProtocolConfigurationIsInvalid:
|
||||
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSavedConfigFailureMessage"))
|
||||
case .dnsResolutionFailure:
|
||||
return (tr("alertTunnelDNSFailureTitle"), tr("alertTunnelDNSFailureMessage"))
|
||||
case .couldNotStartBackend:
|
||||
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationBackendFailureMessage"))
|
||||
case .couldNotDetermineFileDescriptor:
|
||||
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFileDescriptorFailureMessage"))
|
||||
case .couldNotSetNetworkSettings:
|
||||
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSetNetworkSettingsMessage"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Error {
|
||||
var localizedUIString: String {
|
||||
if let systemError = self as? NEVPNError {
|
||||
switch systemError {
|
||||
case NEVPNError.configurationInvalid:
|
||||
return tr("alertSystemErrorMessageTunnelConfigurationInvalid")
|
||||
case NEVPNError.configurationDisabled:
|
||||
return tr("alertSystemErrorMessageTunnelConfigurationDisabled")
|
||||
case NEVPNError.connectionFailed:
|
||||
return tr("alertSystemErrorMessageTunnelConnectionFailed")
|
||||
case NEVPNError.configurationStale:
|
||||
return tr("alertSystemErrorMessageTunnelConfigurationStale")
|
||||
case NEVPNError.configurationReadWriteFailed:
|
||||
return tr("alertSystemErrorMessageTunnelConfigurationReadWriteFailed")
|
||||
case NEVPNError.configurationUnknown:
|
||||
return tr("alertSystemErrorMessageTunnelConfigurationUnknown")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
return localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
|
||||
@objc enum TunnelStatus: Int {
|
||||
case inactive
|
||||
case activating
|
||||
case active
|
||||
case deactivating
|
||||
case reasserting // Not a possible state at present
|
||||
case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
|
||||
case waiting // Waiting for another tunnel to be brought down
|
||||
|
||||
init(from systemStatus: NEVPNStatus) {
|
||||
switch systemStatus {
|
||||
case .connected:
|
||||
self = .active
|
||||
case .connecting:
|
||||
self = .activating
|
||||
case .disconnected:
|
||||
self = .inactive
|
||||
case .disconnecting:
|
||||
self = .deactivating
|
||||
case .reasserting:
|
||||
self = .reasserting
|
||||
case .invalid:
|
||||
self = .inactive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelStatus: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .inactive: return "inactive"
|
||||
case .activating: return "activating"
|
||||
case .active: return "active"
|
||||
case .deactivating: return "deactivating"
|
||||
case .reasserting: return "reasserting"
|
||||
case .restarting: return "restarting"
|
||||
case .waiting: return "waiting"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NEVPNStatus: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .connected: return "connected"
|
||||
case .connecting: return "connecting"
|
||||
case .disconnected: return "disconnected"
|
||||
case .disconnecting: return "disconnecting"
|
||||
case .reasserting: return "reasserting"
|
||||
case .invalid: return "invalid"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
import os.log
|
||||
|
||||
protocol TunnelsManagerListDelegate: class {
|
||||
func tunnelAdded(at index: Int)
|
||||
func tunnelModified(at index: Int)
|
||||
func tunnelMoved(from oldIndex: Int, to newIndex: Int)
|
||||
func tunnelRemoved(at index: Int)
|
||||
}
|
||||
|
||||
protocol TunnelsManagerActivationDelegate: class {
|
||||
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
|
||||
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
|
||||
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
|
||||
func tunnelActivationSucceeded(tunnel: TunnelContainer) // status changed to connected
|
||||
}
|
||||
|
||||
class TunnelsManager {
|
||||
private var tunnels: [TunnelContainer]
|
||||
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
|
||||
weak var activationDelegate: TunnelsManagerActivationDelegate?
|
||||
private var statusObservationToken: AnyObject?
|
||||
private var waiteeObservationToken: AnyObject?
|
||||
|
||||
init(tunnelProviders: [NETunnelProviderManager]) {
|
||||
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
|
||||
startObservingTunnelStatuses()
|
||||
}
|
||||
|
||||
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) {
|
||||
#if targetEnvironment(simulator)
|
||||
completionHandler(.success(TunnelsManager(tunnelProviders: MockTunnels.createMockTunnels())))
|
||||
#else
|
||||
NETunnelProviderManager.loadAllFromPreferences { managers, error in
|
||||
if let error = error {
|
||||
wg_log(.error, message: "Failed to load tunnel provider managers: \(error)")
|
||||
completionHandler(.failure(TunnelsManagerError.systemErrorOnListingTunnels(systemError: error)))
|
||||
return
|
||||
}
|
||||
|
||||
let tunnelManagers = managers ?? []
|
||||
tunnelManagers.forEach { tunnelManager in
|
||||
if (tunnelManager.protocolConfiguration as? NETunnelProviderProtocol)?.migrateConfigurationIfNeeded() == true {
|
||||
tunnelManager.saveToPreferences { _ in }
|
||||
}
|
||||
}
|
||||
completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func reload(completionHandler: @escaping (Bool) -> Void) {
|
||||
#if targetEnvironment(simulator)
|
||||
completionHandler(false)
|
||||
#else
|
||||
NETunnelProviderManager.loadAllFromPreferences { managers, _ in
|
||||
guard let managers = managers else {
|
||||
completionHandler(false)
|
||||
return
|
||||
}
|
||||
|
||||
let newTunnels = managers.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
|
||||
let hasChanges = self.tunnels.map { $0.tunnelConfiguration } != newTunnels.map { $0.tunnelConfiguration }
|
||||
if hasChanges {
|
||||
self.tunnels = newTunnels
|
||||
completionHandler(true)
|
||||
} else {
|
||||
completionHandler(false)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.name ?? ""
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
|
||||
return
|
||||
}
|
||||
|
||||
if tunnels.contains(where: { $0.name == tunnelName }) {
|
||||
completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
|
||||
return
|
||||
}
|
||||
|
||||
let tunnelProviderManager = NETunnelProviderManager()
|
||||
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
|
||||
tunnelProviderManager.localizedDescription = tunnelConfiguration.name
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
|
||||
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!)))
|
||||
return
|
||||
}
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
|
||||
self.tunnels.append(tunnel)
|
||||
self.tunnels.sort { $0.name < $1.name }
|
||||
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
|
||||
completionHandler(.success(tunnel))
|
||||
}
|
||||
}
|
||||
|
||||
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) {
|
||||
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, completionHandler: @escaping (UInt) -> Void) {
|
||||
guard let head = tunnelConfigurations.first else {
|
||||
completionHandler(numberSuccessful)
|
||||
return
|
||||
}
|
||||
let tail = tunnelConfigurations.dropFirst()
|
||||
add(tunnelConfiguration: head) { [weak self, tail] result in
|
||||
DispatchQueue.main.async {
|
||||
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (result.isSuccess ? 1 : 0), completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.name ?? ""
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(TunnelsManagerError.tunnelNameEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
let tunnelProviderManager = tunnel.tunnelProvider
|
||||
let isNameChanged = tunnelName != tunnelProviderManager.localizedDescription
|
||||
if isNameChanged {
|
||||
guard !tunnels.contains(where: { $0.name == tunnelName }) else {
|
||||
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
|
||||
return
|
||||
}
|
||||
tunnel.name = tunnelName
|
||||
}
|
||||
|
||||
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
|
||||
tunnelProviderManager.localizedDescription = tunnelConfiguration.name
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled
|
||||
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
|
||||
return
|
||||
}
|
||||
guard let self = self else { return }
|
||||
|
||||
if isNameChanged {
|
||||
let oldIndex = self.tunnels.firstIndex(of: tunnel)!
|
||||
self.tunnels.sort { $0.name < $1.name }
|
||||
let newIndex = self.tunnels.firstIndex(of: tunnel)!
|
||||
self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
|
||||
}
|
||||
self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
|
||||
|
||||
if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
|
||||
// Turn off the tunnel, and then turn it back on, so the changes are made effective
|
||||
tunnel.status = .restarting
|
||||
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
|
||||
}
|
||||
|
||||
if isActivatingOnDemand {
|
||||
// Reload tunnel after saving.
|
||||
// Without this, the tunnel stopes getting updates on the tunnel status from iOS.
|
||||
tunnelProviderManager.loadFromPreferences { error in
|
||||
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
||||
guard error == nil else {
|
||||
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
|
||||
return
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelProviderManager = tunnel.tunnelProvider
|
||||
|
||||
tunnelProviderManager.removeFromPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!))
|
||||
return
|
||||
}
|
||||
if let self = self {
|
||||
let index = self.tunnels.firstIndex(of: tunnel)!
|
||||
self.tunnels.remove(at: index)
|
||||
self.tunnelsListDelegate?.tunnelRemoved(at: index)
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfTunnels() -> Int {
|
||||
return tunnels.count
|
||||
}
|
||||
|
||||
func tunnel(at index: Int) -> TunnelContainer {
|
||||
return tunnels[index]
|
||||
}
|
||||
|
||||
func tunnel(named tunnelName: String) -> TunnelContainer? {
|
||||
return tunnels.first { $0.name == tunnelName }
|
||||
}
|
||||
|
||||
func startActivation(of tunnel: TunnelContainer) {
|
||||
guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
|
||||
guard tunnel.status == .inactive else {
|
||||
activationDelegate?.tunnelActivationAttemptFailed(tunnel: tunnel, error: .tunnelIsNotInactive)
|
||||
return
|
||||
}
|
||||
|
||||
if let alreadyWaitingTunnel = tunnels.first(where: { $0.status == .waiting }) {
|
||||
alreadyWaitingTunnel.status = .inactive
|
||||
}
|
||||
|
||||
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
|
||||
wg_log(.info, message: "Tunnel '\(tunnel.name)' waiting for deactivation of '\(tunnelInOperation.name)'")
|
||||
tunnel.status = .waiting
|
||||
activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
|
||||
if tunnelInOperation.status != .deactivating {
|
||||
startDeactivation(of: tunnelInOperation)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
tunnel.status = .active
|
||||
#else
|
||||
tunnel.startActivation(activationDelegate: activationDelegate)
|
||||
#endif
|
||||
}
|
||||
|
||||
func startDeactivation(of tunnel: TunnelContainer) {
|
||||
tunnel.isAttemptingActivation = false
|
||||
guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
|
||||
#if targetEnvironment(simulator)
|
||||
tunnel.status = .inactive
|
||||
#else
|
||||
tunnel.startDeactivation()
|
||||
#endif
|
||||
}
|
||||
|
||||
func refreshStatuses() {
|
||||
tunnels.forEach { $0.refreshStatus() }
|
||||
}
|
||||
|
||||
private func activateWaitingTunnelOnDeactivation(of tunnel: TunnelContainer) {
|
||||
waiteeObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
|
||||
guard let self = self else { return }
|
||||
if tunnel.status == .inactive {
|
||||
if let waitingTunnel = self.tunnels.first(where: { $0.status == .waiting }) {
|
||||
waitingTunnel.startActivation(activationDelegate: self.activationDelegate)
|
||||
}
|
||||
self.waiteeObservationToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startObservingTunnelStatuses() {
|
||||
statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
|
||||
guard let self = self,
|
||||
let session = statusChangeNotification.object as? NETunnelProviderSession,
|
||||
let tunnelProvider = session.manager as? NETunnelProviderManager,
|
||||
let tunnelConfiguration = TunnelContainer(tunnel: tunnelProvider).tunnelConfiguration,
|
||||
let tunnel = self.tunnels.first(where: { $0.tunnelConfiguration == tunnelConfiguration }) else { return }
|
||||
if tunnel.tunnelProvider != tunnelProvider {
|
||||
tunnel.tunnelProvider = tunnelProvider
|
||||
tunnel.refreshStatus()
|
||||
}
|
||||
|
||||
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
|
||||
|
||||
if tunnel.isAttemptingActivation {
|
||||
if session.status == .connected {
|
||||
tunnel.isAttemptingActivation = false
|
||||
self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel)
|
||||
} else if session.status == .disconnected {
|
||||
tunnel.isAttemptingActivation = false
|
||||
if let (title, message) = lastErrorTextFromNetworkExtension(for: tunnel) {
|
||||
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message, wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
|
||||
} else {
|
||||
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed(wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) {
|
||||
if session.status == .disconnected {
|
||||
tunnel.startActivation(activationDelegate: self.activationDelegate)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
tunnel.refreshStatus()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
|
||||
guard let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL else { return nil }
|
||||
guard let lastErrorData = try? Data(contentsOf: lastErrorFileURL) else { return nil }
|
||||
guard let lastErrorStrings = String(data: lastErrorData, encoding: .utf8)?.splitToArray(separator: "\n") else { return nil }
|
||||
guard lastErrorStrings.count == 2 && tunnel.activationAttemptId == lastErrorStrings[0] else { return nil }
|
||||
|
||||
if let extensionError = PacketTunnelProviderError(rawValue: lastErrorStrings[1]) {
|
||||
return extensionError.alertText
|
||||
}
|
||||
|
||||
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
|
||||
}
|
||||
|
||||
class TunnelContainer: NSObject {
|
||||
@objc dynamic var name: String
|
||||
@objc dynamic var status: TunnelStatus
|
||||
|
||||
@objc dynamic var isActivateOnDemandEnabled: Bool
|
||||
|
||||
var isAttemptingActivation = false {
|
||||
didSet {
|
||||
if isAttemptingActivation {
|
||||
self.activationTimer?.invalidate()
|
||||
let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
wg_log(.debug, message: "Status update notification timeout for tunnel '\(self.name)'. Tunnel status is now '\(self.tunnelProvider.connection.status)'.")
|
||||
switch self.tunnelProvider.connection.status {
|
||||
case .connected, .disconnected, .invalid:
|
||||
self.activationTimer?.invalidate()
|
||||
self.activationTimer = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.refreshStatus()
|
||||
}
|
||||
self.activationTimer = activationTimer
|
||||
RunLoop.main.add(activationTimer, forMode: .default)
|
||||
}
|
||||
}
|
||||
}
|
||||
var activationAttemptId: String?
|
||||
var activationTimer: Timer?
|
||||
|
||||
fileprivate var tunnelProvider: NETunnelProviderManager
|
||||
private var lastTunnelConnectionStatus: NEVPNStatus?
|
||||
|
||||
var tunnelConfiguration: TunnelConfiguration? {
|
||||
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: tunnelProvider.localizedDescription)
|
||||
}
|
||||
|
||||
var activateOnDemandSetting: ActivateOnDemandSetting {
|
||||
return ActivateOnDemandSetting(from: tunnelProvider)
|
||||
}
|
||||
|
||||
init(tunnel: NETunnelProviderManager) {
|
||||
name = tunnel.localizedDescription ?? "Unnamed"
|
||||
let status = TunnelStatus(from: tunnel.connection.status)
|
||||
self.status = status
|
||||
isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
|
||||
tunnelProvider = tunnel
|
||||
super.init()
|
||||
}
|
||||
|
||||
func refreshStatus() {
|
||||
let status = TunnelStatus(from: tunnelProvider.connection.status)
|
||||
self.status = status
|
||||
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
|
||||
}
|
||||
|
||||
//swiftlint:disable:next function_body_length
|
||||
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
|
||||
if recursionCount >= 8 {
|
||||
wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
|
||||
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors(lastSystemError: lastError!))
|
||||
return
|
||||
}
|
||||
|
||||
wg_log(.debug, message: "startActivation: Entering (tunnel: \(name))")
|
||||
|
||||
status = .activating // Ensure that no other tunnel can attempt activation until this tunnel is done trying
|
||||
|
||||
guard tunnelProvider.isEnabled else {
|
||||
// In case the tunnel had gotten disabled, re-enable and save it,
|
||||
// then call this function again.
|
||||
wg_log(.debug, staticMessage: "startActivation: Tunnel is disabled. Re-enabling and saving")
|
||||
tunnelProvider.isEnabled = true
|
||||
tunnelProvider.saveToPreferences { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
if error != nil {
|
||||
wg_log(.error, message: "Error saving tunnel after re-enabling: \(error!)")
|
||||
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileSaving(systemError: error!))
|
||||
return
|
||||
}
|
||||
wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling, invoking startActivation")
|
||||
self.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), activationDelegate: activationDelegate)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Start the tunnel
|
||||
do {
|
||||
wg_log(.debug, staticMessage: "startActivation: Starting tunnel")
|
||||
isAttemptingActivation = true
|
||||
let activationAttemptId = UUID().uuidString
|
||||
self.activationAttemptId = activationAttemptId
|
||||
try (tunnelProvider.connection as? NETunnelProviderSession)?.startTunnel(options: ["activationAttemptId": activationAttemptId])
|
||||
wg_log(.debug, staticMessage: "startActivation: Success")
|
||||
activationDelegate?.tunnelActivationAttemptSucceeded(tunnel: self)
|
||||
} catch let error {
|
||||
isAttemptingActivation = false
|
||||
guard let systemError = error as? NEVPNError else {
|
||||
wg_log(.error, message: "Failed to activate tunnel: Error: \(error)")
|
||||
status = .inactive
|
||||
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: error))
|
||||
return
|
||||
}
|
||||
guard systemError.code == NEVPNError.configurationInvalid || systemError.code == NEVPNError.configurationStale else {
|
||||
wg_log(.error, message: "Failed to activate tunnel: VPN Error: \(error)")
|
||||
status = .inactive
|
||||
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: systemError))
|
||||
return
|
||||
}
|
||||
wg_log(.debug, staticMessage: "startActivation: Will reload tunnel and then try to start it.")
|
||||
tunnelProvider.loadFromPreferences { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
if error != nil {
|
||||
wg_log(.error, message: "startActivation: Error reloading tunnel: \(error!)")
|
||||
self.status = .inactive
|
||||
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileLoading(systemError: systemError))
|
||||
return
|
||||
}
|
||||
wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded, invoking startActivation")
|
||||
self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func startDeactivation() {
|
||||
(tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,59 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
//swiftlint:disable:next type_body_length
|
||||
class TunnelViewModel {
|
||||
|
||||
enum InterfaceField: String {
|
||||
case name = "Name"
|
||||
case privateKey = "Private key"
|
||||
case publicKey = "Public key"
|
||||
case generateKeyPair = "Generate keypair"
|
||||
case addresses = "Addresses"
|
||||
case listenPort = "Listen port"
|
||||
case mtu = "MTU"
|
||||
case dns = "DNS servers"
|
||||
enum InterfaceField {
|
||||
case name
|
||||
case privateKey
|
||||
case publicKey
|
||||
case generateKeyPair
|
||||
case addresses
|
||||
case listenPort
|
||||
case mtu
|
||||
case dns
|
||||
|
||||
var localizedUIString: String {
|
||||
switch self {
|
||||
case .name: return tr("tunnelInterfaceName")
|
||||
case .privateKey: return tr("tunnelInterfacePrivateKey")
|
||||
case .publicKey: return tr("tunnelInterfacePublicKey")
|
||||
case .generateKeyPair: return tr("tunnelInterfaceGenerateKeypair")
|
||||
case .addresses: return tr("tunnelInterfaceAddresses")
|
||||
case .listenPort: return tr("tunnelInterfaceListenPort")
|
||||
case .mtu: return tr("tunnelInterfaceMTU")
|
||||
case .dns: return tr("tunnelInterfaceDNS")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static let interfaceFieldsWithControl: Set<InterfaceField> = [
|
||||
.generateKeyPair
|
||||
]
|
||||
|
||||
enum PeerField: String {
|
||||
case publicKey = "Public key"
|
||||
case preSharedKey = "Preshared key"
|
||||
case endpoint = "Endpoint"
|
||||
case persistentKeepAlive = "Persistent keepalive"
|
||||
case allowedIPs = "Allowed IPs"
|
||||
case excludePrivateIPs = "Exclude private IPs"
|
||||
case deletePeer = "Delete peer"
|
||||
enum PeerField {
|
||||
case publicKey
|
||||
case preSharedKey
|
||||
case endpoint
|
||||
case persistentKeepAlive
|
||||
case allowedIPs
|
||||
case excludePrivateIPs
|
||||
case deletePeer
|
||||
|
||||
var localizedUIString: String {
|
||||
switch self {
|
||||
case .publicKey: return tr("tunnelPeerPublicKey")
|
||||
case .preSharedKey: return tr("tunnelPeerPreSharedKey")
|
||||
case .endpoint: return tr("tunnelPeerEndpoint")
|
||||
case .persistentKeepAlive: return tr("tunnelPeerPersistentKeepalive")
|
||||
case .allowedIPs: return tr("tunnelPeerAllowedIPs")
|
||||
case .excludePrivateIPs: return tr("tunnelPeerExcludePrivateIPs")
|
||||
case .deletePeer: return tr("deletePeerButtonTitle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static let peerFieldsWithControl: Set<PeerField> = [
|
||||
@@ -37,35 +63,31 @@ class TunnelViewModel {
|
||||
static let keyLengthInBase64 = 44
|
||||
|
||||
class InterfaceData {
|
||||
var scratchpad: [InterfaceField: String] = [:]
|
||||
var fieldsWithError: Set<InterfaceField> = []
|
||||
var scratchpad = [InterfaceField: String]()
|
||||
var fieldsWithError = Set<InterfaceField>()
|
||||
var validatedConfiguration: InterfaceConfiguration?
|
||||
var validatedName: String?
|
||||
|
||||
subscript(field: InterfaceField) -> String {
|
||||
get {
|
||||
if (scratchpad.isEmpty) {
|
||||
// When starting to read a config, setup the scratchpad.
|
||||
// The scratchpad shall serve as a cache of what we want to show in the UI.
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
return scratchpad[field] ?? ""
|
||||
}
|
||||
set(stringValue) {
|
||||
if (scratchpad.isEmpty) {
|
||||
// When starting to edit a config, setup the scratchpad and remove the configuration.
|
||||
// The scratchpad shall be the sole source of the being-edited configuration.
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
validatedConfiguration = nil
|
||||
if (stringValue.isEmpty) {
|
||||
validatedName = nil
|
||||
if stringValue.isEmpty {
|
||||
scratchpad.removeValue(forKey: field)
|
||||
} else {
|
||||
scratchpad[field] = stringValue
|
||||
}
|
||||
if (field == .privateKey) {
|
||||
if (stringValue.count == TunnelViewModel.keyLengthInBase64),
|
||||
let privateKey = Data(base64Encoded: stringValue),
|
||||
privateKey.count == 32 {
|
||||
if field == .privateKey {
|
||||
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Encoded: stringValue), privateKey.count == TunnelConfiguration.keyLength {
|
||||
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
||||
scratchpad[.publicKey] = publicKey.base64EncodedString()
|
||||
} else {
|
||||
@@ -76,13 +98,13 @@ class TunnelViewModel {
|
||||
}
|
||||
|
||||
func populateScratchpad() {
|
||||
// Populate the scratchpad from the configuration object
|
||||
guard let config = validatedConfiguration else { return }
|
||||
scratchpad[.name] = config.name
|
||||
guard let name = validatedName else { return }
|
||||
scratchpad[.name] = name
|
||||
scratchpad[.privateKey] = config.privateKey.base64EncodedString()
|
||||
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
|
||||
if (!config.addresses.isEmpty) {
|
||||
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
|
||||
if !config.addresses.isEmpty {
|
||||
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
}
|
||||
if let listenPort = config.listenPort {
|
||||
scratchpad[.listenPort] = String(listenPort)
|
||||
@@ -90,40 +112,39 @@ class TunnelViewModel {
|
||||
if let mtu = config.mtu {
|
||||
scratchpad[.mtu] = String(mtu)
|
||||
}
|
||||
if (!config.dns.isEmpty) {
|
||||
scratchpad[.dns] = config.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
|
||||
if !config.dns.isEmpty {
|
||||
scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
func save() -> SaveResult<InterfaceConfiguration> {
|
||||
if let validatedConfiguration = validatedConfiguration {
|
||||
// It's already validated and saved
|
||||
return .saved(validatedConfiguration)
|
||||
//swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
func save() -> SaveResult<(String, InterfaceConfiguration)> {
|
||||
if let config = validatedConfiguration, let name = validatedName {
|
||||
return .saved((name, config))
|
||||
}
|
||||
fieldsWithError.removeAll()
|
||||
guard let name = scratchpad[.name]?.trimmingCharacters(in: .whitespacesAndNewlines), (!name.isEmpty) else {
|
||||
fieldsWithError.insert(.name)
|
||||
return .error("Interface name is required")
|
||||
return .error(tr("alertInvalidInterfaceMessageNameRequired"))
|
||||
}
|
||||
guard let privateKeyString = scratchpad[.privateKey] else {
|
||||
fieldsWithError.insert(.privateKey)
|
||||
return .error("Interface's private key is required")
|
||||
return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired"))
|
||||
}
|
||||
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else {
|
||||
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
||||
fieldsWithError.insert(.privateKey)
|
||||
return .error("Interface's private key must be a 32-byte key in base64 encoding")
|
||||
return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
|
||||
}
|
||||
var config = InterfaceConfiguration(name: name, privateKey: privateKey)
|
||||
var errorMessages: [String] = []
|
||||
var config = InterfaceConfiguration(privateKey: privateKey)
|
||||
var errorMessages = [String]()
|
||||
if let addressesString = scratchpad[.addresses] {
|
||||
var addresses: [IPAddressRange] = []
|
||||
for addressString in addressesString.split(separator: ",") {
|
||||
let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
if let address = IPAddressRange(from: trimmedString) {
|
||||
var addresses = [IPAddressRange]()
|
||||
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
if let address = IPAddressRange(from: addressString) {
|
||||
addresses.append(address)
|
||||
} else {
|
||||
fieldsWithError.insert(.addresses)
|
||||
errorMessages.append("Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation")
|
||||
errorMessages.append(tr("alertInvalidInterfaceMessageAddressInvalid"))
|
||||
}
|
||||
}
|
||||
config.addresses = addresses
|
||||
@@ -133,7 +154,7 @@ class TunnelViewModel {
|
||||
config.listenPort = listenPort
|
||||
} else {
|
||||
fieldsWithError.insert(.listenPort)
|
||||
errorMessages.append("Interface's listen port must be between 0 and 65535, or unspecified")
|
||||
errorMessages.append(tr("alertInvalidInterfaceMessageListenPortInvalid"))
|
||||
}
|
||||
}
|
||||
if let mtuString = scratchpad[.mtu] {
|
||||
@@ -141,51 +162,49 @@ class TunnelViewModel {
|
||||
config.mtu = mtu
|
||||
} else {
|
||||
fieldsWithError.insert(.mtu)
|
||||
errorMessages.append("Interface's MTU must be between 576 and 65535, or unspecified")
|
||||
errorMessages.append(tr("alertInvalidInterfaceMessageMTUInvalid"))
|
||||
}
|
||||
}
|
||||
if let dnsString = scratchpad[.dns] {
|
||||
var dnsServers: [DNSServer] = []
|
||||
for dnsServerString in dnsString.split(separator: ",") {
|
||||
let trimmedString = dnsServerString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
if let dnsServer = DNSServer(from: trimmedString) {
|
||||
var dnsServers = [DNSServer]()
|
||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
if let dnsServer = DNSServer(from: dnsServerString) {
|
||||
dnsServers.append(dnsServer)
|
||||
} else {
|
||||
fieldsWithError.insert(.dns)
|
||||
errorMessages.append("Interface's DNS servers must be a list of comma-separated IP addresses")
|
||||
errorMessages.append(tr("alertInvalidInterfaceMessageDNSInvalid"))
|
||||
}
|
||||
}
|
||||
config.dns = dnsServers
|
||||
}
|
||||
|
||||
guard (errorMessages.isEmpty) else {
|
||||
return .error(errorMessages.first!)
|
||||
}
|
||||
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
|
||||
|
||||
validatedConfiguration = config
|
||||
return .saved(config)
|
||||
validatedName = name
|
||||
return .saved((name, config))
|
||||
}
|
||||
|
||||
func filterFieldsWithValueOrControl(interfaceFields: [InterfaceField]) -> [InterfaceField] {
|
||||
return interfaceFields.filter { (field) -> Bool in
|
||||
if (TunnelViewModel.interfaceFieldsWithControl.contains(field)) {
|
||||
return interfaceFields.filter { field in
|
||||
if TunnelViewModel.interfaceFieldsWithControl.contains(field) {
|
||||
return true
|
||||
}
|
||||
return (!self[field].isEmpty)
|
||||
return !self[field].isEmpty
|
||||
}
|
||||
// TODO: Cache this to avoid recomputing
|
||||
}
|
||||
}
|
||||
|
||||
class PeerData {
|
||||
var index: Int
|
||||
var scratchpad: [PeerField: String] = [:]
|
||||
var fieldsWithError: Set<PeerField> = []
|
||||
var scratchpad = [PeerField: String]()
|
||||
var fieldsWithError = Set<PeerField>()
|
||||
var validatedConfiguration: PeerConfiguration?
|
||||
|
||||
// For exclude private IPs
|
||||
var shouldAllowExcludePrivateIPsControl: Bool = false /* Read-only from the VC's point of view */
|
||||
var excludePrivateIPsValue: Bool = false /* Read-only from the VC's point of view */
|
||||
fileprivate var numberOfPeers: Int = 0
|
||||
private(set) var shouldAllowExcludePrivateIPsControl = false
|
||||
private(set) var shouldStronglyRecommendDNS = false
|
||||
private(set) var excludePrivateIPsValue = false
|
||||
fileprivate var numberOfPeers = 0
|
||||
|
||||
init(index: Int) {
|
||||
self.index = index
|
||||
@@ -193,43 +212,38 @@ class TunnelViewModel {
|
||||
|
||||
subscript(field: PeerField) -> String {
|
||||
get {
|
||||
if (scratchpad.isEmpty) {
|
||||
// When starting to read a config, setup the scratchpad.
|
||||
// The scratchpad shall serve as a cache of what we want to show in the UI.
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
return scratchpad[field] ?? ""
|
||||
}
|
||||
set(stringValue) {
|
||||
if (scratchpad.isEmpty) {
|
||||
// When starting to edit a config, setup the scratchpad and remove the configuration.
|
||||
// The scratchpad shall be the sole source of the being-edited configuration.
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
validatedConfiguration = nil
|
||||
if (stringValue.isEmpty) {
|
||||
if stringValue.isEmpty {
|
||||
scratchpad.removeValue(forKey: field)
|
||||
} else {
|
||||
scratchpad[field] = stringValue
|
||||
}
|
||||
if (field == .allowedIPs) {
|
||||
if field == .allowedIPs {
|
||||
updateExcludePrivateIPsFieldState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func populateScratchpad() {
|
||||
// Populate the scratchpad from the configuration object
|
||||
guard let config = validatedConfiguration else { return }
|
||||
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
|
||||
if let preSharedKey = config.preSharedKey {
|
||||
scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
|
||||
}
|
||||
if (!config.allowedIPs.isEmpty) {
|
||||
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
|
||||
if !config.allowedIPs.isEmpty {
|
||||
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
}
|
||||
if let endpoint = config.endpoint {
|
||||
scratchpad[.endpoint] = endpoint.stringRepresentation()
|
||||
scratchpad[.endpoint] = endpoint.stringRepresentation
|
||||
}
|
||||
if let persistentKeepAlive = config.persistentKeepAlive {
|
||||
scratchpad[.persistentKeepAlive] = String(persistentKeepAlive)
|
||||
@@ -237,39 +251,38 @@ class TunnelViewModel {
|
||||
updateExcludePrivateIPsFieldState()
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity
|
||||
func save() -> SaveResult<PeerConfiguration> {
|
||||
if let validatedConfiguration = validatedConfiguration {
|
||||
// It's already validated and saved
|
||||
return .saved(validatedConfiguration)
|
||||
}
|
||||
fieldsWithError.removeAll()
|
||||
guard let publicKeyString = scratchpad[.publicKey] else {
|
||||
fieldsWithError.insert(.publicKey)
|
||||
return .error("Peer's public key is required")
|
||||
return .error(tr("alertInvalidPeerMessagePublicKeyRequired"))
|
||||
}
|
||||
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else {
|
||||
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
||||
fieldsWithError.insert(.publicKey)
|
||||
return .error("Peer's public key must be a 32-byte key in base64 encoding")
|
||||
return .error(tr("alertInvalidPeerMessagePublicKeyInvalid"))
|
||||
}
|
||||
var config = PeerConfiguration(publicKey: publicKey)
|
||||
var errorMessages: [String] = []
|
||||
var errorMessages = [String]()
|
||||
if let preSharedKeyString = scratchpad[.preSharedKey] {
|
||||
if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 {
|
||||
if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength {
|
||||
config.preSharedKey = preSharedKey
|
||||
} else {
|
||||
fieldsWithError.insert(.preSharedKey)
|
||||
errorMessages.append("Peer's preshared key must be a 32-byte key in base64 encoding")
|
||||
errorMessages.append(tr("alertInvalidPeerMessagePreSharedKeyInvalid"))
|
||||
}
|
||||
}
|
||||
if let allowedIPsString = scratchpad[.allowedIPs] {
|
||||
var allowedIPs: [IPAddressRange] = []
|
||||
for allowedIPString in allowedIPsString.split(separator: ",") {
|
||||
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
if let allowedIP = IPAddressRange(from: trimmedString) {
|
||||
var allowedIPs = [IPAddressRange]()
|
||||
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
if let allowedIP = IPAddressRange(from: allowedIPString) {
|
||||
allowedIPs.append(allowedIP)
|
||||
} else {
|
||||
fieldsWithError.insert(.allowedIPs)
|
||||
errorMessages.append("Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation")
|
||||
errorMessages.append(tr("alertInvalidPeerMessageAllowedIPsInvalid"))
|
||||
}
|
||||
}
|
||||
config.allowedIPs = allowedIPs
|
||||
@@ -279,7 +292,7 @@ class TunnelViewModel {
|
||||
config.endpoint = endpoint
|
||||
} else {
|
||||
fieldsWithError.insert(.endpoint)
|
||||
errorMessages.append("Peer's endpoint must be of the form 'host:port' or '[host]:port'")
|
||||
errorMessages.append(tr("alertInvalidPeerMessageEndpointInvalid"))
|
||||
}
|
||||
}
|
||||
if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] {
|
||||
@@ -287,25 +300,23 @@ class TunnelViewModel {
|
||||
config.persistentKeepAlive = persistentKeepAlive
|
||||
} else {
|
||||
fieldsWithError.insert(.persistentKeepAlive)
|
||||
errorMessages.append("Peer's persistent keepalive must be between 0 to 65535, or unspecified")
|
||||
errorMessages.append(tr("alertInvalidPeerMessagePersistentKeepaliveInvalid"))
|
||||
}
|
||||
}
|
||||
|
||||
guard (errorMessages.isEmpty) else {
|
||||
return .error(errorMessages.first!)
|
||||
}
|
||||
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
|
||||
|
||||
validatedConfiguration = config
|
||||
return .saved(config)
|
||||
}
|
||||
|
||||
func filterFieldsWithValueOrControl(peerFields: [PeerField]) -> [PeerField] {
|
||||
return peerFields.filter { (field) -> Bool in
|
||||
if (TunnelViewModel.peerFieldsWithControl.contains(field)) {
|
||||
return peerFields.filter { field in
|
||||
if TunnelViewModel.peerFieldsWithControl.contains(field) {
|
||||
return true
|
||||
}
|
||||
return (!self[field].isEmpty)
|
||||
}
|
||||
// TODO: Cache this to avoid recomputing
|
||||
}
|
||||
|
||||
static let ipv4DefaultRouteString = "0.0.0.0/0"
|
||||
@@ -319,23 +330,20 @@ class TunnelViewModel {
|
||||
]
|
||||
|
||||
func updateExcludePrivateIPsFieldState() {
|
||||
guard (numberOfPeers == 1) else {
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
let allowedIPStrings = Set<String>(scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
|
||||
shouldStronglyRecommendDNS = allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) || allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)
|
||||
guard numberOfPeers == 1 else {
|
||||
shouldAllowExcludePrivateIPsControl = false
|
||||
excludePrivateIPsValue = false
|
||||
return
|
||||
}
|
||||
if (scratchpad.isEmpty) {
|
||||
populateScratchpad()
|
||||
}
|
||||
let allowedIPStrings = Set<String>(
|
||||
(scratchpad[.allowedIPs] ?? "")
|
||||
.split(separator: ",")
|
||||
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
|
||||
)
|
||||
if (allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString)) {
|
||||
if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
|
||||
shouldAllowExcludePrivateIPsControl = true
|
||||
excludePrivateIPsValue = false
|
||||
} else if (allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)) {
|
||||
} else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
|
||||
shouldAllowExcludePrivateIPsControl = true
|
||||
excludePrivateIPsValue = true
|
||||
} else {
|
||||
@@ -345,41 +353,37 @@ class TunnelViewModel {
|
||||
}
|
||||
|
||||
func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String) {
|
||||
let allowedIPStrings = (scratchpad[.allowedIPs] ?? "")
|
||||
.split(separator: ",")
|
||||
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
|
||||
let dnsServerStrings = dnsServers
|
||||
.split(separator: ",")
|
||||
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
|
||||
let allowedIPStrings = scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let dnsServerStrings = dnsServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let ipv6Addresses = allowedIPStrings.filter { $0.contains(":") }
|
||||
let modifiedAllowedIPStrings: [String]
|
||||
if (isOn) {
|
||||
modifiedAllowedIPStrings = ipv6Addresses +
|
||||
TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
|
||||
if isOn {
|
||||
modifiedAllowedIPStrings = ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
|
||||
} else {
|
||||
modifiedAllowedIPStrings = ipv6Addresses +
|
||||
[TunnelViewModel.PeerData.ipv4DefaultRouteString]
|
||||
modifiedAllowedIPStrings = ipv6Addresses + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
|
||||
}
|
||||
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
|
||||
validatedConfiguration = nil
|
||||
excludePrivateIPsValue = isOn
|
||||
}
|
||||
}
|
||||
|
||||
enum SaveResult<Configuration> {
|
||||
case saved(Configuration)
|
||||
case error(String) // TODO: Localize error messages
|
||||
case error(String)
|
||||
}
|
||||
|
||||
var interfaceData: InterfaceData
|
||||
var peersData: [PeerData]
|
||||
|
||||
init(tunnelConfiguration: TunnelConfiguration?) {
|
||||
let interfaceData: InterfaceData = InterfaceData()
|
||||
var peersData: [PeerData] = []
|
||||
let interfaceData = InterfaceData()
|
||||
var peersData = [PeerData]()
|
||||
if let tunnelConfiguration = tunnelConfiguration {
|
||||
interfaceData.validatedConfiguration = tunnelConfiguration.interface
|
||||
for (i, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
|
||||
let peerData = PeerData(index: i)
|
||||
interfaceData.validatedName = tunnelConfiguration.name
|
||||
for (index, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
|
||||
let peerData = PeerData(index: index)
|
||||
peerData.validatedConfiguration = peerConfiguration
|
||||
peersData.append(peerData)
|
||||
}
|
||||
@@ -396,47 +400,82 @@ class TunnelViewModel {
|
||||
func appendEmptyPeer() {
|
||||
let peer = PeerData(index: peersData.count)
|
||||
peersData.append(peer)
|
||||
for p in peersData {
|
||||
p.numberOfPeers = peersData.count
|
||||
p.updateExcludePrivateIPsFieldState()
|
||||
for peer in peersData {
|
||||
peer.numberOfPeers = peersData.count
|
||||
peer.updateExcludePrivateIPsFieldState()
|
||||
}
|
||||
}
|
||||
|
||||
func deletePeer(peer: PeerData) {
|
||||
let removedPeer = peersData.remove(at: peer.index)
|
||||
assert(removedPeer.index == peer.index)
|
||||
for p in peersData[peer.index ..< peersData.count] {
|
||||
assert(p.index > 0)
|
||||
p.index = p.index - 1
|
||||
for peer in peersData[peer.index ..< peersData.count] {
|
||||
assert(peer.index > 0)
|
||||
peer.index -= 1
|
||||
}
|
||||
for p in peersData {
|
||||
p.numberOfPeers = peersData.count
|
||||
p.updateExcludePrivateIPsFieldState()
|
||||
for peer in peersData {
|
||||
peer.numberOfPeers = peersData.count
|
||||
peer.updateExcludePrivateIPsFieldState()
|
||||
}
|
||||
}
|
||||
|
||||
func save() -> SaveResult<TunnelConfiguration> {
|
||||
// Attempt to save the interface and all peers, so that all erroring fields are collected
|
||||
let interfaceSaveResult = interfaceData.save()
|
||||
let peerSaveResults = peersData.map { $0.save() }
|
||||
// Collate the results
|
||||
switch (interfaceSaveResult) {
|
||||
let peerSaveResults = peersData.map { $0.save() } // Save all, to help mark erroring fields in red
|
||||
switch interfaceSaveResult {
|
||||
case .error(let errorMessage):
|
||||
return .error(errorMessage)
|
||||
case .saved(let interfaceConfiguration):
|
||||
var peerConfigurations: [PeerConfiguration] = []
|
||||
var peerConfigurations = [PeerConfiguration]()
|
||||
peerConfigurations.reserveCapacity(peerSaveResults.count)
|
||||
for peerSaveResult in peerSaveResults {
|
||||
switch (peerSaveResult) {
|
||||
switch peerSaveResult {
|
||||
case .error(let errorMessage):
|
||||
return .error(errorMessage)
|
||||
case .saved(let peerConfiguration):
|
||||
peerConfigurations.append(peerConfiguration)
|
||||
}
|
||||
}
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration)
|
||||
tunnelConfiguration.peers = peerConfigurations
|
||||
|
||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||
return .error(tr("alertInvalidPeerMessagePublicKeyDuplicated"))
|
||||
}
|
||||
|
||||
let tunnelConfiguration = TunnelConfiguration(name: interfaceConfiguration.0, interface: interfaceConfiguration.1, peers: peerConfigurations)
|
||||
return .saved(tunnelConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelViewModel {
|
||||
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
|
||||
switch activateOnDemandOption {
|
||||
case .none:
|
||||
return tr("tunnelOnDemandOptionOff")
|
||||
case .useOnDemandOverWiFiOrCellular:
|
||||
return tr("tunnelOnDemandOptionWiFiOrCellular")
|
||||
case .useOnDemandOverWiFiOnly:
|
||||
return tr("tunnelOnDemandOptionWiFiOnly")
|
||||
case .useOnDemandOverCellularOnly:
|
||||
return tr("tunnelOnDemandOptionCellularOnly")
|
||||
}
|
||||
}
|
||||
|
||||
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
|
||||
if let activateOnDemandSetting = activateOnDemandSetting {
|
||||
if activateOnDemandSetting.isActivateOnDemandEnabled {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
|
||||
} else {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
||||
}
|
||||
} else {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
||||
}
|
||||
}
|
||||
|
||||
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
|
||||
return .useOnDemandOverWiFiOrCellular
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import os.log
|
||||
@@ -10,11 +10,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
var mainVC: MainViewController?
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
|
||||
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.backgroundColor = UIColor.white
|
||||
window.backgroundColor = .white
|
||||
self.window = window
|
||||
|
||||
let mainVC = MainViewController()
|
||||
@@ -27,14 +27,39 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
defer {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {
|
||||
os_log("Failed to remove item from Inbox: %{public}@", log: OSLog.default, type: .debug, url.absoluteString)
|
||||
}
|
||||
mainVC?.tunnelsListVC?.importFromFile(url: url) {
|
||||
_ = FileManager.deleteFile(at: url)
|
||||
}
|
||||
mainVC?.tunnelsListVC?.importFromFile(url: url)
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
mainVC?.refreshTunnelConnectionStatuses()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate {
|
||||
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
|
||||
guard let vcIdentifier = identifierComponents.last else { return nil }
|
||||
if vcIdentifier.hasPrefix("TunnelDetailVC:") {
|
||||
let tunnelName = String(vcIdentifier.suffix(vcIdentifier.count - "TunnelDetailVC:".count))
|
||||
if let tunnelsManager = mainVC?.tunnelsManager {
|
||||
if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
|
||||
return TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
|
||||
}
|
||||
} else {
|
||||
// Show it when tunnelsManager is available
|
||||
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class CopyableLabelTableViewCell: UITableViewCell {
|
||||
var copyableGesture = true
|
||||
|
||||
var textToCopy: String? {
|
||||
fatalError("textToCopy must be implemented by subclass")
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
|
||||
self.addGestureRecognizer(gestureRecognizer)
|
||||
self.isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizer
|
||||
@objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
|
||||
if !self.copyableGesture {
|
||||
return
|
||||
}
|
||||
guard recognizer.state == .recognized else { return }
|
||||
|
||||
if let recognizerView = recognizer.view,
|
||||
let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
|
||||
let menuController = UIMenuController.shared
|
||||
menuController.setTargetRect(self.detailTextLabel?.frame ?? recognizerView.frame, in: self.detailTextLabel?.superview ?? recognizerSuperView)
|
||||
menuController.setMenuVisible(true, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
return (action == #selector(UIResponderStandardEditActions.copy(_:)))
|
||||
}
|
||||
|
||||
override func copy(_ sender: Any?) {
|
||||
UIPasteboard.general.string = textToCopy
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
self.copyableGesture = true
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,19 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import os.log
|
||||
|
||||
class ErrorPresenter {
|
||||
static func errorMessage(for error: Error) -> (String, String)? {
|
||||
switch (error) {
|
||||
|
||||
// TunnelManagementError
|
||||
case TunnelManagementError.tunnelAlreadyExistsWithThatName:
|
||||
return ("Name already exists", "A tunnel with that name already exists")
|
||||
case TunnelManagementError.tunnelInvalidName:
|
||||
return ("Name already exists", "The tunnel name is invalid")
|
||||
case TunnelManagementError.vpnSystemErrorOnAddTunnel:
|
||||
return ("Unable to create tunnel", "Internal error")
|
||||
case TunnelManagementError.vpnSystemErrorOnModifyTunnel:
|
||||
return ("Unable to modify tunnel", "Internal error")
|
||||
case TunnelManagementError.vpnSystemErrorOnRemoveTunnel:
|
||||
return ("Unable to remove tunnel", "Internal error")
|
||||
|
||||
// TunnelActivationError
|
||||
case TunnelActivationError.dnsResolutionFailed:
|
||||
return ("DNS resolution failure", "One or more endpoint domains could not be resolved")
|
||||
case TunnelActivationError.tunnelActivationFailed:
|
||||
return ("Activation failure", "The tunnel could not be activated due to an internal error")
|
||||
case TunnelActivationError.attemptingActivationWhenAnotherTunnelIsBusy(let otherTunnelStatus):
|
||||
let statusString: String = {
|
||||
switch (otherTunnelStatus) {
|
||||
case .active: fallthrough
|
||||
case .reasserting: fallthrough
|
||||
case .restarting:
|
||||
return "active"
|
||||
case .activating: fallthrough
|
||||
case .resolvingEndpointDomains:
|
||||
return "being activated"
|
||||
case .deactivating:
|
||||
return "being deactivated"
|
||||
case .inactive:
|
||||
fatalError()
|
||||
}
|
||||
}()
|
||||
return ("Activation failure", "Another tunnel is currently \(statusString)")
|
||||
|
||||
default:
|
||||
os_log("ErrorPresenter: Error not presented: %{public}@", log: OSLog.default, type: .error, "\(error)")
|
||||
return nil
|
||||
}
|
||||
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
|
||||
let (title, message) = error.alertText
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: onDismissal)
|
||||
}
|
||||
|
||||
static func showErrorAlert(error: Error, from sourceVC: UIViewController?,
|
||||
onDismissal: (() -> Void)? = nil, onPresented: (() -> Void)? = nil) {
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
|
||||
guard let sourceVC = sourceVC else { return }
|
||||
guard let (title, message) = ErrorPresenter.errorMessage(for: error) else { return }
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
|
||||
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
||||
onDismissal?()
|
||||
}
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
@@ -61,15 +21,4 @@ class ErrorPresenter {
|
||||
|
||||
sourceVC.present(alert, animated: true, completion: onPresented)
|
||||
}
|
||||
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: UIViewController?, onDismissal: (() -> Void)? = nil) {
|
||||
guard let sourceVC = sourceVC else { return }
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
|
||||
onDismissal?()
|
||||
}
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(okAction)
|
||||
|
||||
sourceVC.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class MainViewController: UISplitViewController {
|
||||
var tunnelsListVC: TunnelsListTableViewController?
|
||||
|
||||
override func loadView() {
|
||||
let detailVC = UIViewController()
|
||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||
|
||||
let masterVC = TunnelsListTableViewController()
|
||||
let masterNC = UINavigationController(rootViewController: masterVC)
|
||||
|
||||
self.viewControllers = [ masterNC, detailNC ]
|
||||
|
||||
super.loadView()
|
||||
|
||||
tunnelsListVC = masterVC
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
self.delegate = self
|
||||
|
||||
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
|
||||
self.preferredDisplayMode = .allVisible
|
||||
}
|
||||
|
||||
func openForEditing(configFileURL: URL) {
|
||||
tunnelsListVC?.openForEditing(configFileURL: configFileURL)
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController: UISplitViewControllerDelegate {
|
||||
func splitViewController(_ splitViewController: UISplitViewController,
|
||||
collapseSecondary secondaryViewController: UIViewController,
|
||||
onto primaryViewController: UIViewController) -> Bool {
|
||||
// On iPhone, if the secondaryVC (detailVC) is just a UIViewController, it indicates that it's empty,
|
||||
// so just show the primaryVC (masterVC).
|
||||
let detailVC = (secondaryViewController as? UINavigationController)?.viewControllers.first
|
||||
let isDetailVCEmpty: Bool
|
||||
if let detailVC = detailVC {
|
||||
isDetailVCEmpty = (type(of: detailVC) == UIViewController.self)
|
||||
} else {
|
||||
isDetailVCEmpty = true
|
||||
}
|
||||
return isDetailVCEmpty
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import os.log
|
||||
|
||||
class SettingsTableViewController: UITableViewController {
|
||||
|
||||
enum SettingsFields: String {
|
||||
case iosAppVersion = "WireGuard for iOS"
|
||||
case goBackendVersion = "WireGuard Go Backend"
|
||||
case exportZipArchive = "Export zip archive"
|
||||
}
|
||||
|
||||
let settingsFieldsBySection : [[SettingsFields]] = [
|
||||
[.iosAppVersion, .goBackendVersion],
|
||||
[.exportZipArchive]
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager?
|
||||
var wireguardCaptionedImage: (view: UIView, size: CGSize)?
|
||||
|
||||
init(tunnelsManager: TunnelsManager?) {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.title = "Settings"
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
|
||||
|
||||
self.tableView.rowHeight = 44
|
||||
self.tableView.allowsSelection = false
|
||||
|
||||
self.tableView.register(TunnelSettingsTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelSettingsTableViewKeyValueCell.id)
|
||||
self.tableView.register(TunnelSettingsTableViewButtonCell.self, forCellReuseIdentifier: TunnelSettingsTableViewButtonCell.id)
|
||||
|
||||
let logo = UIImageView(image: UIImage(named: "wireguard.pdf", in: Bundle.main, compatibleWith: nil)!)
|
||||
logo.contentMode = .scaleAspectFit
|
||||
let height = self.tableView.rowHeight * 1.5
|
||||
let width = height * logo.image!.size.width / logo.image!.size.height
|
||||
logo.frame = CGRect(x: 0, y: 0, width: width, height: height)
|
||||
logo.bounds = logo.frame.insetBy(dx: 2, dy: 2)
|
||||
self.tableView.tableFooterView = logo
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
guard let logo = self.tableView.tableFooterView else { return }
|
||||
let bottomPadding = max(self.tableView.layoutMargins.bottom, CGFloat(10))
|
||||
let fullHeight = max(self.tableView.contentSize.height, self.tableView.bounds.size.height - self.tableView.layoutMargins.top - bottomPadding)
|
||||
let e = logo.frame
|
||||
logo.frame = CGRect(x: e.minX, y: fullHeight - e.height, width: e.width, height: e.height)
|
||||
}
|
||||
|
||||
@objc func doneTapped() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func exportConfigurationsAsZipFile(sourceView: UIView) {
|
||||
guard let tunnelsManager = tunnelsManager, tunnelsManager.numberOfTunnels() > 0 else {
|
||||
showErrorAlert(title: "Nothing to export", message: "There are no tunnels to export")
|
||||
return
|
||||
}
|
||||
var inputsToArchiver: [(fileName: String, contents: Data)] = []
|
||||
var usedNames: Set<String> = []
|
||||
for i in 0 ..< tunnelsManager.numberOfTunnels() {
|
||||
guard let tunnelConfiguration = tunnelsManager.tunnel(at: i).tunnelConfiguration() else { continue }
|
||||
if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) {
|
||||
let name = tunnelConfiguration.interface.name
|
||||
var nameToCheck = name
|
||||
var i = 0
|
||||
while (usedNames.contains(nameToCheck)) {
|
||||
i = i + 1
|
||||
nameToCheck = "\(name)\(i)"
|
||||
}
|
||||
usedNames.insert(nameToCheck)
|
||||
inputsToArchiver.append((fileName: "\(nameToCheck).conf", contents: contents))
|
||||
}
|
||||
}
|
||||
|
||||
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
return
|
||||
}
|
||||
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
|
||||
do {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
} catch {
|
||||
os_log("Failed to delete file: %{public}@ : %{public}@", log: OSLog.default, type: .error, destinationURL.absoluteString, error.localizedDescription)
|
||||
}
|
||||
|
||||
do {
|
||||
try ZipArchive.archive(inputs: inputsToArchiver, to: destinationURL)
|
||||
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
|
||||
// popoverPresentationController shall be non-nil on the iPad
|
||||
activityVC.popoverPresentationController?.sourceView = sourceView
|
||||
present(activityVC, animated: true)
|
||||
|
||||
} catch (let error) {
|
||||
showErrorAlert(title: "Unable to export", message: "There was an error exporting the tunnel configuration archive: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
|
||||
func showErrorAlert(title: String, message: String) {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default)
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(okAction)
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
extension SettingsTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return settingsFieldsBySection.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return settingsFieldsBySection[section].count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch (section) {
|
||||
case 0:
|
||||
return "About"
|
||||
case 1:
|
||||
return "Export configurations"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let field = settingsFieldsBySection[indexPath.section][indexPath.row]
|
||||
if (field == .iosAppVersion || field == .goBackendVersion) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewKeyValueCell.id, for: indexPath) as! TunnelSettingsTableViewKeyValueCell
|
||||
cell.key = field.rawValue
|
||||
if (field == .iosAppVersion) {
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
|
||||
cell.value = appVersion
|
||||
} else if (field == .goBackendVersion) {
|
||||
cell.value = WIREGUARD_GO_VERSION
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
assert(field == .exportZipArchive)
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = { [weak self] in
|
||||
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelSettingsTableViewKeyValueCell: UITableViewCell {
|
||||
static let id: String = "TunnelSettingsTableViewKeyValueCell"
|
||||
var key: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel?.text = value }
|
||||
}
|
||||
var value: String {
|
||||
get { return detailTextLabel?.text ?? "" }
|
||||
set(value) { detailTextLabel?.text = value }
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .value1, reuseIdentifier: TunnelSettingsTableViewKeyValueCell.id)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
key = ""
|
||||
value = ""
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelSettingsTableViewButtonCell: UITableViewCell {
|
||||
static let id: String = "TunnelSettingsTableViewButtonCell"
|
||||
var buttonText: String {
|
||||
get { return button.title(for: .normal) ?? "" }
|
||||
set(value) { button.setTitle(value, for: .normal) }
|
||||
}
|
||||
var onTapped: (() -> Void)?
|
||||
|
||||
let button: UIButton
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
button = UIButton(type: .system)
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
contentView.addSubview(button)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
|
||||
])
|
||||
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func buttonTapped() {
|
||||
onTapped?()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
buttonText = ""
|
||||
onTapped = nil
|
||||
}
|
||||
}
|
||||
@@ -1,417 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
// MARK: TunnelDetailTableViewController
|
||||
|
||||
class TunnelDetailTableViewController: UITableViewController {
|
||||
|
||||
let interfaceFields: [TunnelViewModel.InterfaceField] = [
|
||||
.name, .publicKey, .addresses,
|
||||
.listenPort, .mtu, .dns
|
||||
]
|
||||
|
||||
let peerFields: [TunnelViewModel.PeerField] = [
|
||||
.publicKey, .preSharedKey, .endpoint,
|
||||
.allowedIPs, .persistentKeepAlive
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager
|
||||
let tunnel: TunnelContainer
|
||||
var tunnelViewModel: TunnelViewModel
|
||||
|
||||
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
|
||||
tunnelsManager = tm
|
||||
tunnel = t
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.title = tunnelViewModel.interfaceData[.name]
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
|
||||
|
||||
self.tableView.rowHeight = 44
|
||||
self.tableView.allowsSelection = false
|
||||
self.tableView.register(TunnelDetailTableViewStatusCell.self, forCellReuseIdentifier: TunnelDetailTableViewStatusCell.id)
|
||||
self.tableView.register(TunnelDetailTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
|
||||
self.tableView.register(TunnelDetailTableViewButtonCell.self, forCellReuseIdentifier: TunnelDetailTableViewButtonCell.id)
|
||||
}
|
||||
|
||||
@objc func editTapped() {
|
||||
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
|
||||
editVC.delegate = self
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
editNC.modalPresentationStyle = .formSheet
|
||||
present(editNC, animated: true)
|
||||
}
|
||||
|
||||
func showErrorAlert(title: String, message: String) {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default)
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(okAction)
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
|
||||
onConfirmed: @escaping (() -> Void)) {
|
||||
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in
|
||||
onConfirmed()
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
|
||||
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
|
||||
alert.addAction(destroyAction)
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
// popoverPresentationController will be nil on iPhone and non-nil on iPad
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: TunnelEditTableViewControllerDelegate
|
||||
|
||||
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
|
||||
func tunnelSaved(tunnel: TunnelContainer) {
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
|
||||
self.title = tunnel.name
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
func tunnelEditingCancelled() {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
extension TunnelDetailTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 3 + tunnelViewModel.peersData.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let interfaceData = tunnelViewModel.interfaceData
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
if (section == 0) {
|
||||
// Status
|
||||
return 1
|
||||
} else if (section == 1) {
|
||||
// Interface
|
||||
return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
|
||||
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
|
||||
// Peer
|
||||
let peerData = tunnelViewModel.peersData[section - 2]
|
||||
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
|
||||
} else {
|
||||
// Delete tunnel
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
if (section == 0) {
|
||||
// Status
|
||||
return "Status"
|
||||
} else if (section == 1) {
|
||||
// Interface
|
||||
return "Interface"
|
||||
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
|
||||
// Peer
|
||||
return "Peer"
|
||||
} else {
|
||||
// Delete tunnel
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let interfaceData = tunnelViewModel.interfaceData
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
let section = indexPath.section
|
||||
let row = indexPath.row
|
||||
|
||||
if (section == 0) {
|
||||
// Status
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewStatusCell.id, for: indexPath) as! TunnelDetailTableViewStatusCell
|
||||
cell.tunnel = self.tunnel
|
||||
cell.onSwitchToggled = { [weak self] isOn in
|
||||
guard let s = self else { return }
|
||||
if (isOn) {
|
||||
s.tunnelsManager.startActivation(of: s.tunnel) { [weak s] error in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: s)
|
||||
DispatchQueue.main.async {
|
||||
cell.statusSwitch.isOn = false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.tunnelsManager.startDeactivation(of: s.tunnel) { error in
|
||||
print("Error while deactivating: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
} else if (section == 1) {
|
||||
// Interface
|
||||
let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
|
||||
// Set key and value
|
||||
cell.key = field.rawValue
|
||||
cell.value = interfaceData[field]
|
||||
return cell
|
||||
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
|
||||
// Peer
|
||||
let peerData = tunnelViewModel.peersData[section - 2]
|
||||
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[row]
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
|
||||
// Set key and value
|
||||
cell.key = field.rawValue
|
||||
cell.value = peerData[field]
|
||||
return cell
|
||||
} else {
|
||||
assert(section == (2 + numberOfPeerSections))
|
||||
// Delete configuration
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
|
||||
cell.buttonText = "Delete tunnel"
|
||||
cell.hasDestructiveAction = true
|
||||
cell.onTapped = { [weak self] in
|
||||
guard let s = self else { return }
|
||||
s.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak s] in
|
||||
guard let tunnelsManager = s?.tunnelsManager, let tunnel = s?.tunnel else { return }
|
||||
tunnelsManager.remove(tunnel: tunnel) { (error) in
|
||||
if (error != nil) {
|
||||
print("Error removing tunnel: \(String(describing: error))")
|
||||
return
|
||||
}
|
||||
}
|
||||
s?.navigationController?.navigationController?.popToRootViewController(animated: true)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelDetailTableViewStatusCell: UITableViewCell {
|
||||
static let id: String = "TunnelDetailTableViewStatusCell"
|
||||
|
||||
var tunnel: TunnelContainer? {
|
||||
didSet(value) {
|
||||
update(from: tunnel?.status)
|
||||
statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in
|
||||
self?.update(from: tunnel.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
var isSwitchInteractionEnabled: Bool {
|
||||
get { return statusSwitch.isUserInteractionEnabled }
|
||||
set(value) { statusSwitch.isUserInteractionEnabled = value }
|
||||
}
|
||||
var onSwitchToggled: ((Bool) -> Void)?
|
||||
private var isOnSwitchToggledHandlerEnabled: Bool = true
|
||||
|
||||
let statusSwitch: UISwitch
|
||||
private var statusObservervationToken: AnyObject?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
statusSwitch = UISwitch()
|
||||
super.init(style: .default, reuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
|
||||
accessoryView = statusSwitch
|
||||
|
||||
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
|
||||
}
|
||||
|
||||
@objc func switchToggled() {
|
||||
if (isOnSwitchToggledHandlerEnabled) {
|
||||
onSwitchToggled?(statusSwitch.isOn)
|
||||
}
|
||||
}
|
||||
|
||||
private func update(from status: TunnelStatus?) {
|
||||
guard let status = status else {
|
||||
reset()
|
||||
return
|
||||
}
|
||||
let text: String
|
||||
switch (status) {
|
||||
case .inactive:
|
||||
text = "Inactive"
|
||||
case .activating:
|
||||
text = "Activating"
|
||||
case .active:
|
||||
text = "Active"
|
||||
case .deactivating:
|
||||
text = "Deactivating"
|
||||
case .reasserting:
|
||||
text = "Reactivating"
|
||||
case .resolvingEndpointDomains:
|
||||
text = "Resolving domains"
|
||||
case .restarting:
|
||||
text = "Restarting"
|
||||
}
|
||||
textLabel?.text = text
|
||||
DispatchQueue.main.async { [weak statusSwitch] in
|
||||
guard let statusSwitch = statusSwitch else { return }
|
||||
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
|
||||
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
|
||||
}
|
||||
textLabel?.textColor = (status == .active || status == .inactive) ? UIColor.black : UIColor.gray
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
textLabel?.text = "Invalid"
|
||||
statusSwitch.isOn = false
|
||||
textLabel?.textColor = UIColor.gray
|
||||
statusSwitch.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
|
||||
static let id: String = "TunnelDetailTableViewKeyValueCell"
|
||||
var key: String {
|
||||
get { return keyLabel.text ?? "" }
|
||||
set(value) { keyLabel.text = value }
|
||||
}
|
||||
var value: String {
|
||||
get { return valueLabel.text ?? "" }
|
||||
set(value) { valueLabel.text = value }
|
||||
}
|
||||
|
||||
override var textToCopy: String? {
|
||||
return self.valueLabel.text
|
||||
}
|
||||
|
||||
let keyLabel: UILabel
|
||||
let valueLabel: UILabel
|
||||
let valueScroller: UIScrollView
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
keyLabel = UILabel()
|
||||
valueLabel = UILabel()
|
||||
valueScroller = UIScrollView()
|
||||
|
||||
keyLabel.textColor = UIColor.black
|
||||
valueLabel.textColor = UIColor.gray
|
||||
valueScroller.isDirectionalLockEnabled = true
|
||||
valueScroller.showsHorizontalScrollIndicator = false
|
||||
valueScroller.showsVerticalScrollIndicator = false
|
||||
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
valueScroller.addSubview(valueLabel)
|
||||
valueLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
valueLabel.textAlignment = .right
|
||||
NSLayoutConstraint.activate([
|
||||
valueLabel.leftAnchor.constraint(equalTo: valueScroller.contentLayoutGuide.leftAnchor),
|
||||
valueLabel.topAnchor.constraint(equalTo: valueScroller.contentLayoutGuide.topAnchor),
|
||||
valueLabel.bottomAnchor.constraint(equalTo: valueScroller.contentLayoutGuide.bottomAnchor),
|
||||
valueLabel.rightAnchor.constraint(equalTo: valueScroller.contentLayoutGuide.rightAnchor),
|
||||
valueLabel.heightAnchor.constraint(equalTo: valueScroller.heightAnchor),
|
||||
])
|
||||
|
||||
// Value label should expand to fit the scrollView, so that the right-alignment works
|
||||
let expandToFitValueLabelConstraint = NSLayoutConstraint(item: valueLabel, attribute: .width, relatedBy: .equal,
|
||||
toItem: valueScroller, attribute: .width, multiplier: 1, constant: 0)
|
||||
expandToFitValueLabelConstraint.priority = .defaultLow + 1
|
||||
expandToFitValueLabelConstraint.isActive = true
|
||||
|
||||
contentView.addSubview(keyLabel)
|
||||
keyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
keyLabel.textAlignment = .left
|
||||
NSLayoutConstraint.activate([
|
||||
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
|
||||
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
|
||||
])
|
||||
|
||||
contentView.addSubview(valueScroller)
|
||||
valueScroller.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
valueScroller.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
|
||||
valueScroller.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
valueScroller.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 8)
|
||||
])
|
||||
|
||||
// Key label should never appear truncated
|
||||
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
|
||||
// Key label should hug it's content; value label should not.
|
||||
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
valueScroller.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
key = ""
|
||||
value = ""
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelDetailTableViewButtonCell: UITableViewCell {
|
||||
static let id: String = "TunnelDetailTableViewButtonCell"
|
||||
var buttonText: String {
|
||||
get { return button.title(for: .normal) ?? "" }
|
||||
set(value) { button.setTitle(value, for: .normal) }
|
||||
}
|
||||
var hasDestructiveAction: Bool {
|
||||
get { return button.tintColor == UIColor.red }
|
||||
set(value) { button.tintColor = value ? UIColor.red : buttonStandardTintColor }
|
||||
}
|
||||
var onTapped: (() -> Void)?
|
||||
|
||||
let button: UIButton
|
||||
var buttonStandardTintColor: UIColor
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
button = UIButton(type: .system)
|
||||
buttonStandardTintColor = button.tintColor
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
contentView.addSubview(button)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
|
||||
])
|
||||
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func buttonTapped() {
|
||||
onTapped?()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
buttonText = ""
|
||||
onTapped = nil
|
||||
hasDestructiveAction = false
|
||||
}
|
||||
}
|
||||
@@ -1,614 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol TunnelEditTableViewControllerDelegate: class {
|
||||
func tunnelSaved(tunnel: TunnelContainer)
|
||||
func tunnelEditingCancelled()
|
||||
}
|
||||
|
||||
// MARK: TunnelEditTableViewController
|
||||
|
||||
class TunnelEditTableViewController: UITableViewController {
|
||||
|
||||
weak var delegate: TunnelEditTableViewControllerDelegate?
|
||||
|
||||
let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [
|
||||
[.name],
|
||||
[.privateKey, .publicKey, .generateKeyPair],
|
||||
[.addresses, .listenPort, .mtu, .dns]
|
||||
]
|
||||
|
||||
let peerFields: [TunnelViewModel.PeerField] = [
|
||||
.publicKey, .preSharedKey, .endpoint,
|
||||
.allowedIPs, .excludePrivateIPs, .persistentKeepAlive,
|
||||
.deletePeer
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager
|
||||
let tunnel: TunnelContainer?
|
||||
let tunnelViewModel: TunnelViewModel
|
||||
|
||||
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
|
||||
// Use this initializer to edit an existing tunnel.
|
||||
tunnelsManager = tm
|
||||
tunnel = t
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
init(tunnelsManager tm: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
|
||||
// Use this initializer to create a new tunnel.
|
||||
// If tunnelConfiguration is passed, data will be prepopulated from that configuration.
|
||||
tunnelsManager = tm
|
||||
tunnel = nil
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.title = (tunnel == nil) ? "New configuration" : "Edit configuration"
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
|
||||
|
||||
self.tableView.rowHeight = 44
|
||||
self.tableView.allowsSelection = false
|
||||
|
||||
self.tableView.register(TunnelEditTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewKeyValueCell.id)
|
||||
self.tableView.register(TunnelEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelEditTableViewButtonCell.id)
|
||||
self.tableView.register(TunnelEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelEditTableViewSwitchCell.id)
|
||||
}
|
||||
|
||||
@objc func saveTapped() {
|
||||
self.tableView.endEditing(false)
|
||||
let tunnelSaveResult = tunnelViewModel.save()
|
||||
switch (tunnelSaveResult) {
|
||||
case .error(let errorMessage):
|
||||
let erroringConfiguration = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? "Interface" : "Peer"
|
||||
ErrorPresenter.showErrorAlert(title: "Invalid \(erroringConfiguration)", message: errorMessage, from: self)
|
||||
self.tableView.reloadData() // Highlight erroring fields
|
||||
case .saved(let tunnelConfiguration):
|
||||
if let tunnel = tunnel {
|
||||
// We're modifying an existing tunnel
|
||||
tunnelsManager.modify(tunnel: tunnel, with: tunnelConfiguration) { [weak self] (error) in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We're adding a new tunnel
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] (tunnel, error) in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
if let tunnel = tunnel {
|
||||
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func cancelTapped() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
self.delegate?.tunnelEditingCancelled()
|
||||
}
|
||||
|
||||
func showErrorAlert(title: String, message: String) {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default)
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(okAction)
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
extension TunnelEditTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
return numberOfInterfaceSections + numberOfPeerSections + 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
if (section < numberOfInterfaceSections) {
|
||||
// Interface
|
||||
return interfaceFieldsBySection[section].count
|
||||
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
|
||||
// Peer
|
||||
let peerIndex = (section - numberOfInterfaceSections)
|
||||
let peerData = tunnelViewModel.peersData[peerIndex]
|
||||
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
|
||||
return peerFieldsToShow.count
|
||||
} else {
|
||||
// Add peer
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
if (section < numberOfInterfaceSections) {
|
||||
// Interface
|
||||
return (section == 0) ? "Interface" : nil
|
||||
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
|
||||
// Peer
|
||||
return "Peer"
|
||||
} else {
|
||||
// Add peer
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
let section = indexPath.section
|
||||
let row = indexPath.row
|
||||
|
||||
if (section < numberOfInterfaceSections) {
|
||||
// Interface
|
||||
let interfaceData = tunnelViewModel.interfaceData
|
||||
let field = interfaceFieldsBySection[section][row]
|
||||
if (field == .generateKeyPair) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = { [weak self, weak interfaceData] in
|
||||
if let interfaceData = interfaceData, let s = self {
|
||||
interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64EncodedString()
|
||||
if let privateKeyRow = s.interfaceFieldsBySection[section].firstIndex(of: .privateKey),
|
||||
let publicKeyRow = s.interfaceFieldsBySection[section].firstIndex(of: .publicKey) {
|
||||
let privateKeyIndex = IndexPath(row: privateKeyRow, section: section)
|
||||
let publicKeyIndex = IndexPath(row: publicKeyRow, section: section)
|
||||
s.tableView.reloadRows(at: [privateKeyIndex, publicKeyIndex], with: .automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewKeyValueCell.id, for: indexPath) as! TunnelEditTableViewKeyValueCell
|
||||
// Set key
|
||||
cell.key = field.rawValue
|
||||
// Set placeholder text
|
||||
switch (field) {
|
||||
case .name:
|
||||
cell.placeholderText = "Required"
|
||||
case .privateKey:
|
||||
cell.placeholderText = "Required"
|
||||
case .addresses:
|
||||
cell.placeholderText = "Optional"
|
||||
case .listenPort:
|
||||
cell.placeholderText = "Automatic"
|
||||
case .mtu:
|
||||
cell.placeholderText = "Automatic"
|
||||
case .dns:
|
||||
cell.placeholderText = "Optional"
|
||||
case .publicKey: break
|
||||
case .generateKeyPair: break
|
||||
}
|
||||
// Set editable
|
||||
if (field == .publicKey) {
|
||||
cell.isValueEditable = false
|
||||
}
|
||||
// Set keyboardType
|
||||
if (field == .mtu || field == .listenPort) {
|
||||
cell.keyboardType = .numberPad
|
||||
} else if (field == .addresses || field == .dns) {
|
||||
cell.keyboardType = .numbersAndPunctuation
|
||||
}
|
||||
// Show erroring fields
|
||||
cell.isValueValid = (!interfaceData.fieldsWithError.contains(field))
|
||||
// Bind values to view model
|
||||
cell.value = interfaceData[field]
|
||||
if (field == .dns) { // While editing DNS, you might directly set exclude private IPs
|
||||
cell.onValueBeingEdited = { [weak interfaceData] value in
|
||||
interfaceData?[field] = value
|
||||
}
|
||||
} else {
|
||||
cell.onValueChanged = { [weak interfaceData] value in
|
||||
interfaceData?[field] = value
|
||||
}
|
||||
}
|
||||
// Compute public key live
|
||||
if (field == .privateKey) {
|
||||
cell.onValueBeingEdited = { [weak self, weak interfaceData] value in
|
||||
if let interfaceData = interfaceData, let s = self {
|
||||
interfaceData[.privateKey] = value
|
||||
if let row = s.interfaceFieldsBySection[section].firstIndex(of: .publicKey) {
|
||||
s.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
|
||||
// Peer
|
||||
let peerIndex = (section - numberOfInterfaceSections)
|
||||
let peerData = tunnelViewModel.peersData[peerIndex]
|
||||
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
|
||||
let field = peerFieldsToShow[row]
|
||||
if (field == .deletePeer) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
|
||||
cell.buttonText = field.rawValue
|
||||
cell.hasDestructiveAction = true
|
||||
cell.onTapped = { [weak self, weak peerData] in
|
||||
guard let peerData = peerData else { return }
|
||||
guard let s = self else { return }
|
||||
s.showConfirmationAlert(message: "Delete this peer?",
|
||||
buttonTitle: "Delete", from: cell,
|
||||
onConfirmed: { [weak s] in
|
||||
guard let s = s else { return }
|
||||
let removedSectionIndices = s.deletePeer(peer: peerData)
|
||||
let shouldShowExcludePrivateIPs = (s.tunnelViewModel.peersData.count == 1 &&
|
||||
s.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
|
||||
tableView.performBatchUpdates({
|
||||
s.tableView.deleteSections(removedSectionIndices, with: .automatic)
|
||||
if (shouldShowExcludePrivateIPs) {
|
||||
if let row = s.peerFields.firstIndex(of: .excludePrivateIPs) {
|
||||
let rowIndexPath = IndexPath(row: row, section: numberOfInterfaceSections /* First peer section */)
|
||||
s.tableView.insertRows(at: [rowIndexPath], with: .automatic)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return cell
|
||||
} else if (field == .excludePrivateIPs) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSwitchCell.id, for: indexPath) as! TunnelEditTableViewSwitchCell
|
||||
cell.message = field.rawValue
|
||||
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
|
||||
cell.isOn = peerData.excludePrivateIPsValue
|
||||
cell.onSwitchToggled = { [weak self] (isOn) in
|
||||
guard let s = self else { return }
|
||||
peerData.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: s.tunnelViewModel.interfaceData[.dns])
|
||||
if let row = s.peerFields.firstIndex(of: .allowedIPs) {
|
||||
s.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewKeyValueCell.id, for: indexPath) as! TunnelEditTableViewKeyValueCell
|
||||
// Set key
|
||||
cell.key = field.rawValue
|
||||
// Set placeholder text
|
||||
switch (field) {
|
||||
case .publicKey:
|
||||
cell.placeholderText = "Required"
|
||||
case .preSharedKey:
|
||||
cell.placeholderText = "Optional"
|
||||
case .endpoint:
|
||||
cell.placeholderText = "Optional"
|
||||
case .allowedIPs:
|
||||
cell.placeholderText = "Optional"
|
||||
case .persistentKeepAlive:
|
||||
cell.placeholderText = "Off"
|
||||
case .excludePrivateIPs: break
|
||||
case .deletePeer: break
|
||||
}
|
||||
// Set keyboardType
|
||||
if (field == .persistentKeepAlive) {
|
||||
cell.keyboardType = .numberPad
|
||||
} else if (field == .allowedIPs) {
|
||||
cell.keyboardType = .numbersAndPunctuation
|
||||
}
|
||||
// Show erroring fields
|
||||
cell.isValueValid = (!peerData.fieldsWithError.contains(field))
|
||||
// Bind values to view model
|
||||
cell.value = peerData[field]
|
||||
if (field != .allowedIPs) {
|
||||
cell.onValueChanged = { [weak peerData] value in
|
||||
peerData?[field] = value
|
||||
}
|
||||
}
|
||||
// Compute state of exclude private IPs live
|
||||
if (field == .allowedIPs) {
|
||||
cell.onValueBeingEdited = { [weak self, weak peerData] value in
|
||||
if let peerData = peerData, let s = self {
|
||||
let oldValue = peerData.shouldAllowExcludePrivateIPsControl
|
||||
peerData[.allowedIPs] = value
|
||||
if (oldValue != peerData.shouldAllowExcludePrivateIPsControl) {
|
||||
if let row = s.peerFields.firstIndex(of: .excludePrivateIPs) {
|
||||
if (peerData.shouldAllowExcludePrivateIPsControl) {
|
||||
s.tableView.insertRows(at: [IndexPath(row: row, section: section)], with: .automatic)
|
||||
} else {
|
||||
s.tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: .automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
} else {
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeerSections))
|
||||
// Add peer
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
|
||||
cell.buttonText = "Add peer"
|
||||
cell.onTapped = { [weak self] in
|
||||
guard let s = self else { return }
|
||||
let shouldHideExcludePrivateIPs = (s.tunnelViewModel.peersData.count == 1 &&
|
||||
s.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
|
||||
let addedSectionIndices = s.appendEmptyPeer()
|
||||
tableView.performBatchUpdates({
|
||||
tableView.insertSections(addedSectionIndices, with: .automatic)
|
||||
if (shouldHideExcludePrivateIPs) {
|
||||
if let row = s.peerFields.firstIndex(of: .excludePrivateIPs) {
|
||||
let rowIndexPath = IndexPath(row: row, section: numberOfInterfaceSections /* First peer section */)
|
||||
s.tableView.deleteRows(at: [rowIndexPath], with: .automatic)
|
||||
}
|
||||
}
|
||||
}, completion: nil)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
func appendEmptyPeer() -> IndexSet {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
|
||||
tunnelViewModel.appendEmptyPeer()
|
||||
let addedPeerIndex = tunnelViewModel.peersData.count - 1
|
||||
|
||||
let addedSectionIndices = IndexSet(integer: (numberOfInterfaceSections + addedPeerIndex))
|
||||
return addedSectionIndices
|
||||
}
|
||||
|
||||
func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
|
||||
assert(peer.index < tunnelViewModel.peersData.count)
|
||||
tunnelViewModel.deletePeer(peer: peer)
|
||||
|
||||
let removedSectionIndices = IndexSet(integer: (numberOfInterfaceSections + peer.index))
|
||||
return removedSectionIndices
|
||||
}
|
||||
|
||||
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
|
||||
onConfirmed: @escaping (() -> Void)) {
|
||||
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in
|
||||
onConfirmed()
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
|
||||
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
|
||||
alert.addAction(destroyAction)
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
// popoverPresentationController will be nil on iPhone and non-nil on iPad
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelEditTableViewKeyValueCell: CopyableLabelTableViewCell {
|
||||
static let id: String = "TunnelEditTableViewKeyValueCell"
|
||||
var key: String {
|
||||
get { return keyLabel.text ?? "" }
|
||||
set(value) {keyLabel.text = value }
|
||||
}
|
||||
var value: String {
|
||||
get { return valueTextField.text ?? "" }
|
||||
set(value) { valueTextField.text = value }
|
||||
}
|
||||
var placeholderText: String {
|
||||
get { return valueTextField.placeholder ?? "" }
|
||||
set(value) { valueTextField.placeholder = value }
|
||||
}
|
||||
var isValueEditable: Bool {
|
||||
get { return valueTextField.isEnabled }
|
||||
set(value) {
|
||||
super.copyableGesture = !value
|
||||
valueTextField.isEnabled = value
|
||||
keyLabel.textColor = value ? UIColor.black : UIColor.gray
|
||||
valueTextField.textColor = value ? UIColor.black : UIColor.gray
|
||||
}
|
||||
}
|
||||
var isValueValid: Bool = true {
|
||||
didSet {
|
||||
if (isValueValid) {
|
||||
keyLabel.textColor = isValueEditable ? UIColor.black : UIColor.gray
|
||||
} else {
|
||||
keyLabel.textColor = UIColor.red
|
||||
}
|
||||
}
|
||||
}
|
||||
var keyboardType: UIKeyboardType {
|
||||
get { return valueTextField.keyboardType }
|
||||
set(value) { valueTextField.keyboardType = value }
|
||||
}
|
||||
|
||||
var onValueChanged: ((String) -> Void)?
|
||||
var onValueBeingEdited: ((String) -> Void)?
|
||||
|
||||
let keyLabel: UILabel
|
||||
let valueTextField: UITextField
|
||||
|
||||
private var textFieldValueOnBeginEditing: String = ""
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
keyLabel = UILabel()
|
||||
valueTextField = UITextField()
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
isValueEditable = true
|
||||
contentView.addSubview(keyLabel)
|
||||
keyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
keyLabel.textAlignment = .right
|
||||
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
|
||||
relatedBy: .equal,
|
||||
toItem: self, attribute: .width,
|
||||
multiplier: 0.4, constant: 0)
|
||||
// The "Persistent Keepalive" key doesn't fit into 0.4 * width on the iPhone SE,
|
||||
// so set a CR priority > the 0.4-constraint's priority.
|
||||
widthRatioConstraint.priority = .defaultHigh + 1
|
||||
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
|
||||
NSLayoutConstraint.activate([
|
||||
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
|
||||
widthRatioConstraint
|
||||
])
|
||||
contentView.addSubview(valueTextField)
|
||||
valueTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
valueTextField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
valueTextField.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 16),
|
||||
valueTextField.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
|
||||
])
|
||||
valueTextField.delegate = self
|
||||
|
||||
valueTextField.autocapitalizationType = .none
|
||||
valueTextField.autocorrectionType = .no
|
||||
valueTextField.spellCheckingType = .no
|
||||
}
|
||||
|
||||
override var textToCopy: String? {
|
||||
return self.valueTextField.text
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
key = ""
|
||||
value = ""
|
||||
placeholderText = ""
|
||||
isValueEditable = true
|
||||
isValueValid = true
|
||||
keyboardType = .default
|
||||
onValueChanged = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelEditTableViewKeyValueCell: UITextFieldDelegate {
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
textFieldValueOnBeginEditing = textField.text ?? ""
|
||||
isValueValid = true
|
||||
}
|
||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
let isModified = (textField.text ?? "" != textFieldValueOnBeginEditing)
|
||||
guard (isModified) else { return }
|
||||
if let onValueChanged = onValueChanged {
|
||||
onValueChanged(textField.text ?? "")
|
||||
}
|
||||
}
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
if let onValueBeingEdited = onValueBeingEdited {
|
||||
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
|
||||
onValueBeingEdited(modifiedText)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelEditTableViewButtonCell: UITableViewCell {
|
||||
static let id: String = "TunnelEditTableViewButtonCell"
|
||||
var buttonText: String {
|
||||
get { return button.title(for: .normal) ?? "" }
|
||||
set(value) { button.setTitle(value, for: .normal) }
|
||||
}
|
||||
var hasDestructiveAction: Bool {
|
||||
get { return button.tintColor == UIColor.red }
|
||||
set(value) { button.tintColor = value ? UIColor.red : buttonStandardTintColor }
|
||||
}
|
||||
var onTapped: (() -> Void)?
|
||||
|
||||
let button: UIButton
|
||||
var buttonStandardTintColor: UIColor
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
button = UIButton(type: .system)
|
||||
buttonStandardTintColor = button.tintColor
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
contentView.addSubview(button)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
|
||||
])
|
||||
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func buttonTapped() {
|
||||
onTapped?()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
buttonText = ""
|
||||
onTapped = nil
|
||||
hasDestructiveAction = false
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelEditTableViewSwitchCell: UITableViewCell {
|
||||
static let id: String = "TunnelEditTableViewSwitchCell"
|
||||
var message: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel!.text = value }
|
||||
}
|
||||
var isOn: Bool {
|
||||
get { return switchView.isOn }
|
||||
set(value) { switchView.isOn = value }
|
||||
}
|
||||
var isEnabled: Bool {
|
||||
get { return switchView.isEnabled }
|
||||
set(value) {
|
||||
switchView.isEnabled = value
|
||||
textLabel?.textColor = value ? UIColor.black : UIColor.gray
|
||||
}
|
||||
}
|
||||
|
||||
var onSwitchToggled: ((Bool) -> Void)?
|
||||
|
||||
let switchView: UISwitch
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
switchView = UISwitch()
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
accessoryView = switchView
|
||||
|
||||
switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
|
||||
}
|
||||
|
||||
@objc func switchToggled() {
|
||||
onSwitchToggled?(switchView.isOn)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
isOn = false
|
||||
}
|
||||
}
|
||||
@@ -1,493 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
|
||||
class TunnelsListTableViewController: UIViewController {
|
||||
|
||||
var tunnelsManager: TunnelsManager?
|
||||
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
|
||||
|
||||
var busyIndicator: UIActivityIndicatorView?
|
||||
var centeredAddButton: BorderedTextButton?
|
||||
var tableView: UITableView?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = UIColor.white
|
||||
|
||||
// Set up the navigation bar
|
||||
self.title = "WireGuard"
|
||||
let addButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
|
||||
self.navigationItem.rightBarButtonItem = addButtonItem
|
||||
let settingsButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
|
||||
self.navigationItem.leftBarButtonItem = settingsButtonItem
|
||||
|
||||
// Set up the busy indicator
|
||||
let busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||
busyIndicator.hidesWhenStopped = true
|
||||
|
||||
// Add the busyIndicator, centered
|
||||
view.addSubview(busyIndicator)
|
||||
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||
])
|
||||
busyIndicator.startAnimating()
|
||||
self.busyIndicator = busyIndicator
|
||||
|
||||
// Create the tunnels manager, and when it's ready, create the tableView
|
||||
TunnelsManager.create { [weak self] tunnelsManager in
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
guard let s = self else { return }
|
||||
|
||||
let tableView = UITableView(frame: CGRect.zero, style: .plain)
|
||||
tableView.rowHeight = 60
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id)
|
||||
|
||||
s.view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.leftAnchor.constraint(equalTo: s.view.leftAnchor),
|
||||
tableView.rightAnchor.constraint(equalTo: s.view.rightAnchor),
|
||||
tableView.topAnchor.constraint(equalTo: s.view.topAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: s.view.bottomAnchor)
|
||||
])
|
||||
tableView.dataSource = s
|
||||
tableView.delegate = s
|
||||
s.tableView = tableView
|
||||
|
||||
// Add an add button, centered
|
||||
let centeredAddButton = BorderedTextButton()
|
||||
centeredAddButton.title = "Add a tunnel"
|
||||
centeredAddButton.isHidden = true
|
||||
s.view.addSubview(centeredAddButton)
|
||||
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
centeredAddButton.centerXAnchor.constraint(equalTo: s.view.centerXAnchor),
|
||||
centeredAddButton.centerYAnchor.constraint(equalTo: s.view.centerYAnchor)
|
||||
])
|
||||
centeredAddButton.onTapped = { [weak self] in
|
||||
self?.addButtonTapped(sender: centeredAddButton)
|
||||
}
|
||||
s.centeredAddButton = centeredAddButton
|
||||
|
||||
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
|
||||
busyIndicator.stopAnimating()
|
||||
|
||||
tunnelsManager.delegate = s
|
||||
s.tunnelsManager = tunnelsManager
|
||||
s.onTunnelsManagerReady?(tunnelsManager)
|
||||
s.onTunnelsManagerReady = nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc func addButtonTapped(sender: AnyObject) {
|
||||
if (self.tunnelsManager == nil) { return } // Do nothing until we've loaded the tunnels
|
||||
let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet)
|
||||
let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] (_) in
|
||||
self?.presentViewControllerForFileImport()
|
||||
}
|
||||
alert.addAction(importFileAction)
|
||||
|
||||
let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] (_) in
|
||||
self?.presentViewControllerForScanningQRCode()
|
||||
}
|
||||
alert.addAction(scanQRCodeAction)
|
||||
|
||||
let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] (_) in
|
||||
if let s = self, let tunnelsManager = s.tunnelsManager {
|
||||
s.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: nil)
|
||||
}
|
||||
}
|
||||
alert.addAction(createFromScratchAction)
|
||||
|
||||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
// popoverPresentationController will be nil on iPhone and non-nil on iPad
|
||||
if let sender = sender as? UIBarButtonItem {
|
||||
alert.popoverPresentationController?.barButtonItem = sender
|
||||
} else if let sender = sender as? UIView {
|
||||
alert.popoverPresentationController?.sourceView = sender
|
||||
}
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func settingsButtonTapped(sender: UIBarButtonItem!) {
|
||||
if (self.tunnelsManager == nil) { return } // Do nothing until we've loaded the tunnels
|
||||
let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager)
|
||||
let settingsNC = UINavigationController(rootViewController: settingsVC)
|
||||
settingsNC.modalPresentationStyle = .formSheet
|
||||
self.present(settingsNC, animated: true)
|
||||
}
|
||||
|
||||
func openForEditing(configFileURL: URL) {
|
||||
let tunnelConfiguration: TunnelConfiguration?
|
||||
let name = configFileURL.deletingPathExtension().lastPathComponent
|
||||
do {
|
||||
let fileContents = try String(contentsOf: configFileURL)
|
||||
try tunnelConfiguration = WgQuickConfigFileParser.parse(fileContents, name: name)
|
||||
} catch (let error) {
|
||||
showErrorAlert(title: "Unable to import tunnel", message: "An error occured when importing the tunnel configuration: \(String(describing: error))")
|
||||
return
|
||||
}
|
||||
tunnelConfiguration?.interface.name = name
|
||||
if let tunnelsManager = tunnelsManager {
|
||||
presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
|
||||
} else {
|
||||
onTunnelsManagerReady = { [weak self] tunnelsManager in
|
||||
self?.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
|
||||
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
editNC.modalPresentationStyle = .formSheet
|
||||
self.present(editNC, animated: true)
|
||||
}
|
||||
|
||||
func presentViewControllerForFileImport() {
|
||||
let documentTypes = ["com.wireguard.config.quick", String(kUTTypeZipArchive)]
|
||||
let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
|
||||
filePicker.delegate = self
|
||||
self.present(filePicker, animated: true)
|
||||
}
|
||||
|
||||
func presentViewControllerForScanningQRCode() {
|
||||
let scanQRCodeVC = QRScanViewController()
|
||||
scanQRCodeVC.delegate = self
|
||||
let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC)
|
||||
scanQRCodeNC.modalPresentationStyle = .fullScreen
|
||||
self.present(scanQRCodeNC, animated: true)
|
||||
}
|
||||
|
||||
func showErrorAlert(title: String, message: String) {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default)
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(okAction)
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func importFromFile(url: URL) {
|
||||
// Import configurations from a .conf or a .zip file
|
||||
if (url.pathExtension == "conf") {
|
||||
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let fileContents = try? String(contentsOf: url),
|
||||
let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) {
|
||||
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showErrorAlert(title: "Unable to import tunnel", message: "An error occured when importing the tunnel configuration.")
|
||||
}
|
||||
} else if (url.pathExtension == "zip") {
|
||||
var unarchivedFiles: [(fileName: String, contents: Data)] = []
|
||||
do {
|
||||
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
|
||||
} catch ZipArchiveError.cantOpenInputZipFile {
|
||||
showErrorAlert(title: "Unable to read zip archive", message: "The zip archive could not be read.")
|
||||
return
|
||||
} catch ZipArchiveError.badArchive {
|
||||
showErrorAlert(title: "Unable to read zip archive", message: "Bad or corrupt zip archive.")
|
||||
return
|
||||
} catch (let error) {
|
||||
showErrorAlert(title: "Unable to read zip archive", message: "Unexpected error: \(String(describing: error))")
|
||||
return
|
||||
}
|
||||
|
||||
for (i, unarchivedFile) in unarchivedFiles.enumerated().reversed() {
|
||||
let fileBaseName = URL(string: unarchivedFile.fileName)?.deletingPathExtension().lastPathComponent
|
||||
if let trimmedName = fileBaseName?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmedName.isEmpty {
|
||||
unarchivedFiles[i].fileName = trimmedName
|
||||
} else {
|
||||
unarchivedFiles.remove(at: i)
|
||||
}
|
||||
}
|
||||
if (unarchivedFiles.isEmpty) {
|
||||
showErrorAlert(title: "No tunnels in zip archive", message: "No .conf tunnel files were found inside the zip archive.")
|
||||
return
|
||||
}
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
unarchivedFiles.sort { $0.fileName < $1.fileName }
|
||||
var lastFileName: String?
|
||||
var configs: [TunnelConfiguration] = []
|
||||
for file in unarchivedFiles {
|
||||
if file.fileName == lastFileName {
|
||||
continue
|
||||
}
|
||||
lastFileName = file.fileName
|
||||
guard let fileContents = String(data: file.contents, encoding: .utf8) else {
|
||||
continue
|
||||
}
|
||||
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileName) else {
|
||||
continue
|
||||
}
|
||||
configs.append(tunnelConfig)
|
||||
}
|
||||
tunnelsManager.addMultiple(tunnelConfigurations: configs) { [weak self] (numberSuccessful) in
|
||||
if numberSuccessful == unarchivedFiles.count {
|
||||
return
|
||||
}
|
||||
self?.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
|
||||
message: "Created \(numberSuccessful) of \(unarchivedFiles.count) tunnels from zip archive")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIDocumentPickerDelegate
|
||||
|
||||
extension TunnelsListTableViewController: UIDocumentPickerDelegate {
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
if let url = urls.first {
|
||||
importFromFile(url: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: QRScanViewControllerDelegate
|
||||
|
||||
extension TunnelsListTableViewController: QRScanViewControllerDelegate {
|
||||
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
|
||||
completionHandler: (() -> Void)?) {
|
||||
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
|
||||
} else {
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
extension TunnelsListTableViewController: UITableViewDataSource {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return (tunnelsManager?.numberOfTunnels() ?? 0)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsListTableViewCell.id, for: indexPath) as! TunnelsListTableViewCell
|
||||
if let tunnelsManager = tunnelsManager {
|
||||
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
|
||||
cell.tunnel = tunnel
|
||||
cell.onSwitchToggled = { [weak self] isOn in
|
||||
guard let s = self, let tunnelsManager = s.tunnelsManager else { return }
|
||||
if (isOn) {
|
||||
tunnelsManager.startActivation(of: tunnel) { [weak s] error in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: s, onPresented: {
|
||||
DispatchQueue.main.async {
|
||||
cell.statusSwitch.isOn = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tunnelsManager.startDeactivation(of: tunnel) { [weak s] error in
|
||||
s?.showErrorAlert(title: "Deactivation error", message: "Error while bringing down tunnel: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDelegate
|
||||
|
||||
extension TunnelsListTableViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
|
||||
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
|
||||
tunnel: tunnel)
|
||||
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
|
||||
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView,
|
||||
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let deleteAction = UIContextualAction(style: .destructive, title: "Delete", handler: { [weak self] (_, _, completionHandler) in
|
||||
guard let tunnelsManager = self?.tunnelsManager else { return }
|
||||
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
|
||||
tunnelsManager.remove(tunnel: tunnel, completionHandler: { (error) in
|
||||
if (error != nil) {
|
||||
ErrorPresenter.showErrorAlert(error: error!, from: self)
|
||||
completionHandler(false)
|
||||
} else {
|
||||
completionHandler(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
return UISwipeActionsConfiguration(actions: [deleteAction])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: TunnelsManagerDelegate
|
||||
|
||||
extension TunnelsListTableViewController: TunnelsManagerDelegate {
|
||||
func tunnelAdded(at index: Int) {
|
||||
tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
|
||||
}
|
||||
|
||||
func tunnelModified(at index: Int) {
|
||||
tableView?.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
}
|
||||
|
||||
func tunnelMoved(at oldIndex: Int, to newIndex: Int) {
|
||||
tableView?.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
|
||||
}
|
||||
|
||||
func tunnelRemoved(at index: Int) {
|
||||
tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelsListTableViewCell: UITableViewCell {
|
||||
static let id: String = "TunnelsListTableViewCell"
|
||||
var tunnel: TunnelContainer? {
|
||||
didSet(value) {
|
||||
// Bind to the tunnel's name
|
||||
nameLabel.text = tunnel?.name ?? ""
|
||||
nameObservervationToken = tunnel?.observe(\.name) { [weak self] (tunnel, _) in
|
||||
self?.nameLabel.text = tunnel.name
|
||||
}
|
||||
// Bind to the tunnel's status
|
||||
update(from: tunnel?.status)
|
||||
statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in
|
||||
self?.update(from: tunnel.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
var onSwitchToggled: ((Bool) -> Void)?
|
||||
|
||||
let nameLabel: UILabel
|
||||
let busyIndicator: UIActivityIndicatorView
|
||||
let statusSwitch: UISwitch
|
||||
|
||||
private var statusObservervationToken: AnyObject?
|
||||
private var nameObservervationToken: AnyObject?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
nameLabel = UILabel()
|
||||
busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||
busyIndicator.hidesWhenStopped = true
|
||||
statusSwitch = UISwitch()
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
contentView.addSubview(statusSwitch)
|
||||
statusSwitch.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
statusSwitch.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -8)
|
||||
])
|
||||
contentView.addSubview(busyIndicator)
|
||||
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
busyIndicator.rightAnchor.constraint(equalTo: statusSwitch.leftAnchor, constant: -8)
|
||||
])
|
||||
contentView.addSubview(nameLabel)
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
nameLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16),
|
||||
nameLabel.rightAnchor.constraint(equalTo: busyIndicator.leftAnchor)
|
||||
])
|
||||
self.accessoryType = .disclosureIndicator
|
||||
|
||||
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
|
||||
}
|
||||
|
||||
@objc func switchToggled() {
|
||||
onSwitchToggled?(statusSwitch.isOn)
|
||||
}
|
||||
|
||||
private func update(from status: TunnelStatus?) {
|
||||
guard let status = status else {
|
||||
reset()
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async { [weak statusSwitch, weak busyIndicator] in
|
||||
guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
|
||||
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
|
||||
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
|
||||
if (status == .inactive || status == .active) {
|
||||
busyIndicator.stopAnimating()
|
||||
} else {
|
||||
busyIndicator.startAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
statusSwitch.isOn = false
|
||||
statusSwitch.isUserInteractionEnabled = false
|
||||
busyIndicator.stopAnimating()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
class BorderedTextButton: UIView {
|
||||
let button: UIButton
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
let buttonSize = button.intrinsicContentSize
|
||||
return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
|
||||
}
|
||||
|
||||
var title: String {
|
||||
get { return button.title(for: .normal) ?? "" }
|
||||
set(value) { button.setTitle(value, for: .normal) }
|
||||
}
|
||||
|
||||
var onTapped: (() -> Void)?
|
||||
|
||||
init() {
|
||||
button = UIButton(type: .system)
|
||||
super.init(frame: CGRect.zero)
|
||||
addSubview(button)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
button.centerXAnchor.constraint(equalTo: self.centerXAnchor),
|
||||
button.centerYAnchor.constraint(equalTo: self.centerYAnchor)
|
||||
])
|
||||
layer.borderWidth = 1
|
||||
layer.cornerRadius = 5
|
||||
layer.borderColor = button.tintColor.cgColor
|
||||
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func buttonTapped() {
|
||||
onTapped?()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UITableViewCell {
|
||||
static var reuseIdentifier: String {
|
||||
return NSStringFromClass(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension UITableView {
|
||||
func register<T: UITableViewCell>(_: T.Type) {
|
||||
register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
|
||||
}
|
||||
|
||||
func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
|
||||
//swiftlint:disable:next force_cast
|
||||
return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class BorderedTextButton: UIView {
|
||||
let button: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
return button
|
||||
}()
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
let buttonSize = button.intrinsicContentSize
|
||||
return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
|
||||
}
|
||||
|
||||
var title: String {
|
||||
get { return button.title(for: .normal) ?? "" }
|
||||
set(value) { button.setTitle(value, for: .normal) }
|
||||
}
|
||||
|
||||
var onTapped: (() -> Void)?
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
layer.borderWidth = 1
|
||||
layer.cornerRadius = 5
|
||||
layer.borderColor = button.tintColor.cgColor
|
||||
|
||||
addSubview(button)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
button.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
button.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
])
|
||||
|
||||
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc func buttonTapped() {
|
||||
onTapped?()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ButtonCell: UITableViewCell {
|
||||
var buttonText: String {
|
||||
get { return button.title(for: .normal) ?? "" }
|
||||
set(value) { button.setTitle(value, for: .normal) }
|
||||
}
|
||||
var hasDestructiveAction: Bool {
|
||||
get { return button.tintColor == .red }
|
||||
set(value) { button.tintColor = value ? .red : buttonStandardTintColor }
|
||||
}
|
||||
var onTapped: (() -> Void)?
|
||||
|
||||
let button: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
return button
|
||||
}()
|
||||
|
||||
var buttonStandardTintColor: UIColor
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
buttonStandardTintColor = button.tintColor
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
contentView.addSubview(button)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
|
||||
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
|
||||
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
|
||||
])
|
||||
|
||||
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func buttonTapped() {
|
||||
onTapped?()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
buttonText = ""
|
||||
onTapped = nil
|
||||
hasDestructiveAction = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class CheckmarkCell: UITableViewCell {
|
||||
var message: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel!.text = value }
|
||||
}
|
||||
var isChecked: Bool {
|
||||
didSet {
|
||||
accessoryType = isChecked ? .checkmark : .none
|
||||
}
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
isChecked = false
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
isChecked = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class KeyValueCell: UITableViewCell {
|
||||
|
||||
let keyLabel: UILabel = {
|
||||
let keyLabel = UILabel()
|
||||
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
keyLabel.adjustsFontForContentSizeCategory = true
|
||||
keyLabel.textColor = .black
|
||||
keyLabel.textAlignment = .left
|
||||
return keyLabel
|
||||
}()
|
||||
|
||||
let valueLabelScrollView: UIScrollView = {
|
||||
let scrollView = UIScrollView(frame: .zero)
|
||||
scrollView.isDirectionalLockEnabled = true
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
return scrollView
|
||||
}()
|
||||
|
||||
let valueTextField: UITextField = {
|
||||
let valueTextField = UITextField()
|
||||
valueTextField.textAlignment = .right
|
||||
valueTextField.isEnabled = false
|
||||
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
valueTextField.adjustsFontForContentSizeCategory = true
|
||||
valueTextField.autocapitalizationType = .none
|
||||
valueTextField.autocorrectionType = .no
|
||||
valueTextField.spellCheckingType = .no
|
||||
valueTextField.textColor = .gray
|
||||
return valueTextField
|
||||
}()
|
||||
|
||||
var copyableGesture = true
|
||||
|
||||
var key: String {
|
||||
get { return keyLabel.text ?? "" }
|
||||
set(value) { keyLabel.text = value }
|
||||
}
|
||||
var value: String {
|
||||
get { return valueTextField.text ?? "" }
|
||||
set(value) { valueTextField.text = value }
|
||||
}
|
||||
var placeholderText: String {
|
||||
get { return valueTextField.placeholder ?? "" }
|
||||
set(value) { valueTextField.placeholder = value }
|
||||
}
|
||||
var keyboardType: UIKeyboardType {
|
||||
get { return valueTextField.keyboardType }
|
||||
set(value) { valueTextField.keyboardType = value }
|
||||
}
|
||||
|
||||
var isValueValid = true {
|
||||
didSet {
|
||||
if isValueValid {
|
||||
keyLabel.textColor = .black
|
||||
} else {
|
||||
keyLabel.textColor = .red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isStackedHorizontally = false
|
||||
var isStackedVertically = false
|
||||
var contentSizeBasedConstraints = [NSLayoutConstraint]()
|
||||
|
||||
var onValueChanged: ((String) -> Void)?
|
||||
var onValueBeingEdited: ((String) -> Void)?
|
||||
|
||||
private var textFieldValueOnBeginEditing: String = ""
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
contentView.addSubview(keyLabel)
|
||||
keyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
keyLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
|
||||
])
|
||||
|
||||
valueTextField.delegate = self
|
||||
valueLabelScrollView.addSubview(valueTextField)
|
||||
valueTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
valueTextField.leadingAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.leadingAnchor),
|
||||
valueTextField.topAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.topAnchor),
|
||||
valueTextField.bottomAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.bottomAnchor),
|
||||
valueTextField.trailingAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.trailingAnchor),
|
||||
valueTextField.heightAnchor.constraint(equalTo: valueLabelScrollView.heightAnchor)
|
||||
])
|
||||
let expandToFitValueLabelConstraint = NSLayoutConstraint(item: valueTextField, attribute: .width, relatedBy: .equal, toItem: valueLabelScrollView, attribute: .width, multiplier: 1, constant: 0)
|
||||
expandToFitValueLabelConstraint.priority = .defaultLow + 1
|
||||
expandToFitValueLabelConstraint.isActive = true
|
||||
|
||||
contentView.addSubview(valueLabelScrollView)
|
||||
|
||||
contentView.addSubview(valueLabelScrollView)
|
||||
valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
valueLabelScrollView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabelScrollView.bottomAnchor, multiplier: 0.5)
|
||||
])
|
||||
|
||||
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
|
||||
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
valueLabelScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
|
||||
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
|
||||
addGestureRecognizer(gestureRecognizer)
|
||||
isUserInteractionEnabled = true
|
||||
|
||||
configureForContentSize()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configureForContentSize() {
|
||||
var constraints = [NSLayoutConstraint]()
|
||||
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
|
||||
// Stack vertically
|
||||
if !isStackedVertically {
|
||||
constraints = [
|
||||
valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
|
||||
valueLabelScrollView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
keyLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor)
|
||||
]
|
||||
isStackedVertically = true
|
||||
isStackedHorizontally = false
|
||||
}
|
||||
} else {
|
||||
// Stack horizontally
|
||||
if !isStackedHorizontally {
|
||||
constraints = [
|
||||
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
|
||||
valueLabelScrollView.leadingAnchor.constraint(equalToSystemSpacingAfter: keyLabel.trailingAnchor, multiplier: 1),
|
||||
valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
|
||||
]
|
||||
isStackedHorizontally = true
|
||||
isStackedVertically = false
|
||||
}
|
||||
}
|
||||
if !constraints.isEmpty {
|
||||
NSLayoutConstraint.deactivate(contentSizeBasedConstraints)
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
contentSizeBasedConstraints = constraints
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
|
||||
if !copyableGesture {
|
||||
return
|
||||
}
|
||||
guard recognizer.state == .recognized else { return }
|
||||
|
||||
if let recognizerView = recognizer.view,
|
||||
let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
|
||||
let menuController = UIMenuController.shared
|
||||
menuController.setTargetRect(detailTextLabel?.frame ?? recognizerView.frame, in: detailTextLabel?.superview ?? recognizerSuperView)
|
||||
menuController.setMenuVisible(true, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
return (action == #selector(UIResponderStandardEditActions.copy(_:)))
|
||||
}
|
||||
|
||||
override func copy(_ sender: Any?) {
|
||||
UIPasteboard.general.string = valueTextField.text
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
copyableGesture = true
|
||||
placeholderText = ""
|
||||
isValueValid = true
|
||||
keyboardType = .default
|
||||
onValueChanged = nil
|
||||
onValueBeingEdited = nil
|
||||
key = ""
|
||||
value = ""
|
||||
configureForContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyValueCell: UITextFieldDelegate {
|
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
textFieldValueOnBeginEditing = textField.text ?? ""
|
||||
isValueValid = true
|
||||
}
|
||||
|
||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
let isModified = textField.text ?? "" != textFieldValueOnBeginEditing
|
||||
guard isModified else { return }
|
||||
onValueChanged?(textField.text ?? "")
|
||||
}
|
||||
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
if let onValueBeingEdited = onValueBeingEdited {
|
||||
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
|
||||
onValueBeingEdited(modifiedText)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class SwitchCell: UITableViewCell {
|
||||
var message: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel?.text = value }
|
||||
}
|
||||
var isOn: Bool {
|
||||
get { return switchView.isOn }
|
||||
set(value) { switchView.isOn = value }
|
||||
}
|
||||
var isEnabled: Bool {
|
||||
get { return switchView.isEnabled }
|
||||
set(value) {
|
||||
switchView.isEnabled = value
|
||||
textLabel?.textColor = value ? .black : .gray
|
||||
}
|
||||
}
|
||||
|
||||
var onSwitchToggled: ((Bool) -> Void)?
|
||||
|
||||
let switchView = UISwitch()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
accessoryView = switchView
|
||||
switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc func switchToggled() {
|
||||
onSwitchToggled?(switchView.isOn)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
onSwitchToggled = nil
|
||||
isEnabled = true
|
||||
message = ""
|
||||
isOn = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class TunnelEditKeyValueCell: KeyValueCell {
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
keyLabel.textAlignment = .right
|
||||
valueTextField.textAlignment = .left
|
||||
|
||||
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 0.4, constant: 0)
|
||||
// In case the key doesn't fit into 0.4 * width,
|
||||
// set a CR priority > the 0.4-constraint's priority.
|
||||
widthRatioConstraint.priority = .defaultHigh + 1
|
||||
widthRatioConstraint.isActive = true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell {
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
copyableGesture = false
|
||||
valueTextField.textColor = .black
|
||||
valueTextField.isEnabled = true
|
||||
valueLabelScrollView.isScrollEnabled = false
|
||||
valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
copyableGesture = false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class TunnelListCell: UITableViewCell {
|
||||
var tunnel: TunnelContainer? {
|
||||
didSet(value) {
|
||||
// Bind to the tunnel's name
|
||||
nameLabel.text = tunnel?.name ?? ""
|
||||
nameObservationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
|
||||
self?.nameLabel.text = tunnel.name
|
||||
}
|
||||
// Bind to the tunnel's status
|
||||
update(from: tunnel?.status)
|
||||
statusObservationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
|
||||
self?.update(from: tunnel.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
var onSwitchToggled: ((Bool) -> Void)?
|
||||
|
||||
let nameLabel: UILabel = {
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
nameLabel.adjustsFontForContentSizeCategory = true
|
||||
nameLabel.numberOfLines = 0
|
||||
return nameLabel
|
||||
}()
|
||||
|
||||
let busyIndicator: UIActivityIndicatorView = {
|
||||
let busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||
busyIndicator.hidesWhenStopped = true
|
||||
return busyIndicator
|
||||
}()
|
||||
|
||||
let statusSwitch = UISwitch()
|
||||
|
||||
private var statusObservationToken: AnyObject?
|
||||
private var nameObservationToken: AnyObject?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
contentView.addSubview(statusSwitch)
|
||||
statusSwitch.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: statusSwitch.trailingAnchor)
|
||||
])
|
||||
|
||||
contentView.addSubview(busyIndicator)
|
||||
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
statusSwitch.leadingAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.trailingAnchor, multiplier: 1)
|
||||
])
|
||||
|
||||
contentView.addSubview(nameLabel)
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
|
||||
bottomAnchorConstraint.priority = .defaultLow
|
||||
NSLayoutConstraint.activate([
|
||||
nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
|
||||
nameLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
|
||||
busyIndicator.leadingAnchor.constraint(equalToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1),
|
||||
bottomAnchorConstraint
|
||||
])
|
||||
|
||||
accessoryType = .disclosureIndicator
|
||||
|
||||
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
|
||||
}
|
||||
|
||||
@objc func switchToggled() {
|
||||
onSwitchToggled?(statusSwitch.isOn)
|
||||
}
|
||||
|
||||
private func update(from status: TunnelStatus?) {
|
||||
guard let status = status else {
|
||||
reset()
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak statusSwitch, weak busyIndicator] in
|
||||
guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
|
||||
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
|
||||
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
|
||||
if status == .inactive || status == .active {
|
||||
busyIndicator.stopAnimating()
|
||||
} else {
|
||||
busyIndicator.startAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
statusSwitch.isOn = false
|
||||
statusSwitch.isUserInteractionEnabled = false
|
||||
busyIndicator.stopAnimating()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
reset()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class MainViewController: UISplitViewController {
|
||||
|
||||
var tunnelsManager: TunnelsManager?
|
||||
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
|
||||
var tunnelsListVC: TunnelsListTableViewController?
|
||||
private var foregroundObservationToken: AnyObject?
|
||||
|
||||
init() {
|
||||
let detailVC = UIViewController()
|
||||
detailVC.view.backgroundColor = .white
|
||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||
|
||||
let masterVC = TunnelsListTableViewController()
|
||||
let masterNC = UINavigationController(rootViewController: masterVC)
|
||||
|
||||
tunnelsListVC = masterVC
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
viewControllers = [ masterNC, detailNC ]
|
||||
|
||||
restorationIdentifier = "MainVC"
|
||||
masterNC.restorationIdentifier = "MasterNC"
|
||||
detailNC.restorationIdentifier = "DetailNC"
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
delegate = self
|
||||
|
||||
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
|
||||
preferredDisplayMode = .allVisible
|
||||
|
||||
// Create the tunnels manager, and when it's ready, inform tunnelsListVC
|
||||
TunnelsManager.create { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
let tunnelsManager: TunnelsManager = result.value!
|
||||
|
||||
self.tunnelsManager = tunnelsManager
|
||||
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
|
||||
|
||||
tunnelsManager.activationDelegate = self
|
||||
|
||||
self.onTunnelsManagerReady?(tunnelsManager)
|
||||
self.onTunnelsManagerReady = nil
|
||||
}
|
||||
|
||||
foregroundObservationToken = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.tunnelsManager?.reload { [weak self] hasChanges in
|
||||
guard let self = self, let tunnelsManager = self.tunnelsManager, hasChanges else { return }
|
||||
|
||||
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
|
||||
|
||||
if self.isCollapsed {
|
||||
(self.viewControllers[0] as? UINavigationController)?.popViewController(animated: false)
|
||||
} else {
|
||||
let detailVC = UIViewController()
|
||||
detailVC.view.backgroundColor = .white
|
||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||
self.showDetailViewController(detailNC, sender: self)
|
||||
}
|
||||
|
||||
if let presentedNavController = self.presentedViewController as? UINavigationController, presentedNavController.viewControllers.first is TunnelEditTableViewController {
|
||||
self.presentedViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MainViewController: TunnelsManagerActivationDelegate {
|
||||
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
}
|
||||
|
||||
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
}
|
||||
|
||||
func tunnelActivationSucceeded(tunnel: TunnelContainer) {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController {
|
||||
func refreshTunnelConnectionStatuses() {
|
||||
if let tunnelsManager = tunnelsManager {
|
||||
tunnelsManager.refreshStatuses()
|
||||
}
|
||||
}
|
||||
|
||||
func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) {
|
||||
let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
|
||||
if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
|
||||
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
|
||||
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
|
||||
tunnelDetailNC.restorationIdentifier = "DetailNC"
|
||||
if let self = self {
|
||||
if animated {
|
||||
self.showDetailViewController(tunnelDetailNC, sender: self)
|
||||
} else {
|
||||
UIView.performWithoutAnimation {
|
||||
self.showDetailViewController(tunnelDetailNC, sender: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let tunnelsManager = tunnelsManager {
|
||||
showTunnelDetailBlock(tunnelsManager)
|
||||
} else {
|
||||
onTunnelsManagerReady = showTunnelDetailBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController: UISplitViewControllerDelegate {
|
||||
func splitViewController(_ splitViewController: UISplitViewController,
|
||||
collapseSecondary secondaryViewController: UIViewController,
|
||||
onto primaryViewController: UIViewController) -> Bool {
|
||||
// On iPhone, if the secondaryVC (detailVC) is just a UIViewController, it indicates that it's empty,
|
||||
// so just show the primaryVC (masterVC).
|
||||
let detailVC = (secondaryViewController as? UINavigationController)?.viewControllers.first
|
||||
let isDetailVCEmpty: Bool
|
||||
if let detailVC = detailVC {
|
||||
isDetailVCEmpty = (type(of: detailVC) == UIViewController.self)
|
||||
} else {
|
||||
isDetailVCEmpty = true
|
||||
}
|
||||
return isDetailVCEmpty
|
||||
}
|
||||
}
|
||||
+29
-35
@@ -1,8 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import AVFoundation
|
||||
import CoreData
|
||||
import UIKit
|
||||
|
||||
protocol QRScanViewControllerDelegate: class {
|
||||
@@ -18,29 +17,29 @@ class QRScanViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.title = "Scan QR code"
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
|
||||
title = tr("scanQRCodeViewTitle")
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
|
||||
|
||||
let tipLabel = UILabel()
|
||||
tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`"
|
||||
tipLabel.text = tr("scanQRCodeTipText")
|
||||
tipLabel.adjustsFontSizeToFitWidth = true
|
||||
tipLabel.textColor = UIColor.lightGray
|
||||
tipLabel.textColor = .lightGray
|
||||
tipLabel.textAlignment = .center
|
||||
|
||||
view.addSubview(tipLabel)
|
||||
tipLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tipLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
|
||||
tipLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
|
||||
tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32),
|
||||
])
|
||||
tipLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
tipLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32)
|
||||
])
|
||||
|
||||
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
|
||||
let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
|
||||
let captureSession = captureSession,
|
||||
captureSession.canAddInput(videoInput),
|
||||
captureSession.canAddOutput(metadataOutput) else {
|
||||
scanDidEncounterError(title: "Camera Unsupported", message: "This device is not able to scan QR codes")
|
||||
scanDidEncounterError(title: tr("alertScanQRCodeCameraUnsupportedTitle"), message: tr("alertScanQRCodeCameraUnsupportedMessage"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,15 +76,11 @@ class QRScanViewController: UIViewController {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
if let connection = previewLayer?.connection {
|
||||
|
||||
let currentDevice: UIDevice = UIDevice.current
|
||||
|
||||
let orientation: UIDeviceOrientation = currentDevice.orientation
|
||||
|
||||
let previewLayerConnection: AVCaptureConnection = connection
|
||||
let currentDevice = UIDevice.current
|
||||
let orientation = currentDevice.orientation
|
||||
let previewLayerConnection = connection
|
||||
|
||||
if previewLayerConnection.isVideoOrientationSupported {
|
||||
|
||||
switch orientation {
|
||||
case .portrait:
|
||||
previewLayerConnection.videoOrientation = .portrait
|
||||
@@ -102,39 +97,38 @@ class QRScanViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
previewLayer?.frame = self.view.bounds
|
||||
previewLayer?.frame = view.bounds
|
||||
}
|
||||
|
||||
func scanDidComplete(withCode code: String) {
|
||||
let scannedTunnelConfiguration = try? WgQuickConfigFileParser.parse(code, name: "Scanned")
|
||||
let scannedTunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: code, called: "Scanned")
|
||||
guard let tunnelConfiguration = scannedTunnelConfiguration else {
|
||||
scanDidEncounterError(title: "Invalid QR Code", message: "The scanned QR code is not a valid WireGuard configuration")
|
||||
scanDidEncounterError(title: tr("alertScanQRCodeInvalidQRCodeTitle"), message: tr("alertScanQRCodeInvalidQRCodeMessage"))
|
||||
return
|
||||
}
|
||||
|
||||
let alert = UIAlertController(title: NSLocalizedString("Please name the scanned tunnel", comment: ""), message: nil, preferredStyle: .alert)
|
||||
let alert = UIAlertController(title: tr("alertScanQRCodeNamePromptTitle"), message: nil, preferredStyle: .alert)
|
||||
alert.addTextField(configurationHandler: nil)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { [weak self] _ in
|
||||
alert.addAction(UIAlertAction(title: tr("actionCancel"), style: .cancel) { [weak self] _ in
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default, handler: { [weak self] _ in
|
||||
let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if (title.isEmpty) { return }
|
||||
tunnelConfiguration.interface.name = title
|
||||
if let s = self {
|
||||
s.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: s) {
|
||||
s.dismiss(animated: true, completion: nil)
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: tr("actionSave"), style: .default) { [weak self] _ in
|
||||
guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return }
|
||||
tunnelConfiguration.name = title
|
||||
if let self = self {
|
||||
self.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: self) {
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
func scanDidEncounterError(title: String, message: String) {
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] _ in
|
||||
alertController.addAction(UIAlertAction(title: tr("actionOK"), style: .default) { [weak self] _ in
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
}))
|
||||
})
|
||||
present(alertController, animated: true)
|
||||
captureSession = nil
|
||||
}
|
||||
@@ -151,7 +145,7 @@ extension QRScanViewController: AVCaptureMetadataOutputObjectsDelegate {
|
||||
guard let metadataObject = metadataObjects.first,
|
||||
let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
|
||||
let stringValue = readableObject.stringValue else {
|
||||
scanDidEncounterError(title: "Invalid Code", message: "The scanned code could not be read")
|
||||
scanDidEncounterError(title: tr("alertScanQRCodeUnreadableQRCodeTitle"), message: tr("alertScanQRCodeUnreadableQRCodeMessage"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import os.log
|
||||
|
||||
class SettingsTableViewController: UITableViewController {
|
||||
|
||||
enum SettingsFields {
|
||||
case iosAppVersion
|
||||
case goBackendVersion
|
||||
case exportZipArchive
|
||||
case exportLogFile
|
||||
|
||||
var localizedUIString: String {
|
||||
switch self {
|
||||
case .iosAppVersion: return tr("settingsVersionKeyWireGuardForIOS")
|
||||
case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend")
|
||||
case .exportZipArchive: return tr("settingsExportZipButtonTitle")
|
||||
case .exportLogFile: return tr("settingsExportLogFileButtonTitle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let settingsFieldsBySection: [[SettingsFields]] = [
|
||||
[.iosAppVersion, .goBackendVersion],
|
||||
[.exportZipArchive],
|
||||
[.exportLogFile]
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager?
|
||||
var wireguardCaptionedImage: (view: UIView, size: CGSize)?
|
||||
|
||||
init(tunnelsManager: TunnelsManager?) {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = tr("settingsViewTitle")
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
|
||||
|
||||
tableView.estimatedRowHeight = 44
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.allowsSelection = false
|
||||
|
||||
tableView.register(KeyValueCell.self)
|
||||
tableView.register(ButtonCell.self)
|
||||
|
||||
tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf"))
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
guard let logo = tableView.tableFooterView else { return }
|
||||
|
||||
let bottomPadding = max(tableView.layoutMargins.bottom, 10)
|
||||
let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding)
|
||||
|
||||
let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height
|
||||
|
||||
var height = tableView.estimatedRowHeight * 1.5
|
||||
var width = height * imageAspectRatio
|
||||
let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20)
|
||||
if width > maxWidth {
|
||||
width = maxWidth
|
||||
height = width / imageAspectRatio
|
||||
}
|
||||
|
||||
let needsReload = height != logo.frame.height
|
||||
|
||||
logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height)
|
||||
|
||||
if needsReload {
|
||||
tableView.tableFooterView = logo
|
||||
}
|
||||
}
|
||||
|
||||
@objc func doneTapped() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func exportConfigurationsAsZipFile(sourceView: UIView) {
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||
|
||||
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
|
||||
_ = FileManager.deleteFile(at: destinationURL)
|
||||
|
||||
let count = tunnelsManager.numberOfTunnels()
|
||||
let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration }
|
||||
ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
|
||||
let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
|
||||
self?.present(fileExportVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func exportLogForLastActivatedTunnel(sourceView: UIView) {
|
||||
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||
|
||||
let dateFormatter = ISO8601DateFormatter()
|
||||
dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
|
||||
let timeStampString = dateFormatter.string(from: Date())
|
||||
let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt")
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
let isDeleted = FileManager.deleteFile(at: destinationURL)
|
||||
if !isDeleted {
|
||||
ErrorPresenter.showErrorAlert(title: tr("alertUnableToRemovePreviousLogTitle"), message: tr("alertUnableToRemovePreviousLogMessage"), from: self)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else {
|
||||
ErrorPresenter.showErrorAlert(title: tr("alertUnableToFindExtensionLogPathTitle"), message: tr("alertUnableToFindExtensionLogPathMessage"), from: self)
|
||||
return
|
||||
}
|
||||
|
||||
let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false
|
||||
guard isWritten else {
|
||||
ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
|
||||
activityVC.popoverPresentationController?.sourceView = sourceView
|
||||
activityVC.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
activityVC.completionWithItemsHandler = { _, _, _, _ in
|
||||
// Remove the exported log file after the activity has completed
|
||||
_ = FileManager.deleteFile(at: destinationURL)
|
||||
}
|
||||
self.present(activityVC, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return settingsFieldsBySection.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return settingsFieldsBySection[section].count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case 0:
|
||||
return tr("settingsSectionTitleAbout")
|
||||
case 1:
|
||||
return tr("settingsSectionTitleExportConfigurations")
|
||||
case 2:
|
||||
return tr("settingsSectionTitleTunnelLog")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let field = settingsFieldsBySection[indexPath.section][indexPath.row]
|
||||
if field == .iosAppVersion || field == .goBackendVersion {
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.copyableGesture = false
|
||||
cell.key = field.localizedUIString
|
||||
if field == .iosAppVersion {
|
||||
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
|
||||
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
appVersion += " (\(appBuild))"
|
||||
}
|
||||
cell.value = appVersion
|
||||
} else if field == .goBackendVersion {
|
||||
cell.value = WIREGUARD_GO_VERSION
|
||||
}
|
||||
return cell
|
||||
} else if field == .exportZipArchive {
|
||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.buttonText = field.localizedUIString
|
||||
cell.onTapped = { [weak self] in
|
||||
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
assert(field == .exportLogFile)
|
||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.buttonText = field.localizedUIString
|
||||
cell.onTapped = { [weak self] in
|
||||
self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class TunnelDetailTableViewController: UITableViewController {
|
||||
|
||||
private enum Section {
|
||||
case status
|
||||
case interface
|
||||
case peer(_ peer: TunnelViewModel.PeerData)
|
||||
case onDemand
|
||||
case delete
|
||||
}
|
||||
|
||||
let interfaceFields: [TunnelViewModel.InterfaceField] = [
|
||||
.name, .publicKey, .addresses,
|
||||
.listenPort, .mtu, .dns
|
||||
]
|
||||
|
||||
let peerFields: [TunnelViewModel.PeerField] = [
|
||||
.publicKey, .preSharedKey, .endpoint,
|
||||
.allowedIPs, .persistentKeepAlive
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager
|
||||
let tunnel: TunnelContainer
|
||||
var tunnelViewModel: TunnelViewModel
|
||||
private var sections = [Section]()
|
||||
private var onDemandStatusObservationToken: AnyObject?
|
||||
private var statusObservationToken: AnyObject?
|
||||
|
||||
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
self.tunnel = tunnel
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
|
||||
super.init(style: .grouped)
|
||||
loadSections()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = tunnelViewModel.interfaceData[.name]
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
|
||||
|
||||
tableView.estimatedRowHeight = 44
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.allowsSelection = false
|
||||
tableView.register(SwitchCell.self)
|
||||
tableView.register(KeyValueCell.self)
|
||||
tableView.register(ButtonCell.self)
|
||||
|
||||
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
|
||||
}
|
||||
|
||||
private func loadSections() {
|
||||
sections.removeAll()
|
||||
sections.append(.status)
|
||||
sections.append(.interface)
|
||||
tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
|
||||
sections.append(.onDemand)
|
||||
sections.append(.delete)
|
||||
}
|
||||
|
||||
@objc func editTapped() {
|
||||
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
|
||||
editVC.delegate = self
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
editNC.modalPresentationStyle = .formSheet
|
||||
present(editNC, animated: true)
|
||||
}
|
||||
|
||||
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
|
||||
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
|
||||
onConfirmed()
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
|
||||
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
|
||||
alert.addAction(destroyAction)
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
alert.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
|
||||
present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
|
||||
func tunnelSaved(tunnel: TunnelContainer) {
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
|
||||
loadSections()
|
||||
title = tunnel.name
|
||||
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
|
||||
tableView.reloadData()
|
||||
}
|
||||
func tunnelEditingCancelled() {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelDetailTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return sections.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch sections[section] {
|
||||
case .status:
|
||||
return 1
|
||||
case .interface:
|
||||
return tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
|
||||
case .peer(let peerData):
|
||||
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
|
||||
case .onDemand:
|
||||
return 1
|
||||
case .delete:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch sections[section] {
|
||||
case .status:
|
||||
return tr("tunnelSectionTitleStatus")
|
||||
case .interface:
|
||||
return tr("tunnelSectionTitleInterface")
|
||||
case .peer:
|
||||
return tr("tunnelSectionTitlePeer")
|
||||
case .onDemand:
|
||||
return tr("tunnelSectionTitleOnDemand")
|
||||
case .delete:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
switch sections[indexPath.section] {
|
||||
case .status:
|
||||
return statusCell(for: tableView, at: indexPath)
|
||||
case .interface:
|
||||
return interfaceCell(for: tableView, at: indexPath)
|
||||
case .peer(let peer):
|
||||
return peerCell(for: tableView, at: indexPath, with: peer)
|
||||
case .onDemand:
|
||||
return onDemandCell(for: tableView, at: indexPath)
|
||||
case .delete:
|
||||
return deleteConfigurationCell(for: tableView, at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
|
||||
let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in
|
||||
let text: String
|
||||
switch status {
|
||||
case .inactive:
|
||||
text = tr("tunnelStatusInactive")
|
||||
case .activating:
|
||||
text = tr("tunnelStatusActivating")
|
||||
case .active:
|
||||
text = tr("tunnelStatusActive")
|
||||
case .deactivating:
|
||||
text = tr("tunnelStatusDeactivating")
|
||||
case .reasserting:
|
||||
text = tr("tunnelStatusReasserting")
|
||||
case .restarting:
|
||||
text = tr("tunnelStatusRestarting")
|
||||
case .waiting:
|
||||
text = tr("tunnelStatusWaiting")
|
||||
}
|
||||
cell.textLabel?.text = text
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in
|
||||
cell?.switchView.isOn = !(status == .deactivating || status == .inactive)
|
||||
cell?.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
|
||||
}
|
||||
cell.isEnabled = status == .active || status == .inactive
|
||||
}
|
||||
|
||||
statusUpdate(cell, tunnel.status)
|
||||
statusObservationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
|
||||
guard let cell = cell else { return }
|
||||
statusUpdate(cell, tunnel.status)
|
||||
}
|
||||
|
||||
cell.onSwitchToggled = { [weak self] isOn in
|
||||
guard let self = self else { return }
|
||||
if isOn {
|
||||
self.tunnelsManager.startActivation(of: self.tunnel)
|
||||
} else {
|
||||
self.tunnelsManager.startDeactivation(of: self.tunnel)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row]
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
cell.value = tunnelViewModel.interfaceData[field]
|
||||
return cell
|
||||
}
|
||||
|
||||
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
|
||||
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row]
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
cell.value = peerData[field]
|
||||
return cell
|
||||
}
|
||||
|
||||
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = tr("tunnelOnDemandKey")
|
||||
cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting)
|
||||
onDemandStatusObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in
|
||||
cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.buttonText = tr("deleteTunnelButtonTitle")
|
||||
cell.hasDestructiveAction = true
|
||||
cell.onTapped = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"), buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"), from: cell) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.tunnelsManager.remove(tunnel: self.tunnel) { error in
|
||||
if error != nil {
|
||||
print("Error removing tunnel: \(String(describing: error))")
|
||||
return
|
||||
}
|
||||
}
|
||||
if self.splitViewController?.isCollapsed != false {
|
||||
self.navigationController?.navigationController?.popToRootViewController(animated: true)
|
||||
} else {
|
||||
let detailVC = UIViewController()
|
||||
detailVC.view.backgroundColor = .white
|
||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||
self.showDetailViewController(detailNC, sender: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol TunnelEditTableViewControllerDelegate: class {
|
||||
func tunnelSaved(tunnel: TunnelContainer)
|
||||
func tunnelEditingCancelled()
|
||||
}
|
||||
|
||||
class TunnelEditTableViewController: UITableViewController {
|
||||
private enum Section {
|
||||
case interface
|
||||
case peer(_ peer: TunnelViewModel.PeerData)
|
||||
case addPeer
|
||||
case onDemand
|
||||
|
||||
static func == (lhs: Section, rhs: Section) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.interface, .interface),
|
||||
(.addPeer, .addPeer),
|
||||
(.onDemand, .onDemand):
|
||||
return true
|
||||
case let (.peer(peerA), .peer(peerB)):
|
||||
return peerA.index == peerB.index
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: TunnelEditTableViewControllerDelegate?
|
||||
|
||||
let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [
|
||||
[.name],
|
||||
[.privateKey, .publicKey, .generateKeyPair],
|
||||
[.addresses, .listenPort, .mtu, .dns]
|
||||
]
|
||||
|
||||
let peerFields: [TunnelViewModel.PeerField] = [
|
||||
.publicKey, .preSharedKey, .endpoint,
|
||||
.allowedIPs, .excludePrivateIPs, .persistentKeepAlive,
|
||||
.deletePeer
|
||||
]
|
||||
|
||||
let activateOnDemandOptions: [ActivateOnDemandOption] = [
|
||||
.useOnDemandOverWiFiOrCellular,
|
||||
.useOnDemandOverWiFiOnly,
|
||||
.useOnDemandOverCellularOnly
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager
|
||||
let tunnel: TunnelContainer?
|
||||
let tunnelViewModel: TunnelViewModel
|
||||
var activateOnDemandSetting: ActivateOnDemandSetting
|
||||
private var sections = [Section]()
|
||||
|
||||
// Use this initializer to edit an existing tunnel.
|
||||
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
self.tunnel = tunnel
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
|
||||
activateOnDemandSetting = tunnel.activateOnDemandSetting
|
||||
super.init(style: .grouped)
|
||||
loadSections()
|
||||
}
|
||||
|
||||
// Use this initializer to create a new tunnel.
|
||||
init(tunnelsManager: TunnelsManager) {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
tunnel = nil
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil)
|
||||
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
|
||||
super.init(style: .grouped)
|
||||
loadSections()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = tunnel == nil ? tr("newTunnelViewTitle") : tr("editTunnelViewTitle")
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
|
||||
|
||||
tableView.estimatedRowHeight = 44
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
|
||||
tableView.register(TunnelEditKeyValueCell.self)
|
||||
tableView.register(TunnelEditEditableKeyValueCell.self)
|
||||
tableView.register(ButtonCell.self)
|
||||
tableView.register(SwitchCell.self)
|
||||
tableView.register(CheckmarkCell.self)
|
||||
}
|
||||
|
||||
private func loadSections() {
|
||||
sections.removeAll()
|
||||
interfaceFieldsBySection.forEach { _ in sections.append(.interface) }
|
||||
tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
|
||||
sections.append(.addPeer)
|
||||
sections.append(.onDemand)
|
||||
}
|
||||
|
||||
@objc func saveTapped() {
|
||||
tableView.endEditing(false)
|
||||
let tunnelSaveResult = tunnelViewModel.save()
|
||||
switch tunnelSaveResult {
|
||||
case .error(let errorMessage):
|
||||
let alertTitle = (tunnelViewModel.interfaceData.validatedConfiguration == nil || tunnelViewModel.interfaceData.validatedName == nil) ?
|
||||
tr("alertInvalidInterfaceTitle") : tr("alertInvalidPeerTitle")
|
||||
ErrorPresenter.showErrorAlert(title: alertTitle, message: errorMessage, from: self)
|
||||
tableView.reloadData() // Highlight erroring fields
|
||||
case .saved(let tunnelConfiguration):
|
||||
if let tunnel = tunnel {
|
||||
// We're modifying an existing tunnel
|
||||
tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: activateOnDemandSetting) { [weak self] error in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We're adding a new tunnel
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
let tunnel: TunnelContainer = result.value!
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func cancelTapped() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
delegate?.tunnelEditingCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
extension TunnelEditTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return sections.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch sections[section] {
|
||||
case .interface:
|
||||
return interfaceFieldsBySection[section].count
|
||||
case .peer(let peerData):
|
||||
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
|
||||
return peerFieldsToShow.count
|
||||
case .addPeer:
|
||||
return 1
|
||||
case .onDemand:
|
||||
if activateOnDemandSetting.isActivateOnDemandEnabled {
|
||||
return 4
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch sections[section] {
|
||||
case .interface:
|
||||
return section == 0 ? tr("tunnelSectionTitleInterface") : nil
|
||||
case .peer:
|
||||
return tr("tunnelSectionTitlePeer")
|
||||
case .addPeer:
|
||||
return nil
|
||||
case .onDemand:
|
||||
return tr("tunnelSectionTitleOnDemand")
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
switch sections[indexPath.section] {
|
||||
case .interface:
|
||||
return interfaceFieldCell(for: tableView, at: indexPath)
|
||||
case .peer(let peerData):
|
||||
return peerCell(for: tableView, at: indexPath, with: peerData)
|
||||
case .addPeer:
|
||||
return addPeerCell(for: tableView, at: indexPath)
|
||||
case .onDemand:
|
||||
return onDemandCell(for: tableView, at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
private func interfaceFieldCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let field = interfaceFieldsBySection[indexPath.section][indexPath.row]
|
||||
switch field {
|
||||
case .generateKeyPair:
|
||||
return generateKeyPairCell(for: tableView, at: indexPath, with: field)
|
||||
case .publicKey:
|
||||
return publicKeyCell(for: tableView, at: indexPath, with: field)
|
||||
default:
|
||||
return interfaceFieldKeyValueCell(for: tableView, at: indexPath, with: field)
|
||||
}
|
||||
}
|
||||
|
||||
private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
|
||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.buttonText = field.localizedUIString
|
||||
cell.onTapped = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.tunnelViewModel.interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64EncodedString()
|
||||
if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey),
|
||||
let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
|
||||
let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
|
||||
let publicKeyIndex = IndexPath(row: publicKeyRow, section: indexPath.section)
|
||||
self.tableView.reloadRows(at: [privateKeyIndex, publicKeyIndex], with: .fade)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
|
||||
let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
cell.value = tunnelViewModel.interfaceData[field]
|
||||
return cell
|
||||
}
|
||||
|
||||
private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
|
||||
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
|
||||
switch field {
|
||||
case .name, .privateKey:
|
||||
cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
|
||||
cell.keyboardType = .default
|
||||
case .addresses:
|
||||
cell.placeholderText = tr("tunnelEditPlaceholderTextStronglyRecommended")
|
||||
cell.keyboardType = .numbersAndPunctuation
|
||||
case .dns:
|
||||
cell.placeholderText = tunnelViewModel.peersData.contains(where: { return $0.shouldStronglyRecommendDNS }) ? tr("tunnelEditPlaceholderTextStronglyRecommended") : tr("tunnelEditPlaceholderTextOptional")
|
||||
cell.keyboardType = .numbersAndPunctuation
|
||||
case .listenPort, .mtu:
|
||||
cell.placeholderText = tr("tunnelEditPlaceholderTextAutomatic")
|
||||
cell.keyboardType = .numberPad
|
||||
case .publicKey, .generateKeyPair:
|
||||
cell.keyboardType = .default
|
||||
}
|
||||
|
||||
cell.isValueValid = (!tunnelViewModel.interfaceData.fieldsWithError.contains(field))
|
||||
// Bind values to view model
|
||||
cell.value = tunnelViewModel.interfaceData[field]
|
||||
if field == .dns { // While editing DNS, you might directly set exclude private IPs
|
||||
cell.onValueBeingEdited = { [weak self] value in
|
||||
self?.tunnelViewModel.interfaceData[field] = value
|
||||
}
|
||||
} else {
|
||||
cell.onValueChanged = { [weak self] value in
|
||||
self?.tunnelViewModel.interfaceData[field] = value
|
||||
}
|
||||
}
|
||||
// Compute public key live
|
||||
if field == .privateKey {
|
||||
cell.onValueBeingEdited = { [weak self] value in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.tunnelViewModel.interfaceData[.privateKey] = value
|
||||
if let row = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
|
||||
self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
|
||||
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
|
||||
let field = peerFieldsToShow[indexPath.row]
|
||||
|
||||
switch field {
|
||||
case .deletePeer:
|
||||
return deletePeerCell(for: tableView, at: indexPath, peerData: peerData, field: field)
|
||||
case .excludePrivateIPs:
|
||||
return excludePrivateIPsCell(for: tableView, at: indexPath, peerData: peerData, field: field)
|
||||
default:
|
||||
return peerFieldKeyValueCell(for: tableView, at: indexPath, peerData: peerData, field: field)
|
||||
}
|
||||
}
|
||||
|
||||
private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
|
||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.buttonText = field.localizedUIString
|
||||
cell.hasDestructiveAction = true
|
||||
cell.onTapped = { [weak self, weak peerData] in
|
||||
guard let self = self, let peerData = peerData else { return }
|
||||
self.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"), buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"), from: cell) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let removedSectionIndices = self.deletePeer(peer: peerData)
|
||||
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
|
||||
tableView.performBatchUpdates({
|
||||
self.tableView.deleteSections(removedSectionIndices, with: .fade)
|
||||
if shouldShowExcludePrivateIPs {
|
||||
if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
|
||||
let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */)
|
||||
self.tableView.insertRows(at: [rowIndexPath], with: .fade)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
|
||||
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = field.localizedUIString
|
||||
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
|
||||
cell.isOn = peerData.excludePrivateIPsValue
|
||||
cell.onSwitchToggled = { [weak self] isOn in
|
||||
guard let self = self else { return }
|
||||
peerData.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: self.tunnelViewModel.interfaceData[.dns])
|
||||
if let row = self.peerFields.firstIndex(of: .allowedIPs) {
|
||||
self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
|
||||
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
|
||||
switch field {
|
||||
case .publicKey:
|
||||
cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
|
||||
cell.keyboardType = .default
|
||||
case .preSharedKey, .endpoint:
|
||||
cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
|
||||
cell.keyboardType = .default
|
||||
case .allowedIPs:
|
||||
cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
|
||||
cell.keyboardType = .numbersAndPunctuation
|
||||
case .persistentKeepAlive:
|
||||
cell.placeholderText = tr("tunnelEditPlaceholderTextOff")
|
||||
cell.keyboardType = .numberPad
|
||||
case .excludePrivateIPs, .deletePeer:
|
||||
cell.keyboardType = .default
|
||||
}
|
||||
|
||||
cell.isValueValid = !peerData.fieldsWithError.contains(field)
|
||||
cell.value = peerData[field]
|
||||
|
||||
if field == .allowedIPs {
|
||||
let firstInterfaceSection = sections.firstIndex(where: { $0 == .interface })!
|
||||
let interfaceSubSection = interfaceFieldsBySection.firstIndex(where: { $0.contains(.dns) })!
|
||||
let dnsRow = interfaceFieldsBySection[interfaceSubSection].firstIndex(where: { $0 == .dns })!
|
||||
|
||||
cell.onValueBeingEdited = { [weak self, weak peerData] value in
|
||||
guard let self = self, let peerData = peerData else { return }
|
||||
|
||||
let oldValue = peerData.shouldAllowExcludePrivateIPsControl
|
||||
peerData[.allowedIPs] = value
|
||||
if oldValue != peerData.shouldAllowExcludePrivateIPsControl, let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
|
||||
if peerData.shouldAllowExcludePrivateIPsControl {
|
||||
self.tableView.insertRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
|
||||
} else {
|
||||
self.tableView.deleteRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
|
||||
}
|
||||
}
|
||||
|
||||
tableView.reloadRows(at: [IndexPath(row: dnsRow, section: firstInterfaceSection + interfaceSubSection)], with: .none)
|
||||
}
|
||||
} else {
|
||||
cell.onValueChanged = { [weak peerData] value in
|
||||
peerData?[field] = value
|
||||
}
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.buttonText = tr("addPeerButtonTitle")
|
||||
cell.onTapped = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let shouldHideExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
|
||||
let addedSectionIndices = self.appendEmptyPeer()
|
||||
tableView.performBatchUpdates({
|
||||
tableView.insertSections(addedSectionIndices, with: .fade)
|
||||
if shouldHideExcludePrivateIPs {
|
||||
if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
|
||||
let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */)
|
||||
self.tableView.deleteRows(at: [rowIndexPath], with: .fade)
|
||||
}
|
||||
}
|
||||
}, completion: nil)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
if indexPath.row == 0 {
|
||||
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = tr("tunnelOnDemandKey")
|
||||
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
|
||||
cell.onSwitchToggled = { [weak self] isOn in
|
||||
guard let self = self else { return }
|
||||
guard isOn != self.activateOnDemandSetting.isActivateOnDemandEnabled else { return }
|
||||
|
||||
self.activateOnDemandSetting.isActivateOnDemandEnabled = isOn
|
||||
self.loadSections()
|
||||
|
||||
let section = self.sections.firstIndex(where: { $0 == .onDemand })!
|
||||
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: section) }
|
||||
if isOn {
|
||||
if self.activateOnDemandSetting.activateOnDemandOption == .none {
|
||||
self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
|
||||
}
|
||||
self.tableView.insertRows(at: indexPaths, with: .fade)
|
||||
} else {
|
||||
self.tableView.deleteRows(at: indexPaths, with: .fade)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let rowOption = activateOnDemandOptions[indexPath.row - 1]
|
||||
let selectedOption = activateOnDemandSetting.activateOnDemandOption
|
||||
assert(selectedOption != .none)
|
||||
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
|
||||
cell.isChecked = selectedOption == rowOption
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
func appendEmptyPeer() -> IndexSet {
|
||||
tunnelViewModel.appendEmptyPeer()
|
||||
loadSections()
|
||||
let addedPeerIndex = tunnelViewModel.peersData.count - 1
|
||||
return IndexSet(integer: interfaceFieldsBySection.count + addedPeerIndex)
|
||||
}
|
||||
|
||||
func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet {
|
||||
tunnelViewModel.deletePeer(peer: peer)
|
||||
loadSections()
|
||||
return IndexSet(integer: interfaceFieldsBySection.count + peer.index)
|
||||
}
|
||||
|
||||
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
|
||||
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
|
||||
onConfirmed()
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
|
||||
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
|
||||
alert.addAction(destroyAction)
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
alert.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
|
||||
present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelEditTableViewController {
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
if case .onDemand = sections[indexPath.section], indexPath.row > 0 {
|
||||
return indexPath
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
switch sections[indexPath.section] {
|
||||
case .onDemand:
|
||||
let option = activateOnDemandOptions[indexPath.row - 1]
|
||||
assert(option != .none)
|
||||
activateOnDemandSetting.activateOnDemandOption = option
|
||||
|
||||
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) }
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.reloadRows(at: indexPaths, with: .none)
|
||||
}
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import UserNotifications
|
||||
|
||||
class TunnelsListTableViewController: UIViewController {
|
||||
|
||||
var tunnelsManager: TunnelsManager?
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = UITableView(frame: CGRect.zero, style: .plain)
|
||||
tableView.estimatedRowHeight = 60
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(TunnelListCell.self)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let centeredAddButton: BorderedTextButton = {
|
||||
let button = BorderedTextButton()
|
||||
button.title = tr("tunnelsListCenteredAddTunnelButtonTitle")
|
||||
button.isHidden = true
|
||||
return button
|
||||
}()
|
||||
|
||||
let busyIndicator: UIActivityIndicatorView = {
|
||||
let busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||
busyIndicator.hidesWhenStopped = true
|
||||
return busyIndicator
|
||||
}()
|
||||
|
||||
override func loadView() {
|
||||
view = UIView()
|
||||
view.backgroundColor = .white
|
||||
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
|
||||
view.addSubview(busyIndicator)
|
||||
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||
])
|
||||
|
||||
view.addSubview(centeredAddButton)
|
||||
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
centeredAddButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
centeredAddButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||
])
|
||||
|
||||
centeredAddButton.onTapped = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.addButtonTapped(sender: self.centeredAddButton)
|
||||
}
|
||||
|
||||
busyIndicator.startAnimating()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = tr("tunnelsListTitle")
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
|
||||
|
||||
restorationIdentifier = "TunnelsListVC"
|
||||
}
|
||||
|
||||
func setTunnelsManager(tunnelsManager: TunnelsManager) {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
tunnelsManager.tunnelsListDelegate = self
|
||||
|
||||
busyIndicator.stopAnimating()
|
||||
tableView.reloadData()
|
||||
centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0
|
||||
}
|
||||
|
||||
override func viewWillAppear(_: Bool) {
|
||||
if let selectedRowIndexPath = tableView.indexPathForSelectedRow {
|
||||
tableView.deselectRow(at: selectedRowIndexPath, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func addButtonTapped(sender: AnyObject) {
|
||||
guard tunnelsManager != nil else { return }
|
||||
|
||||
let alert = UIAlertController(title: "", message: tr("addTunnelMenuHeader"), preferredStyle: .actionSheet)
|
||||
let importFileAction = UIAlertAction(title: tr("addTunnelMenuImportFile"), style: .default) { [weak self] _ in
|
||||
self?.presentViewControllerForFileImport()
|
||||
}
|
||||
alert.addAction(importFileAction)
|
||||
|
||||
let scanQRCodeAction = UIAlertAction(title: tr("addTunnelMenuQRCode"), style: .default) { [weak self] _ in
|
||||
self?.presentViewControllerForScanningQRCode()
|
||||
}
|
||||
alert.addAction(scanQRCodeAction)
|
||||
|
||||
let createFromScratchAction = UIAlertAction(title: tr("addTunnelMenuFromScratch"), style: .default) { [weak self] _ in
|
||||
if let self = self, let tunnelsManager = self.tunnelsManager {
|
||||
self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager)
|
||||
}
|
||||
}
|
||||
alert.addAction(createFromScratchAction)
|
||||
|
||||
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
if let sender = sender as? UIBarButtonItem {
|
||||
alert.popoverPresentationController?.barButtonItem = sender
|
||||
} else if let sender = sender as? UIView {
|
||||
alert.popoverPresentationController?.sourceView = sender
|
||||
alert.popoverPresentationController?.sourceRect = sender.bounds
|
||||
}
|
||||
present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func settingsButtonTapped(sender: UIBarButtonItem) {
|
||||
guard tunnelsManager != nil else { return }
|
||||
|
||||
let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager)
|
||||
let settingsNC = UINavigationController(rootViewController: settingsVC)
|
||||
settingsNC.modalPresentationStyle = .formSheet
|
||||
present(settingsNC, animated: true)
|
||||
}
|
||||
|
||||
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) {
|
||||
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager)
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
editNC.modalPresentationStyle = .formSheet
|
||||
present(editNC, animated: true)
|
||||
}
|
||||
|
||||
func presentViewControllerForFileImport() {
|
||||
let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)]
|
||||
let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
|
||||
filePicker.delegate = self
|
||||
present(filePicker, animated: true)
|
||||
}
|
||||
|
||||
func presentViewControllerForScanningQRCode() {
|
||||
let scanQRCodeVC = QRScanViewController()
|
||||
scanQRCodeVC.delegate = self
|
||||
let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC)
|
||||
scanQRCodeNC.modalPresentationStyle = .fullScreen
|
||||
present(scanQRCodeNC, animated: true)
|
||||
}
|
||||
|
||||
func importFromFile(url: URL, completionHandler: (() -> Void)?) {
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
if url.pathExtension == "zip" {
|
||||
ZipImporter.importConfigFiles(from: url) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
let configs = result.value!
|
||||
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] numberSuccessful in
|
||||
if numberSuccessful == configs.count {
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
let title = tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful)
|
||||
let message = tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count)
|
||||
ErrorPresenter.showErrorAlert(title: title, message: message, from: self, onPresented: completionHandler)
|
||||
}
|
||||
}
|
||||
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
|
||||
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let fileContents = try? String(contentsOf: url),
|
||||
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName) {
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self, onPresented: completionHandler)
|
||||
} else {
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ErrorPresenter.showErrorAlert(title: tr("alertUnableToImportTitle"), message: tr("alertUnableToImportMessage"),
|
||||
from: self, onPresented: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelsListTableViewController: UIDocumentPickerDelegate {
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
urls.forEach {
|
||||
importFromFile(url: $0, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelsListTableViewController: QRScanViewControllerDelegate {
|
||||
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
|
||||
completionHandler: (() -> Void)?) {
|
||||
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
|
||||
} else {
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelsListTableViewController: UITableViewDataSource {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return (tunnelsManager?.numberOfTunnels() ?? 0)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: TunnelListCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
if let tunnelsManager = tunnelsManager {
|
||||
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
|
||||
cell.tunnel = tunnel
|
||||
cell.onSwitchToggled = { [weak self] isOn in
|
||||
guard let self = self, let tunnelsManager = self.tunnelsManager else { return }
|
||||
if isOn {
|
||||
tunnelsManager.startActivation(of: tunnel)
|
||||
} else {
|
||||
tunnelsManager.startDeactivation(of: tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelsListTableViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
|
||||
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
|
||||
tunnel: tunnel)
|
||||
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
|
||||
tunnelDetailNC.restorationIdentifier = "DetailNC"
|
||||
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView,
|
||||
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let deleteAction = UIContextualAction(style: .destructive, title: tr("tunnelsListSwipeDeleteButtonTitle")) { [weak self] _, _, completionHandler in
|
||||
guard let tunnelsManager = self?.tunnelsManager else { return }
|
||||
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
|
||||
tunnelsManager.remove(tunnel: tunnel) { error in
|
||||
if error != nil {
|
||||
ErrorPresenter.showErrorAlert(error: error!, from: self)
|
||||
completionHandler(false)
|
||||
} else {
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
return UISwipeActionsConfiguration(actions: [deleteAction])
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelsListTableViewController: TunnelsManagerListDelegate {
|
||||
func tunnelAdded(at index: Int) {
|
||||
tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
|
||||
}
|
||||
|
||||
func tunnelModified(at index: Int) {
|
||||
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
}
|
||||
|
||||
func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
|
||||
tableView.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
|
||||
}
|
||||
|
||||
func tunnelRemoved(at index: Int) {
|
||||
tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton.isHidden = tunnelsManager?.numberOfTunnels() ?? 0 > 0
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Network
|
||||
import Foundation
|
||||
|
||||
class DNSResolver {
|
||||
let endpoints: [Endpoint?]
|
||||
let dispatchGroup: DispatchGroup
|
||||
var dispatchWorkItems: [DispatchWorkItem]
|
||||
static var cache = NSCache<NSString, NSString>()
|
||||
|
||||
init(endpoints: [Endpoint?]) {
|
||||
self.endpoints = endpoints
|
||||
self.dispatchWorkItems = []
|
||||
self.dispatchGroup = DispatchGroup()
|
||||
}
|
||||
|
||||
func resolveWithoutNetworkRequests() -> [Endpoint?]? {
|
||||
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
|
||||
for (i, endpoint) in self.endpoints.enumerated() {
|
||||
guard let endpoint = endpoint else { continue }
|
||||
if let resolvedEndpointStringInCache = DNSResolver.cache.object(forKey: endpoint.stringRepresentation() as NSString),
|
||||
let resolvedEndpointInCache = Endpoint(from: resolvedEndpointStringInCache as String) {
|
||||
resolvedEndpoints[i] = resolvedEndpointInCache
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return resolvedEndpoints
|
||||
}
|
||||
|
||||
func resolve(completionHandler: @escaping ([Endpoint?]?) -> Void) {
|
||||
let endpoints = self.endpoints
|
||||
let dispatchGroup = self.dispatchGroup
|
||||
dispatchWorkItems = []
|
||||
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
|
||||
var isResolvedByDNSRequest: [Bool] = Array<Bool>(repeating: false, count: endpoints.count)
|
||||
for (i, endpoint) in self.endpoints.enumerated() {
|
||||
guard let endpoint = endpoint else { continue }
|
||||
if let resolvedEndpointStringInCache = DNSResolver.cache.object(forKey: endpoint.stringRepresentation() as NSString),
|
||||
let resolvedEndpointInCache = Endpoint(from: resolvedEndpointStringInCache as String) {
|
||||
resolvedEndpoints[i] = resolvedEndpointInCache
|
||||
} else {
|
||||
let workItem = DispatchWorkItem {
|
||||
resolvedEndpoints[i] = DNSResolver.resolveSync(endpoint: endpoint)
|
||||
isResolvedByDNSRequest[i] = true
|
||||
}
|
||||
dispatchWorkItems.append(workItem)
|
||||
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
|
||||
}
|
||||
}
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
assert(endpoints.count == resolvedEndpoints.count)
|
||||
for (i, endpoint) in endpoints.enumerated() {
|
||||
guard let endpoint = endpoint, let resolvedEndpoint = resolvedEndpoints[i] else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
if (isResolvedByDNSRequest[i]) {
|
||||
DNSResolver.cache.setObject(resolvedEndpoint.stringRepresentation() as NSString,
|
||||
forKey: endpoint.stringRepresentation() as NSString)
|
||||
}
|
||||
}
|
||||
let numberOfEndpointsToResolve = endpoints.compactMap { $0 }.count
|
||||
let numberOfResolvedEndpoints = resolvedEndpoints.compactMap { $0 }.count
|
||||
if (numberOfResolvedEndpoints < numberOfEndpointsToResolve) {
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
completionHandler(resolvedEndpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
for workItem in dispatchWorkItems {
|
||||
workItem.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension DNSResolver {
|
||||
// Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com>
|
||||
// in parse_endpoint() in src/tools/config.c in the WireGuard codebase
|
||||
private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
|
||||
var hints = addrinfo(
|
||||
ai_flags: 0,
|
||||
ai_family: AF_UNSPEC,
|
||||
ai_socktype: SOCK_DGRAM, // WireGuard is UDP-only
|
||||
ai_protocol: IPPROTO_UDP, // WireGuard is UDP-only
|
||||
ai_addrlen: 0,
|
||||
ai_canonname: nil,
|
||||
ai_addr: nil,
|
||||
ai_next: nil)
|
||||
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
|
||||
switch (endpoint.host) {
|
||||
case .name(let name, _):
|
||||
// The endpoint is a hostname and needs DNS resolution
|
||||
let returnValue = getaddrinfo(
|
||||
name.cString(using: .utf8), // Hostname
|
||||
"\(endpoint.port)".cString(using: .utf8), // Port
|
||||
&hints,
|
||||
&resultPointer)
|
||||
if (returnValue == 0) {
|
||||
// getaddrinfo succeeded
|
||||
let ipv4Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET_ADDRSTRLEN))
|
||||
let ipv6Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET6_ADDRSTRLEN))
|
||||
var ipv4AddressString: String?
|
||||
var ipv6AddressString: String?
|
||||
while (resultPointer != nil) {
|
||||
let result = resultPointer!.pointee
|
||||
resultPointer = result.ai_next
|
||||
if (result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size) {
|
||||
var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
|
||||
if (inet_ntop(result.ai_family, &sa4.sin_addr, ipv4Buffer, socklen_t(INET_ADDRSTRLEN)) != nil) {
|
||||
ipv4AddressString = String(cString: ipv4Buffer)
|
||||
// If we found an IPv4 address, we can stop
|
||||
break
|
||||
}
|
||||
} else if (result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size) {
|
||||
if (ipv6AddressString != nil) {
|
||||
// If we already have an IPv6 address, we can skip this one
|
||||
continue
|
||||
}
|
||||
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
|
||||
if (inet_ntop(result.ai_family, &sa6.sin6_addr, ipv6Buffer, socklen_t(INET6_ADDRSTRLEN)) != nil) {
|
||||
ipv6AddressString = String(cString: ipv6Buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
ipv4Buffer.deallocate()
|
||||
ipv6Buffer.deallocate()
|
||||
// We prefer an IPv4 address over an IPv6 address
|
||||
if let ipv4AddressString = ipv4AddressString, let ipv4Address = IPv4Address(ipv4AddressString) {
|
||||
return Endpoint(host: NWEndpoint.Host.ipv4(ipv4Address), port: endpoint.port)
|
||||
} else if let ipv6AddressString = ipv6AddressString, let ipv6Address = IPv6Address(ipv6AddressString) {
|
||||
return Endpoint(host: NWEndpoint.Host.ipv6(ipv6Address), port: endpoint.port)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// getaddrinfo failed
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
// The endpoint is already resolved
|
||||
return endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
class PacketTunnelOptionsGenerator {
|
||||
static func generateOptions(from tc: TunnelConfiguration,
|
||||
withResolvedEndpoints resolvedEndpoints: [Endpoint?]) -> [String: NSObject] {
|
||||
var options: [String: NSObject] = [:]
|
||||
|
||||
// Interface name
|
||||
|
||||
options[.interfaceName] = tc.interface.name as NSObject
|
||||
|
||||
// WireGuard settings
|
||||
|
||||
var wgSettings = ""
|
||||
let privateKey = tc.interface.privateKey.hexEncodedString()
|
||||
wgSettings.append("private_key=\(privateKey)\n")
|
||||
if let listenPort = tc.interface.listenPort {
|
||||
wgSettings.append("listen_port=\(listenPort)\n")
|
||||
}
|
||||
if (tc.peers.count > 0) {
|
||||
wgSettings.append("replace_peers=true\n")
|
||||
}
|
||||
assert(tc.peers.count == resolvedEndpoints.count)
|
||||
for (i, peer) in tc.peers.enumerated() {
|
||||
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
|
||||
if let preSharedKey = peer.preSharedKey {
|
||||
wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n")
|
||||
}
|
||||
if let endpoint = resolvedEndpoints[i] {
|
||||
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
|
||||
wgSettings.append("endpoint=\(endpoint.stringRepresentation())\n")
|
||||
}
|
||||
let persistentKeepAlive = peer.persistentKeepAlive ?? 0
|
||||
wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n")
|
||||
if (!peer.allowedIPs.isEmpty) {
|
||||
wgSettings.append("replace_allowed_ips=true\n")
|
||||
for ip in peer.allowedIPs {
|
||||
wgSettings.append("allowed_ip=\(ip.stringRepresentation())\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options[.wireguardSettings] = wgSettings as NSObject
|
||||
|
||||
// Remote address
|
||||
|
||||
/* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for
|
||||
* a tunnel to have no endpoint, or for there to be many endpoints, in
|
||||
* which case, displaying a single one in settings doesn't really
|
||||
* make sense. So, we fill it in with this placeholder, which is not
|
||||
* a valid IP address that will actually route over the Internet.
|
||||
*/
|
||||
var remoteAddress: String = "0.0.0.0"
|
||||
let endpointsCompact = resolvedEndpoints.compactMap({ $0 })
|
||||
if endpointsCompact.count == 1 {
|
||||
switch (endpointsCompact.first!.host) {
|
||||
case .ipv4(let address):
|
||||
remoteAddress = "\(address)"
|
||||
case .ipv6(let address):
|
||||
remoteAddress = "\(address)"
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
options[.remoteAddress] = remoteAddress as NSObject
|
||||
|
||||
// DNS
|
||||
|
||||
options[.dnsServers] = tc.interface.dns.map { $0.stringRepresentation() } as NSObject
|
||||
|
||||
// MTU
|
||||
|
||||
options[.mtu] = NSNumber(value: tc.interface.mtu ?? 0) // 0 implies auto-MTU
|
||||
|
||||
// Addresses from interface addresses
|
||||
|
||||
var ipv4Addresses: [String] = []
|
||||
var ipv4SubnetMasks: [String] = []
|
||||
|
||||
var ipv6Addresses: [String] = []
|
||||
var ipv6NetworkPrefixLengths: [NSNumber] = []
|
||||
|
||||
for addressRange in tc.interface.addresses {
|
||||
if (addressRange.address is IPv4Address) {
|
||||
ipv4Addresses.append("\(addressRange.address)")
|
||||
ipv4SubnetMasks.append(ipv4SubnetMaskString(of: addressRange))
|
||||
} else if (addressRange.address is IPv6Address) {
|
||||
ipv6Addresses.append("\(addressRange.address)")
|
||||
ipv6NetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
|
||||
}
|
||||
}
|
||||
|
||||
options[.ipv4Addresses] = ipv4Addresses as NSObject
|
||||
options[.ipv4SubnetMasks] = ipv4SubnetMasks as NSObject
|
||||
|
||||
options[.ipv6Addresses] = ipv6Addresses as NSObject
|
||||
options[.ipv6NetworkPrefixLengths] = ipv6NetworkPrefixLengths as NSObject
|
||||
|
||||
// Included routes from AllowedIPs
|
||||
|
||||
var ipv4IncludedRouteAddresses: [String] = []
|
||||
var ipv4IncludedRouteSubnetMasks: [String] = []
|
||||
|
||||
var ipv6IncludedRouteAddresses: [String] = []
|
||||
var ipv6IncludedRouteNetworkPrefixLengths: [NSNumber] = []
|
||||
|
||||
for peer in tc.peers {
|
||||
for addressRange in peer.allowedIPs {
|
||||
if (addressRange.address is IPv4Address) {
|
||||
ipv4IncludedRouteAddresses.append("\(addressRange.address)")
|
||||
ipv4IncludedRouteSubnetMasks.append(ipv4SubnetMaskString(of: addressRange))
|
||||
} else if (addressRange.address is IPv6Address) {
|
||||
ipv6IncludedRouteAddresses.append("\(addressRange.address)")
|
||||
ipv6IncludedRouteNetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options[.ipv4IncludedRouteAddresses] = ipv4IncludedRouteAddresses as NSObject
|
||||
options[.ipv4IncludedRouteSubnetMasks] = ipv4IncludedRouteSubnetMasks as NSObject
|
||||
|
||||
options[.ipv6IncludedRouteAddresses] = ipv6IncludedRouteAddresses as NSObject
|
||||
options[.ipv6IncludedRouteNetworkPrefixLengths] = ipv6IncludedRouteNetworkPrefixLengths as NSObject
|
||||
|
||||
// Excluded routes from endpoints
|
||||
|
||||
var ipv4ExcludedRouteAddresses: [String] = []
|
||||
var ipv4ExcludedRouteSubnetMasks: [String] = []
|
||||
|
||||
var ipv6ExcludedRouteAddresses: [String] = []
|
||||
var ipv6ExcludedRouteNetworkPrefixLengths: [NSNumber] = []
|
||||
|
||||
for endpoint in resolvedEndpoints {
|
||||
guard let endpoint = endpoint else { continue }
|
||||
switch (endpoint.host) {
|
||||
case .ipv4(let address):
|
||||
ipv4ExcludedRouteAddresses.append("\(address)")
|
||||
ipv4ExcludedRouteSubnetMasks.append("255.255.255.255") // A single IPv4 address
|
||||
case .ipv6(let address):
|
||||
ipv6ExcludedRouteAddresses.append("\(address)")
|
||||
ipv6ExcludedRouteNetworkPrefixLengths.append(NSNumber(value: UInt8(128))) // A single IPv6 address
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
options[.ipv4ExcludedRouteAddresses] = ipv4ExcludedRouteAddresses as NSObject
|
||||
options[.ipv4ExcludedRouteSubnetMasks] = ipv4ExcludedRouteSubnetMasks as NSObject
|
||||
|
||||
options[.ipv6ExcludedRouteAddresses] = ipv6ExcludedRouteAddresses as NSObject
|
||||
options[.ipv6ExcludedRouteNetworkPrefixLengths] = ipv6ExcludedRouteNetworkPrefixLengths as NSObject
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
static func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
|
||||
let n: UInt8 = addressRange.networkPrefixLength
|
||||
assert(n <= 32)
|
||||
var octets: [UInt8] = [0, 0, 0, 0]
|
||||
let subnetMask: UInt32 = n > 0 ? ~UInt32(0) << (32 - n) : UInt32(0)
|
||||
octets[0] = UInt8(truncatingIfNeeded: subnetMask >> 24)
|
||||
octets[1] = UInt8(truncatingIfNeeded: subnetMask >> 16)
|
||||
octets[2] = UInt8(truncatingIfNeeded: subnetMask >> 8)
|
||||
octets[3] = UInt8(truncatingIfNeeded: subnetMask)
|
||||
return octets.map { String($0) }.joined(separator: ".")
|
||||
}
|
||||
}
|
||||
|
||||
private extension Data {
|
||||
func hexEncodedString() -> String {
|
||||
return self.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
}
|
||||
@@ -1,450 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
import os.log
|
||||
|
||||
protocol TunnelsManagerDelegate: class {
|
||||
func tunnelAdded(at: Int)
|
||||
func tunnelModified(at: Int)
|
||||
func tunnelMoved(at oldIndex: Int, to newIndex: Int)
|
||||
func tunnelRemoved(at: Int)
|
||||
}
|
||||
|
||||
enum TunnelActivationError: Error {
|
||||
case dnsResolutionFailed
|
||||
case tunnelActivationFailed
|
||||
case attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: TunnelStatus)
|
||||
case attemptingActivationWhenTunnelIsNotInactive
|
||||
case attemptingDeactivationWhenTunnelIsInactive
|
||||
}
|
||||
|
||||
enum TunnelManagementError: Error {
|
||||
case tunnelAlreadyExistsWithThatName
|
||||
case tunnelInvalidName
|
||||
case vpnSystemErrorOnAddTunnel
|
||||
case vpnSystemErrorOnModifyTunnel
|
||||
case vpnSystemErrorOnRemoveTunnel
|
||||
}
|
||||
|
||||
class TunnelsManager {
|
||||
|
||||
private var tunnels: [TunnelContainer]
|
||||
weak var delegate: TunnelsManagerDelegate?
|
||||
|
||||
private var isAddingTunnel: Bool = false
|
||||
private var isModifyingTunnel: Bool = false
|
||||
private var isDeletingTunnel: Bool = false
|
||||
|
||||
init(tunnelProviders: [NETunnelProviderManager]) {
|
||||
self.tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
|
||||
}
|
||||
|
||||
static func create(completionHandler: @escaping (TunnelsManager?) -> Void) {
|
||||
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
|
||||
if let error = error {
|
||||
os_log("Failed to load tunnel provider managers: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
return
|
||||
}
|
||||
completionHandler(TunnelsManager(tunnelProviders: managers ?? []))
|
||||
}
|
||||
}
|
||||
|
||||
func add(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelContainer?, TunnelManagementError?) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.interface.name
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
return
|
||||
}
|
||||
|
||||
if self.tunnels.contains(where: { $0.name == tunnelName }) {
|
||||
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
return
|
||||
}
|
||||
|
||||
isAddingTunnel = true
|
||||
let tunnelProviderManager = NETunnelProviderManager()
|
||||
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
|
||||
tunnelProviderManager.localizedDescription = tunnelName
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] (error) in
|
||||
defer { self?.isAddingTunnel = false }
|
||||
guard (error == nil) else {
|
||||
os_log("Add: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(nil, TunnelManagementError.vpnSystemErrorOnAddTunnel)
|
||||
return
|
||||
}
|
||||
if let s = self {
|
||||
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
|
||||
s.tunnels.append(tunnel)
|
||||
s.tunnels.sort { $0.name < $1.name }
|
||||
s.delegate?.tunnelAdded(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
completionHandler(tunnel, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) {
|
||||
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, completionHandler: @escaping (UInt) -> Void) {
|
||||
guard let head = tunnelConfigurations.first else {
|
||||
completionHandler(numberSuccessful)
|
||||
return
|
||||
}
|
||||
let tail = tunnelConfigurations.dropFirst()
|
||||
self.add(tunnelConfiguration: head) { [weak self, tail] (_, error) in
|
||||
DispatchQueue.main.async {
|
||||
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (error == nil ? 1 : 0), completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modify(tunnel: TunnelContainer, with tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelManagementError?) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.interface.name
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
return
|
||||
}
|
||||
|
||||
isModifyingTunnel = true
|
||||
|
||||
let tunnelProviderManager = tunnel.tunnelProvider
|
||||
let isNameChanged = (tunnelName != tunnelProviderManager.localizedDescription)
|
||||
var oldName: String?
|
||||
if (isNameChanged) {
|
||||
if self.tunnels.contains(where: { $0.name == tunnelName }) {
|
||||
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
return
|
||||
}
|
||||
oldName = tunnel.name
|
||||
tunnel.name = tunnelName
|
||||
}
|
||||
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
|
||||
tunnelProviderManager.localizedDescription = tunnelName
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] (error) in
|
||||
defer { self?.isModifyingTunnel = false }
|
||||
guard (error == nil) else {
|
||||
os_log("Modify: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(TunnelManagementError.vpnSystemErrorOnModifyTunnel)
|
||||
return
|
||||
}
|
||||
if let s = self {
|
||||
if (isNameChanged) {
|
||||
let oldIndex = s.tunnels.firstIndex(of: tunnel)!
|
||||
s.tunnels.sort { $0.name < $1.name }
|
||||
let newIndex = s.tunnels.firstIndex(of: tunnel)!
|
||||
s.delegate?.tunnelMoved(at: oldIndex, to: newIndex)
|
||||
}
|
||||
s.delegate?.tunnelModified(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
|
||||
if (tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting) {
|
||||
// Turn off the tunnel, and then turn it back on, so the changes are made effective
|
||||
tunnel.beginRestart()
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelManagementError?) -> Void) {
|
||||
let tunnelProviderManager = tunnel.tunnelProvider
|
||||
|
||||
isDeletingTunnel = true
|
||||
|
||||
tunnelProviderManager.removeFromPreferences { [weak self] (error) in
|
||||
defer { self?.isDeletingTunnel = false }
|
||||
guard (error == nil) else {
|
||||
os_log("Remove: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(TunnelManagementError.vpnSystemErrorOnRemoveTunnel)
|
||||
return
|
||||
}
|
||||
if let s = self {
|
||||
let index = s.tunnels.firstIndex(of: tunnel)!
|
||||
s.tunnels.remove(at: index)
|
||||
s.delegate?.tunnelRemoved(at: index)
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfTunnels() -> Int {
|
||||
return tunnels.count
|
||||
}
|
||||
|
||||
func tunnel(at index: Int) -> TunnelContainer {
|
||||
return tunnels[index]
|
||||
}
|
||||
|
||||
func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (Error?) -> Void) {
|
||||
guard (tunnel.status == .inactive) else {
|
||||
completionHandler(TunnelActivationError.attemptingActivationWhenTunnelIsNotInactive)
|
||||
return
|
||||
}
|
||||
for t in tunnels {
|
||||
if t.status != .inactive {
|
||||
completionHandler(TunnelActivationError.attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: t.status))
|
||||
return
|
||||
}
|
||||
}
|
||||
tunnel.startActivation(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func startDeactivation(of tunnel: TunnelContainer, completionHandler: @escaping (Error?) -> Void) {
|
||||
if (tunnel.status == .inactive) {
|
||||
completionHandler(TunnelActivationError.attemptingDeactivationWhenTunnelIsInactive)
|
||||
return
|
||||
}
|
||||
tunnel.startDeactivation()
|
||||
}
|
||||
}
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration) {
|
||||
assert(!tunnelConfiguration.interface.name.isEmpty)
|
||||
guard let serializedTunnelConfiguration = try? JSONEncoder().encode(tunnelConfiguration) else { return nil }
|
||||
|
||||
self.init()
|
||||
|
||||
let appId = Bundle.main.bundleIdentifier!
|
||||
providerBundleIdentifier = "\(appId).network-extension"
|
||||
providerConfiguration = [
|
||||
"tunnelConfiguration": serializedTunnelConfiguration,
|
||||
"tunnelConfigurationVersion": 1
|
||||
]
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint})
|
||||
if endpoints.count == 1 {
|
||||
serverAddress = endpoints.first!.stringRepresentation()
|
||||
} else if endpoints.isEmpty {
|
||||
serverAddress = "Unspecified"
|
||||
} else {
|
||||
serverAddress = "Multiple endpoints"
|
||||
}
|
||||
username = tunnelConfiguration.interface.name
|
||||
}
|
||||
|
||||
func tunnelConfiguration() -> TunnelConfiguration? {
|
||||
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return nil }
|
||||
return try? JSONDecoder().decode(TunnelConfiguration.self, from: serializedTunnelConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelContainer: NSObject {
|
||||
@objc dynamic var name: String
|
||||
@objc dynamic var status: TunnelStatus
|
||||
|
||||
fileprivate let tunnelProvider: NETunnelProviderManager
|
||||
private var statusObservationToken: AnyObject?
|
||||
|
||||
private var dnsResolver: DNSResolver?
|
||||
|
||||
init(tunnel: NETunnelProviderManager) {
|
||||
self.name = tunnel.localizedDescription ?? "Unnamed"
|
||||
let status = TunnelStatus(from: tunnel.connection.status)
|
||||
self.status = status
|
||||
self.tunnelProvider = tunnel
|
||||
super.init()
|
||||
if (status != .inactive) {
|
||||
startObservingTunnelStatus()
|
||||
}
|
||||
}
|
||||
|
||||
func tunnelConfiguration() -> TunnelConfiguration? {
|
||||
return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration()
|
||||
}
|
||||
|
||||
fileprivate func startActivation(completionHandler: @escaping (Error?) -> Void) {
|
||||
assert(status == .inactive || status == .restarting)
|
||||
assert(self.dnsResolver == nil)
|
||||
|
||||
guard let tunnelConfiguration = tunnelConfiguration() else { fatalError() }
|
||||
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
|
||||
|
||||
// Resolve DNS and start the tunnel
|
||||
let dnsResolver = DNSResolver(endpoints: endpoints)
|
||||
let resolvedEndpoints = dnsResolver.resolveWithoutNetworkRequests()
|
||||
if let resolvedEndpoints = resolvedEndpoints {
|
||||
// If we don't have to make a DNS network request, we never
|
||||
// change the status to .resolvingEndpointDomains
|
||||
startActivation(tunnelConfiguration: tunnelConfiguration,
|
||||
resolvedEndpoints: resolvedEndpoints,
|
||||
completionHandler: completionHandler)
|
||||
} else {
|
||||
status = .resolvingEndpointDomains
|
||||
self.dnsResolver = dnsResolver
|
||||
dnsResolver.resolve { [weak self] resolvedEndpoints in
|
||||
guard let s = self else { return }
|
||||
assert(s.status == .resolvingEndpointDomains)
|
||||
s.dnsResolver = nil
|
||||
guard let resolvedEndpoints = resolvedEndpoints else {
|
||||
s.status = .inactive
|
||||
completionHandler(TunnelActivationError.dnsResolutionFailed)
|
||||
return
|
||||
}
|
||||
s.startActivation(tunnelConfiguration: tunnelConfiguration,
|
||||
resolvedEndpoints: resolvedEndpoints,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func startActivation(recursionCount: UInt = 0,
|
||||
lastError: Error? = nil,
|
||||
tunnelConfiguration: TunnelConfiguration,
|
||||
resolvedEndpoints: [Endpoint?],
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
if (recursionCount >= 8) {
|
||||
os_log("startActivation: Failed after 8 attempts. Giving up with %{public}@", log: OSLog.default, type: .error, "\(lastError!)")
|
||||
completionHandler(TunnelActivationError.tunnelActivationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// resolvedEndpoints should contain only IP addresses, not any named endpoints
|
||||
assert(resolvedEndpoints.allSatisfy { (resolvedEndpoint) in
|
||||
guard let resolvedEndpoint = resolvedEndpoint else { return true }
|
||||
switch (resolvedEndpoint.host) {
|
||||
case .ipv4: return true
|
||||
case .ipv6: return true
|
||||
case .name: return false
|
||||
}
|
||||
})
|
||||
|
||||
os_log("startActivation: Entering", log: OSLog.default, type: .debug)
|
||||
|
||||
guard (tunnelProvider.isEnabled) else {
|
||||
// In case the tunnel had gotten disabled, re-enable and save it,
|
||||
// then call this function again.
|
||||
os_log("startActivation: Tunnel is disabled. Re-enabling and saving", log: OSLog.default, type: .info)
|
||||
tunnelProvider.isEnabled = true
|
||||
tunnelProvider.saveToPreferences { [weak self] (error) in
|
||||
if (error != nil) {
|
||||
os_log("Error saving tunnel after re-enabling: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
os_log("startActivation: Tunnel saved after re-enabling", log: OSLog.default, type: .info)
|
||||
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints, completionHandler: completionHandler)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Start the tunnel
|
||||
startObservingTunnelStatus()
|
||||
let session = (tunnelProvider.connection as! NETunnelProviderSession)
|
||||
do {
|
||||
os_log("startActivation: Generating options", log: OSLog.default, type: .debug)
|
||||
let tunnelOptions = PacketTunnelOptionsGenerator.generateOptions(
|
||||
from: tunnelConfiguration, withResolvedEndpoints: resolvedEndpoints)
|
||||
os_log("startActivation: Starting tunnel", log: OSLog.default, type: .debug)
|
||||
try session.startTunnel(options: tunnelOptions)
|
||||
os_log("startActivation: Success", log: OSLog.default, type: .debug)
|
||||
completionHandler(nil)
|
||||
} catch (let error) {
|
||||
os_log("startActivation: Error starting tunnel. Examining error", log: OSLog.default, type: .debug)
|
||||
guard let vpnError = error as? NEVPNError else {
|
||||
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
status = .inactive
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
guard (vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale) else {
|
||||
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
status = .inactive
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
assert(vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale)
|
||||
os_log("startActivation: Error says: %{public}@", log: OSLog.default, type: .debug,
|
||||
vpnError.code == NEVPNError.configurationInvalid ? "Configuration invalid" : "Configuration stale")
|
||||
os_log("startActivation: Will reload tunnel and then try to start it. ", log: OSLog.default, type: .info)
|
||||
tunnelProvider.loadFromPreferences { [weak self] (error) in
|
||||
if (error != nil) {
|
||||
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error!)")
|
||||
self?.status = .inactive
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
os_log("startActivation: Tunnel reloaded", log: OSLog.default, type: .info)
|
||||
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: vpnError, tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func startDeactivation() {
|
||||
assert(status == .active)
|
||||
assert(statusObservationToken != nil)
|
||||
let session = (tunnelProvider.connection as! NETunnelProviderSession)
|
||||
session.stopTunnel()
|
||||
}
|
||||
|
||||
fileprivate func beginRestart() {
|
||||
assert(status == .active || status == .activating || status == .reasserting)
|
||||
assert(statusObservationToken != nil)
|
||||
status = .restarting
|
||||
let session = (tunnelProvider.connection as! NETunnelProviderSession)
|
||||
session.stopTunnel()
|
||||
}
|
||||
|
||||
private func startObservingTunnelStatus() {
|
||||
if (statusObservationToken != nil) { return }
|
||||
let connection = tunnelProvider.connection
|
||||
statusObservationToken = NotificationCenter.default.addObserver(
|
||||
forName: .NEVPNStatusDidChange,
|
||||
object: connection,
|
||||
queue: nil) { [weak self] (_) in
|
||||
guard let s = self else { return }
|
||||
if ((s.status == .restarting) && (connection.status == .disconnected || connection.status == .disconnecting)) {
|
||||
// Don't change s.status when disconnecting for a restart
|
||||
if (connection.status == .disconnected) {
|
||||
self?.startActivation(completionHandler: { _ in })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (s.status == .resolvingEndpointDomains && connection.status == .disconnected) {
|
||||
// Don't change to .inactive if we're still resolving endpoints
|
||||
return
|
||||
}
|
||||
s.status = TunnelStatus(from: connection.status)
|
||||
if (s.status == .inactive) {
|
||||
s.statusObservationToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc enum TunnelStatus: Int {
|
||||
case inactive
|
||||
case activating
|
||||
case active
|
||||
case deactivating
|
||||
case reasserting // Not a possible state at present
|
||||
|
||||
case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
|
||||
case resolvingEndpointDomains // DNS resolution in progress
|
||||
|
||||
init(from vpnStatus: NEVPNStatus) {
|
||||
switch (vpnStatus) {
|
||||
case .connected:
|
||||
self = .active
|
||||
case .connecting:
|
||||
self = .activating
|
||||
case .disconnected:
|
||||
self = .inactive
|
||||
case .disconnecting:
|
||||
self = .deactivating
|
||||
case .reasserting:
|
||||
self = .reasserting
|
||||
case .invalid:
|
||||
self = .inactive
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,4 @@
|
||||
#include "unzip.h"
|
||||
#include "zip.h"
|
||||
#include "wireguard-go-version.h"
|
||||
#include "ringlogger.h"
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>packet-tunnel-provider</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
protocol WireGuardAppError: Error {
|
||||
typealias AlertText = (title: String, message: String)
|
||||
|
||||
var alertText: AlertText { get }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
enum WireGuardResult<T> {
|
||||
case success(_ value: T)
|
||||
case failure(_ error: WireGuardAppError)
|
||||
|
||||
var value: T? {
|
||||
switch self {
|
||||
case .success(let value): return value
|
||||
case .failure: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var error: WireGuardAppError? {
|
||||
switch self {
|
||||
case .success: return nil
|
||||
case .failure(let error): return error
|
||||
}
|
||||
}
|
||||
|
||||
var isSuccess: Bool {
|
||||
switch self {
|
||||
case .success: return true
|
||||
case .failure: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,10 +68,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef NOUNCRYPT
|
||||
#define NOUNCRYPT
|
||||
#endif
|
||||
|
||||
#include "zlib.h"
|
||||
#include "unzip.h"
|
||||
|
||||
@@ -185,18 +181,8 @@ typedef struct
|
||||
int encrypted;
|
||||
|
||||
int isZip64;
|
||||
|
||||
# ifndef NOUNCRYPT
|
||||
unsigned long keys[3]; /* keys defining the pseudo-random sequence */
|
||||
const z_crc_t* pcrc_32_tab;
|
||||
# endif
|
||||
} unz64_s;
|
||||
|
||||
|
||||
#ifndef NOUNCRYPT
|
||||
#include "crypt.h"
|
||||
#endif
|
||||
|
||||
/* ===========================================================================
|
||||
Read a byte from a gz_stream; update next_in and avail_in. Return EOF
|
||||
for end of file.
|
||||
@@ -1478,12 +1464,8 @@ extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method,
|
||||
file_in_zip64_read_info_s* pfile_in_zip_read_info;
|
||||
ZPOS64_T offset_local_extrafield; /* offset of the local extra field */
|
||||
uInt size_local_extrafield; /* size of the local extra field */
|
||||
# ifndef NOUNCRYPT
|
||||
char source[12];
|
||||
# else
|
||||
if (password != NULL)
|
||||
return UNZ_PARAMERROR;
|
||||
# endif
|
||||
|
||||
if (file==NULL)
|
||||
return UNZ_PARAMERROR;
|
||||
@@ -1612,29 +1594,6 @@ extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method,
|
||||
s->pfile_in_zip_read = pfile_in_zip_read_info;
|
||||
s->encrypted = 0;
|
||||
|
||||
# ifndef NOUNCRYPT
|
||||
if (password != NULL)
|
||||
{
|
||||
int i;
|
||||
s->pcrc_32_tab = get_crc_table();
|
||||
init_keys(password,s->keys,s->pcrc_32_tab);
|
||||
if (ZSEEK64(s->z_filefunc, s->filestream,
|
||||
s->pfile_in_zip_read->pos_in_zipfile +
|
||||
s->pfile_in_zip_read->byte_before_the_zipfile,
|
||||
SEEK_SET)!=0)
|
||||
return UNZ_INTERNALERROR;
|
||||
if(ZREAD64(s->z_filefunc, s->filestream,source, 12)<12)
|
||||
return UNZ_INTERNALERROR;
|
||||
|
||||
for (i = 0; i<12; i++)
|
||||
zdecode(s->keys,s->pcrc_32_tab,source[i]);
|
||||
|
||||
s->pfile_in_zip_read->pos_in_zipfile+=12;
|
||||
s->encrypted=1;
|
||||
}
|
||||
# endif
|
||||
|
||||
|
||||
return UNZ_OK;
|
||||
}
|
||||
|
||||
@@ -1739,19 +1698,6 @@ extern int ZEXPORT unzReadCurrentFile (unzFile file, voidp buf, unsigned len)
|
||||
uReadThis)!=uReadThis)
|
||||
return UNZ_ERRNO;
|
||||
|
||||
|
||||
# ifndef NOUNCRYPT
|
||||
if(s->encrypted)
|
||||
{
|
||||
uInt i;
|
||||
for(i=0;i<uReadThis;i++)
|
||||
pfile_in_zip_read_info->read_buffer[i] =
|
||||
zdecode(s->keys,s->pcrc_32_tab,
|
||||
pfile_in_zip_read_info->read_buffer[i]);
|
||||
}
|
||||
# endif
|
||||
|
||||
|
||||
pfile_in_zip_read_info->pos_in_zipfile += uReadThis;
|
||||
|
||||
pfile_in_zip_read_info->rest_read_compressed-=uReadThis;
|
||||
|
||||
@@ -153,11 +153,6 @@ typedef struct
|
||||
ZPOS64_T pos_zip64extrainfo;
|
||||
ZPOS64_T totalCompressedData;
|
||||
ZPOS64_T totalUncompressedData;
|
||||
#ifndef NOCRYPT
|
||||
unsigned long keys[3]; /* keys defining the pseudo-random sequence */
|
||||
const z_crc_t* pcrc_32_tab;
|
||||
int crypt_header_size;
|
||||
#endif
|
||||
} curfile64_info;
|
||||
|
||||
typedef struct
|
||||
@@ -178,12 +173,6 @@ typedef struct
|
||||
|
||||
} zip64_internal;
|
||||
|
||||
|
||||
#ifndef NOCRYPT
|
||||
#define INCLUDECRYPTINGCODE_IFCRYPTALLOWED
|
||||
#include "crypt.h"
|
||||
#endif
|
||||
|
||||
local linkedlist_datablock_internal* allocate_new_datablock()
|
||||
{
|
||||
linkedlist_datablock_internal* ldi;
|
||||
@@ -1064,12 +1053,6 @@ extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename,
|
||||
uInt i;
|
||||
int err = ZIP_OK;
|
||||
|
||||
# ifdef NOCRYPT
|
||||
(void)(crcForCrypting);
|
||||
if (password != NULL)
|
||||
return ZIP_PARAMERROR;
|
||||
# endif
|
||||
|
||||
if (file == NULL)
|
||||
return ZIP_PARAMERROR;
|
||||
|
||||
@@ -1237,24 +1220,6 @@ extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename,
|
||||
|
||||
}
|
||||
|
||||
# ifndef NOCRYPT
|
||||
zi->ci.crypt_header_size = 0;
|
||||
if ((err==Z_OK) && (password != NULL))
|
||||
{
|
||||
unsigned char bufHead[RAND_HEAD_LEN];
|
||||
unsigned int sizeHead;
|
||||
zi->ci.encrypt = 1;
|
||||
zi->ci.pcrc_32_tab = get_crc_table();
|
||||
/*init_keys(password,zi->ci.keys,zi->ci.pcrc_32_tab);*/
|
||||
|
||||
sizeHead=crypthead(password,bufHead,RAND_HEAD_LEN,zi->ci.keys,zi->ci.pcrc_32_tab,crcForCrypting);
|
||||
zi->ci.crypt_header_size = sizeHead;
|
||||
|
||||
if (ZWRITE64(zi->z_filefunc,zi->filestream,bufHead,sizeHead) != sizeHead)
|
||||
err = ZIP_ERRNO;
|
||||
}
|
||||
# endif
|
||||
|
||||
if (err==Z_OK)
|
||||
zi->in_opened_file_inzip = 1;
|
||||
return err;
|
||||
@@ -1362,16 +1327,6 @@ local int zip64FlushWriteBuffer(zip64_internal* zi)
|
||||
{
|
||||
int err=ZIP_OK;
|
||||
|
||||
if (zi->ci.encrypt != 0)
|
||||
{
|
||||
#ifndef NOCRYPT
|
||||
uInt i;
|
||||
int t;
|
||||
for (i=0;i<zi->ci.pos_in_buffered_data;i++)
|
||||
zi->ci.buffered_data[i] = zencode(zi->ci.keys, zi->ci.pcrc_32_tab, zi->ci.buffered_data[i],t);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data)
|
||||
err = ZIP_ERRNO;
|
||||
|
||||
@@ -1602,10 +1557,6 @@ extern int ZEXPORT zipCloseFileInZipRaw64 (zipFile file, ZPOS64_T uncompressed_s
|
||||
}
|
||||
compressed_size = zi->ci.totalCompressedData;
|
||||
|
||||
# ifndef NOCRYPT
|
||||
compressed_size += zi->ci.crypt_header_size;
|
||||
# endif
|
||||
|
||||
// update Current Item crc and sizes,
|
||||
if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff)
|
||||
{
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ZipArchiveError: Error {
|
||||
enum ZipArchiveError: WireGuardAppError {
|
||||
case cantOpenInputZipFile
|
||||
case cantOpenOutputZipFileForWriting
|
||||
case badArchive
|
||||
|
||||
var alertText: AlertText {
|
||||
switch self {
|
||||
case .cantOpenInputZipFile:
|
||||
return (tr("alertCantOpenInputZipFileTitle"), tr("alertCantOpenInputZipFileMessage"))
|
||||
case .cantOpenOutputZipFileForWriting:
|
||||
return (tr("alertCantOpenOutputZipFileForWritingTitle"), tr("alertCantOpenOutputZipFileForWritingMessage"))
|
||||
case .badArchive:
|
||||
return (tr("alertBadArchiveTitle"), tr("alertBadArchiveMessage"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZipArchive {
|
||||
@@ -28,9 +39,9 @@ class ZipArchive {
|
||||
zipClose(zipFile, nil)
|
||||
}
|
||||
|
||||
static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileName: String, contents: Data)] {
|
||||
static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileBaseName: String, contents: Data)] {
|
||||
|
||||
var results: [(fileName: String, contents: Data)] = []
|
||||
var results = [(fileBaseName: String, contents: Data)]()
|
||||
|
||||
guard let zipFile = unzOpen64(url.path) else {
|
||||
throw ZipArchiveError.cantOpenInputZipFile
|
||||
@@ -38,17 +49,13 @@ class ZipArchive {
|
||||
defer {
|
||||
unzClose(zipFile)
|
||||
}
|
||||
guard (unzGoToFirstFile(zipFile) == UNZ_OK) else {
|
||||
throw ZipArchiveError.badArchive
|
||||
}
|
||||
guard unzGoToFirstFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
|
||||
|
||||
var resultOfGoToNextFile: Int32
|
||||
repeat {
|
||||
guard (unzOpenCurrentFile(zipFile) == UNZ_OK) else {
|
||||
throw ZipArchiveError.badArchive
|
||||
}
|
||||
guard unzOpenCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
|
||||
|
||||
let bufferSize = 1024
|
||||
let bufferSize = 16384 // 16 KiB
|
||||
var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
|
||||
var dataBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
|
||||
|
||||
@@ -57,37 +64,33 @@ class ZipArchive {
|
||||
dataBuffer.deallocate()
|
||||
}
|
||||
|
||||
guard (unzGetCurrentFileInfo64(zipFile, nil, fileNameBuffer, UInt(bufferSize), nil, 0, nil, 0) == UNZ_OK) else {
|
||||
throw ZipArchiveError.badArchive
|
||||
}
|
||||
guard unzGetCurrentFileInfo64(zipFile, nil, fileNameBuffer, UInt(bufferSize), nil, 0, nil, 0) == UNZ_OK else { throw ZipArchiveError.badArchive }
|
||||
|
||||
if let fileURL = URL(string: String(cString: fileNameBuffer)),
|
||||
!fileURL.hasDirectoryPath,
|
||||
requiredFileExtensions.contains(fileURL.pathExtension) {
|
||||
let lastChar = String(cString: fileNameBuffer).suffix(1)
|
||||
let isDirectory = (lastChar == "/" || lastChar == "\\")
|
||||
let fileURL = URL(fileURLWithFileSystemRepresentation: fileNameBuffer, isDirectory: isDirectory, relativeTo: nil)
|
||||
|
||||
if !isDirectory && requiredFileExtensions.contains(fileURL.pathExtension) {
|
||||
var unzippedData = Data()
|
||||
var bytesRead: Int32 = 0
|
||||
repeat {
|
||||
bytesRead = unzReadCurrentFile(zipFile, dataBuffer, UInt32(bufferSize))
|
||||
if (bytesRead > 0) {
|
||||
if bytesRead > 0 {
|
||||
let dataRead = dataBuffer.withMemoryRebound(to: UInt8.self, capacity: bufferSize) {
|
||||
(buf: UnsafeMutablePointer<UInt8>) -> Data in
|
||||
return Data(bytes: buf, count: Int(bytesRead))
|
||||
return Data(bytes: $0, count: Int(bytesRead))
|
||||
}
|
||||
unzippedData.append(dataRead)
|
||||
}
|
||||
} while (bytesRead > 0)
|
||||
results.append((fileName: fileURL.lastPathComponent, contents: unzippedData))
|
||||
} while bytesRead > 0
|
||||
results.append((fileBaseName: fileURL.deletingPathExtension().lastPathComponent, contents: unzippedData))
|
||||
}
|
||||
|
||||
guard (unzCloseCurrentFile(zipFile) == UNZ_OK) else {
|
||||
throw ZipArchiveError.badArchive
|
||||
}
|
||||
guard unzCloseCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
|
||||
|
||||
resultOfGoToNextFile = unzGoToNextFile(zipFile)
|
||||
} while (resultOfGoToNextFile == UNZ_OK)
|
||||
} while resultOfGoToNextFile == UNZ_OK
|
||||
|
||||
if (resultOfGoToNextFile == UNZ_END_OF_LIST_OF_FILE) {
|
||||
if resultOfGoToNextFile == UNZ_END_OF_LIST_OF_FILE {
|
||||
return results
|
||||
} else {
|
||||
throw ZipArchiveError.badArchive
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
enum ZipExporterError: WireGuardAppError {
|
||||
case noTunnelsToExport
|
||||
|
||||
var alertText: AlertText {
|
||||
return (tr("alertNoTunnelsToExportTitle"), tr("alertNoTunnelsToExportMessage"))
|
||||
}
|
||||
}
|
||||
|
||||
class ZipExporter {
|
||||
static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL, completion: @escaping (WireGuardAppError?) -> Void) {
|
||||
|
||||
guard !tunnelConfigurations.isEmpty else {
|
||||
completion(ZipExporterError.noTunnelsToExport)
|
||||
return
|
||||
}
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
var inputsToArchiver: [(fileName: String, contents: Data)] = []
|
||||
var lastTunnelName: String = ""
|
||||
for tunnelConfiguration in tunnelConfigurations {
|
||||
if let contents = tunnelConfiguration.asWgQuickConfig().data(using: .utf8) {
|
||||
let name = tunnelConfiguration.name ?? "untitled"
|
||||
if name.isEmpty || name == lastTunnelName { continue }
|
||||
inputsToArchiver.append((fileName: "\(name).conf", contents: contents))
|
||||
lastTunnelName = name
|
||||
}
|
||||
}
|
||||
do {
|
||||
try ZipArchive.archive(inputs: inputsToArchiver, to: url)
|
||||
} catch let error as WireGuardAppError {
|
||||
DispatchQueue.main.async { completion(error) }
|
||||
return
|
||||
} catch {
|
||||
fatalError()
|
||||
}
|
||||
DispatchQueue.main.async { completion(nil) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
enum ZipImporterError: WireGuardAppError {
|
||||
case noTunnelsInZipArchive
|
||||
|
||||
var alertText: AlertText {
|
||||
return (tr("alertNoTunnelsInImportedZipArchiveTitle"), tr("alertNoTunnelsInImportedZipArchiveMessage"))
|
||||
}
|
||||
}
|
||||
|
||||
class ZipImporter {
|
||||
static func importConfigFiles(from url: URL, completion: @escaping (WireGuardResult<[TunnelConfiguration?]>) -> Void) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
var unarchivedFiles: [(fileBaseName: String, contents: Data)]
|
||||
do {
|
||||
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
|
||||
for (index, unarchivedFile) in unarchivedFiles.enumerated().reversed() {
|
||||
let fileBaseName = unarchivedFile.fileBaseName
|
||||
let trimmedName = fileBaseName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmedName.isEmpty {
|
||||
unarchivedFiles[index].fileBaseName = trimmedName
|
||||
} else {
|
||||
unarchivedFiles.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
if unarchivedFiles.isEmpty {
|
||||
throw ZipImporterError.noTunnelsInZipArchive
|
||||
}
|
||||
} catch let error as WireGuardAppError {
|
||||
DispatchQueue.main.async { completion(.failure(error)) }
|
||||
return
|
||||
} catch {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
unarchivedFiles.sort { $0.fileBaseName < $1.fileBaseName }
|
||||
var configs: [TunnelConfiguration?] = Array(repeating: nil, count: unarchivedFiles.count)
|
||||
for (index, file) in unarchivedFiles.enumerated() {
|
||||
if index > 0 && file == unarchivedFiles[index - 1] {
|
||||
continue
|
||||
}
|
||||
guard let fileContents = String(data: file.contents, encoding: .utf8) else { continue }
|
||||
guard let tunnelConfig = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: file.fileBaseName) else { continue }
|
||||
configs[index] = tunnelConfig
|
||||
}
|
||||
DispatchQueue.main.async { completion(.success(configs)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Network
|
||||
import Foundation
|
||||
|
||||
class DNSResolver {
|
||||
|
||||
static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool {
|
||||
for endpoint in endpoints {
|
||||
guard let endpoint = endpoint else { continue }
|
||||
if !endpoint.hasHostAsIPAddress() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func resolveSync(endpoints: [Endpoint?]) -> [Endpoint?]? {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
if isAllEndpointsAlreadyResolved(endpoints: endpoints) {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
var resolvedEndpoints: [Endpoint?] = Array(repeating: nil, count: endpoints.count)
|
||||
for (index, endpoint) in endpoints.enumerated() {
|
||||
guard let endpoint = endpoint else { continue }
|
||||
if endpoint.hasHostAsIPAddress() {
|
||||
resolvedEndpoints[index] = endpoint
|
||||
} else {
|
||||
let workItem = DispatchWorkItem {
|
||||
resolvedEndpoints[index] = DNSResolver.resolveSync(endpoint: endpoint)
|
||||
}
|
||||
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.wait() // TODO: Timeout?
|
||||
|
||||
var hostnamesWithDnsResolutionFailure = [String]()
|
||||
assert(endpoints.count == resolvedEndpoints.count)
|
||||
for tuple in zip(endpoints, resolvedEndpoints) {
|
||||
let endpoint = tuple.0
|
||||
let resolvedEndpoint = tuple.1
|
||||
if let endpoint = endpoint {
|
||||
if resolvedEndpoint == nil {
|
||||
guard let hostname = endpoint.hostname() else { fatalError() }
|
||||
hostnamesWithDnsResolutionFailure.append(hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hostnamesWithDnsResolutionFailure.isEmpty {
|
||||
wg_log(.error, message: "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure.joined(separator: ", "))")
|
||||
return nil
|
||||
}
|
||||
return resolvedEndpoints
|
||||
}
|
||||
|
||||
private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
|
||||
switch endpoint.host {
|
||||
case .name(let name, _):
|
||||
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
|
||||
var hints = addrinfo(
|
||||
ai_flags: AI_ALL, // We set this to ALL so that we get v4 addresses even on DNS64 networks
|
||||
ai_family: AF_UNSPEC,
|
||||
ai_socktype: SOCK_DGRAM,
|
||||
ai_protocol: IPPROTO_UDP,
|
||||
ai_addrlen: 0,
|
||||
ai_canonname: nil,
|
||||
ai_addr: nil,
|
||||
ai_next: nil)
|
||||
if getaddrinfo(name, "\(endpoint.port)", &hints, &resultPointer) != 0 {
|
||||
return nil
|
||||
}
|
||||
var next = resultPointer
|
||||
var ipv4Address: IPv4Address?
|
||||
var ipv6Address: IPv6Address?
|
||||
while next != nil {
|
||||
let result = next!.pointee
|
||||
next = result.ai_next
|
||||
if result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size {
|
||||
var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
|
||||
ipv4Address = IPv4Address(Data(bytes: &sa4.sin_addr, count: MemoryLayout<in_addr>.size))
|
||||
break // If we found an IPv4 address, we can stop
|
||||
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
|
||||
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
|
||||
ipv6Address = IPv6Address(Data(bytes: &sa6.sin6_addr, count: MemoryLayout<in6_addr>.size))
|
||||
continue // If we already have an IPv6 address, we can skip this one
|
||||
}
|
||||
}
|
||||
freeaddrinfo(resultPointer)
|
||||
|
||||
// We prefer an IPv4 address over an IPv6 address
|
||||
if let ipv4Address = ipv4Address {
|
||||
return Endpoint(host: .ipv4(ipv4Address), port: endpoint.port)
|
||||
} else if let ipv6Address = ipv6Address {
|
||||
return Endpoint(host: .ipv6(ipv6Address), port: endpoint.port)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Endpoint {
|
||||
func withReresolvedIP() -> Endpoint {
|
||||
var ret = self
|
||||
let hostname: String
|
||||
switch host {
|
||||
case .name(let name, _):
|
||||
hostname = name
|
||||
case .ipv4(let address):
|
||||
hostname = "\(address)"
|
||||
case .ipv6(let address):
|
||||
hostname = "\(address)"
|
||||
}
|
||||
|
||||
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
|
||||
var hints = addrinfo(
|
||||
ai_flags: 0, // We set this to zero so that we actually resolve this using DNS64
|
||||
ai_family: AF_UNSPEC,
|
||||
ai_socktype: SOCK_DGRAM,
|
||||
ai_protocol: IPPROTO_UDP,
|
||||
ai_addrlen: 0,
|
||||
ai_canonname: nil,
|
||||
ai_addr: nil,
|
||||
ai_next: nil)
|
||||
if getaddrinfo(hostname, "\(port)", &hints, &resultPointer) != 0 || resultPointer == nil {
|
||||
return ret
|
||||
}
|
||||
let result = resultPointer!.pointee
|
||||
if result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size {
|
||||
var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
|
||||
let addr = IPv4Address(Data(bytes: &sa4.sin_addr, count: MemoryLayout<in_addr>.size))
|
||||
ret = Endpoint(host: .ipv4(addr!), port: port)
|
||||
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
|
||||
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
|
||||
let addr = IPv6Address(Data(bytes: &sa6.sin6_addr, count: MemoryLayout<in6_addr>.size))
|
||||
ret = Endpoint(host: .ipv6(addr!), port: port)
|
||||
}
|
||||
freeaddrinfo(resultPointer)
|
||||
if ret.host != host {
|
||||
wg_log(.debug, message: "DNS64: mapped \(host) to \(ret.host)")
|
||||
} else {
|
||||
wg_log(.debug, message: "DNS64: mapped \(host) to itself.")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
class ErrorNotifier {
|
||||
let activationAttemptId: String?
|
||||
|
||||
init(activationAttemptId: String?) {
|
||||
self.activationAttemptId = activationAttemptId
|
||||
ErrorNotifier.removeLastErrorFile()
|
||||
}
|
||||
|
||||
func notify(_ error: PacketTunnelProviderError) {
|
||||
guard let activationAttemptId = activationAttemptId, let lastErrorFilePath = FileManager.networkExtensionLastErrorFileURL?.path else { return }
|
||||
let errorMessageData = "\(activationAttemptId)\n\(error)".data(using: .utf8)
|
||||
FileManager.default.createFile(atPath: lastErrorFilePath, contents: errorMessageData, attributes: nil)
|
||||
}
|
||||
|
||||
static func removeLastErrorFile() {
|
||||
if let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL {
|
||||
_ = FileManager.deleteFile(at: lastErrorFileURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -27,5 +29,7 @@
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
|
||||
</dict>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,148 +1,103 @@
|
||||
//
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import NetworkExtension
|
||||
import os.log
|
||||
|
||||
enum PacketTunnelProviderError: Error {
|
||||
case invalidOptions
|
||||
case couldNotStartWireGuard
|
||||
case coultNotSetNetworkSettings
|
||||
}
|
||||
|
||||
/// A packet tunnel provider object.
|
||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
// MARK: Properties
|
||||
private var handle: Int32?
|
||||
private var networkMonitor: NWPathMonitor?
|
||||
private var ifname: String?
|
||||
private var packetTunnelSettingsGenerator: PacketTunnelSettingsGenerator?
|
||||
|
||||
private var wgHandle: Int32?
|
||||
private var wgContext: WireGuardContext?
|
||||
deinit {
|
||||
networkMonitor?.cancel()
|
||||
}
|
||||
|
||||
// MARK: NEPacketTunnelProvider
|
||||
//swiftlint:disable:next function_body_length
|
||||
override func startTunnel(options: [String: NSObject]?, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
|
||||
let activationAttemptId = options?["activationAttemptId"] as? String
|
||||
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
||||
|
||||
/// Begin the process of establishing the tunnel.
|
||||
override func startTunnel(options: [String: NSObject]?,
|
||||
completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
|
||||
os_log("Starting tunnel", log: OSLog.default, type: .info)
|
||||
|
||||
guard let options = options else {
|
||||
os_log("Starting tunnel failed: No options passed. Possible connection request from preferences", log: OSLog.default, type: .error)
|
||||
// displayMessage is deprecated API
|
||||
displayMessage("Please use the WireGuard app to start WireGuard tunnels") { (_) in
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.invalidOptions)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let interfaceName = options[.interfaceName] as? String,
|
||||
let wireguardSettings = options[.wireguardSettings] as? String,
|
||||
let remoteAddress = options[.remoteAddress] as? String,
|
||||
let dnsServers = options[.dnsServers] as? [String],
|
||||
let mtu = options[.mtu] as? NSNumber,
|
||||
|
||||
// IPv4 settings
|
||||
let ipv4Addresses = options[.ipv4Addresses] as? [String],
|
||||
let ipv4SubnetMasks = options[.ipv4SubnetMasks] as? [String],
|
||||
let ipv4IncludedRouteAddresses = options[.ipv4IncludedRouteAddresses] as? [String],
|
||||
let ipv4IncludedRouteSubnetMasks = options[.ipv4IncludedRouteSubnetMasks] as? [String],
|
||||
let ipv4ExcludedRouteAddresses = options[.ipv4ExcludedRouteAddresses] as? [String],
|
||||
let ipv4ExcludedRouteSubnetMasks = options[.ipv4ExcludedRouteSubnetMasks] as? [String],
|
||||
|
||||
// IPv6 settings
|
||||
let ipv6Addresses = options[.ipv6Addresses] as? [String],
|
||||
let ipv6NetworkPrefixLengths = options[.ipv6NetworkPrefixLengths] as? [NSNumber],
|
||||
let ipv6IncludedRouteAddresses = options[.ipv6IncludedRouteAddresses] as? [String],
|
||||
let ipv6IncludedRouteNetworkPrefixLengths = options[.ipv6IncludedRouteNetworkPrefixLengths] as? [NSNumber],
|
||||
let ipv6ExcludedRouteAddresses = options[.ipv6ExcludedRouteAddresses] as? [String],
|
||||
let ipv6ExcludedRouteNetworkPrefixLengths = options[.ipv6ExcludedRouteNetworkPrefixLengths] as? [NSNumber]
|
||||
|
||||
else {
|
||||
os_log("Starting tunnel failed: Invalid options passed", log: OSLog.default, type: .error)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.invalidOptions)
|
||||
guard let tunnelProviderProtocol = protocolConfiguration as? NETunnelProviderProtocol,
|
||||
let tunnelConfiguration = tunnelProviderProtocol.asTunnelConfiguration() else {
|
||||
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
configureLogger()
|
||||
wgContext = WireGuardContext(packetFlow: self.packetFlow)
|
||||
|
||||
let handle = connect(interfaceName: interfaceName, settings: wireguardSettings, mtu: mtu.uint16Value)
|
||||
wg_log(.info, message: "Starting tunnel from the " + (activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
|
||||
|
||||
if handle < 0 {
|
||||
os_log("Starting tunnel failed: Could not start WireGuard", log: OSLog.default, type: .error)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
|
||||
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
|
||||
guard let resolvedEndpoints = DNSResolver.resolveSync(endpoints: endpoints) else {
|
||||
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure)
|
||||
return
|
||||
}
|
||||
assert(endpoints.count == resolvedEndpoints.count)
|
||||
|
||||
wgHandle = handle
|
||||
packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints)
|
||||
|
||||
// Network settings
|
||||
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
|
||||
|
||||
// IPv4 settings
|
||||
let ipv4Settings = NEIPv4Settings(addresses: ipv4Addresses, subnetMasks: ipv4SubnetMasks)
|
||||
assert(ipv4IncludedRouteAddresses.count == ipv4IncludedRouteSubnetMasks.count)
|
||||
ipv4Settings.includedRoutes = zip(ipv4IncludedRouteAddresses, ipv4IncludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
assert(ipv4ExcludedRouteAddresses.count == ipv4ExcludedRouteSubnetMasks.count)
|
||||
ipv4Settings.excludedRoutes = zip(ipv4ExcludedRouteAddresses, ipv4ExcludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
networkSettings.ipv4Settings = ipv4Settings
|
||||
|
||||
// IPv6 settings
|
||||
|
||||
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
|
||||
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
|
||||
* very bad, if various network parameters were actually relying on that subnet being
|
||||
* intentionally small. TODO: talk about this with upstream iOS devs.
|
||||
*/
|
||||
let ipv6Settings = NEIPv6Settings(addresses: ipv6Addresses, networkPrefixLengths: ipv6NetworkPrefixLengths.map { NSNumber(value: min(120, $0.intValue)) })
|
||||
assert(ipv6IncludedRouteAddresses.count == ipv6IncludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.includedRoutes = zip(ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
assert(ipv6ExcludedRouteAddresses.count == ipv6ExcludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.excludedRoutes = zip(ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
networkSettings.ipv6Settings = ipv6Settings
|
||||
|
||||
// DNS
|
||||
networkSettings.dnsSettings = NEDNSSettings(servers: dnsServers)
|
||||
|
||||
// MTU
|
||||
if (mtu == 0) {
|
||||
// 0 imples automatic MTU, where we set overhead as 95 bytes,
|
||||
// 80 for WireGuard and the 15 to make sure WireGuard's padding will work.
|
||||
networkSettings.tunnelOverheadBytes = 95
|
||||
} else {
|
||||
networkSettings.mtu = mtu
|
||||
}
|
||||
|
||||
setTunnelNetworkSettings(networkSettings) { (error) in
|
||||
setTunnelNetworkSettings(packetTunnelSettingsGenerator!.generateNetworkSettings()) { error in
|
||||
if let error = error {
|
||||
os_log("Starting tunnel failed: Error setting network settings: %s", log: OSLog.default, type: .error, error.localizedDescription)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
|
||||
wg_log(.error, message: "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
|
||||
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
|
||||
} else {
|
||||
startTunnelCompletionHandler(nil /* No errors */)
|
||||
self.networkMonitor = NWPathMonitor()
|
||||
self.networkMonitor!.pathUpdateHandler = self.pathUpdate
|
||||
self.networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor"))
|
||||
|
||||
let fileDescriptor = (self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32) ?? -1
|
||||
if fileDescriptor < 0 {
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
|
||||
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
|
||||
return
|
||||
}
|
||||
var ifnameSize = socklen_t(IFNAMSIZ)
|
||||
let ifnamePtr = UnsafeMutablePointer<CChar>.allocate(capacity: Int(ifnameSize))
|
||||
ifnamePtr.initialize(repeating: 0, count: Int(ifnameSize))
|
||||
if getsockopt(fileDescriptor, 2 /* SYSPROTO_CONTROL */, 2 /* UTUN_OPT_IFNAME */, ifnamePtr, &ifnameSize) == 0 {
|
||||
self.ifname = String(cString: ifnamePtr)
|
||||
}
|
||||
ifnamePtr.deallocate()
|
||||
wg_log(.info, message: "Tunnel interface is \(self.ifname ?? "unknown")")
|
||||
let handle = self.packetTunnelSettingsGenerator!.uapiConfiguration().withGoString { return wgTurnOn($0, fileDescriptor) }
|
||||
if handle < 0 {
|
||||
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(handle)")
|
||||
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartBackend)
|
||||
return
|
||||
}
|
||||
self.handle = handle
|
||||
startTunnelCompletionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Begin the process of stopping the tunnel.
|
||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
os_log("Stopping tunnel", log: OSLog.default, type: .info)
|
||||
wgContext?.closeTunnel()
|
||||
if let handle = wgHandle {
|
||||
networkMonitor?.cancel()
|
||||
networkMonitor = nil
|
||||
|
||||
ErrorNotifier.removeLastErrorFile()
|
||||
|
||||
wg_log(.info, staticMessage: "Stopping tunnel")
|
||||
if let handle = handle {
|
||||
wgTurnOff(handle)
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
private func configureLogger() {
|
||||
wgSetLogger { (level, msgCStr) in
|
||||
Logger.configureGlobal(withFilePath: FileManager.networkExtensionLogFileURL?.path)
|
||||
wgSetLogger { level, msgC in
|
||||
guard let msgC = msgC else { return }
|
||||
let logType: OSLogType
|
||||
switch level {
|
||||
case 0:
|
||||
@@ -154,110 +109,29 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
default:
|
||||
logType = .default
|
||||
}
|
||||
let msg = (msgCStr != nil) ? String(cString: msgCStr!) : ""
|
||||
os_log("%{public}s", log: OSLog.default, type: logType, msg)
|
||||
wg_log(logType, message: String(cString: msgC))
|
||||
}
|
||||
}
|
||||
|
||||
private func connect(interfaceName: String, settings: String, mtu: UInt16) -> Int32 { // swiftlint:disable:this cyclomatic_complexity
|
||||
return withStringsAsGoStrings(interfaceName, settings) { (nameGoStr, settingsGoStr) -> Int32 in
|
||||
return withUnsafeMutablePointer(to: &wgContext) { (wgCtxPtr) -> Int32 in
|
||||
return wgTurnOn(nameGoStr, settingsGoStr, mtu, { (wgCtxPtr, buf, len) -> Int in
|
||||
autoreleasepool {
|
||||
// read_fn: Read from the TUN interface and pass it on to WireGuard
|
||||
guard let wgCtxPtr = wgCtxPtr else { return 0 }
|
||||
guard let buf = buf else { return 0 }
|
||||
let wgContext = wgCtxPtr.bindMemory(to: WireGuardContext.self, capacity: 1).pointee
|
||||
var isTunnelClosed = false
|
||||
let packet = wgContext.readPacket(isTunnelClosed: &isTunnelClosed)
|
||||
if isTunnelClosed { return -1 }
|
||||
guard let packetData = packet?.data else { return 0 }
|
||||
if packetData.count <= len {
|
||||
packetData.copyBytes(to: buf, count: packetData.count)
|
||||
return packetData.count
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}, { (wgCtxPtr, buf, len) -> Int in
|
||||
autoreleasepool {
|
||||
// write_fn: Receive packets from WireGuard and write to the TUN interface
|
||||
guard let wgCtxPtr = wgCtxPtr else { return 0 }
|
||||
guard let buf = buf else { return 0 }
|
||||
guard len > 0 else { return 0 }
|
||||
let wgContext = wgCtxPtr.bindMemory(to: WireGuardContext.self, capacity: 1).pointee
|
||||
let ipVersionBits = (buf[0] & 0xf0) >> 4
|
||||
let ipVersion: sa_family_t? = {
|
||||
if ipVersionBits == 4 { return sa_family_t(AF_INET) } // IPv4
|
||||
if ipVersionBits == 6 { return sa_family_t(AF_INET6) } // IPv6
|
||||
return nil
|
||||
}()
|
||||
guard let protocolFamily = ipVersion else { fatalError("Unknown IP version") }
|
||||
let packet = NEPacket(data: Data(bytes: buf, count: len), protocolFamily: protocolFamily)
|
||||
var isTunnelClosed = false
|
||||
let isWritten = wgContext.writePacket(packet: packet, isTunnelClosed: &isTunnelClosed)
|
||||
if isTunnelClosed { return -1 }
|
||||
if isWritten {
|
||||
return len
|
||||
}
|
||||
return 0
|
||||
}
|
||||
},
|
||||
wgCtxPtr)
|
||||
}
|
||||
private func pathUpdate(path: Network.NWPath) {
|
||||
guard let handle = handle, let packetTunnelSettingsGenerator = packetTunnelSettingsGenerator else { return }
|
||||
wg_log(.debug, message: "Network change detected with \(path.status) route and interface order \(path.availableInterfaces)")
|
||||
_ = packetTunnelSettingsGenerator.endpointUapiConfiguration().withGoString { return wgSetConfig(handle, $0) }
|
||||
var interfaces = path.availableInterfaces
|
||||
if let ifname = ifname {
|
||||
interfaces = interfaces.filter { $0.name != ifname }
|
||||
}
|
||||
if let ifscope = interfaces.first?.index {
|
||||
wgBindInterfaceScope(handle, Int32(ifscope))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WireGuardContext {
|
||||
private var packetFlow: NEPacketTunnelFlow
|
||||
private var outboundPackets: [NEPacket] = []
|
||||
private var isTunnelClosed: Bool = false
|
||||
private var readPacketCondition = NSCondition()
|
||||
|
||||
init(packetFlow: NEPacketTunnelFlow) {
|
||||
self.packetFlow = packetFlow
|
||||
}
|
||||
|
||||
func closeTunnel() {
|
||||
isTunnelClosed = true
|
||||
readPacketCondition.signal()
|
||||
}
|
||||
|
||||
func packetsRead(packets: [NEPacket]) {
|
||||
readPacketCondition.lock()
|
||||
outboundPackets.append(contentsOf: packets)
|
||||
readPacketCondition.unlock()
|
||||
readPacketCondition.signal()
|
||||
}
|
||||
|
||||
func readPacket(isTunnelClosed: inout Bool) -> NEPacket? {
|
||||
if outboundPackets.isEmpty {
|
||||
readPacketCondition.lock()
|
||||
packetFlow.readPacketObjects(completionHandler: packetsRead)
|
||||
while outboundPackets.isEmpty && !self.isTunnelClosed {
|
||||
readPacketCondition.wait()
|
||||
}
|
||||
readPacketCondition.unlock()
|
||||
}
|
||||
isTunnelClosed = self.isTunnelClosed
|
||||
if !outboundPackets.isEmpty {
|
||||
return outboundPackets.removeFirst()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writePacket(packet: NEPacket, isTunnelClosed: inout Bool) -> Bool {
|
||||
isTunnelClosed = self.isTunnelClosed
|
||||
return packetFlow.writePacketObjects([packet])
|
||||
}
|
||||
}
|
||||
|
||||
private func withStringsAsGoStrings<R>(_ str1: String, _ str2: String, closure: (gostring_t, gostring_t) -> R) -> R {
|
||||
return str1.withCString { (s1cStr) -> R in
|
||||
let gstr1 = gostring_t(p: s1cStr, n: str1.utf8.count)
|
||||
return str2.withCString { (s2cStr) -> R in
|
||||
let gstr2 = gostring_t(p: s2cStr, n: str2.utf8.count)
|
||||
return closure(gstr1, gstr2)
|
||||
extension String {
|
||||
func withGoString<R>(_ call: (gostring_t) -> R) -> R {
|
||||
func helper(_ pointer: UnsafePointer<Int8>?, _ call: (gostring_t) -> R) -> R {
|
||||
return call(gostring_t(p: pointer, n: utf8.count))
|
||||
}
|
||||
return helper(self, call)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import NetworkExtension
|
||||
|
||||
class PacketTunnelSettingsGenerator {
|
||||
let tunnelConfiguration: TunnelConfiguration
|
||||
let resolvedEndpoints: [Endpoint?]
|
||||
|
||||
init(tunnelConfiguration: TunnelConfiguration, resolvedEndpoints: [Endpoint?]) {
|
||||
self.tunnelConfiguration = tunnelConfiguration
|
||||
self.resolvedEndpoints = resolvedEndpoints
|
||||
}
|
||||
|
||||
func endpointUapiConfiguration() -> String {
|
||||
var wgSettings = ""
|
||||
for (index, peer) in tunnelConfiguration.peers.enumerated() {
|
||||
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
|
||||
if let endpoint = resolvedEndpoints[index]?.withReresolvedIP() {
|
||||
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
|
||||
wgSettings.append("endpoint=\(endpoint.stringRepresentation)\n")
|
||||
}
|
||||
}
|
||||
return wgSettings
|
||||
}
|
||||
|
||||
func uapiConfiguration() -> String {
|
||||
var wgSettings = ""
|
||||
let privateKey = tunnelConfiguration.interface.privateKey.hexEncodedString()
|
||||
wgSettings.append("private_key=\(privateKey)\n")
|
||||
if let listenPort = tunnelConfiguration.interface.listenPort {
|
||||
wgSettings.append("listen_port=\(listenPort)\n")
|
||||
}
|
||||
if !tunnelConfiguration.peers.isEmpty {
|
||||
wgSettings.append("replace_peers=true\n")
|
||||
}
|
||||
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
|
||||
for (index, peer) in tunnelConfiguration.peers.enumerated() {
|
||||
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
|
||||
if let preSharedKey = peer.preSharedKey {
|
||||
wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n")
|
||||
}
|
||||
if let endpoint = resolvedEndpoints[index]?.withReresolvedIP() {
|
||||
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
|
||||
wgSettings.append("endpoint=\(endpoint.stringRepresentation)\n")
|
||||
}
|
||||
let persistentKeepAlive = peer.persistentKeepAlive ?? 0
|
||||
wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n")
|
||||
if !peer.allowedIPs.isEmpty {
|
||||
wgSettings.append("replace_allowed_ips=true\n")
|
||||
peer.allowedIPs.forEach { wgSettings.append("allowed_ip=\($0.stringRepresentation)\n") }
|
||||
}
|
||||
}
|
||||
return wgSettings
|
||||
}
|
||||
|
||||
func generateNetworkSettings() -> NEPacketTunnelNetworkSettings {
|
||||
/* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for
|
||||
* a tunnel to have no endpoint, or for there to be many endpoints, in
|
||||
* which case, displaying a single one in settings doesn't really
|
||||
* make sense. So, we fill it in with this placeholder, which is not
|
||||
* a valid IP address that will actually route over the Internet.
|
||||
*/
|
||||
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
||||
|
||||
let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation }
|
||||
let dnsSettings = NEDNSSettings(servers: dnsServerStrings)
|
||||
dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS
|
||||
networkSettings.dnsSettings = dnsSettings
|
||||
|
||||
var mtu = tunnelConfiguration.interface.mtu ?? 0
|
||||
|
||||
/* 0 means automatic MTU. In theory, we should just do
|
||||
* `networkSettings.tunnelOverheadBytes = 80` but in
|
||||
* practice there are too many broken networks out there.
|
||||
* Instead set it to 1280. Boohoo. Maybe someday we'll
|
||||
* add a nob, maybe, or iOS will do probing for us.
|
||||
*/
|
||||
if mtu == 0 {
|
||||
mtu = 1280
|
||||
}
|
||||
|
||||
networkSettings.mtu = NSNumber(value: mtu)
|
||||
|
||||
let (ipv4Routes, ipv6Routes) = routes()
|
||||
let (ipv4IncludedRoutes, ipv6IncludedRoutes) = includedRoutes()
|
||||
|
||||
let ipv4Settings = NEIPv4Settings(addresses: ipv4Routes.map { $0.destinationAddress }, subnetMasks: ipv4Routes.map { $0.destinationSubnetMask })
|
||||
ipv4Settings.includedRoutes = ipv4IncludedRoutes
|
||||
networkSettings.ipv4Settings = ipv4Settings
|
||||
|
||||
let ipv6Settings = NEIPv6Settings(addresses: ipv6Routes.map { $0.destinationAddress }, networkPrefixLengths: ipv6Routes.map { $0.destinationNetworkPrefixLength })
|
||||
ipv6Settings.includedRoutes = ipv6IncludedRoutes
|
||||
networkSettings.ipv6Settings = ipv6Settings
|
||||
|
||||
return networkSettings
|
||||
}
|
||||
|
||||
private func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
|
||||
let length: UInt8 = addressRange.networkPrefixLength
|
||||
assert(length <= 32)
|
||||
var octets: [UInt8] = [0, 0, 0, 0]
|
||||
let subnetMask: UInt32 = length > 0 ? ~UInt32(0) << (32 - length) : UInt32(0)
|
||||
octets[0] = UInt8(truncatingIfNeeded: subnetMask >> 24)
|
||||
octets[1] = UInt8(truncatingIfNeeded: subnetMask >> 16)
|
||||
octets[2] = UInt8(truncatingIfNeeded: subnetMask >> 8)
|
||||
octets[3] = UInt8(truncatingIfNeeded: subnetMask)
|
||||
return octets.map { String($0) }.joined(separator: ".")
|
||||
}
|
||||
|
||||
private func routes() -> ([NEIPv4Route], [NEIPv6Route]) {
|
||||
var ipv4Routes = [NEIPv4Route]()
|
||||
var ipv6Routes = [NEIPv6Route]()
|
||||
for addressRange in tunnelConfiguration.interface.addresses {
|
||||
if addressRange.address is IPv4Address {
|
||||
ipv4Routes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: ipv4SubnetMaskString(of: addressRange)))
|
||||
} else if addressRange.address is IPv6Address {
|
||||
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
|
||||
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
|
||||
* very bad, if various network parameters were actually relying on that subnet being
|
||||
* intentionally small. TODO: talk about this with upstream iOS devs.
|
||||
*/
|
||||
ipv6Routes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: min(120, addressRange.networkPrefixLength))))
|
||||
}
|
||||
}
|
||||
return (ipv4Routes, ipv6Routes)
|
||||
}
|
||||
|
||||
private func includedRoutes() -> ([NEIPv4Route], [NEIPv6Route]) {
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
var ipv6IncludedRoutes = [NEIPv6Route]()
|
||||
for peer in tunnelConfiguration.peers {
|
||||
for addressRange in peer.allowedIPs {
|
||||
if addressRange.address is IPv4Address {
|
||||
ipv4IncludedRoutes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: ipv4SubnetMaskString(of: addressRange)))
|
||||
} else if addressRange.address is IPv6Address {
|
||||
ipv6IncludedRoutes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: addressRange.networkPrefixLength)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return (ipv4IncludedRoutes, ipv6IncludedRoutes)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Data {
|
||||
func hexEncodedString() -> String {
|
||||
return self.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
#include "../../wireguard-go-bridge/wireguard.h"
|
||||
#include "wireguard-go-version.h"
|
||||
#include "ringlogger.h"
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>packet-tunnel-provider</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+1
-1
Submodule wireguard-go updated: 738d027f0b...8fde8334dc
@@ -1,25 +1,25 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
#
|
||||
# Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
# Copyright (C) 2018-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
|
||||
# These are generally passed to us by xcode, but we set working defaults for standalone compilation too.
|
||||
ARCHS ?= arm64 armv7
|
||||
SDK_NAME ?= iphoneos
|
||||
SDKROOT ?= $(shell xcrun --sdk $(SDK_NAME) --show-sdk-path)
|
||||
CONFIGURATION_BUILD_DIR ?= $(CURDIR)/out
|
||||
CONFIGURATION_TEMP_DIR ?= $(CURDIR)/.tmp
|
||||
|
||||
export CC ?= clang
|
||||
LIPO ?= lipo
|
||||
DESTDIR ?= $(CONFIGURATION_BUILD_DIR)
|
||||
BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge
|
||||
|
||||
UPSTREAM_FILES := $(filter-out %/main.go %/queueconstants.go,$(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go)) ../wireguard-go/go.mod ../wireguard-go/go.sum
|
||||
DOWNSTREAM_FILES := $(wildcard src/*.go) $(wildcard src/*/*.go)
|
||||
|
||||
ARCHS ?= arm64 armv7 x86_64
|
||||
CFLAGS_PREFIX := $(DEPLOYMENT_TARGET_CLANG_FLAG_PREFIX)$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)) -isysroot $(SDKROOT) -arch
|
||||
GOARCH_arm64 := arm64
|
||||
GOARCH_armv7 := arm
|
||||
GOARCH_x86_64 := amd64
|
||||
DEPLOYMENT_CFLAGS := $(DEPLOYMENT_TARGET_CLANG_FLAG_PREFIX)$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME))
|
||||
CGO_FLAGS_x86_64 := -arch x86_64 -isysroot $(shell xcrun --sdk iphonesimulator --show-sdk-path) $(DEPLOYMENT_CFLAGS)
|
||||
CGO_FLAGS_arm64 := -arch arm64 -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) $(DEPLOYMENT_CFLAGS)
|
||||
CGO_FLAGS_armv7 := -arch armv7 -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) $(DEPLOYMENT_CFLAGS)
|
||||
CC_x86_64 := $(shell xcrun --sdk iphonesimulator --find clang)
|
||||
CC_arm64 := $(shell xcrun --sdk iphoneos --find clang)
|
||||
CC_armv7 := $(shell xcrun --sdk iphoneos --find clang)
|
||||
CONFIGURATION_BUILD_DIR ?= $(CURDIR)/out
|
||||
CONFIGURATION_TEMP_DIR ?= $(CURDIR)/.tmp
|
||||
DESTDIR ?= $(CONFIGURATION_BUILD_DIR)
|
||||
BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge
|
||||
export GOOS := darwin
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
@@ -28,7 +28,7 @@ version-header: $(DESTDIR)/wireguard-go-version.h
|
||||
|
||||
GOBUILDARCH := $(GOARCH_$(shell uname -m))
|
||||
GOBUILDOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
|
||||
GOBUILDVERSION := 1.11.2
|
||||
GOBUILDVERSION := 1.11.4
|
||||
GOBUILDTARBALL := https://dl.google.com/go/go$(GOBUILDVERSION).$(GOBUILDOS)-$(GOBUILDARCH).tar.gz
|
||||
GOBUILDVERSION_NEEDED := go version go$(GOBUILDVERSION) $(GOBUILDOS)/$(GOBUILDARCH)
|
||||
export GOROOT := $(BUILDDIR)/goroot
|
||||
@@ -57,16 +57,15 @@ $(foreach FILE,$(UPSTREAM_FILES),$(eval $(call copy-src-to-build,../wireguard-go
|
||||
$(foreach FILE,$(DOWNSTREAM_FILES),$(eval $(call copy-src-to-build,src/,$(FILE))))
|
||||
|
||||
$(BUILDDIR)/.prepared: $(GOROOT)/bin/go
|
||||
cd "$(BUILDDIR)" || exit $$?; $(foreach ARCH,$(ARCHS),CC="$(CC_$(ARCH))" CGO_CFLAGS="$(CGO_FLAGS_$(ARCH))" CGO_LDFLAGS="$(CGO_FLAGS_$(ARCH))" GOARCH="$(GOARCH_$(ARCH))" go get -tags ios || { ret=$$?; chmod -fR +w "$(GOPATH)/pkg/mod"; rm -rf "$(GOPATH)/pkg/mod"; exit $$ret; };)
|
||||
cd "$(BUILDDIR)" || exit $$?; $(foreach ARCH,$(ARCHS),CGO_CFLAGS="$(CFLAGS_PREFIX) $(ARCH)" CGO_LDFLAGS="$(CFLAGS_PREFIX) $(ARCH)" GOARCH="$(GOARCH_$(ARCH))" go get -tags ios || { ret=$$?; chmod -fR +w "$(GOPATH)/pkg/mod"; rm -rf "$(GOPATH)/pkg/mod"; exit $$ret; };)
|
||||
chmod -fR +w "$(GOPATH)/pkg/mod"
|
||||
touch "$@"
|
||||
|
||||
define libwg-go-a
|
||||
$(BUILDDIR)/libwg-go-$(1).a: $(BUILDDIR)/.prepared
|
||||
cd "$(BUILDDIR)" || exit $$$$?; \
|
||||
CC="$(CC_$(1))" \
|
||||
CGO_CFLAGS="$(CGO_FLAGS_$(1))" \
|
||||
CGO_LDFLAGS="$(CGO_FLAGS_$(1))" \
|
||||
CGO_CFLAGS="$(CFLAGS_PREFIX) $(ARCH)" \
|
||||
CGO_LDFLAGS="$(CFLAGS_PREFIX) $(ARCH)" \
|
||||
GOARCH="$(GOARCH_$(1))" \
|
||||
go build -tags ios -ldflags=-w -v -o "$(BUILDDIR)/libwg-go-$(1).a" -buildmode c-archive && go version > "$(BUILDDIR)/.gobuildversion"; \
|
||||
ret=$$$$?; \
|
||||
@@ -80,16 +79,11 @@ $(DESTDIR)/wireguard-go-version.h: ../wireguard-go/version.go
|
||||
|
||||
$(DESTDIR)/libwg-go.a: $(foreach ARCH,$(ARCHS),$(BUILDDIR)/libwg-go-$(ARCH).a)
|
||||
@mkdir -vp "$(DESTDIR)"
|
||||
xcrun --sdk iphoneos lipo -create -output "$@" $^
|
||||
|
||||
$(DESTDIR)/example: example.c $(DESTDIR)/libwg-go.a
|
||||
xcrun --sdk iphoneos clang -framework CoreFoundation -isysroot "$(shell xcrun --sdk iphoneos --show-sdk-path)" -arch arm64 -arch armv7 -L"$(DESTDIR)" -lwg-go -o "$@" "$<"
|
||||
|
||||
example: $(DESTDIR)/example
|
||||
$(LIPO) -create -output "$@" $^
|
||||
|
||||
clean:
|
||||
rm -rf "$(BUILDDIR)" "$(DESTDIR)/libwg-go.a" "$(DESTDIR)/example" "$(DESTDIR)/wireguard-go-version.h"
|
||||
rm -rf "$(BUILDDIR)" "$(DESTDIR)/libwg-go.a" "$(DESTDIR)/wireguard-go-version.h"
|
||||
|
||||
install: build
|
||||
|
||||
.PHONY: clean build example version-header install
|
||||
.PHONY: clean build version-header install
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "wireguard.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static struct {
|
||||
int something;
|
||||
} ctx;
|
||||
|
||||
static bool is_closed = false;
|
||||
|
||||
ssize_t do_read(void *ctx, unsigned char *buf, size_t len)
|
||||
{
|
||||
printf("Reading from instance with ctx %p into buffer %p of length %zu\n", ctx, buf, len);
|
||||
sleep(1);
|
||||
return is_closed ? -1 : 0;
|
||||
}
|
||||
|
||||
ssize_t do_write(void *ctx, unsigned char *buf, size_t len)
|
||||
{
|
||||
printf("Writing from instance with ctx %p into buffer %p of length %zu\n", ctx, buf, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
void do_log(int level, const char *msg)
|
||||
{
|
||||
printf("Log level %d: %s", level, msg);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
int handle;
|
||||
|
||||
printf("WireGuard Go Version %s\n", wgVersion());
|
||||
wgSetLogger(do_log);
|
||||
handle = wgTurnOn((gostring_t){ .p = "test", .n = 4 }, (gostring_t){ .p = "", .n = 0 }, 0, do_read, do_write, &ctx);
|
||||
sleep(5);
|
||||
is_closed = true;
|
||||
wgTurnOff(handle);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* Copyright (C) 2018-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
@@ -15,6 +15,7 @@ import "C"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"git.zx2c4.com/wireguard-go/tun"
|
||||
"golang.org/x/sys/unix"
|
||||
"io/ioutil"
|
||||
@@ -25,22 +26,20 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var loggerFunc unsafe.Pointer
|
||||
var versionString *C.char
|
||||
|
||||
type CLogger struct {
|
||||
level C.int
|
||||
interfaceName string
|
||||
level C.int
|
||||
}
|
||||
|
||||
func (l *CLogger) Write(p []byte) (int, error) {
|
||||
if uintptr(loggerFunc) == 0 {
|
||||
return 0, errors.New("No logger initialized")
|
||||
}
|
||||
message := C.CString(l.interfaceName + ": " + string(p))
|
||||
message := C.CString(string(p))
|
||||
C.callLogger(loggerFunc, l.level, message)
|
||||
C.free(unsafe.Pointer(message))
|
||||
return len(p), nil
|
||||
@@ -75,23 +74,21 @@ func wgSetLogger(loggerFn uintptr) {
|
||||
}
|
||||
|
||||
//export wgTurnOn
|
||||
func wgTurnOn(ifnameRef string, settings string, mtu uint16, readFn uintptr, writeFn uintptr, ctx uintptr) int32 {
|
||||
interfaceName := string([]byte(ifnameRef))
|
||||
|
||||
func wgTurnOn(settings string, tunFd int32) int32 {
|
||||
logger := &Logger{
|
||||
Debug: log.New(&CLogger{level: 0, interfaceName: interfaceName}, "", 0),
|
||||
Info: log.New(&CLogger{level: 1, interfaceName: interfaceName}, "", 0),
|
||||
Error: log.New(&CLogger{level: 2, interfaceName: interfaceName}, "", 0),
|
||||
Debug: log.New(&CLogger{level: 0}, "", 0),
|
||||
Info: log.New(&CLogger{level: 1}, "", 0),
|
||||
Error: log.New(&CLogger{level: 2}, "", 0),
|
||||
}
|
||||
|
||||
logger.Debug.Println("Debug log enabled")
|
||||
|
||||
tun := tun.CreateTUN(mtu, unsafe.Pointer(readFn), unsafe.Pointer(writeFn), unsafe.Pointer(ctx))
|
||||
tun, _, err := tun.CreateTUNFromFD(int(tunFd))
|
||||
if err != nil {
|
||||
logger.Error.Println(err)
|
||||
return -1
|
||||
}
|
||||
logger.Info.Println("Attaching to interface")
|
||||
device := NewDevice(tun, logger)
|
||||
|
||||
logger.Debug.Println("Interface has MTU", device.tun.mtu)
|
||||
|
||||
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(settings)), bufio.NewWriter(ioutil.Discard))
|
||||
setError := ipcSetOperation(device, bufferedSettings)
|
||||
if setError != nil {
|
||||
@@ -125,6 +122,66 @@ func wgTurnOff(tunnelHandle int32) {
|
||||
device.Close()
|
||||
}
|
||||
|
||||
//export wgSetConfig
|
||||
func wgSetConfig(tunnelHandle int32, settings string) int64 {
|
||||
device, ok := tunnelHandles[tunnelHandle]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(settings)), bufio.NewWriter(ioutil.Discard))
|
||||
err := ipcSetOperation(device, bufferedSettings)
|
||||
if err != nil {
|
||||
device.log.Error.Println(err)
|
||||
return err.Code
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export wgBindInterfaceScope
|
||||
func wgBindInterfaceScope(tunnelHandle int32, ifscope int32) {
|
||||
var operr error
|
||||
device, ok := tunnelHandles[tunnelHandle]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
device.log.Info.Printf("Binding sockets to interface %d\n", ifscope)
|
||||
bind := device.net.bind.(*NativeBind)
|
||||
for bind.ipv4 != nil {
|
||||
fd, err := bind.ipv4.SyscallConn()
|
||||
if err != nil {
|
||||
device.log.Error.Printf("Unable to bind v4 socket to interface:", err)
|
||||
break
|
||||
}
|
||||
err = fd.Control(func(fd uintptr) {
|
||||
operr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, int(ifscope))
|
||||
})
|
||||
if err == nil {
|
||||
err = operr
|
||||
}
|
||||
if err != nil {
|
||||
device.log.Error.Printf("Unable to bind v4 socket to interface:", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
for bind.ipv6 != nil {
|
||||
fd, err := bind.ipv6.SyscallConn()
|
||||
if err != nil {
|
||||
device.log.Error.Printf("Unable to bind v6 socket to interface:", err)
|
||||
break
|
||||
}
|
||||
err = fd.Control(func(fd uintptr) {
|
||||
operr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, int(ifscope))
|
||||
})
|
||||
if err == nil {
|
||||
err = operr
|
||||
}
|
||||
if err != nil {
|
||||
device.log.Error.Printf("Unable to bind v6 socket to interface:", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//export wgVersion
|
||||
func wgVersion() *C.char {
|
||||
return versionString
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright (C) 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
@@ -8,9 +8,9 @@ package main
|
||||
/* Fit within memory limits for iOS */
|
||||
|
||||
const (
|
||||
QueueOutboundSize = 1024
|
||||
QueueInboundSize = 1024
|
||||
QueueHandshakeSize = 1024
|
||||
MaxSegmentSize = 1700
|
||||
PreallocatedBuffersPerPool = 1024
|
||||
QueueOutboundSize = 1024
|
||||
QueueInboundSize = 1024
|
||||
QueueHandshakeSize = 1024
|
||||
MaxSegmentSize = 1700
|
||||
PreallocatedBuffersPerPool = 1024
|
||||
)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"git.zx2c4.com/wireguard-go/rwcancel"
|
||||
"golang.org/x/sys/unix"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
func CreateTUNFromFD(tunFd int) (TUNDevice, string, error) {
|
||||
file := os.NewFile(uintptr(tunFd), "/dev/tun")
|
||||
tun := &nativeTun{
|
||||
tunFile: file,
|
||||
fd: file.Fd(),
|
||||
events: make(chan TUNEvent, 5),
|
||||
errors: make(chan error, 5),
|
||||
}
|
||||
var err error
|
||||
tun.rwcancel, err = rwcancel.NewRWCancel(tunFd)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
name, err := tun.Name()
|
||||
if err != nil {
|
||||
tun.rwcancel.Cancel()
|
||||
return nil, "", err
|
||||
}
|
||||
tunIfindex, err := func() (int, error) {
|
||||
iface, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return iface.Index, nil
|
||||
}()
|
||||
if err != nil {
|
||||
tun.tunFile.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
tun.routeSocket, err = unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
|
||||
if err != nil {
|
||||
tun.tunFile.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
go tun.routineRouteListener(tunIfindex)
|
||||
|
||||
return tun, name, nil
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package tun
|
||||
|
||||
// #include <sys/types.h>
|
||||
// static ssize_t callFnWithCtx(const void *func, const void *ctx, const void *buffer, size_t len)
|
||||
// {
|
||||
// return ((ssize_t(*)(const void *, const unsigned char *, size_t))func)(ctx, buffer, len);
|
||||
// }
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type nativeTun struct {
|
||||
events chan TUNEvent
|
||||
mtu int
|
||||
readFn unsafe.Pointer
|
||||
writeFn unsafe.Pointer
|
||||
ctx unsafe.Pointer
|
||||
}
|
||||
|
||||
func CreateTUN(mtu uint16, readFn unsafe.Pointer, writeFn unsafe.Pointer, ctx unsafe.Pointer) TUNDevice {
|
||||
if mtu == 0 {
|
||||
/* 0 means automatic MTU, which iOS makes outerMTU-80-15. The 80 is for
|
||||
* WireGuard and the 15 ensures our padding will work. Therefore, it's
|
||||
* safe to have this code assume a massive MTU.
|
||||
*/
|
||||
mtu = ^mtu
|
||||
}
|
||||
tun := &nativeTun{
|
||||
events: make(chan TUNEvent, 10),
|
||||
mtu: int(mtu),
|
||||
readFn: readFn,
|
||||
writeFn: writeFn,
|
||||
ctx: ctx,
|
||||
}
|
||||
tun.events <- TUNEventUp
|
||||
return tun
|
||||
}
|
||||
|
||||
func (tun *nativeTun) Name() (string, error) {
|
||||
return "tun", nil
|
||||
}
|
||||
|
||||
func (tun *nativeTun) File() *os.File {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tun *nativeTun) Events() chan TUNEvent {
|
||||
return tun.events
|
||||
}
|
||||
|
||||
func (tun *nativeTun) Read(buff []byte, offset int) (int, error) {
|
||||
ret := C.callFnWithCtx(tun.readFn, tun.ctx, unsafe.Pointer(&buff[offset]), C.size_t(len(buff) - offset))
|
||||
if ret < 0 {
|
||||
return 0, syscall.Errno(-ret)
|
||||
}
|
||||
return int(ret), nil
|
||||
}
|
||||
|
||||
func (tun *nativeTun) Write(buff []byte, offset int) (int, error) {
|
||||
ret := C.callFnWithCtx(tun.writeFn, tun.ctx, unsafe.Pointer(&buff[offset]), C.size_t(len(buff) - offset))
|
||||
if ret < 0 {
|
||||
return 0, syscall.Errno(-ret)
|
||||
}
|
||||
return int(ret), nil
|
||||
}
|
||||
|
||||
func (tun *nativeTun) Close() error {
|
||||
if tun.events != nil {
|
||||
close(tun.events)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tun *nativeTun) setMTU(n int) error {
|
||||
tun.mtu = n
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tun *nativeTun) MTU() (int, error) {
|
||||
return tun.mtu, nil
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright (C) 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef WIREGUARD_H
|
||||
@@ -10,11 +10,12 @@
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct { const char *p; size_t n; } gostring_t;
|
||||
typedef ssize_t(*read_write_fn_t)(void *ctx, unsigned char *buf, size_t len);
|
||||
typedef void(*logger_fn_t)(int level, const char *msg);
|
||||
extern void wgSetLogger(logger_fn_t logger_fn);
|
||||
extern int wgTurnOn(gostring_t ifname, gostring_t settings, uint16_t mtu, read_write_fn_t read_fn, read_write_fn_t write_fn, void *ctx);
|
||||
extern int wgTurnOn(gostring_t settings, int32_t tun_fd);
|
||||
extern void wgTurnOff(int handle);
|
||||
extern int64_t wgSetConfig(int handle, gostring_t settings);
|
||||
extern void wgBindInterfaceScope(int handle, int32_t ifscope);
|
||||
extern char *wgVersion();
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user