Compare commits
557 Commits
0.0.201811
...
0.0.201903
Author | SHA1 | Date | |
---|---|---|---|
a9b925c69b | |||
f93f9d62f4 | |||
fc163fc9ff | |||
adc5a7cac2 | |||
0dd22ca45a | |||
bca9fead5e | |||
51822f722a | |||
121d223229 | |||
439fb6bbac | |||
9c8231dcf7 | |||
0440c4a33a | |||
01be43aa7a | |||
e29c6900e5 | |||
a334c25aff | |||
f56b2ad968 | |||
503ac6c8a2 | |||
5f30e021ef | |||
d748382fce | |||
63299a2752 | |||
b7f8f74b56 | |||
8e5a9215de | |||
64925cab89 | |||
062b4d4b16 | |||
d9bdc61fb9 | |||
0ae8d25134 | |||
574d8433b3 | |||
bd339e2876 | |||
fff75adfe1 | |||
01604dd8d1 | |||
bdeb89a9e5 | |||
36dc252512 | |||
5941bf181c | |||
7a450089c0 | |||
5d757982ba | |||
3767a12983 | |||
9795b0609a | |||
0f98312d15 | |||
f81275812c | |||
b2b5e0e379 | |||
a6f80135ef | |||
e23c221aff | |||
50bc994762 | |||
3e05da4486 | |||
c750f28c67 | |||
f6c70500a7 | |||
663923864c | |||
9250780ffc | |||
9bc17034dd | |||
db6f0729c6 | |||
4503c11b0c | |||
fe4f8b666d | |||
90c0f7e92e | |||
3afcee04be | |||
202e7a4890 | |||
d7b16ffb1f | |||
b1dabf5a00 | |||
a389bd93cb | |||
b2a2110d8c | |||
5ed28907ec | |||
ab6d714070 | |||
d3df8734c2 | |||
ea5996abe0 | |||
ce405f856e | |||
98a967acc8 | |||
b01d09dfb5 | |||
7a580e8941 | |||
39fb52a2e3 | |||
69a064d954 | |||
eb684ef711 | |||
b0eff424f9 | |||
c195760b15 | |||
ba3f0db92c | |||
5031a7db4c | |||
a355232e09 | |||
6f7214ff38 | |||
4c88f477a2 | |||
2fb9d6af71 | |||
38ac66071c | |||
910fdfc321 | |||
c38a88988b | |||
cf51344444 | |||
fda36b571d | |||
1be3224d7a | |||
ca088e9ddc | |||
fcca2d4fec | |||
58181a4d40 | |||
cf816ede13 | |||
fbac282bdc | |||
61f5e017c8 | |||
4547e01283 | |||
5792db22a6 | |||
9d5aa1d8fa | |||
6331b81b5d | |||
77f929789c | |||
b5b72b309f | |||
966fa7909b | |||
115059f2bb | |||
e53c2d4d17 | |||
0a3a5ee900 | |||
7720307fc9 | |||
ea827e2ebd | |||
91b1734b7a | |||
bac4851e95 | |||
0e2556544e | |||
2ec51ba8cf | |||
998bbd73e4 | |||
38a6ba7091 | |||
407b367c8d | |||
a231410c52 | |||
f518c00722 | |||
0539929d0c | |||
05547861b6 | |||
9eed5fd898 | |||
1b8b9ed7ee | |||
ef6af03412 | |||
a99a755c34 | |||
ecd66defe5 | |||
1f3ec042e0 | |||
631e9bb70d | |||
446c3e3698 | |||
02e9172940 | |||
8676f3a663 | |||
394a0cbeb0 | |||
868fee0477 | |||
0cddb562fc | |||
bebcaa012b | |||
ed8dc516dc | |||
8c3557a907 | |||
a26d620f11 | |||
30a73a75fd | |||
71d26b4122 | |||
71525c9d4e | |||
02a96d4566 | |||
466db151b8 | |||
1be133f269 | |||
80de2ac6ac | |||
8a6a60482c | |||
657ec34d19 | |||
f7a31ca7bb | |||
3c61db3a21 | |||
618d89941a | |||
cbc602245e | |||
ca61e09536 | |||
4ff6105053 | |||
4134baced1 | |||
0c5739db82 | |||
1f51ff6b17 | |||
08e5d65045 | |||
1189b3d700 | |||
f292a0ec7a | |||
3b29578524 | |||
70ac48ceba | |||
acecc70397 | |||
b0bb2e993a | |||
d1f83d167e | |||
a796c6c485 | |||
6ad3487a9d | |||
eabeb8ff05 | |||
52eec55d36 | |||
3c80490273 | |||
c36a9e4ffd | |||
812e660491 | |||
2fe9f83ba5 | |||
22625e8cc4 | |||
ab3cbee6a2 | |||
fadef52e1b | |||
19f353127e | |||
e5a76be6fd | |||
76527ef0f8 | |||
11e44f9ae5 | |||
77d4a02139 | |||
54f45cb3f8 | |||
704de3b26c | |||
d05b735703 | |||
9f362e8cb0 | |||
2677efc9bf | |||
2aad8cf0e8 | |||
668c4a475c | |||
557b093232 | |||
6f9fa35c5a | |||
d37e230ee0 | |||
e812683d6c | |||
6d8e5cf3ed | |||
244a2df2dd | |||
0848765f50 | |||
8951ea338f | |||
273ee04450 | |||
e2b068af1a | |||
4e2f4e7124 | |||
6509009afc | |||
0b2a4c2811 | |||
e1198b6e08 | |||
d7a88300f6 | |||
b22aeaeb20 | |||
e0798b8a97 | |||
1bc2177883 | |||
15517c4c3d | |||
f6c9ce2d71 | |||
5aa0f1a25f | |||
602639ee25 | |||
d06887d435 | |||
470e4e7f7f | |||
86165d25f7 | |||
69b973efdc | |||
dc8f27c5c3 | |||
796342ddec | |||
b7a5b8eff1 | |||
8a0245190c | |||
98775bf8a0 | |||
7d5eb476e4 | |||
c477d24d67 | |||
bf9cb092a9 | |||
dbd5108475 | |||
47c5f23a0a | |||
a2871e63a7 | |||
811714e21a | |||
f63c9fd598 | |||
e29cf19fdd | |||
26ea353933 | |||
cd5427ef92 | |||
595f7943e2 | |||
55f022688d | |||
96bd50504b | |||
97fec0d992 | |||
470532d146 | |||
321b88864c | |||
ba731e0099 | |||
70848c04de | |||
13e8c6b178 | |||
53e915c578 | |||
bc8ea55023 | |||
e0af06844d | |||
8980b5a524 | |||
df8ab96139 | |||
4a8366421f | |||
c9ee549a2e | |||
f5059ce55b | |||
5a73244ec9 | |||
922b6f76b2 | |||
fc9e2de72c | |||
80977b95de | |||
bbeb732ef3 | |||
94c4922913 | |||
fc03c635c1 | |||
b0612df990 | |||
c2a6241b5c | |||
96fa6d3ba6 | |||
64fe415879 | |||
59bfa7f1df | |||
c2633987c3 | |||
f7b2f73015 | |||
c72f7056b3 | |||
cb778fe7e0 | |||
f3c2904241 | |||
df8b400850 | |||
252d940d34 | |||
efb64b1959 | |||
dfc4b37518 | |||
60cfceec4f | |||
361830a69e | |||
f6ea25573b | |||
de12c27d5b | |||
a221cb566b | |||
f33cd0b6fd | |||
38bb0faf86 | |||
8d9c5e2950 | |||
09f4be17de | |||
60e18dfdd5 | |||
5bc0c5b2b4 | |||
4a4eeb4a21 | |||
ada7db3dca | |||
c946c0ea48 | |||
4a4690b5fa | |||
37fce31d16 | |||
7934d6b0c7 | |||
98e9088aba | |||
2c81c3a379 | |||
04f6ee0f11 | |||
545f8c88f4 | |||
6a27626fc0 | |||
fb1607d4a2 | |||
51a2c272b9 | |||
b5751b6321 | |||
110012dbcc | |||
5c7a149167 | |||
629009d3be | |||
d7d4355f5e | |||
55d6961a2f | |||
c8cd663a05 | |||
a754c4d7ab | |||
95415cd917 | |||
b32b897181 | |||
d5c1acb57e | |||
573f9640de | |||
f6772dc353 | |||
0cbe66df99 | |||
3cd33ebe8f | |||
c7a40d3cb0 | |||
d02b0fd10e | |||
09d7a5229a | |||
1ca5407b85 | |||
10982a57ef | |||
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 |
7
.gitignore
vendored
@ -19,6 +19,7 @@ DerivedData
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xcscheme
|
||||
|
||||
## Other
|
||||
*.xccheckout
|
||||
@ -38,3 +39,9 @@ fastlane/screenshots
|
||||
fastlane/test_output
|
||||
Preview.html
|
||||
output
|
||||
|
||||
# Wireguard specific
|
||||
WireGuard/WireGuard/Config/Developer.xcconfig
|
||||
|
||||
# Vim
|
||||
.*.sw*
|
||||
|
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "wireguard-go"]
|
||||
path = wireguard-go
|
||||
url = https://git.zx2c4.com/wireguard-go
|
@ -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
|
351
COPYING
@ -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.
|
||||
|
140
MOBILECONFIG.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Installing WireGuard tunnels using Configuration Profiles
|
||||
|
||||
WireGuard configurations can be installed using Configuration Profiles
|
||||
through .mobileconfig files.
|
||||
|
||||
### Top-level payload entries
|
||||
|
||||
A .mobileconfig file is a plist file in XML format. The top-level XML item is a top-level payload dictionary (dict). This payload dictionary should contain the following keys:
|
||||
|
||||
- `PayloadDisplayName` (string): The name of the configuration profile, visible when installing the profile
|
||||
|
||||
- `PayloadType` (string): Should be `Configuration`
|
||||
|
||||
- `PayloadVersion` (integer): Should be `1`
|
||||
|
||||
- `PayloadIdentifier` (string): A reverse-DNS style unique identifier for the profile file.
|
||||
If you install another .mobileconfig file with the same identifier, the new one
|
||||
overwrites the old one.
|
||||
|
||||
- `PayloadUUID` (string): A randomly generated UUID for this payload
|
||||
|
||||
- `PayloadContent` (array): Should contain an array of payload dictionaries.
|
||||
Each of these payload dictionaries can represent a WireGuard tunnel
|
||||
configuration.
|
||||
|
||||
Here's an example .mobileconfig with the above fields filled in:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-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>PayloadDisplayName</key>
|
||||
<string>WireGuard Demo Configuration Profile</string>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.your-org.wireguard.FCC9BF80-C540-44C1-B243-521FDD1B2905</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>F346AAF4-53A2-4FA1-ACA3-EEE74DBED029</string>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<!-- An array of WireGuard configuration payload dictionaries -->
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### WireGuard payload entries
|
||||
|
||||
Each WireGuard configuration payload dictionary should contain the following
|
||||
keys:
|
||||
|
||||
- `PayloadDisplayName` (string): Should be `VPN`
|
||||
|
||||
- `PayloadType` (string): Should be `com.apple.vpn.managed`
|
||||
|
||||
- `PayloadVersion` (integer): Should be `1`
|
||||
|
||||
- `PayloadIdentifier` (string): A reverse-DNS style unique identifier for the WireGuard configuration profile.
|
||||
|
||||
- `PayloadUUID` (string): A randomly generated UUID for this payload
|
||||
|
||||
- `UserDefinedName` (string): The name of the WireGuard tunnel.
|
||||
This name shall be used to represent the tunnel in the WireGuard app, and in the System UI for VPNs (Settings > VPN on iOS, System Preferences > Network on macOS).
|
||||
|
||||
- `VPNType` (string): Should be `VPN`
|
||||
|
||||
- `VPNSubType` (string): Should be set as the bundle identifier of the WireGuard app.
|
||||
|
||||
- iOS: `com.wireguard.ios`
|
||||
- macOS: `com.wireguard.macos`
|
||||
|
||||
- `VendorConfig` (dict): Should be a dictionary with the following key:
|
||||
|
||||
- `WgQuickConfig` (string): Should be a WireGuard configuration in [wg-quick(8)] / [wg(8)] format.
|
||||
The keys 'FwMark', 'Table', 'PreUp', 'PostUp', 'PreDown', 'PostDown' and 'SaveConfig' are not supported.
|
||||
|
||||
- `VPN` (dict): Should be a dictionary with the following keys:
|
||||
|
||||
- `RemoteAddress` (string): A non-empty string.
|
||||
This string is displayed as the server name in the System UI for
|
||||
VPNs (Settings > VPN on iOS, System Preferences > Network on macOS).
|
||||
|
||||
- `AuthenticationMethod` (string): Should be `Password`
|
||||
|
||||
Here's an example WireGuard configuration payload dictionary:
|
||||
|
||||
```xml
|
||||
<!-- A WireGuard configuration payload dictionary -->
|
||||
<dict>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>VPN</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.vpn.managed</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.your-org.wireguard.demo-profile-1.demo-tunnel</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>44CDFE9F-4DC7-472A-956F-61C68055117C</string>
|
||||
<key>UserDefinedName</key>
|
||||
<string>Demo from MobileConfig file</string>
|
||||
<key>VPNType</key>
|
||||
<string>VPN</string>
|
||||
<key>VPNSubType</key>
|
||||
<string>com.wireguard.ios</string>
|
||||
<key>VendorConfig</key>
|
||||
<dict>
|
||||
<key>WgQuickConfig</key>
|
||||
<string>
|
||||
[Interface]
|
||||
PrivateKey = mInDaw06K0NgfULRObHJjkWD3ahUC8XC1tVjIf6W+Vo=
|
||||
Address = 10.10.1.0/24
|
||||
DNS = 1.1.1.1, 1.0.0.1
|
||||
|
||||
[Peer]
|
||||
PublicKey = JRI8Xc0zKP9kXk8qP84NdUQA04h6DLfFbwJn4g+/PFs=
|
||||
Endpoint = demo.wireguard.com:12912
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
</string>
|
||||
</dict>
|
||||
<key>VPN</key>
|
||||
<dict>
|
||||
<key>RemoteAddress</key>
|
||||
<string>demo.wireguard.com:12912</string>
|
||||
<key>AuthenticationMethod</key>
|
||||
<string>Password</string>
|
||||
</dict>
|
||||
</dict>
|
||||
```
|
||||
|
||||
### Caveats
|
||||
|
||||
Configurations added via .mobileconfig will not be migrated into keychain until the WireGuard application is opened once.
|
||||
|
||||
[wg-quick(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
|
||||
[wg(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg.8
|
28
README.md
@ -1,26 +1,14 @@
|
||||
|
||||
# ***DO NOT USE: CODEBASE HORRIBLY BROKEN***
|
||||
|
||||
----
|
||||
|
||||
# [WireGuard](https://www.wireguard.com/) for iOS
|
||||
# [WireGuard](https://www.wireguard.com/) for iOS and macOS
|
||||
|
||||
## Building
|
||||
|
||||
- Clone this repo:
|
||||
- Clone this repo recursively:
|
||||
|
||||
```
|
||||
$ git clone https://git.zx2c4.com/wireguard-ios
|
||||
$ git clone --recursive https://git.zx2c4.com/wireguard-ios
|
||||
$ cd wireguard-ios
|
||||
```
|
||||
|
||||
- Init and update submodule:
|
||||
|
||||
```
|
||||
$ git submodule init
|
||||
$ git submodule update
|
||||
```
|
||||
|
||||
- Rename and populate developer team ID file:
|
||||
|
||||
```
|
||||
@ -28,13 +16,19 @@ $ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/
|
||||
$ vim WireGuard/WireGuard/Config/Developer.xcconfig
|
||||
```
|
||||
|
||||
- Open project in XCode:
|
||||
- Install swiftlint and go:
|
||||
|
||||
```
|
||||
$ brew install swiftlint go
|
||||
```
|
||||
|
||||
- Open project in Xcode:
|
||||
|
||||
```
|
||||
$ open ./WireGuard/WireGuard.xcodeproj
|
||||
```
|
||||
|
||||
- Flip switches, press buttons, and make whirling noises until XCode builds it.
|
||||
- Flip switches, press buttons, and make whirling noises until Xcode builds it.
|
||||
|
||||
## MIT License
|
||||
|
||||
|
25
WireGuard/.swiftlint.yml
Normal file
@ -0,0 +1,25 @@
|
||||
disabled_rules:
|
||||
- line_length
|
||||
- trailing_whitespace
|
||||
- todo
|
||||
- cyclomatic_complexity
|
||||
- file_length
|
||||
- type_body_length
|
||||
- function_body_length
|
||||
- nesting
|
||||
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
|
||||
variable_name:
|
||||
min_length:
|
||||
warning: 0
|
46
WireGuard/Shared/FileManager+Extension.swift
Normal file
@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension FileManager {
|
||||
static var appGroupId: String? {
|
||||
#if os(iOS)
|
||||
let appGroupIdInfoDictionaryKey = "com.wireguard.ios.app_group_id"
|
||||
#elseif os(macOS)
|
||||
let appGroupIdInfoDictionaryKey = "com.wireguard.macos.app_group_id"
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
return Bundle.main.object(forInfoDictionaryKey: appGroupIdInfoDictionaryKey) as? String
|
||||
}
|
||||
private static var sharedFolderURL: URL? {
|
||||
guard let appGroupId = FileManager.appGroupId 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 logFileURL: URL? {
|
||||
return sharedFolderURL?.appendingPathComponent("tunnel-log.bin")
|
||||
}
|
||||
|
||||
static var networkExtensionLastErrorFileURL: URL? {
|
||||
return sharedFolderURL?.appendingPathComponent("last-error.txt")
|
||||
}
|
||||
|
||||
static func deleteFile(at url: URL) -> Bool {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
117
WireGuard/Shared/Keychain.swift
Normal file
@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
class Keychain {
|
||||
static func openReference(called ref: Data) -> String? {
|
||||
var result: CFTypeRef?
|
||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecValuePersistentRef as String: ref,
|
||||
kSecReturnData as String: true] as CFDictionary,
|
||||
&result)
|
||||
if ret != errSecSuccess || result == nil {
|
||||
wg_log(.error, message: "Unable to open config from keychain: \(ret)")
|
||||
return nil
|
||||
}
|
||||
guard let data = result as? Data else { return nil }
|
||||
return String(data: data, encoding: String.Encoding.utf8)
|
||||
}
|
||||
|
||||
static func makeReference(containing value: String, called name: String, previouslyReferencedBy oldRef: Data? = nil) -> Data? {
|
||||
var ret: OSStatus
|
||||
guard var id = Bundle.main.bundleIdentifier else {
|
||||
wg_log(.error, staticMessage: "Unable to determine bundle identifier")
|
||||
return nil
|
||||
}
|
||||
if id.hasSuffix(".network-extension") {
|
||||
id.removeLast(".network-extension".count)
|
||||
}
|
||||
var items: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrLabel as String: "WireGuard Tunnel: " + name,
|
||||
kSecAttrAccount as String: name + ": " + UUID().uuidString,
|
||||
kSecAttrDescription as String: "wg-quick(8) config",
|
||||
kSecAttrService as String: id,
|
||||
kSecValueData as String: value.data(using: .utf8) as Any,
|
||||
kSecReturnPersistentRef as String: true]
|
||||
|
||||
#if os(iOS)
|
||||
items[kSecAttrAccessGroup as String] = FileManager.appGroupId
|
||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
#elseif os(macOS)
|
||||
items[kSecAttrSynchronizable as String] = false
|
||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||
|
||||
guard let extensionPath = Bundle.main.builtInPlugInsURL?.appendingPathComponent("WireGuardNetworkExtension.appex").path else {
|
||||
wg_log(.error, staticMessage: "Unable to determine app extension path")
|
||||
return nil
|
||||
}
|
||||
var extensionApp: SecTrustedApplication?
|
||||
var mainApp: SecTrustedApplication?
|
||||
ret = SecTrustedApplicationCreateFromPath(extensionPath, &extensionApp)
|
||||
if ret != kOSReturnSuccess || extensionApp == nil {
|
||||
wg_log(.error, message: "Unable to create keychain extension trusted application object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
ret = SecTrustedApplicationCreateFromPath(nil, &mainApp)
|
||||
if ret != errSecSuccess || mainApp == nil {
|
||||
wg_log(.error, message: "Unable to create keychain local trusted application object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
var access: SecAccess?
|
||||
ret = SecAccessCreate((items[kSecAttrLabel as String] as? String)! as CFString,
|
||||
[extensionApp!, mainApp!] as CFArray,
|
||||
&access)
|
||||
if ret != errSecSuccess || access == nil {
|
||||
wg_log(.error, message: "Unable to create keychain ACL object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
items[kSecAttrAccess as String] = access!
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
|
||||
var ref: CFTypeRef?
|
||||
ret = SecItemAdd(items as CFDictionary, &ref)
|
||||
if ret != errSecSuccess || ref == nil {
|
||||
wg_log(.error, message: "Unable to add config to keychain: \(ret)")
|
||||
return nil
|
||||
}
|
||||
if let oldRef = oldRef {
|
||||
deleteReference(called: oldRef)
|
||||
}
|
||||
return ref as? Data
|
||||
}
|
||||
|
||||
static func deleteReference(called ref: Data) {
|
||||
let ret = SecItemDelete([kSecValuePersistentRef as String: ref] as CFDictionary)
|
||||
if ret != errSecSuccess {
|
||||
wg_log(.error, message: "Unable to delete config from keychain: \(ret)")
|
||||
}
|
||||
}
|
||||
|
||||
static func deleteReferences(except whitelist: Set<Data>) {
|
||||
var result: CFTypeRef?
|
||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: Bundle.main.bundleIdentifier as Any,
|
||||
kSecMatchLimit as String: kSecMatchLimitAll,
|
||||
kSecReturnPersistentRef as String: true] as CFDictionary,
|
||||
&result)
|
||||
if ret != errSecSuccess || result == nil {
|
||||
return
|
||||
}
|
||||
guard let items = result as? [Data] else { return }
|
||||
for item in items {
|
||||
if !whitelist.contains(item) {
|
||||
deleteReference(called: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func verifyReference(called ref: Data) -> Bool {
|
||||
return SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecValuePersistentRef as String: ref] as CFDictionary,
|
||||
nil) == errSecSuccess
|
||||
}
|
||||
}
|
65
WireGuard/Shared/Logging/Logger.swift
Normal file
@ -0,0 +1,65 @@
|
||||
// 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
|
||||
var tag: String
|
||||
|
||||
init(tagged tag: String, withFilePath filePath: String) throws {
|
||||
guard let log = open_log(filePath) else { throw LoggerError.openFailure }
|
||||
self.log = log
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
deinit {
|
||||
close_log(self.log)
|
||||
}
|
||||
|
||||
func log(message: String) {
|
||||
write_msg_to_log(log, tag, message.trimmingCharacters(in: .newlines))
|
||||
}
|
||||
|
||||
func writeLog(to targetFile: String) -> Bool {
|
||||
return write_log_to_file(targetFile, self.log) == 0
|
||||
}
|
||||
|
||||
static func configureGlobal(tagged tag: String, 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
|
||||
}
|
||||
guard let logger = try? Logger(tagged: tag, withFilePath: filePath) else {
|
||||
os_log("Unable to open log file for writing. Log will not be saved to file.", log: OSLog.default, type: .error)
|
||||
return
|
||||
}
|
||||
Logger.global = logger
|
||||
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)
|
||||
}
|
173
WireGuard/Shared/Logging/ringlogger.c
Normal file
@ -0,0 +1,173 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdatomic.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 = 2048,
|
||||
MAGIC = 0xabadbeefU
|
||||
};
|
||||
|
||||
struct log_line {
|
||||
atomic_uint_fast64_t time_ns;
|
||||
char line[MAX_LOG_LINE_LENGTH];
|
||||
};
|
||||
|
||||
struct log {
|
||||
atomic_uint_fast32_t next_index;
|
||||
struct log_line lines[MAX_LINES];
|
||||
uint32_t magic;
|
||||
};
|
||||
|
||||
void write_msg_to_log(struct log *log, const char *tag, const char *msg)
|
||||
{
|
||||
uint32_t index;
|
||||
struct log_line *line;
|
||||
struct timespec ts;
|
||||
|
||||
// Race: This isn't synchronized with the fetch_add below, so items might be slightly out of order.
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
|
||||
// Race: More than MAX_LINES writers and this will clash.
|
||||
index = atomic_fetch_add(&log->next_index, 1);
|
||||
line = &log->lines[index % MAX_LINES];
|
||||
|
||||
// Race: Before this line executes, we'll display old data after new data.
|
||||
atomic_store(&line->time_ns, 0);
|
||||
memset(line->line, 0, MAX_LOG_LINE_LENGTH);
|
||||
|
||||
snprintf(line->line, MAX_LOG_LINE_LENGTH, "[%s] %s", tag, msg);
|
||||
atomic_store(&line->time_ns, ts.tv_sec * 1000000000ULL + ts.tv_nsec);
|
||||
|
||||
msync(&log->next_index, sizeof(log->next_index), MS_ASYNC);
|
||||
msync(line, sizeof(*line), MS_ASYNC);
|
||||
}
|
||||
|
||||
int write_log_to_file(const char *file_name, const struct log *input_log)
|
||||
{
|
||||
struct log *log;
|
||||
uint32_t l, i;
|
||||
FILE *file;
|
||||
int ret;
|
||||
|
||||
log = malloc(sizeof(*log));
|
||||
if (!log)
|
||||
return -errno;
|
||||
memcpy(log, input_log, sizeof(*log));
|
||||
|
||||
file = fopen(file_name, "w");
|
||||
if (!file) {
|
||||
free(log);
|
||||
return -errno;
|
||||
}
|
||||
|
||||
for (l = 0, i = log->next_index; l < MAX_LINES; ++l, ++i) {
|
||||
const struct log_line *line = &log->lines[i % MAX_LINES];
|
||||
time_t seconds = line->time_ns / 1000000000ULL;
|
||||
uint32_t useconds = (line->time_ns % 1000000000ULL) / 1000ULL;
|
||||
struct tm tm;
|
||||
|
||||
if (!line->time_ns)
|
||||
continue;
|
||||
|
||||
if (!localtime_r(&seconds, &tm))
|
||||
goto err;
|
||||
|
||||
if (fprintf(file, "%04d-%02d-%02d %02d:%02d:%02d.%06d: %s\n",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec, useconds,
|
||||
line->line) < 0)
|
||||
goto err;
|
||||
|
||||
|
||||
}
|
||||
errno = 0;
|
||||
|
||||
err:
|
||||
ret = -errno;
|
||||
fclose(file);
|
||||
free(log);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t view_lines_from_cursor(const struct log *input_log, uint32_t cursor, void(*cb)(const char *, uint64_t))
|
||||
{
|
||||
struct log *log;
|
||||
uint32_t l, i = cursor;
|
||||
|
||||
log = malloc(sizeof(*log));
|
||||
if (!log)
|
||||
return cursor;
|
||||
memcpy(log, input_log, sizeof(*log));
|
||||
|
||||
if (i == -1)
|
||||
i = log->next_index;
|
||||
|
||||
for (l = 0; l < MAX_LINES; ++l, ++i) {
|
||||
const struct log_line *line = &log->lines[i % MAX_LINES];
|
||||
|
||||
if (cursor != -1 && i % MAX_LINES == log->next_index % MAX_LINES)
|
||||
break;
|
||||
|
||||
if (!line->time_ns) {
|
||||
if (cursor == -1)
|
||||
continue;
|
||||
else
|
||||
break;
|
||||
}
|
||||
cb(line->line, line->time_ns);
|
||||
cursor = (i + 1) % MAX_LINES;
|
||||
}
|
||||
free(log);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
18
WireGuard/Shared/Logging/ringlogger.h
Normal file
@ -0,0 +1,18 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef RINGLOGGER_H
|
||||
#define RINGLOGGER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct log;
|
||||
void write_msg_to_log(struct log *log, const char *tag, const char *msg);
|
||||
int write_log_to_file(const char *file_name, const struct log *input_log);
|
||||
uint32_t view_lines_from_cursor(const struct log *input_log, uint32_t cursor, void(*)(const char *, uint64_t));
|
||||
struct log *open_log(const char *file_name);
|
||||
void close_log(struct log *log);
|
||||
|
||||
#endif
|
63
WireGuard/Shared/Logging/test_ringlogger.c
Normal file
@ -0,0 +1,63 @@
|
||||
#include "ringlogger.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <inttypes.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
static void forkwrite(void)
|
||||
{
|
||||
struct log *log = open_log("/tmp/test_log");
|
||||
char c[512];
|
||||
int i, base;
|
||||
bool in_fork = !fork();
|
||||
|
||||
base = 10000 * in_fork;
|
||||
for (i = 0; i < 1024; ++i) {
|
||||
snprintf(c, 512, "bla bla bla %d", base + i);
|
||||
write_msg_to_log(log, "HMM", c);
|
||||
}
|
||||
|
||||
|
||||
if (in_fork)
|
||||
_exit(0);
|
||||
wait(NULL);
|
||||
|
||||
write_log_to_file("/dev/stdout", log);
|
||||
close_log(log);
|
||||
}
|
||||
|
||||
static void writetext(const char *text)
|
||||
{
|
||||
struct log *log = open_log("/tmp/test_log");
|
||||
write_msg_to_log(log, "TXT", text);
|
||||
close_log(log);
|
||||
}
|
||||
|
||||
static void show_line(const char *line, uint64_t time_ns)
|
||||
{
|
||||
printf("%" PRIu64 ": %s\n", time_ns, line);
|
||||
}
|
||||
|
||||
static void follow(void)
|
||||
{
|
||||
uint32_t cursor = -1;
|
||||
struct log *log = open_log("/tmp/test_log");
|
||||
|
||||
for (;;) {
|
||||
cursor = view_lines_from_cursor(log, cursor, show_line);
|
||||
usleep(1000 * 300);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (!strcmp(argv[1], "fork"))
|
||||
forkwrite();
|
||||
else if (!strcmp(argv[1], "write"))
|
||||
writetext(argv[2]);
|
||||
else if (!strcmp(argv[1], "follow"))
|
||||
follow();
|
||||
return 0;
|
||||
}
|
35
WireGuard/Shared/Model/DNSServer.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
54
WireGuard/Shared/Model/Data+KeyEncoding.swift
Normal file
@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
func isKey() -> Bool {
|
||||
return self.count == WG_KEY_LEN
|
||||
}
|
||||
|
||||
func hexKey() -> String? {
|
||||
if self.count != WG_KEY_LEN {
|
||||
return nil
|
||||
}
|
||||
var out = Data(repeating: 0, count: Int(WG_KEY_LEN_HEX))
|
||||
out.withUnsafeMutableBytes { outBytes in
|
||||
self.withUnsafeBytes { inBytes in
|
||||
key_to_hex(outBytes, inBytes)
|
||||
}
|
||||
}
|
||||
out.removeLast()
|
||||
return String(data: out, encoding: .ascii)
|
||||
}
|
||||
|
||||
init?(hexKey hexString: String) {
|
||||
self.init(repeating: 0, count: Int(WG_KEY_LEN))
|
||||
|
||||
if !self.withUnsafeMutableBytes { key_from_hex($0, hexString) } {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func base64Key() -> String? {
|
||||
if self.count != WG_KEY_LEN {
|
||||
return nil
|
||||
}
|
||||
var out = Data(repeating: 0, count: Int(WG_KEY_LEN_BASE64))
|
||||
out.withUnsafeMutableBytes { outBytes in
|
||||
self.withUnsafeBytes { inBytes in
|
||||
key_to_base64(outBytes, inBytes)
|
||||
}
|
||||
}
|
||||
out.removeLast()
|
||||
return String(data: out, encoding: .ascii)
|
||||
}
|
||||
|
||||
init?(base64Key base64String: String) {
|
||||
self.init(repeating: 0, count: Int(WG_KEY_LEN))
|
||||
|
||||
if !self.withUnsafeMutableBytes { key_from_base64($0, base64String) } {
|
||||
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
|
||||
}
|
||||
}
|
67
WireGuard/Shared/Model/IPAddressRange.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
33
WireGuard/Shared/Model/InterfaceConfiguration.swift
Normal file
@ -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,70 @@
|
||||
// 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 {
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration, previouslyFrom old: NEVPNProtocol? = nil) {
|
||||
self.init()
|
||||
|
||||
guard let name = tunnelConfiguration.name else { return nil }
|
||||
guard let appId = Bundle.main.bundleIdentifier else { return nil }
|
||||
providerBundleIdentifier = "\(appId).network-extension"
|
||||
passwordReference = Keychain.makeReference(containing: tunnelConfiguration.asWgQuickConfig(), called: name, previouslyReferencedBy: old?.passwordReference)
|
||||
if passwordReference == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
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? {
|
||||
if let passwordReference = passwordReference,
|
||||
let config = Keychain.openReference(called: passwordReference) {
|
||||
return try? TunnelConfiguration(fromWgQuickConfig: config, called: name)
|
||||
}
|
||||
if let oldConfig = providerConfiguration?["WgQuickConfig"] as? String {
|
||||
return try? TunnelConfiguration(fromWgQuickConfig: oldConfig, called: name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func destroyConfigurationReference() {
|
||||
guard let ref = passwordReference else { return }
|
||||
Keychain.deleteReference(called: ref)
|
||||
}
|
||||
|
||||
func verifyConfigurationReference() -> Data? {
|
||||
guard let ref = passwordReference else { return nil }
|
||||
return Keychain.verifyReference(called: ref) ? ref : nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func migrateConfigurationIfNeeded(called name: String) -> Bool {
|
||||
/* This is how we did things before we switched to putting items
|
||||
* in the keychain. But it's still useful to keep the migration
|
||||
* around so that .mobileconfig files are easier.
|
||||
*/
|
||||
guard let oldConfig = providerConfiguration?["WgQuickConfig"] as? String else { return false }
|
||||
providerConfiguration = nil
|
||||
guard passwordReference == nil else { return true }
|
||||
wg_log(.debug, message: "Migrating tunnel configuration '\(name)'")
|
||||
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
|
||||
return true
|
||||
}
|
||||
}
|
51
WireGuard/Shared/Model/PeerConfiguration.swift
Normal file
@ -0,0 +1,51 @@
|
||||
// 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?
|
||||
var rxBytes: UInt64?
|
||||
var txBytes: UInt64?
|
||||
var lastHandshakeTime: Date?
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
32
WireGuard/Shared/Model/String+ArrayConversion.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
251
WireGuard/Shared/Model/TunnelConfiguration+WgQuickConfig.swift
Normal file
@ -0,0 +1,251 @@
|
||||
// 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(String.SubSequence)
|
||||
case noInterface
|
||||
case multipleInterfaces
|
||||
case interfaceHasNoPrivateKey
|
||||
case interfaceHasInvalidPrivateKey(String)
|
||||
case interfaceHasInvalidListenPort(String)
|
||||
case interfaceHasInvalidAddress(String)
|
||||
case interfaceHasInvalidDNS(String)
|
||||
case interfaceHasInvalidMTU(String)
|
||||
case interfaceHasUnrecognizedKey(String)
|
||||
case peerHasNoPublicKey
|
||||
case peerHasInvalidPublicKey(String)
|
||||
case peerHasInvalidPreSharedKey(String)
|
||||
case peerHasInvalidAllowedIP(String)
|
||||
case peerHasInvalidEndpoint(String)
|
||||
case peerHasInvalidPersistentKeepAlive(String)
|
||||
case peerHasInvalidTransferBytes(String)
|
||||
case peerHasInvalidLastHandshakeTime(String)
|
||||
case peerHasUnrecognizedKey(String)
|
||||
case multiplePeersWithSamePublicKey
|
||||
case multipleEntriesForKey(String)
|
||||
}
|
||||
|
||||
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: .whitespacesAndNewlines)
|
||||
let lowercasedLine = trimmedLine.lowercased()
|
||||
|
||||
if !trimmedLine.isEmpty {
|
||||
if let equalsIndex = trimmedLine.firstIndex(of: "=") {
|
||||
// Line contains an attribute
|
||||
let keyWithCase = trimmedLine[..<equalsIndex].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let key = keyWithCase.lowercased()
|
||||
let value = trimmedLine[trimmedLine.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
|
||||
if let presentValue = attributes[key] {
|
||||
if keysWithMultipleEntriesAllowed.contains(key) {
|
||||
attributes[key] = presentValue + "," + value
|
||||
} else {
|
||||
throw ParseError.multipleEntriesForKey(keyWithCase)
|
||||
}
|
||||
} else {
|
||||
attributes[key] = value
|
||||
}
|
||||
let interfaceSectionKeys: Set<String> = ["privatekey", "listenport", "address", "dns", "mtu"]
|
||||
let peerSectionKeys: Set<String> = ["publickey", "presharedkey", "allowedips", "endpoint", "persistentkeepalive"]
|
||||
if parserState == .inInterfaceSection {
|
||||
guard interfaceSectionKeys.contains(key) else {
|
||||
throw ParseError.interfaceHasUnrecognizedKey(keyWithCase)
|
||||
}
|
||||
} else if parserState == .inPeerSection {
|
||||
guard peerSectionKeys.contains(key) else {
|
||||
throw ParseError.peerHasUnrecognizedKey(keyWithCase)
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
|
||||
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
|
||||
interfaceConfiguration = interface
|
||||
} else if parserState == .inPeerSection {
|
||||
let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
|
||||
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"
|
||||
if let privateKey = interface.privateKey.base64Key() {
|
||||
output.append("PrivateKey = \(privateKey)\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")
|
||||
if let publicKey = peer.publicKey.base64Key() {
|
||||
output.append("PublicKey = \(publicKey)\n")
|
||||
}
|
||||
if let preSharedKey = peer.preSharedKey?.base64Key() {
|
||||
output.append("PresharedKey = \(preSharedKey)\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
|
||||
}
|
||||
|
||||
private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
|
||||
guard let privateKeyString = attributes["privatekey"] else {
|
||||
throw ParseError.interfaceHasNoPrivateKey
|
||||
}
|
||||
guard let privateKey = Data(base64Key: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
|
||||
}
|
||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||
if let listenPortString = attributes["listenport"] {
|
||||
guard let listenPort = UInt16(listenPortString) else {
|
||||
throw ParseError.interfaceHasInvalidListenPort(listenPortString)
|
||||
}
|
||||
interface.listenPort = listenPort
|
||||
}
|
||||
if let addressesString = attributes["address"] {
|
||||
var addresses = [IPAddressRange]()
|
||||
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
guard let address = IPAddressRange(from: addressString) else {
|
||||
throw ParseError.interfaceHasInvalidAddress(addressString)
|
||||
}
|
||||
addresses.append(address)
|
||||
}
|
||||
interface.addresses = addresses
|
||||
}
|
||||
if let dnsString = attributes["dns"] {
|
||||
var dnsServers = [DNSServer]()
|
||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
guard let dnsServer = DNSServer(from: dnsServerString) else {
|
||||
throw ParseError.interfaceHasInvalidDNS(dnsServerString)
|
||||
}
|
||||
dnsServers.append(dnsServer)
|
||||
}
|
||||
interface.dns = dnsServers
|
||||
}
|
||||
if let mtuString = attributes["mtu"] {
|
||||
guard let mtu = UInt16(mtuString) else {
|
||||
throw ParseError.interfaceHasInvalidMTU(mtuString)
|
||||
}
|
||||
interface.mtu = mtu
|
||||
}
|
||||
return interface
|
||||
}
|
||||
|
||||
private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
|
||||
guard let publicKeyString = attributes["publickey"] else {
|
||||
throw ParseError.peerHasNoPublicKey
|
||||
}
|
||||
guard let publicKey = Data(base64Key: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
|
||||
}
|
||||
var peer = PeerConfiguration(publicKey: publicKey)
|
||||
if let preSharedKeyString = attributes["presharedkey"] {
|
||||
guard let preSharedKey = Data(base64Key: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
|
||||
}
|
||||
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 {
|
||||
throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
|
||||
}
|
||||
allowedIPs.append(allowedIP)
|
||||
}
|
||||
peer.allowedIPs = allowedIPs
|
||||
}
|
||||
if let endpointString = attributes["endpoint"] {
|
||||
guard let endpoint = Endpoint(from: endpointString) else {
|
||||
throw ParseError.peerHasInvalidEndpoint(endpointString)
|
||||
}
|
||||
peer.endpoint = endpoint
|
||||
}
|
||||
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
|
||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
|
||||
throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
|
||||
}
|
||||
peer.persistentKeepAlive = persistentKeepAlive
|
||||
}
|
||||
return peer
|
||||
}
|
||||
|
||||
}
|
32
WireGuard/Shared/Model/TunnelConfiguration.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
114
WireGuard/Shared/Model/key.c
Normal file
@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*
|
||||
* This is a specialized constant-time base64/hex implementation that resists side-channel attacks.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "key.h"
|
||||
|
||||
static inline void encode_base64(char dest[static 4], const uint8_t src[static 3])
|
||||
{
|
||||
const uint8_t input[] = { (src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63 };
|
||||
|
||||
for (unsigned int i = 0; i < 4; ++i)
|
||||
dest[i] = input[i] + 'A'
|
||||
+ (((25 - input[i]) >> 8) & 6)
|
||||
- (((51 - input[i]) >> 8) & 75)
|
||||
- (((61 - input[i]) >> 8) & 15)
|
||||
+ (((62 - input[i]) >> 8) & 3);
|
||||
|
||||
}
|
||||
|
||||
void key_to_base64(char base64[static WG_KEY_LEN_BASE64], const uint8_t key[static WG_KEY_LEN])
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
for (i = 0; i < WG_KEY_LEN / 3; ++i)
|
||||
encode_base64(&base64[i * 4], &key[i * 3]);
|
||||
encode_base64(&base64[i * 4], (const uint8_t[]){ key[i * 3 + 0], key[i * 3 + 1], 0 });
|
||||
base64[WG_KEY_LEN_BASE64 - 2] = '=';
|
||||
base64[WG_KEY_LEN_BASE64 - 1] = '\0';
|
||||
}
|
||||
|
||||
static inline int decode_base64(const char src[static 4])
|
||||
{
|
||||
int val = 0;
|
||||
|
||||
for (unsigned int i = 0; i < 4; ++i)
|
||||
val |= (-1
|
||||
+ ((((('A' - 1) - src[i]) & (src[i] - ('Z' + 1))) >> 8) & (src[i] - 64))
|
||||
+ ((((('a' - 1) - src[i]) & (src[i] - ('z' + 1))) >> 8) & (src[i] - 70))
|
||||
+ ((((('0' - 1) - src[i]) & (src[i] - ('9' + 1))) >> 8) & (src[i] + 5))
|
||||
+ ((((('+' - 1) - src[i]) & (src[i] - ('+' + 1))) >> 8) & 63)
|
||||
+ ((((('/' - 1) - src[i]) & (src[i] - ('/' + 1))) >> 8) & 64)
|
||||
) << (18 - 6 * i);
|
||||
return val;
|
||||
}
|
||||
|
||||
bool key_from_base64(uint8_t key[static WG_KEY_LEN], const char *base64)
|
||||
{
|
||||
unsigned int i;
|
||||
volatile uint8_t ret = 0;
|
||||
int val;
|
||||
|
||||
if (strlen(base64) != WG_KEY_LEN_BASE64 - 1 || base64[WG_KEY_LEN_BASE64 - 2] != '=')
|
||||
return false;
|
||||
|
||||
for (i = 0; i < WG_KEY_LEN / 3; ++i) {
|
||||
val = decode_base64(&base64[i * 4]);
|
||||
ret |= (uint32_t)val >> 31;
|
||||
key[i * 3 + 0] = (val >> 16) & 0xff;
|
||||
key[i * 3 + 1] = (val >> 8) & 0xff;
|
||||
key[i * 3 + 2] = val & 0xff;
|
||||
}
|
||||
val = decode_base64((const char[]){ base64[i * 4 + 0], base64[i * 4 + 1], base64[i * 4 + 2], 'A' });
|
||||
ret |= ((uint32_t)val >> 31) | (val & 0xff);
|
||||
key[i * 3 + 0] = (val >> 16) & 0xff;
|
||||
key[i * 3 + 1] = (val >> 8) & 0xff;
|
||||
|
||||
return 1 & ((ret - 1) >> 8);
|
||||
}
|
||||
|
||||
void key_to_hex(char hex[static WG_KEY_LEN_HEX], const uint8_t key[static WG_KEY_LEN])
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
for (i = 0; i < WG_KEY_LEN; ++i) {
|
||||
hex[i * 2] = 87U + (key[i] >> 4) + ((((key[i] >> 4) - 10U) >> 8) & ~38U);
|
||||
hex[i * 2 + 1] = 87U + (key[i] & 0xf) + ((((key[i] & 0xf) - 10U) >> 8) & ~38U);
|
||||
}
|
||||
hex[i * 2] = '\0';
|
||||
}
|
||||
|
||||
bool key_from_hex(uint8_t key[static WG_KEY_LEN], const char *hex)
|
||||
{
|
||||
uint8_t c, c_acc, c_alpha0, c_alpha, c_num0, c_num, c_val;
|
||||
volatile uint8_t ret = 0;
|
||||
|
||||
if (strlen(hex) != WG_KEY_LEN_HEX - 1)
|
||||
return false;
|
||||
|
||||
for (unsigned int i = 0; i < WG_KEY_LEN_HEX - 1; i += 2) {
|
||||
c = (uint8_t)hex[i];
|
||||
c_num = c ^ 48U;
|
||||
c_num0 = (c_num - 10U) >> 8;
|
||||
c_alpha = (c & ~32U) - 55U;
|
||||
c_alpha0 = ((c_alpha - 10U) ^ (c_alpha - 16U)) >> 8;
|
||||
ret |= ((c_num0 | c_alpha0) - 1) >> 8;
|
||||
c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha);
|
||||
c_acc = c_val * 16U;
|
||||
|
||||
c = (uint8_t)hex[i + 1];
|
||||
c_num = c ^ 48U;
|
||||
c_num0 = (c_num - 10U) >> 8;
|
||||
c_alpha = (c & ~32U) - 55U;
|
||||
c_alpha0 = ((c_alpha - 10U) ^ (c_alpha - 16U)) >> 8;
|
||||
ret |= ((c_num0 | c_alpha0) - 1) >> 8;
|
||||
c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha);
|
||||
key[i / 2] = c_acc | c_val;
|
||||
}
|
||||
|
||||
return 1 & ((ret - 1) >> 8);
|
||||
}
|
22
WireGuard/Shared/Model/key.h
Normal file
@ -0,0 +1,22 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/*
|
||||
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef KEY_H
|
||||
#define KEY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define WG_KEY_LEN (32)
|
||||
#define WG_KEY_LEN_BASE64 (45)
|
||||
#define WG_KEY_LEN_HEX (65)
|
||||
|
||||
void key_to_base64(char base64[static WG_KEY_LEN_BASE64], const uint8_t key[static WG_KEY_LEN]);
|
||||
bool key_from_base64(uint8_t key[static WG_KEY_LEN], const char *base64);
|
||||
|
||||
void key_to_hex(char hex[static WG_KEY_LEN_HEX], const uint8_t key[static WG_KEY_LEN]);
|
||||
bool key_from_hex(uint8_t key[static WG_KEY_LEN], const char *hex);
|
||||
|
||||
#endif
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
7
WireGuard/WireGuard/Base.lproj/InfoPlist.strings
Normal file
@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
// iOS permission prompts
|
||||
|
||||
NSCameraUsageDescription = "Camera is used for scanning QR codes for importing WireGuard configurations";
|
||||
NSFaceIDUsageDescription = "Face ID is used for authenticating viewing and exporting of private keys";
|
389
WireGuard/WireGuard/Base.lproj/Localizable.strings
Normal file
@ -0,0 +1,389 @@
|
||||
// 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";
|
||||
"tunnelsListSelectButtonTitle" = "Select";
|
||||
"tunnelsListSelectAllButtonTitle" = "Select All";
|
||||
"tunnelsListDeleteButtonTitle" = "Delete";
|
||||
"tunnelsListSelectedTitle (%d)" = "%d selected";
|
||||
|
||||
// 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
|
||||
|
||||
"alertImportedFromMultipleFilesTitle (%d)" = "Created %d tunnels";
|
||||
"alertImportedFromMultipleFilesMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from imported files";
|
||||
|
||||
"alertImportedFromZipTitle (%d)" = "Created %d tunnels";
|
||||
"alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive";
|
||||
|
||||
"alertBadConfigImportTitle" = "Unable to import tunnel";
|
||||
"alertBadConfigImportMessage (%@)" = "The file ‘%@’ does not contain a valid WireGuard configuration";
|
||||
|
||||
"deleteTunnelsConfirmationAlertButtonTitle" = "Delete";
|
||||
"deleteTunnelConfirmationAlertButtonMessage (%d)" = "Delete %d tunnel";
|
||||
"deleteTunnelsConfirmationAlertButtonMessage (%d)" = "Delete %d tunnels";
|
||||
|
||||
// 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";
|
||||
|
||||
"macToggleStatusButtonActivate" = "Activate";
|
||||
"macToggleStatusButtonActivating" = "Activating…";
|
||||
"macToggleStatusButtonDeactivate" = "Deactivate";
|
||||
"macToggleStatusButtonDeactivating" = "Deactivating…";
|
||||
"macToggleStatusButtonReasserting" = "Reactivating…";
|
||||
"macToggleStatusButtonRestarting" = "Restarting…";
|
||||
"macToggleStatusButtonWaiting" = "Waiting…";
|
||||
|
||||
"tunnelSectionTitleInterface" = "Interface";
|
||||
|
||||
"tunnelInterfaceName" = "Name";
|
||||
"tunnelInterfacePrivateKey" = "Private key";
|
||||
"tunnelInterfacePublicKey" = "Public key";
|
||||
"tunnelInterfaceGenerateKeypair" = "Generate keypair";
|
||||
"tunnelInterfaceAddresses" = "Addresses";
|
||||
"tunnelInterfaceListenPort" = "Listen port";
|
||||
"tunnelInterfaceMTU" = "MTU";
|
||||
"tunnelInterfaceDNS" = "DNS servers";
|
||||
"tunnelInterfaceStatus" = "Status";
|
||||
|
||||
"tunnelSectionTitlePeer" = "Peer";
|
||||
|
||||
"tunnelPeerPublicKey" = "Public key";
|
||||
"tunnelPeerPreSharedKey" = "Preshared key";
|
||||
"tunnelPeerEndpoint" = "Endpoint";
|
||||
"tunnelPeerPersistentKeepalive" = "Persistent keepalive";
|
||||
"tunnelPeerAllowedIPs" = "Allowed IPs";
|
||||
"tunnelPeerRxBytes" = "Data received";
|
||||
"tunnelPeerTxBytes" = "Data sent";
|
||||
"tunnelPeerLastHandshakeTime" = "Latest handshake";
|
||||
"tunnelPeerExcludePrivateIPs" = "Exclude private IPs";
|
||||
|
||||
"tunnelSectionTitleOnDemand" = "On-Demand Activation";
|
||||
|
||||
"tunnelOnDemandCellular" = "Cellular";
|
||||
"tunnelOnDemandEthernet" = "Ethernet";
|
||||
"tunnelOnDemandWiFi" = "Wi-Fi";
|
||||
"tunnelOnDemandSSIDsKey" = "SSIDs";
|
||||
|
||||
"tunnelOnDemandAnySSID" = "Any SSID";
|
||||
"tunnelOnDemandOnlyTheseSSIDs" = "Only these SSIDs";
|
||||
"tunnelOnDemandExceptTheseSSIDs" = "Except these SSIDs";
|
||||
"tunnelOnDemandOnlySSID (%d)" = "Only %d SSID";
|
||||
"tunnelOnDemandOnlySSIDs (%d)" = "Only %d SSIDs";
|
||||
"tunnelOnDemandExceptSSID (%d)" = "Except %d SSID";
|
||||
"tunnelOnDemandExceptSSIDs (%d)" = "Except %d SSIDs";
|
||||
"tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)" = "%1$@: %2$@";
|
||||
|
||||
"tunnelOnDemandSSIDViewTitle" = "SSIDs";
|
||||
"tunnelOnDemandSectionTitleSelectedSSIDs" = "SSIDs";
|
||||
"tunnelOnDemandNoSSIDs" = "No SSIDs";
|
||||
"tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs";
|
||||
"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Add connected: %@";
|
||||
"tunnelOnDemandAddMessageAddNewSSID" = "Add new";
|
||||
|
||||
"tunnelOnDemandKey" = "On demand";
|
||||
"tunnelOnDemandOptionOff" = "Off";
|
||||
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
|
||||
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
|
||||
"tunnelOnDemandOptionCellularOnly" = "Cellular only";
|
||||
"tunnelOnDemandOptionWiFiOrEthernet" = "Wi-Fi or ethernet";
|
||||
"tunnelOnDemandOptionEthernetOnly" = "Ethernet 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";
|
||||
|
||||
"tunnelPeerPersistentKeepaliveValue (%@)" = "every %@ seconds";
|
||||
"tunnelHandshakeTimestampNow" = "Now";
|
||||
"tunnelHandshakeTimestampSystemClockBackward" = "(System clock wound backwards)";
|
||||
"tunnelHandshakeTimestampAgo (%@)" = "%@ ago";
|
||||
"tunnelHandshakeTimestampYear (%d)" = "%d year";
|
||||
"tunnelHandshakeTimestampYears (%d)" = "%d years";
|
||||
"tunnelHandshakeTimestampDay (%d)" = "%d day";
|
||||
"tunnelHandshakeTimestampDays (%d)" = "%d days";
|
||||
"tunnelHandshakeTimestampHour (%d)" = "%d hour";
|
||||
"tunnelHandshakeTimestampHours (%d)" = "%d hours";
|
||||
"tunnelHandshakeTimestampMinute (%d)" = "%d minute";
|
||||
"tunnelHandshakeTimestampMinutes (%d)" = "%d minutes";
|
||||
"tunnelHandshakeTimestampSecond (%d)" = "%d second";
|
||||
"tunnelHandshakeTimestampSeconds (%d)" = "%d seconds";
|
||||
|
||||
"tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ hours";
|
||||
"tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ minutes";
|
||||
|
||||
"tunnelPeerPresharedKeyEnabled" = "enabled";
|
||||
|
||||
// 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";
|
||||
|
||||
"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.";
|
||||
|
||||
// Conf import error alerts
|
||||
|
||||
"alertCantOpenInputConfFileTitle" = "Unable to import from file";
|
||||
"alertCantOpenInputConfFileMessage (%@)" = "The file ‘%@’ could not be read.";
|
||||
|
||||
// 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.";
|
||||
|
||||
// Mac status bar menu / pulldown menu
|
||||
|
||||
"macMenuNetworks (%@)" = "Networks: %@";
|
||||
"macMenuNetworksNone" = "Networks: None";
|
||||
|
||||
"macMenuTitle" = "WireGuard";
|
||||
"macMenuManageTunnels" = "Manage tunnels";
|
||||
"macMenuImportTunnels" = "Import tunnel(s) from file…";
|
||||
"macMenuAddEmptyTunnel" = "Add empty tunnel…";
|
||||
"macMenuExportLog" = "Export log to file…";
|
||||
"macMenuExportTunnels" = "Export tunnels to zip…";
|
||||
"macMenuAbout" = "About WireGuard";
|
||||
"macMenuQuit" = "Quit";
|
||||
|
||||
// Mac manage tunnels window
|
||||
|
||||
"macWindowTitleManageTunnels" = "Manage WireGuard Tunnels";
|
||||
|
||||
"macDeleteTunnelConfirmationAlertMessage (%@)" = "Are you sure you want to delete ‘%@’?";
|
||||
"macDeleteMultipleTunnelsConfirmationAlertMessage (%d)" = "Are you sure you want to delete %d tunnels?";
|
||||
"macDeleteTunnelConfirmationAlertInfo" = "You cannot undo this action.";
|
||||
"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Delete";
|
||||
"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Cancel";
|
||||
|
||||
"macButtonImportTunnels" = "Import tunnel(s) from file";
|
||||
"macSheetButtonImport" = "Import";
|
||||
|
||||
"macNameFieldExportLog" = "Export log to";
|
||||
"macSheetButtonExportLog" = "Save";
|
||||
|
||||
"macNameFieldExportZip" = "Export tunnels to";
|
||||
"macSheetButtonExportZip" = "Save";
|
||||
|
||||
"macButtonDeleteTunnels (%d)" = "Delete %d tunnels";
|
||||
|
||||
// Mac detail/edit view fields
|
||||
|
||||
"macFieldKey (%@)" = "%@:";
|
||||
"macFieldOnDemand" = "On-Demand:";
|
||||
"macFieldOnDemandSSIDs" = "SSIDs:";
|
||||
|
||||
// Mac status display
|
||||
|
||||
"macStatus (%@)" = "Status: %@";
|
||||
|
||||
// Mac editing config
|
||||
|
||||
"macEditDiscard" = "Discard";
|
||||
"macEditSave" = "Save";
|
||||
|
||||
"macAlertNameIsEmpty" = "Name is required";
|
||||
"macAlertDuplicateName (%@)" = "Another tunnel already exists with the name ‘%@’.";
|
||||
|
||||
"macAlertInvalidLine (%@)" = "Invalid line: ‘%@’.";
|
||||
|
||||
"macAlertNoInterface" = "Configuration must have an ‘Interface’ section.";
|
||||
"macAlertMultipleInterfaces" = "Configuration must have only one ‘Interface’ section.";
|
||||
"macAlertPrivateKeyInvalid" = "Private key is invalid.";
|
||||
"macAlertListenPortInvalid (%@)" = "Listen port ‘%@’ is invalid.";
|
||||
"macAlertAddressInvalid (%@)" = "Address ‘%@’ is invalid.";
|
||||
"macAlertDNSInvalid (%@)" = "DNS ‘%@’ is invalid.";
|
||||
"macAlertMTUInvalid (%@)" = "MTU ‘%@’ is invalid.";
|
||||
|
||||
"macAlertUnrecognizedInterfaceKey (%@)" = "Interface contains unrecognized key ‘%@’";
|
||||
"macAlertInfoUnrecognizedInterfaceKey" = "Valid keys are: ‘PrivateKey’, ‘ListenPort’, ‘Address’, ‘DNS’ and ‘MTU’.";
|
||||
|
||||
"macAlertPublicKeyInvalid" = "Public key is invalid";
|
||||
"macAlertPreSharedKeyInvalid" = "Preshared key is invalid";
|
||||
"macAlertAllowedIPInvalid (%@)" = "Allowed IP ‘%@’ is invalid";
|
||||
"macAlertEndpointInvalid (%@)" = "Endpoint ‘%@’ is invalid";
|
||||
"macAlertPersistentKeepliveInvalid (%@)" = "Persistent keepalive value ‘%@’ is invalid";
|
||||
|
||||
"macAlertUnrecognizedPeerKey (%@)" = "Peer contains unrecognized key ‘%@’";
|
||||
"macAlertInfoUnrecognizedPeerKey" = "Valid keys are: ‘PublicKey’, ‘PresharedKey’, ‘AllowedIPs’, ‘Endpoint’ and ‘PersistentKeepalive’";
|
||||
|
||||
"macAlertMultipleEntriesForKey (%@)" = "There should be only one entry per section for key ‘%@’";
|
||||
|
||||
// Mac about dialog
|
||||
|
||||
"macAppVersion (%@)" = "App version: %@";
|
||||
"macGoBackendVersion (%@)" = "Go backend version: %@";
|
||||
|
||||
// Privacy
|
||||
|
||||
"macExportPrivateData" = "export tunnel private keys";
|
||||
"macViewPrivateData" = "view tunnel private keys";
|
||||
"iosExportPrivateData" = "Authenticate to export tunnel private keys.";
|
||||
"iosViewPrivateData" = "Authenticate to view tunnel private keys.";
|
||||
|
||||
// Mac alert
|
||||
|
||||
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
|
||||
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
|
||||
"macPrivacyNoticeMessage" = "Privacy notice: be sure you trust this configuration file";
|
||||
"macPrivacyNoticeInfo" = "You will be prompted by the system to allow or disallow adding a VPN configuration. While this application does not send any information to the WireGuard project, information is by design sent to the servers specified inside of the configuration file you have just added, which configures your computer to use those servers as a VPN. Be certain that you trust this configuration before clicking “Allow” in the following dialog.";
|
||||
|
||||
// Mac tooltip
|
||||
|
||||
"macToolTipEditTunnel" = "Edit tunnel (⌘E)";
|
||||
"macToolTipToggleStatus" = "Toggle status (⌘T)";
|
@ -3,7 +3,8 @@
|
||||
// You Apple developer account's Team ID
|
||||
DEVELOPMENT_TEAM = <team_id>
|
||||
|
||||
// The bundle identifier of this app.
|
||||
// The bundle identifier of the apps.
|
||||
// Should be an app id created at developer.apple.com
|
||||
// with Network Extensions capabilty.
|
||||
APP_ID = <app_id>
|
||||
APP_ID_IOS = <app_id>
|
||||
APP_ID_MACOS = <app_id>
|
||||
|
@ -1,2 +1,2 @@
|
||||
VERSION_NAME = 0.0.20181104
|
||||
VERSION_ID = 2
|
||||
VERSION_NAME = 0.0.20190319
|
||||
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
|
||||
import Foundation
|
||||
|
||||
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,12 +1,13 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <CommonCrypto/CommonRandom.h>
|
||||
|
||||
#include "x25519.h"
|
||||
@ -171,7 +172,7 @@ void curve25519_derive_public_key(uint8_t public_key[32], const uint8_t private_
|
||||
|
||||
void curve25519_generate_private_key(uint8_t private_key[32])
|
||||
{
|
||||
CCRandomGenerateBytes(private_key, 32);
|
||||
assert(CCRandomGenerateBytes(private_key, 32) == kCCSuccess);
|
||||
private_key[31] = (private_key[31] & 127) | 64;
|
||||
private_key[0] &= 248;
|
||||
}
|
||||
|
12
WireGuard/WireGuard/LocalizationHelper.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
120
WireGuard/WireGuard/Tunnel/ActivateOnDemandOption.swift
Normal file
@ -0,0 +1,120 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
enum ActivateOnDemandOption: Equatable {
|
||||
case off
|
||||
case wiFiInterfaceOnly(ActivateOnDemandSSIDOption)
|
||||
case nonWiFiInterfaceOnly
|
||||
case anyInterface(ActivateOnDemandSSIDOption)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .cellular
|
||||
#elseif os(macOS)
|
||||
private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .ethernet
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
|
||||
enum ActivateOnDemandSSIDOption: Equatable {
|
||||
case anySSID
|
||||
case onlySpecificSSIDs([String])
|
||||
case exceptSpecificSSIDs([String])
|
||||
}
|
||||
|
||||
extension ActivateOnDemandOption {
|
||||
func apply(on tunnelProviderManager: NETunnelProviderManager) {
|
||||
let rules: [NEOnDemandRule]?
|
||||
switch self {
|
||||
case .off:
|
||||
rules = nil
|
||||
case .wiFiInterfaceOnly(let ssidOption):
|
||||
rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleDisconnect(interfaceType: nonWiFiInterfaceType)]
|
||||
case .nonWiFiInterfaceOnly:
|
||||
rules = [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType), NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
|
||||
case .anyInterface(let ssidOption):
|
||||
if case .anySSID = ssidOption {
|
||||
rules = [NEOnDemandRuleConnect(interfaceType: .any)]
|
||||
} else {
|
||||
rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType)]
|
||||
}
|
||||
}
|
||||
tunnelProviderManager.onDemandRules = rules
|
||||
tunnelProviderManager.isOnDemandEnabled = self != .off
|
||||
}
|
||||
|
||||
init(from tunnelProviderManager: NETunnelProviderManager) {
|
||||
let rules = tunnelProviderManager.onDemandRules ?? []
|
||||
let activateOnDemandOption: ActivateOnDemandOption
|
||||
switch rules.count {
|
||||
case 0:
|
||||
activateOnDemandOption = .off
|
||||
case 1:
|
||||
let rule = rules[0]
|
||||
precondition(rule.action == .connect)
|
||||
activateOnDemandOption = .anyInterface(.anySSID)
|
||||
case 2:
|
||||
let connectRule = rules.first(where: { $0.action == .connect })!
|
||||
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
|
||||
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
|
||||
activateOnDemandOption = .wiFiInterfaceOnly(.anySSID)
|
||||
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
|
||||
activateOnDemandOption = .nonWiFiInterfaceOnly
|
||||
} else {
|
||||
fatalError("Unexpected onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
case 3:
|
||||
let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil })!
|
||||
let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType })!
|
||||
let ssids = ssidRule.ssidMatch!
|
||||
switch (ssidRule.action, nonWiFiRule.action) {
|
||||
case (.connect, .connect):
|
||||
activateOnDemandOption = .anyInterface(.onlySpecificSSIDs(ssids))
|
||||
case (.connect, .disconnect):
|
||||
activateOnDemandOption = .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
|
||||
case (.disconnect, .connect):
|
||||
activateOnDemandOption = .anyInterface(.exceptSpecificSSIDs(ssids))
|
||||
case (.disconnect, .disconnect):
|
||||
activateOnDemandOption = .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
|
||||
default:
|
||||
fatalError("Unexpected SSID onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
default:
|
||||
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
|
||||
self = activateOnDemandOption
|
||||
}
|
||||
}
|
||||
|
||||
private extension NEOnDemandRuleConnect {
|
||||
convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
|
||||
self.init()
|
||||
interfaceTypeMatch = interfaceType
|
||||
ssidMatch = ssids
|
||||
}
|
||||
}
|
||||
|
||||
private extension NEOnDemandRuleDisconnect {
|
||||
convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
|
||||
self.init()
|
||||
interfaceTypeMatch = interfaceType
|
||||
ssidMatch = ssids
|
||||
}
|
||||
}
|
||||
|
||||
private func ssidOnDemandRules(option: ActivateOnDemandSSIDOption) -> [NEOnDemandRule] {
|
||||
switch option {
|
||||
case .anySSID:
|
||||
return [NEOnDemandRuleConnect(interfaceType: .wiFi)]
|
||||
case .onlySpecificSSIDs(let ssids):
|
||||
assert(!ssids.isEmpty)
|
||||
return [NEOnDemandRuleConnect(interfaceType: .wiFi, ssids: ssids),
|
||||
NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
|
||||
case .exceptSpecificSSIDs(let ssids):
|
||||
return [NEOnDemandRuleDisconnect(interfaceType: .wiFi, ssids: ssids),
|
||||
NEOnDemandRuleConnect(interfaceType: .wiFi)]
|
||||
}
|
||||
}
|
48
WireGuard/WireGuard/Tunnel/MockTunnels.swift
Normal file
@ -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
|
184
WireGuard/WireGuard/Tunnel/TunnelConfiguration+UapiConfig.swift
Normal file
@ -0,0 +1,184 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension TunnelConfiguration {
|
||||
convenience init(fromUapiConfig uapiConfig: String, basedOn base: TunnelConfiguration? = nil) throws {
|
||||
var interfaceConfiguration: InterfaceConfiguration?
|
||||
var peerConfigurations = [PeerConfiguration]()
|
||||
|
||||
var lines = uapiConfig.split(separator: "\n")
|
||||
lines.append("")
|
||||
|
||||
var parserState = ParserState.inInterfaceSection
|
||||
var attributes = [String: String]()
|
||||
|
||||
for line in lines {
|
||||
var key = ""
|
||||
var value = ""
|
||||
|
||||
if !line.isEmpty {
|
||||
guard let equalsIndex = line.firstIndex(of: "=") else { throw ParseError.invalidLine(line) }
|
||||
key = String(line[..<equalsIndex])
|
||||
value = String(line[line.index(equalsIndex, offsetBy: 1)...])
|
||||
}
|
||||
|
||||
if line.isEmpty || key == "public_key" {
|
||||
// Previous section has ended; process the attributes collected so far
|
||||
if parserState == .inInterfaceSection {
|
||||
let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
|
||||
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
|
||||
interfaceConfiguration = interface
|
||||
parserState = .inPeerSection
|
||||
} else if parserState == .inPeerSection {
|
||||
let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
|
||||
peerConfigurations.append(peer)
|
||||
}
|
||||
attributes.removeAll()
|
||||
if line.isEmpty {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let presentValue = attributes[key] {
|
||||
if key == "allowed_ip" {
|
||||
attributes[key] = presentValue + "," + value
|
||||
} else {
|
||||
throw ParseError.multipleEntriesForKey(key)
|
||||
}
|
||||
} else {
|
||||
attributes[key] = value
|
||||
}
|
||||
|
||||
let interfaceSectionKeys: Set<String> = ["private_key", "listen_port", "fwmark"]
|
||||
let peerSectionKeys: Set<String> = ["public_key", "preshared_key", "allowed_ip", "endpoint", "persistent_keepalive_interval", "last_handshake_time_sec", "last_handshake_time_nsec", "rx_bytes", "tx_bytes", "protocol_version"]
|
||||
|
||||
if parserState == .inInterfaceSection {
|
||||
guard interfaceSectionKeys.contains(key) else {
|
||||
throw ParseError.interfaceHasUnrecognizedKey(key)
|
||||
}
|
||||
}
|
||||
if parserState == .inPeerSection {
|
||||
guard peerSectionKeys.contains(key) else {
|
||||
throw ParseError.peerHasUnrecognizedKey(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||
throw ParseError.multiplePeersWithSamePublicKey
|
||||
}
|
||||
|
||||
interfaceConfiguration?.addresses = base?.interface.addresses ?? []
|
||||
interfaceConfiguration?.dns = base?.interface.dns ?? []
|
||||
interfaceConfiguration?.mtu = base?.interface.mtu
|
||||
|
||||
if let interfaceConfiguration = interfaceConfiguration {
|
||||
self.init(name: base?.name, interface: interfaceConfiguration, peers: peerConfigurations)
|
||||
} else {
|
||||
throw ParseError.noInterface
|
||||
}
|
||||
}
|
||||
|
||||
private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
|
||||
guard let privateKeyString = attributes["private_key"] else {
|
||||
throw ParseError.interfaceHasNoPrivateKey
|
||||
}
|
||||
guard let privateKey = Data(hexKey: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
|
||||
}
|
||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||
if let listenPortString = attributes["listen_port"] {
|
||||
guard let listenPort = UInt16(listenPortString) else {
|
||||
throw ParseError.interfaceHasInvalidListenPort(listenPortString)
|
||||
}
|
||||
if listenPort != 0 {
|
||||
interface.listenPort = listenPort
|
||||
}
|
||||
}
|
||||
return interface
|
||||
}
|
||||
|
||||
private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
|
||||
guard let publicKeyString = attributes["public_key"] else {
|
||||
throw ParseError.peerHasNoPublicKey
|
||||
}
|
||||
guard let publicKey = Data(hexKey: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
|
||||
}
|
||||
var peer = PeerConfiguration(publicKey: publicKey)
|
||||
if let preSharedKeyString = attributes["preshared_key"] {
|
||||
guard let preSharedKey = Data(hexKey: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
|
||||
}
|
||||
// TODO(zx2c4): does the compiler optimize this away?
|
||||
var accumulator: UInt8 = 0
|
||||
for index in 0..<preSharedKey.count {
|
||||
accumulator |= preSharedKey[index]
|
||||
}
|
||||
if accumulator != 0 {
|
||||
peer.preSharedKey = preSharedKey
|
||||
}
|
||||
}
|
||||
if let allowedIPsString = attributes["allowed_ip"] {
|
||||
var allowedIPs = [IPAddressRange]()
|
||||
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
guard let allowedIP = IPAddressRange(from: allowedIPString) else {
|
||||
throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
|
||||
}
|
||||
allowedIPs.append(allowedIP)
|
||||
}
|
||||
peer.allowedIPs = allowedIPs
|
||||
}
|
||||
if let endpointString = attributes["endpoint"] {
|
||||
guard let endpoint = Endpoint(from: endpointString) else {
|
||||
throw ParseError.peerHasInvalidEndpoint(endpointString)
|
||||
}
|
||||
peer.endpoint = endpoint
|
||||
}
|
||||
if let persistentKeepAliveString = attributes["persistent_keepalive_interval"] {
|
||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
|
||||
throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
|
||||
}
|
||||
if persistentKeepAlive != 0 {
|
||||
peer.persistentKeepAlive = persistentKeepAlive
|
||||
}
|
||||
}
|
||||
if let rxBytesString = attributes["rx_bytes"] {
|
||||
guard let rxBytes = UInt64(rxBytesString) else {
|
||||
throw ParseError.peerHasInvalidTransferBytes(rxBytesString)
|
||||
}
|
||||
if rxBytes != 0 {
|
||||
peer.rxBytes = rxBytes
|
||||
}
|
||||
}
|
||||
if let txBytesString = attributes["tx_bytes"] {
|
||||
guard let txBytes = UInt64(txBytesString) else {
|
||||
throw ParseError.peerHasInvalidTransferBytes(txBytesString)
|
||||
}
|
||||
if txBytes != 0 {
|
||||
peer.txBytes = txBytes
|
||||
}
|
||||
}
|
||||
if let lastHandshakeTimeSecString = attributes["last_handshake_time_sec"] {
|
||||
var lastHandshakeTimeSince1970: TimeInterval = 0
|
||||
guard let lastHandshakeTimeSec = UInt64(lastHandshakeTimeSecString) else {
|
||||
throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeSecString)
|
||||
}
|
||||
if lastHandshakeTimeSec != 0 {
|
||||
lastHandshakeTimeSince1970 += Double(lastHandshakeTimeSec)
|
||||
if let lastHandshakeTimeNsecString = attributes["last_handshake_time_nsec"] {
|
||||
guard let lastHandshakeTimeNsec = UInt64(lastHandshakeTimeNsecString) else {
|
||||
throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeNsecString)
|
||||
}
|
||||
lastHandshakeTimeSince1970 += Double(lastHandshakeTimeNsec) / 1000000000.0
|
||||
}
|
||||
peer.lastHandshakeTime = Date(timeIntervalSince1970: lastHandshakeTimeSince1970)
|
||||
}
|
||||
}
|
||||
return peer
|
||||
}
|
||||
}
|
107
WireGuard/WireGuard/Tunnel/TunnelErrors.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
59
WireGuard/WireGuard/Tunnel/TunnelStatus.swift
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
587
WireGuard/WireGuard/Tunnel/TunnelsManager.swift
Normal file
@ -0,0 +1,587 @@
|
||||
// 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, tunnel: TunnelContainer)
|
||||
}
|
||||
|
||||
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?
|
||||
private var configurationsObservationToken: AnyObject?
|
||||
|
||||
init(tunnelProviders: [NETunnelProviderManager]) {
|
||||
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
||||
startObservingTunnelStatuses()
|
||||
startObservingTunnelConfigurations()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var tunnelManagers = managers ?? []
|
||||
var refs: Set<Data> = []
|
||||
for (index, tunnelManager) in tunnelManagers.enumerated().reversed() {
|
||||
let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol
|
||||
if proto?.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") ?? false {
|
||||
tunnelManager.saveToPreferences { _ in }
|
||||
}
|
||||
if let ref = proto?.verifyConfigurationReference() {
|
||||
refs.insert(ref)
|
||||
} else {
|
||||
tunnelManager.removeFromPreferences { _ in }
|
||||
tunnelManagers.remove(at: index)
|
||||
}
|
||||
}
|
||||
Keychain.deleteReferences(except: refs)
|
||||
completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func reload() {
|
||||
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
let loadedTunnelProviders = managers ?? []
|
||||
|
||||
for (index, currentTunnel) in self.tunnels.enumerated().reversed() {
|
||||
if !loadedTunnelProviders.contains(where: { $0.tunnelConfiguration == currentTunnel.tunnelConfiguration }) {
|
||||
// Tunnel was deleted outside the app
|
||||
self.tunnels.remove(at: index)
|
||||
self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: currentTunnel)
|
||||
}
|
||||
}
|
||||
for loadedTunnelProvider in loadedTunnelProviders {
|
||||
if let matchingTunnel = self.tunnels.first(where: { $0.tunnelConfiguration == loadedTunnelProvider.tunnelConfiguration }) {
|
||||
matchingTunnel.tunnelProvider = loadedTunnelProvider
|
||||
matchingTunnel.refreshStatus()
|
||||
} else {
|
||||
// Tunnel was added outside the app
|
||||
if let proto = loadedTunnelProvider.protocolConfiguration as? NETunnelProviderProtocol {
|
||||
if proto.migrateConfigurationIfNeeded(called: loadedTunnelProvider.localizedDescription ?? "unknown") {
|
||||
loadedTunnelProvider.saveToPreferences { _ in }
|
||||
}
|
||||
}
|
||||
let tunnel = TunnelContainer(tunnel: loadedTunnelProvider)
|
||||
self.tunnels.append(tunnel)
|
||||
self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
||||
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func add(tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption = .off, 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.setTunnelConfiguration(tunnelConfiguration)
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
onDemandOption.apply(on: tunnelProviderManager)
|
||||
|
||||
let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
|
||||
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!)))
|
||||
return
|
||||
}
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
#if os(iOS)
|
||||
// HACK: In iOS, adding a tunnel causes deactivation of any currently active tunnel.
|
||||
// This is an ugly hack to reactivate the tunnel that has been deactivated like that.
|
||||
if let activeTunnel = activeTunnel {
|
||||
if activeTunnel.status == .inactive || activeTunnel.status == .deactivating {
|
||||
self.startActivation(of: activeTunnel)
|
||||
}
|
||||
if activeTunnel.status == .active || activeTunnel.status == .activating {
|
||||
activeTunnel.status = .restarting
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
|
||||
self.tunnels.append(tunnel)
|
||||
self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
||||
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
|
||||
completionHandler(.success(tunnel))
|
||||
}
|
||||
}
|
||||
|
||||
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
|
||||
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
|
||||
guard let head = tunnelConfigurations.first else {
|
||||
completionHandler(numberSuccessful, lastError)
|
||||
return
|
||||
}
|
||||
let tail = tunnelConfigurations.dropFirst()
|
||||
add(tunnelConfiguration: head) { [weak self, tail] result in
|
||||
DispatchQueue.main.async {
|
||||
let numberSuccessful = numberSuccessful + (result.isSuccess ? 1 : 0)
|
||||
let lastError = lastError ?? (result.error as? TunnelsManagerError)
|
||||
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful, lastError: lastError, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption, 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
|
||||
}
|
||||
|
||||
var isTunnelConfigurationChanged = false
|
||||
if tunnelProviderManager.tunnelConfiguration != tunnelConfiguration {
|
||||
tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
|
||||
isTunnelConfigurationChanged = true
|
||||
}
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && onDemandOption != .off
|
||||
onDemandOption.apply(on: tunnelProviderManager)
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
//TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back!
|
||||
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 { TunnelsManager.tunnelNameIsLessThan($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 isTunnelConfigurationChanged {
|
||||
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.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||
|
||||
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, tunnel: tunnel)
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
removeMultiple(tunnels: ArraySlice(tunnels), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
guard let head = tunnels.first else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
let tail = tunnels.dropFirst()
|
||||
remove(tunnel: head) { [weak self, tail] error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
} else {
|
||||
self?.removeMultiple(tunnels: tail, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfTunnels() -> Int {
|
||||
return tunnels.count
|
||||
}
|
||||
|
||||
func tunnel(at index: Int) -> TunnelContainer {
|
||||
return tunnels[index]
|
||||
}
|
||||
|
||||
func index(of tunnel: TunnelContainer) -> Int? {
|
||||
return tunnels.firstIndex(of: tunnel)
|
||||
}
|
||||
|
||||
func tunnel(named tunnelName: String) -> TunnelContainer? {
|
||||
return tunnels.first { $0.name == tunnelName }
|
||||
}
|
||||
|
||||
func waitingTunnel() -> TunnelContainer? {
|
||||
return tunnels.first { $0.status == .waiting }
|
||||
}
|
||||
|
||||
func tunnelInOperation() -> TunnelContainer? {
|
||||
if let waitingTunnelObject = waitingTunnel() {
|
||||
return waitingTunnelObject
|
||||
}
|
||||
return tunnels.first { $0.status != .inactive }
|
||||
}
|
||||
|
||||
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 tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
|
||||
|
||||
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 {
|
||||
tunnel.startActivation(activationDelegate: self.activationDelegate)
|
||||
return
|
||||
}
|
||||
|
||||
tunnel.refreshStatus()
|
||||
}
|
||||
}
|
||||
|
||||
func startObservingTunnelConfigurations() {
|
||||
configurationsObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
// We schedule reload() in a subsequent runloop to ensure that the completion handler of loadAllFromPreferences
|
||||
// (reload() calls loadAllFromPreferences) is called after the completion handler of the saveToPreferences or
|
||||
// removeFromPreferences call, if any, that caused this notification to fire. This notification can also fire
|
||||
// as a result of a tunnel getting added or removed outside of the app.
|
||||
self?.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func tunnelNameIsLessThan(_ a: String, _ b: String) -> Bool {
|
||||
return a.compare(b, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
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: .common)
|
||||
}
|
||||
}
|
||||
}
|
||||
var activationAttemptId: String?
|
||||
var activationTimer: Timer?
|
||||
var deactivationTimer: Timer?
|
||||
|
||||
fileprivate var tunnelProvider: NETunnelProviderManager
|
||||
|
||||
var tunnelConfiguration: TunnelConfiguration? {
|
||||
return tunnelProvider.tunnelConfiguration
|
||||
}
|
||||
|
||||
var onDemandOption: ActivateOnDemandOption {
|
||||
return ActivateOnDemandOption(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 getRuntimeTunnelConfiguration(completionHandler: @escaping ((TunnelConfiguration?) -> Void)) {
|
||||
guard status != .inactive, let session = tunnelProvider.connection as? NETunnelProviderSession else {
|
||||
completionHandler(tunnelConfiguration)
|
||||
return
|
||||
}
|
||||
guard nil != (try? session.sendProviderMessage(Data(bytes: [ 0 ]), responseHandler: {
|
||||
guard self.status != .inactive, let data = $0, let base = self.tunnelConfiguration, let settings = String(data: data, encoding: .utf8) else {
|
||||
completionHandler(self.tunnelConfiguration)
|
||||
return
|
||||
}
|
||||
completionHandler((try? TunnelConfiguration(fromUapiConfig: settings, basedOn: base)) ?? self.tunnelConfiguration)
|
||||
})) else {
|
||||
completionHandler(tunnelConfiguration)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func refreshStatus() {
|
||||
if status == .restarting {
|
||||
return
|
||||
}
|
||||
status = TunnelStatus(from: tunnelProvider.connection.status)
|
||||
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
|
||||
}
|
||||
|
||||
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() {
|
||||
wg_log(.debug, message: "startDeactivation: Tunnel: \(name)")
|
||||
(tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
|
||||
}
|
||||
}
|
||||
|
||||
extension NETunnelProviderManager {
|
||||
private static var cachedConfigKey: UInt8 = 0
|
||||
var tunnelConfiguration: TunnelConfiguration? {
|
||||
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
|
||||
return cached
|
||||
}
|
||||
let config = (protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: localizedDescription)
|
||||
if config != nil {
|
||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, config, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func setTunnelConfiguration(_ tunnelConfiguration: TunnelConfiguration) {
|
||||
protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
|
||||
localizedDescription = tunnelConfiguration.name
|
||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
198
WireGuard/WireGuard/UI/ActivateOnDemandViewModel.swift
Normal file
@ -0,0 +1,198 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
class ActivateOnDemandViewModel {
|
||||
enum OnDemandField {
|
||||
case onDemand
|
||||
case nonWiFiInterface
|
||||
case wiFiInterface
|
||||
case ssid
|
||||
|
||||
var localizedUIString: String {
|
||||
switch self {
|
||||
case .onDemand:
|
||||
return tr("tunnelOnDemandKey")
|
||||
case .nonWiFiInterface:
|
||||
#if os(iOS)
|
||||
return tr("tunnelOnDemandCellular")
|
||||
#elseif os(macOS)
|
||||
return tr("tunnelOnDemandEthernet")
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
case .wiFiInterface: return tr("tunnelOnDemandWiFi")
|
||||
case .ssid: return tr("tunnelOnDemandSSIDsKey")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum OnDemandSSIDOption {
|
||||
case anySSID
|
||||
case onlySpecificSSIDs
|
||||
case exceptSpecificSSIDs
|
||||
|
||||
var localizedUIString: String {
|
||||
switch self {
|
||||
case .anySSID: return tr("tunnelOnDemandAnySSID")
|
||||
case .onlySpecificSSIDs: return tr("tunnelOnDemandOnlyTheseSSIDs")
|
||||
case .exceptSpecificSSIDs: return tr("tunnelOnDemandExceptTheseSSIDs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isNonWiFiInterfaceEnabled = false
|
||||
var isWiFiInterfaceEnabled = false
|
||||
var selectedSSIDs = [String]()
|
||||
var ssidOption: OnDemandSSIDOption = .anySSID
|
||||
}
|
||||
|
||||
extension ActivateOnDemandViewModel {
|
||||
convenience init(tunnel: TunnelContainer) {
|
||||
self.init()
|
||||
if tunnel.isActivateOnDemandEnabled {
|
||||
switch tunnel.onDemandOption {
|
||||
case .off:
|
||||
break
|
||||
case .wiFiInterfaceOnly(let onDemandSSIDOption):
|
||||
isWiFiInterfaceEnabled = true
|
||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||
case .nonWiFiInterfaceOnly:
|
||||
isNonWiFiInterfaceEnabled = true
|
||||
case .anyInterface(let onDemandSSIDOption):
|
||||
isWiFiInterfaceEnabled = true
|
||||
isNonWiFiInterfaceEnabled = true
|
||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toOnDemandOption() -> ActivateOnDemandOption {
|
||||
switch (isWiFiInterfaceEnabled, isNonWiFiInterfaceEnabled) {
|
||||
case (false, false):
|
||||
return .off
|
||||
case (false, true):
|
||||
return .nonWiFiInterfaceOnly
|
||||
case (true, false):
|
||||
return .wiFiInterfaceOnly(toSSIDOption())
|
||||
case (true, true):
|
||||
return .anyInterface(toSSIDOption())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActivateOnDemandViewModel {
|
||||
func isEnabled(field: OnDemandField) -> Bool {
|
||||
switch field {
|
||||
case .nonWiFiInterface:
|
||||
return isNonWiFiInterfaceEnabled
|
||||
case .wiFiInterface:
|
||||
return isWiFiInterfaceEnabled
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func setEnabled(field: OnDemandField, isEnabled: Bool) {
|
||||
switch field {
|
||||
case .nonWiFiInterface:
|
||||
isNonWiFiInterfaceEnabled = isEnabled
|
||||
case .wiFiInterface:
|
||||
isWiFiInterfaceEnabled = isEnabled
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActivateOnDemandViewModel {
|
||||
var localizedInterfaceDescription: String {
|
||||
switch (isWiFiInterfaceEnabled, isNonWiFiInterfaceEnabled) {
|
||||
case (false, false):
|
||||
return tr("tunnelOnDemandOptionOff")
|
||||
case (true, false):
|
||||
return tr("tunnelOnDemandOptionWiFiOnly")
|
||||
case (false, true):
|
||||
#if os(iOS)
|
||||
return tr("tunnelOnDemandOptionCellularOnly")
|
||||
#elseif os(macOS)
|
||||
return tr("tunnelOnDemandOptionEthernetOnly")
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
case (true, true):
|
||||
#if os(iOS)
|
||||
return tr("tunnelOnDemandOptionWiFiOrCellular")
|
||||
#elseif os(macOS)
|
||||
return tr("tunnelOnDemandOptionWiFiOrEthernet")
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
var localizedSSIDDescription: String {
|
||||
guard isWiFiInterfaceEnabled else { return "" }
|
||||
switch ssidOption {
|
||||
case .anySSID: return tr("tunnelOnDemandAnySSID")
|
||||
case .onlySpecificSSIDs:
|
||||
if selectedSSIDs.count == 1 {
|
||||
return tr(format: "tunnelOnDemandOnlySSID (%d)", selectedSSIDs.count)
|
||||
} else {
|
||||
return tr(format: "tunnelOnDemandOnlySSIDs (%d)", selectedSSIDs.count)
|
||||
}
|
||||
case .exceptSpecificSSIDs:
|
||||
if selectedSSIDs.count == 1 {
|
||||
return tr(format: "tunnelOnDemandExceptSSID (%d)", selectedSSIDs.count)
|
||||
} else {
|
||||
return tr(format: "tunnelOnDemandExceptSSIDs (%d)", selectedSSIDs.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fixSSIDOption() {
|
||||
selectedSSIDs = uniquifiedNonEmptySelectedSSIDs()
|
||||
if selectedSSIDs.isEmpty {
|
||||
ssidOption = .anySSID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ActivateOnDemandViewModel {
|
||||
func ssidViewModel(from ssidOption: ActivateOnDemandSSIDOption) -> (OnDemandSSIDOption, [String]) {
|
||||
switch ssidOption {
|
||||
case .anySSID:
|
||||
return (.anySSID, [])
|
||||
case .onlySpecificSSIDs(let ssids):
|
||||
return (.onlySpecificSSIDs, ssids)
|
||||
case .exceptSpecificSSIDs(let ssids):
|
||||
return (.exceptSpecificSSIDs, ssids)
|
||||
}
|
||||
}
|
||||
|
||||
func toSSIDOption() -> ActivateOnDemandSSIDOption {
|
||||
switch ssidOption {
|
||||
case .anySSID:
|
||||
return .anySSID
|
||||
case .onlySpecificSSIDs:
|
||||
let ssids = uniquifiedNonEmptySelectedSSIDs()
|
||||
return ssids.isEmpty ? .anySSID : .onlySpecificSSIDs(selectedSSIDs)
|
||||
case .exceptSpecificSSIDs:
|
||||
let ssids = uniquifiedNonEmptySelectedSSIDs()
|
||||
return ssids.isEmpty ? .anySSID : .exceptSpecificSSIDs(selectedSSIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func uniquifiedNonEmptySelectedSSIDs() -> [String] {
|
||||
let nonEmptySSIDs = selectedSSIDs.filter { !$0.isEmpty }
|
||||
var seenSSIDs = Set<String>()
|
||||
var uniquified = [String]()
|
||||
for ssid in nonEmptySSIDs {
|
||||
guard !seenSSIDs.contains(ssid) else { continue }
|
||||
uniquified.append(ssid)
|
||||
seenSSIDs.insert(ssid)
|
||||
}
|
||||
return uniquified
|
||||
}
|
||||
}
|
25
WireGuard/WireGuard/UI/ErrorPresenterProtocol.swift
Normal file
@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
protocol ErrorPresenterProtocol {
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?)
|
||||
}
|
||||
|
||||
extension ErrorPresenterProtocol {
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?) {
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: nil)
|
||||
}
|
||||
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onDismissal: (() -> Void)?) {
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: nil, onDismissal: onDismissal)
|
||||
}
|
||||
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?) {
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: nil, onDismissal: nil)
|
||||
}
|
||||
|
||||
static func showErrorAlert(error: WireGuardAppError, from sourceVC: AnyObject?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
|
||||
let (title, message) = error.alertText
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: onDismissal)
|
||||
}
|
||||
}
|
37
WireGuard/WireGuard/UI/PrivateDataConfirmation.swift
Normal file
@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
class PrivateDataConfirmation {
|
||||
static func confirmAccess(to reason: String, _ after: @escaping () -> Void) {
|
||||
let context = LAContext()
|
||||
|
||||
var error: NSError?
|
||||
if !context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
|
||||
guard let error = error as? LAError else { return }
|
||||
if error.code == .passcodeNotSet {
|
||||
// We give no protection to folks who just don't set a passcode.
|
||||
after()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
|
||||
DispatchQueue.main.async {
|
||||
#if os(macOS)
|
||||
if !NSApp.isActive {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
#endif
|
||||
if success {
|
||||
after()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
84
WireGuard/WireGuard/UI/TunnelImporter.swift
Normal file
@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
class TunnelImporter {
|
||||
static func importFromFile(urls: [URL], into tunnelsManager: TunnelsManager, sourceVC: AnyObject?, errorPresenterType: ErrorPresenterProtocol.Type, completionHandler: (() -> Void)? = nil) {
|
||||
guard !urls.isEmpty else {
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var configs = [TunnelConfiguration?]()
|
||||
var lastFileImportErrorText: (title: String, message: String)?
|
||||
for url in urls {
|
||||
if url.pathExtension.lowercased() == "zip" {
|
||||
dispatchGroup.enter()
|
||||
ZipImporter.importConfigFiles(from: url) { result in
|
||||
if let error = result.error {
|
||||
lastFileImportErrorText = error.alertText
|
||||
}
|
||||
if let configsInZip = result.value {
|
||||
configs.append(contentsOf: configsInZip)
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else { /* if it is not a zip, we assume it is a conf */
|
||||
let fileName = url.lastPathComponent
|
||||
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let fileContents: String
|
||||
do {
|
||||
fileContents = try String(contentsOf: url)
|
||||
} catch let error {
|
||||
DispatchQueue.main.async {
|
||||
if let cocoaError = error as? CocoaError, cocoaError.isFileError {
|
||||
lastFileImportErrorText = (title: tr("alertCantOpenInputConfFileTitle"), message: error.localizedDescription)
|
||||
} else {
|
||||
lastFileImportErrorText = (title: tr("alertCantOpenInputConfFileTitle"), message: tr(format: "alertCantOpenInputConfFileMessage (%@)", fileName))
|
||||
}
|
||||
configs.append(nil)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
return
|
||||
}
|
||||
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
|
||||
DispatchQueue.main.async {
|
||||
if tunnelConfiguration == nil {
|
||||
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
|
||||
}
|
||||
configs.append(tunnelConfiguration)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { numberSuccessful, lastAddError in
|
||||
if !configs.isEmpty && numberSuccessful == configs.count {
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
let alertText: (title: String, message: String)?
|
||||
if urls.count == 1 {
|
||||
if urls.first!.pathExtension.lowercased() == "zip" && !configs.isEmpty {
|
||||
alertText = (title: tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful),
|
||||
message: tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count))
|
||||
} else {
|
||||
alertText = lastFileImportErrorText ?? lastAddError?.alertText
|
||||
}
|
||||
} else {
|
||||
alertText = (title: tr(format: "alertImportedFromMultipleFilesTitle (%d)", numberSuccessful),
|
||||
message: tr(format: "alertImportedFromMultipleFilesMessage (%1$d of %2$d)", numberSuccessful, configs.count))
|
||||
}
|
||||
if let alertText = alertText {
|
||||
errorPresenterType.showErrorAlert(title: alertText.title, message: alertText.message, from: sourceVC, onPresented: completionHandler)
|
||||
} else {
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +1,68 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
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: CaseIterable {
|
||||
case name
|
||||
case privateKey
|
||||
case publicKey
|
||||
case generateKeyPair
|
||||
case addresses
|
||||
case listenPort
|
||||
case mtu
|
||||
case dns
|
||||
case status
|
||||
case toggleStatus
|
||||
|
||||
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")
|
||||
case .status: return tr("tunnelInterfaceStatus")
|
||||
case .toggleStatus: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: CaseIterable {
|
||||
case publicKey
|
||||
case preSharedKey
|
||||
case endpoint
|
||||
case persistentKeepAlive
|
||||
case allowedIPs
|
||||
case rxBytes
|
||||
case txBytes
|
||||
case lastHandshakeTime
|
||||
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 .rxBytes: return tr("tunnelPeerRxBytes")
|
||||
case .txBytes: return tr("tunnelPeerTxBytes")
|
||||
case .lastHandshakeTime: return tr("tunnelPeerLastHandshakeTime")
|
||||
case .excludePrivateIPs: return tr("tunnelPeerExcludePrivateIPs")
|
||||
case .deletePeer: return tr("deletePeerButtonTitle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static let peerFieldsWithControl: Set<PeerField> = [
|
||||
@ -36,38 +71,47 @@ class TunnelViewModel {
|
||||
|
||||
static let keyLengthInBase64 = 44
|
||||
|
||||
struct Changes {
|
||||
enum FieldChange: Equatable {
|
||||
case added
|
||||
case removed
|
||||
case modified(newValue: String)
|
||||
}
|
||||
|
||||
var interfaceChanges: [InterfaceField: FieldChange]
|
||||
var peerChanges: [(peerIndex: Int, changes: [PeerField: FieldChange])]
|
||||
var peersRemovedIndices: [Int]
|
||||
var peersInsertedIndices: [Int]
|
||||
}
|
||||
|
||||
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 {
|
||||
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
||||
scratchpad[.publicKey] = publicKey.base64EncodedString()
|
||||
if field == .privateKey {
|
||||
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Key: stringValue), privateKey.count == TunnelConfiguration.keyLength {
|
||||
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey).base64Key() ?? ""
|
||||
scratchpad[.publicKey] = publicKey
|
||||
} else {
|
||||
scratchpad.removeValue(forKey: .publicKey)
|
||||
}
|
||||
@ -76,13 +120,18 @@ class TunnelViewModel {
|
||||
}
|
||||
|
||||
func populateScratchpad() {
|
||||
// Populate the scratchpad from the configuration object
|
||||
guard let config = validatedConfiguration else { return }
|
||||
scratchpad[.name] = config.name
|
||||
scratchpad[.privateKey] = config.privateKey.base64EncodedString()
|
||||
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
|
||||
if (!config.addresses.isEmpty) {
|
||||
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
|
||||
guard let name = validatedName else { return }
|
||||
scratchpad = TunnelViewModel.InterfaceData.createScratchPad(from: config, name: name)
|
||||
}
|
||||
|
||||
private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
|
||||
var scratchpad = [InterfaceField: String]()
|
||||
scratchpad[.name] = name
|
||||
scratchpad[.privateKey] = config.privateKey.base64Key() ?? ""
|
||||
scratchpad[.publicKey] = config.publicKey.base64Key() ?? ""
|
||||
if !config.addresses.isEmpty {
|
||||
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
}
|
||||
if let listenPort = config.listenPort {
|
||||
scratchpad[.listenPort] = String(listenPort)
|
||||
@ -90,40 +139,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: ", ")
|
||||
}
|
||||
return scratchpad
|
||||
}
|
||||
|
||||
func save() -> SaveResult<InterfaceConfiguration> {
|
||||
if let validatedConfiguration = validatedConfiguration {
|
||||
// It's already validated and saved
|
||||
return .saved(validatedConfiguration)
|
||||
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(base64Key: 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 +181,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 +189,82 @@ 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
|
||||
}
|
||||
|
||||
func applyConfiguration(other: InterfaceConfiguration, otherName: String) -> [InterfaceField: Changes.FieldChange] {
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
|
||||
var changes = [InterfaceField: Changes.FieldChange]()
|
||||
for field in InterfaceField.allCases {
|
||||
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
|
||||
case ("", ""):
|
||||
break
|
||||
case ("", _):
|
||||
changes[field] = .added
|
||||
case (_, ""):
|
||||
changes[field] = .removed
|
||||
case (let this, let other):
|
||||
if this != other {
|
||||
changes[field] = .modified(newValue: other)
|
||||
}
|
||||
}
|
||||
}
|
||||
scratchpad = otherScratchPad
|
||||
return changes
|
||||
}
|
||||
}
|
||||
|
||||
class PeerData {
|
||||
var index: Int
|
||||
var scratchpad: [PeerField: String] = [:]
|
||||
var fieldsWithError: Set<PeerField> = []
|
||||
var scratchpad = [PeerField: String]()
|
||||
var fieldsWithError = Set<PeerField>()
|
||||
var validatedConfiguration: PeerConfiguration?
|
||||
var publicKey: Data? {
|
||||
if let validatedConfiguration = validatedConfiguration {
|
||||
return validatedConfiguration.publicKey
|
||||
}
|
||||
if let scratchPadPublicKey = scratchpad[.publicKey] {
|
||||
return Data(base64Key: scratchPadPublicKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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,83 +272,93 @@ 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()
|
||||
scratchpad = TunnelViewModel.PeerData.createScratchPad(from: config)
|
||||
updateExcludePrivateIPsFieldState()
|
||||
}
|
||||
|
||||
private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
|
||||
var scratchpad = [PeerField: String]()
|
||||
if let publicKey = config.publicKey.base64Key() {
|
||||
scratchpad[.publicKey] = publicKey
|
||||
}
|
||||
if (!config.allowedIPs.isEmpty) {
|
||||
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
|
||||
if let preSharedKey = config.preSharedKey?.base64Key() {
|
||||
scratchpad[.preSharedKey] = preSharedKey
|
||||
}
|
||||
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)
|
||||
}
|
||||
updateExcludePrivateIPsFieldState()
|
||||
if let rxBytes = config.rxBytes {
|
||||
scratchpad[.rxBytes] = prettyBytes(rxBytes)
|
||||
}
|
||||
if let txBytes = config.txBytes {
|
||||
scratchpad[.txBytes] = prettyBytes(txBytes)
|
||||
}
|
||||
if let lastHandshakeTime = config.lastHandshakeTime {
|
||||
scratchpad[.lastHandshakeTime] = prettyTimeAgo(timestamp: lastHandshakeTime)
|
||||
}
|
||||
return scratchpad
|
||||
}
|
||||
|
||||
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(base64Key: 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(base64Key: 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 +368,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 +376,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"
|
||||
@ -318,68 +405,95 @@ class TunnelViewModel {
|
||||
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
||||
]
|
||||
|
||||
func updateExcludePrivateIPsFieldState() {
|
||||
guard (numberOfPeers == 1) else {
|
||||
shouldAllowExcludePrivateIPsControl = false
|
||||
excludePrivateIPsValue = false
|
||||
return
|
||||
static func excludePrivateIPsFieldStates(isSinglePeer: Bool, allowedIPs: Set<String>) -> (shouldAllowExcludePrivateIPsControl: Bool, excludePrivateIPsValue: Bool) {
|
||||
guard isSinglePeer else {
|
||||
return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
|
||||
}
|
||||
if (scratchpad.isEmpty) {
|
||||
populateScratchpad()
|
||||
}
|
||||
let allowedIPStrings = Set<String>(
|
||||
(scratchpad[.allowedIPs] ?? "")
|
||||
.split(separator: ",")
|
||||
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
|
||||
)
|
||||
if (allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString)) {
|
||||
shouldAllowExcludePrivateIPsControl = true
|
||||
excludePrivateIPsValue = false
|
||||
} else if (allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)) {
|
||||
shouldAllowExcludePrivateIPsControl = true
|
||||
excludePrivateIPsValue = true
|
||||
let allowedIPStrings = Set<String>(allowedIPs)
|
||||
if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
|
||||
return (shouldAllowExcludePrivateIPsControl: true, excludePrivateIPsValue: false)
|
||||
} else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
|
||||
return (shouldAllowExcludePrivateIPsControl: true, excludePrivateIPsValue: true)
|
||||
} else {
|
||||
shouldAllowExcludePrivateIPsControl = false
|
||||
excludePrivateIPsValue = false
|
||||
return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
|
||||
}
|
||||
}
|
||||
|
||||
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 ipv6Addresses = allowedIPStrings.filter { $0.contains(":") }
|
||||
let modifiedAllowedIPStrings: [String]
|
||||
if (isOn) {
|
||||
modifiedAllowedIPStrings = ipv6Addresses +
|
||||
TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
|
||||
} else {
|
||||
modifiedAllowedIPStrings = ipv6Addresses +
|
||||
[TunnelViewModel.PeerData.ipv4DefaultRouteString]
|
||||
func updateExcludePrivateIPsFieldState() {
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
let allowedIPStrings = Set<String>(scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
|
||||
(shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: numberOfPeers == 1, allowedIPs: allowedIPStrings)
|
||||
shouldStronglyRecommendDNS = allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) || allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)
|
||||
}
|
||||
|
||||
static func normalizedIPAddressRangeStrings(_ list: [String]) -> [String] {
|
||||
return list.compactMap { IPAddressRange(from: $0) }.map { $0.stringRepresentation }
|
||||
}
|
||||
|
||||
static func modifiedAllowedIPs(currentAllowedIPs: [String], excludePrivateIPs: Bool, dnsServers: [String], oldDNSServers: [String]?) -> [String] {
|
||||
let normalizedDNSServers = normalizedIPAddressRangeStrings(dnsServers)
|
||||
let normalizedOldDNSServers = oldDNSServers == nil ? normalizedDNSServers : normalizedIPAddressRangeStrings(oldDNSServers!)
|
||||
let ipv6Addresses = normalizedIPAddressRangeStrings(currentAllowedIPs.filter { $0.contains(":") })
|
||||
if excludePrivateIPs {
|
||||
return ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + normalizedDNSServers
|
||||
} else {
|
||||
return ipv6Addresses.filter { !normalizedOldDNSServers.contains($0) } + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
|
||||
}
|
||||
}
|
||||
|
||||
func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String, oldDNSServers: String? = nil) {
|
||||
let allowedIPStrings = scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let dnsServerStrings = dnsServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let oldDNSServerStrings = oldDNSServers?.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let modifiedAllowedIPStrings = TunnelViewModel.PeerData.modifiedAllowedIPs(currentAllowedIPs: allowedIPStrings, excludePrivateIPs: isOn, dnsServers: dnsServerStrings, oldDNSServers: oldDNSServerStrings)
|
||||
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
|
||||
validatedConfiguration = nil
|
||||
excludePrivateIPsValue = isOn
|
||||
}
|
||||
|
||||
func applyConfiguration(other: PeerConfiguration) -> [PeerField: Changes.FieldChange] {
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
let otherScratchPad = PeerData.createScratchPad(from: other)
|
||||
var changes = [PeerField: Changes.FieldChange]()
|
||||
for field in PeerField.allCases {
|
||||
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
|
||||
case ("", ""):
|
||||
break
|
||||
case ("", _):
|
||||
changes[field] = .added
|
||||
case (_, ""):
|
||||
changes[field] = .removed
|
||||
case (let this, let other):
|
||||
if this != other {
|
||||
changes[field] = .modified(newValue: other)
|
||||
}
|
||||
}
|
||||
}
|
||||
scratchpad = otherScratchPad
|
||||
return changes
|
||||
}
|
||||
}
|
||||
|
||||
enum SaveResult<Configuration> {
|
||||
case saved(Configuration)
|
||||
case error(String) // TODO: Localize error messages
|
||||
case error(String)
|
||||
}
|
||||
|
||||
var interfaceData: InterfaceData
|
||||
var peersData: [PeerData]
|
||||
private(set) var interfaceData: InterfaceData
|
||||
private(set) 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 +510,188 @@ 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 updateDNSServersInAllowedIPsIfRequired(oldDNSServers: String, newDNSServers: String) -> Bool {
|
||||
guard peersData.count == 1, let firstPeer = peersData.first else { return false }
|
||||
guard firstPeer.shouldAllowExcludePrivateIPsControl && firstPeer.excludePrivateIPsValue else { return false }
|
||||
let allowedIPStrings = firstPeer[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let oldDNSServerStrings = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(oldDNSServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
|
||||
let newDNSServerStrings = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(newDNSServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
|
||||
let updatedAllowedIPStrings = allowedIPStrings.filter { !oldDNSServerStrings.contains($0) } + newDNSServerStrings
|
||||
firstPeer[.allowedIPs] = updatedAllowedIPStrings.joined(separator: ", ")
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func asWgQuickConfig() -> String? {
|
||||
let saveResult = save()
|
||||
if case .saved(let tunnelConfiguration) = saveResult {
|
||||
return tunnelConfiguration.asWgQuickConfig()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func applyConfiguration(other: TunnelConfiguration) -> Changes {
|
||||
// Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering.
|
||||
|
||||
let interfaceChanges = interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "")
|
||||
|
||||
var peerChanges = [(peerIndex: Int, changes: [PeerField: Changes.FieldChange])]()
|
||||
for otherPeer in other.peers {
|
||||
if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) {
|
||||
let peerData = peersData[peersDataIndex]
|
||||
let changes = peerData.applyConfiguration(other: otherPeer)
|
||||
if !changes.isEmpty {
|
||||
peerChanges.append((peerIndex: peersDataIndex, changes: changes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removedPeerIndices = [Int]()
|
||||
for (index, peerData) in peersData.enumerated().reversed() {
|
||||
if let peerPublicKey = peerData.publicKey, !other.peers.contains(where: { $0.publicKey == peerPublicKey}) {
|
||||
removedPeerIndices.append(index)
|
||||
peersData.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
var addedPeerIndices = [Int]()
|
||||
for otherPeer in other.peers {
|
||||
if !peersData.contains(where: { $0.publicKey == otherPeer.publicKey }) {
|
||||
addedPeerIndices.append(peersData.count)
|
||||
let peerData = PeerData(index: peersData.count)
|
||||
peerData.validatedConfiguration = otherPeer
|
||||
peersData.append(peerData)
|
||||
}
|
||||
}
|
||||
|
||||
for (index, peer) in peersData.enumerated() {
|
||||
peer.index = index
|
||||
peer.numberOfPeers = peersData.count
|
||||
peer.updateExcludePrivateIPsFieldState()
|
||||
}
|
||||
|
||||
return Changes(interfaceChanges: interfaceChanges, peerChanges: peerChanges, peersRemovedIndices: removedPeerIndices, peersInsertedIndices: addedPeerIndices)
|
||||
}
|
||||
}
|
||||
|
||||
private func prettyBytes(_ bytes: UInt64) -> String {
|
||||
switch bytes {
|
||||
case 0..<1024:
|
||||
return "\(bytes) B"
|
||||
case 1024 ..< (1024 * 1024):
|
||||
return String(format: "%.2f", Double(bytes) / 1024) + " KiB"
|
||||
case 1024 ..< (1024 * 1024 * 1024):
|
||||
return String(format: "%.2f", Double(bytes) / (1024 * 1024)) + " MiB"
|
||||
case 1024 ..< (1024 * 1024 * 1024 * 1024):
|
||||
return String(format: "%.2f", Double(bytes) / (1024 * 1024 * 1024)) + " GiB"
|
||||
default:
|
||||
return String(format: "%.2f", Double(bytes) / (1024 * 1024 * 1024 * 1024)) + " TiB"
|
||||
}
|
||||
}
|
||||
|
||||
private func prettyTimeAgo(timestamp: Date) -> String {
|
||||
let now = Date()
|
||||
let timeInterval = Int64(now.timeIntervalSince(timestamp))
|
||||
switch timeInterval {
|
||||
case ..<0: return tr("tunnelHandshakeTimestampSystemClockBackward")
|
||||
case 0: return tr("tunnelHandshakeTimestampNow")
|
||||
default:
|
||||
return tr(format: "tunnelHandshakeTimestampAgo (%@)", prettyTime(secondsLeft: timeInterval))
|
||||
}
|
||||
}
|
||||
|
||||
private func prettyTime(secondsLeft: Int64) -> String {
|
||||
var left = secondsLeft
|
||||
var timeStrings = [String]()
|
||||
let years = left / (365 * 24 * 60 * 60)
|
||||
left = left % (365 * 24 * 60 * 60)
|
||||
let days = left / (24 * 60 * 60)
|
||||
left = left % (24 * 60 * 60)
|
||||
let hours = left / (60 * 60)
|
||||
left = left % (60 * 60)
|
||||
let minutes = left / 60
|
||||
let seconds = left % 60
|
||||
|
||||
#if os(iOS)
|
||||
if years > 0 {
|
||||
return years == 1 ? tr(format: "tunnelHandshakeTimestampYear (%d)", years) : tr(format: "tunnelHandshakeTimestampYears (%d)", years)
|
||||
}
|
||||
if days > 0 {
|
||||
return days == 1 ? tr(format: "tunnelHandshakeTimestampDay (%d)", days) : tr(format: "tunnelHandshakeTimestampDays (%d)", days)
|
||||
}
|
||||
if hours > 0 {
|
||||
let hhmmss = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
|
||||
return tr(format: "tunnelHandshakeTimestampHours hh:mm:ss (%@)", hhmmss)
|
||||
}
|
||||
if minutes > 0 {
|
||||
let mmss = String(format: "%02d:%02d", minutes, seconds)
|
||||
return tr(format: "tunnelHandshakeTimestampMinutes mm:ss (%@)", mmss)
|
||||
}
|
||||
return seconds == 1 ? tr(format: "tunnelHandshakeTimestampSecond (%d)", seconds) : tr(format: "tunnelHandshakeTimestampSeconds (%d)", seconds)
|
||||
#elseif os(macOS)
|
||||
if years > 0 {
|
||||
timeStrings.append(years == 1 ? tr(format: "tunnelHandshakeTimestampYear (%d)", years) : tr(format: "tunnelHandshakeTimestampYears (%d)", years))
|
||||
}
|
||||
if days > 0 {
|
||||
timeStrings.append(days == 1 ? tr(format: "tunnelHandshakeTimestampDay (%d)", days) : tr(format: "tunnelHandshakeTimestampDays (%d)", days))
|
||||
}
|
||||
if hours > 0 {
|
||||
timeStrings.append(hours == 1 ? tr(format: "tunnelHandshakeTimestampHour (%d)", hours) : tr(format: "tunnelHandshakeTimestampHours (%d)", hours))
|
||||
}
|
||||
if minutes > 0 {
|
||||
timeStrings.append(minutes == 1 ? tr(format: "tunnelHandshakeTimestampMinute (%d)", minutes) : tr(format: "tunnelHandshakeTimestampMinutes (%d)", minutes))
|
||||
}
|
||||
if seconds > 0 {
|
||||
timeStrings.append(seconds == 1 ? tr(format: "tunnelHandshakeTimestampSecond (%d)", seconds) : tr(format: "tunnelHandshakeTimestampSeconds (%d)", seconds))
|
||||
}
|
||||
return timeStrings.joined(separator: ", ")
|
||||
#endif
|
||||
}
|
||||
|
@ -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(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
|
||||
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.backgroundColor = UIColor.white
|
||||
window.backgroundColor = .white
|
||||
self.window = window
|
||||
|
||||
let mainVC = MainViewController()
|
||||
@ -27,14 +27,40 @@ 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)
|
||||
}
|
||||
guard let tunnelsManager = mainVC?.tunnelsManager else { return true }
|
||||
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: mainVC, errorPresenterType: ErrorPresenter.self) {
|
||||
_ = 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
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 865 B After Width: | Height: | Size: 865 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
@ -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>
|
25
WireGuard/WireGuard/UI/iOS/ConfirmationAlertPresenter.swift
Normal file
@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ConfirmationAlertPresenter {
|
||||
static func showConfirmationAlert(message: String, buttonTitle: String, from sourceObject: AnyObject, presentingVC: UIViewController, 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)
|
||||
|
||||
if let sourceView = sourceObject as? UIView {
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
alert.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
} else if let sourceBarButtonItem = sourceObject as? UIBarButtonItem {
|
||||
alert.popoverPresentationController?.barButtonItem = sourceBarButtonItem
|
||||
}
|
||||
|
||||
presentingVC.present(alert, animated: true, completion: 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,14 @@
|
||||
// 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) {
|
||||
class ErrorPresenter: ErrorPresenterProtocol {
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?) {
|
||||
guard let sourceVC = sourceVC as? UIViewController else { return }
|
||||
|
||||
// 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: Error, from sourceVC: UIViewController?,
|
||||
onDismissal: (() -> Void)? = nil, onPresented: (() -> 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 +16,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)
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
@ -61,7 +77,7 @@
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<false/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Camera is used for scanning QR codes for importing WireGuard configurations</string>
|
||||
<string>Localized</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
@ -106,5 +122,9 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Localized</string>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>group.$(APP_ID_IOS)</string>
|
||||
</dict>
|
||||
</plist>
|
@ -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")
|
||||
}
|
||||
}
|
21
WireGuard/WireGuard/UI/iOS/UITableViewCell+Reuse.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
51
WireGuard/WireGuard/UI/iOS/View/BorderedTextButton.swift
Normal file
@ -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?()
|
||||
}
|
||||
|
||||
}
|
55
WireGuard/WireGuard/UI/iOS/View/ButtonCell.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
31
WireGuard/WireGuard/UI/iOS/View/CheckmarkCell.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
30
WireGuard/WireGuard/UI/iOS/View/ChevronCell.swift
Normal file
@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ChevronCell: UITableViewCell {
|
||||
var message: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel?.text = value }
|
||||
}
|
||||
|
||||
var detailMessage: String {
|
||||
get { return detailTextLabel?.text ?? "" }
|
||||
set(value) { detailTextLabel?.text = value }
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
|
||||
accessoryType = .disclosureIndicator
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
}
|
||||
}
|
64
WireGuard/WireGuard/UI/iOS/View/EditableTextCell.swift
Normal file
@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class EditableTextCell: UITableViewCell {
|
||||
var message: String {
|
||||
get { return valueTextField.text ?? "" }
|
||||
set(value) { valueTextField.text = value }
|
||||
}
|
||||
|
||||
let valueTextField: UITextField = {
|
||||
let valueTextField = UITextField()
|
||||
valueTextField.textAlignment = .left
|
||||
valueTextField.isEnabled = true
|
||||
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
valueTextField.adjustsFontForContentSizeCategory = true
|
||||
valueTextField.autocapitalizationType = .none
|
||||
valueTextField.autocorrectionType = .no
|
||||
valueTextField.spellCheckingType = .no
|
||||
return valueTextField
|
||||
}()
|
||||
|
||||
var onValueBeingEdited: ((String) -> Void)?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
valueTextField.delegate = self
|
||||
contentView.addSubview(valueTextField)
|
||||
valueTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 1)
|
||||
bottomAnchorConstraint.priority = .defaultLow
|
||||
NSLayoutConstraint.activate([
|
||||
valueTextField.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
|
||||
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: valueTextField.trailingAnchor, multiplier: 1),
|
||||
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
|
||||
bottomAnchorConstraint
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func beginEditing() {
|
||||
valueTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
}
|
||||
}
|
||||
|
||||
extension EditableTextCell: UITextFieldDelegate {
|
||||
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
|
||||
}
|
||||
}
|
220
WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift
Normal file
@ -0,0 +1,220 @@
|
||||
// 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, String) -> Void)?
|
||||
var onValueBeingEdited: ((String) -> Void)?
|
||||
|
||||
var observationToken: AnyObject?
|
||||
|
||||
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
|
||||
observationToken = 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?(textFieldValueOnBeginEditing, 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
|
||||
}
|
||||
|
||||
}
|
52
WireGuard/WireGuard/UI/iOS/View/SwitchCell.swift
Normal file
@ -0,0 +1,52 @@
|
||||
// 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)?
|
||||
|
||||
var observationToken: AnyObject?
|
||||
|
||||
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
|
||||
observationToken = nil
|
||||
}
|
||||
}
|
34
WireGuard/WireGuard/UI/iOS/View/TextCell.swift
Normal file
@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class TextCell: UITableViewCell {
|
||||
var message: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel!.text = value }
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setTextColor(_ color: UIColor) {
|
||||
textLabel?.textColor = color
|
||||
}
|
||||
|
||||
func setTextAlignment(_ alignment: NSTextAlignment) {
|
||||
textLabel?.textAlignment = alignment
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
setTextColor(.black)
|
||||
setTextAlignment(.left)
|
||||
}
|
||||
}
|
48
WireGuard/WireGuard/UI/iOS/View/TunnelEditKeyValueCell.swift
Normal file
@ -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
|
||||
}
|
||||
|
||||
}
|
116
WireGuard/WireGuard/UI/iOS/View/TunnelListCell.swift
Normal file
@ -0,0 +1,116 @@
|
||||
// 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")
|
||||
}
|
||||
|
||||
override func setEditing(_ editing: Bool, animated: Bool) {
|
||||
super.setEditing(editing, animated: animated)
|
||||
statusSwitch.isEnabled = !editing
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
statusSwitch.isOn = false
|
||||
statusSwitch.isUserInteractionEnabled = false
|
||||
busyIndicator.stopAnimating()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
reset()
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
// 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?
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -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,49 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class SSIDOptionDetailTableViewController: UITableViewController {
|
||||
|
||||
let selectedSSIDs: [String]
|
||||
|
||||
init(title: String, ssids: [String]) {
|
||||
selectedSSIDs = ssids
|
||||
super.init(style: .grouped)
|
||||
self.title = title
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.estimatedRowHeight = 44
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.allowsSelection = false
|
||||
|
||||
tableView.register(TextCell.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension SSIDOptionDetailTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return selectedSSIDs.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return tr("tunnelOnDemandSectionTitleSelectedSSIDs")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = selectedSSIDs[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
}
|
@ -0,0 +1,295 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import SystemConfiguration.CaptiveNetwork
|
||||
|
||||
protocol SSIDOptionEditTableViewControllerDelegate: class {
|
||||
func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String])
|
||||
}
|
||||
|
||||
class SSIDOptionEditTableViewController: UITableViewController {
|
||||
private enum Section {
|
||||
case ssidOption
|
||||
case selectedSSIDs
|
||||
case addSSIDs
|
||||
}
|
||||
|
||||
private enum AddSSIDRow {
|
||||
case addConnectedSSID(connectedSSID: String)
|
||||
case addNewSSID
|
||||
}
|
||||
|
||||
weak var delegate: SSIDOptionEditTableViewControllerDelegate?
|
||||
|
||||
private var sections = [Section]()
|
||||
private var addSSIDRows = [AddSSIDRow]()
|
||||
|
||||
let ssidOptionFields: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
|
||||
.anySSID,
|
||||
.onlySpecificSSIDs,
|
||||
.exceptSpecificSSIDs
|
||||
]
|
||||
|
||||
var selectedOption: ActivateOnDemandViewModel.OnDemandSSIDOption
|
||||
var selectedSSIDs: [String]
|
||||
var connectedSSID: String?
|
||||
|
||||
init(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) {
|
||||
selectedOption = option
|
||||
selectedSSIDs = ssids
|
||||
super.init(style: .grouped)
|
||||
connectedSSID = getConnectedSSID()
|
||||
loadSections()
|
||||
loadAddSSIDRows()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = tr("tunnelOnDemandSSIDViewTitle")
|
||||
|
||||
tableView.estimatedRowHeight = 44
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
|
||||
tableView.register(CheckmarkCell.self)
|
||||
tableView.register(EditableTextCell.self)
|
||||
tableView.register(TextCell.self)
|
||||
tableView.isEditing = true
|
||||
tableView.allowsSelectionDuringEditing = true
|
||||
}
|
||||
|
||||
func loadSections() {
|
||||
sections.removeAll()
|
||||
sections.append(.ssidOption)
|
||||
if selectedOption != .anySSID {
|
||||
sections.append(.selectedSSIDs)
|
||||
sections.append(.addSSIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func loadAddSSIDRows() {
|
||||
addSSIDRows.removeAll()
|
||||
if let connectedSSID = connectedSSID {
|
||||
if !selectedSSIDs.contains(connectedSSID) {
|
||||
addSSIDRows.append(.addConnectedSSID(connectedSSID: connectedSSID))
|
||||
}
|
||||
}
|
||||
addSSIDRows.append(.addNewSSID)
|
||||
}
|
||||
|
||||
func updateTableViewAddSSIDRows() {
|
||||
guard let addSSIDSection = sections.firstIndex(of: .addSSIDs) else { return }
|
||||
let numberOfAddSSIDRows = addSSIDRows.count
|
||||
let numberOfAddSSIDRowsInTableView = tableView.numberOfRows(inSection: addSSIDSection)
|
||||
switch (numberOfAddSSIDRowsInTableView, numberOfAddSSIDRows) {
|
||||
case (1, 2):
|
||||
tableView.insertRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
|
||||
case (2, 1):
|
||||
tableView.deleteRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
delegate?.ssidOptionSaved(option: selectedOption, ssids: selectedSSIDs)
|
||||
}
|
||||
}
|
||||
|
||||
extension SSIDOptionEditTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return sections.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch sections[section] {
|
||||
case .ssidOption:
|
||||
return ssidOptionFields.count
|
||||
case .selectedSSIDs:
|
||||
return selectedSSIDs.isEmpty ? 1 : selectedSSIDs.count
|
||||
case .addSSIDs:
|
||||
return addSSIDRows.count
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
return ssidOptionCell(for: tableView, at: indexPath)
|
||||
case .selectedSSIDs:
|
||||
if !selectedSSIDs.isEmpty {
|
||||
return selectedSSIDCell(for: tableView, at: indexPath)
|
||||
} else {
|
||||
return noSSIDsCell(for: tableView, at: indexPath)
|
||||
}
|
||||
case .addSSIDs:
|
||||
return addSSIDCell(for: tableView, at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
return false
|
||||
case .selectedSSIDs:
|
||||
return !selectedSSIDs.isEmpty
|
||||
case .addSSIDs:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
return .none
|
||||
case .selectedSSIDs:
|
||||
return .delete
|
||||
case .addSSIDs:
|
||||
return .insert
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch sections[section] {
|
||||
case .ssidOption:
|
||||
return nil
|
||||
case .selectedSSIDs:
|
||||
return tr("tunnelOnDemandSectionTitleSelectedSSIDs")
|
||||
case .addSSIDs:
|
||||
return tr("tunnelOnDemandSectionTitleAddSSIDs")
|
||||
}
|
||||
}
|
||||
|
||||
private func ssidOptionCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let field = ssidOptionFields[indexPath.row]
|
||||
let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = field.localizedUIString
|
||||
cell.isChecked = selectedOption == field
|
||||
cell.isEditing = false
|
||||
return cell
|
||||
}
|
||||
|
||||
private func noSSIDsCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = tr("tunnelOnDemandNoSSIDs")
|
||||
cell.setTextColor(.gray)
|
||||
cell.setTextAlignment(.center)
|
||||
return cell
|
||||
}
|
||||
|
||||
private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = selectedSSIDs[indexPath.row]
|
||||
cell.isEditing = true
|
||||
cell.onValueBeingEdited = { [weak self, weak cell] text in
|
||||
guard let self = self, let cell = cell else { return }
|
||||
if let row = self.tableView.indexPath(for: cell)?.row {
|
||||
self.selectedSSIDs[row] = text
|
||||
self.loadAddSSIDRows()
|
||||
self.updateTableViewAddSSIDRows()
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func addSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
switch addSSIDRows[indexPath.row] {
|
||||
case .addConnectedSSID:
|
||||
cell.message = tr(format: "tunnelOnDemandAddMessageAddConnectedSSID (%@)", connectedSSID!)
|
||||
case .addNewSSID:
|
||||
cell.message = tr("tunnelOnDemandAddMessageAddNewSSID")
|
||||
}
|
||||
cell.isEditing = true
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
assertionFailure()
|
||||
case .selectedSSIDs:
|
||||
assert(editingStyle == .delete)
|
||||
selectedSSIDs.remove(at: indexPath.row)
|
||||
if !selectedSSIDs.isEmpty {
|
||||
tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||
} else {
|
||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
||||
}
|
||||
loadAddSSIDRows()
|
||||
updateTableViewAddSSIDRows()
|
||||
case .addSSIDs:
|
||||
assert(editingStyle == .insert)
|
||||
let newSSID: String
|
||||
switch addSSIDRows[indexPath.row] {
|
||||
case .addConnectedSSID(let connectedSSID):
|
||||
newSSID = connectedSSID
|
||||
case .addNewSSID:
|
||||
newSSID = ""
|
||||
}
|
||||
selectedSSIDs.append(newSSID)
|
||||
loadSections()
|
||||
let selectedSSIDsSection = sections.firstIndex(of: .selectedSSIDs)!
|
||||
let indexPath = IndexPath(row: selectedSSIDs.count - 1, section: selectedSSIDsSection)
|
||||
if selectedSSIDs.count == 1 {
|
||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
||||
} else {
|
||||
tableView.insertRows(at: [indexPath], with: .automatic)
|
||||
}
|
||||
loadAddSSIDRows()
|
||||
updateTableViewAddSSIDRows()
|
||||
if newSSID.isEmpty {
|
||||
if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell {
|
||||
selectedSSIDCell.beginEditing()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SSIDOptionEditTableViewController {
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
return indexPath
|
||||
case .selectedSSIDs, .addSSIDs:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
let previousOption = selectedOption
|
||||
selectedOption = ssidOptionFields[indexPath.row]
|
||||
loadSections()
|
||||
if previousOption == .anySSID {
|
||||
let indexSet = IndexSet(1 ... 2)
|
||||
tableView.insertSections(indexSet, with: .fade)
|
||||
}
|
||||
if selectedOption == .anySSID {
|
||||
let indexSet = IndexSet(1 ... 2)
|
||||
tableView.deleteSections(indexSet, with: .fade)
|
||||
}
|
||||
tableView.reloadSections(IndexSet(integer: indexPath.section), with: .none)
|
||||
case .selectedSSIDs, .addSSIDs:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getConnectedSSID() -> String? {
|
||||
guard let supportedInterfaces = CNCopySupportedInterfaces() as? [CFString] else { return nil }
|
||||
for interface in supportedInterfaces {
|
||||
if let networkInfo = CNCopyCurrentNetworkInfo(interface) {
|
||||
if let ssid = (networkInfo as NSDictionary)[kCNNetworkInfoKeySSID as String] as? String {
|
||||
return !ssid.isEmpty ? ssid : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
// 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) {
|
||||
PrivateDataConfirmation.confirmAccess(to: tr("iosExportPrivateData")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let tunnelsManager = self.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
|
||||
}
|
||||
}
|
||||
|
||||
let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard isWritten else {
|
||||
ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|