Compare commits
87 Commits
0.0.201811
...
0.0.201811
Author | SHA1 | Date | |
---|---|---|---|
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 |
351
COPYING
351
COPYING
@ -1,338 +1,19 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
Copyright © 2018 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.
|
||||
|
29
WireGuard/Shared/FileManager+Extension.swift
Normal file
29
WireGuard/Shared/FileManager+Extension.swift
Normal file
@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension FileManager {
|
||||
static var networkExtensionLogFileURL: URL? {
|
||||
guard let appGroupId = Bundle.main.object(forInfoDictionaryKey: "com.wireguard.ios.app_group_id") as? String else {
|
||||
os_log("Can't obtain app group id from bundle", log: OSLog.default, type: .error)
|
||||
return nil
|
||||
}
|
||||
guard let sharedFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
|
||||
os_log("Can't obtain shared folder URL", log: OSLog.default, type: .error)
|
||||
return nil
|
||||
}
|
||||
return sharedFolderURL.appendingPathComponent("last-activated-tunnel-log.txt")
|
||||
}
|
||||
|
||||
static func deleteFile(at url: URL) -> Bool {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch(let e) {
|
||||
os_log("Failed to delete file '%{public}@': %{public}@", log: OSLog.default, type: .debug, url.absoluteString, e.localizedDescription)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@ -4,11 +4,18 @@
|
||||
import Foundation
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
class TunnelConfiguration: Codable {
|
||||
final class TunnelConfiguration: Codable {
|
||||
var interface: InterfaceConfiguration
|
||||
var peers: [PeerConfiguration] = []
|
||||
init(interface: InterfaceConfiguration) {
|
||||
let peers: [PeerConfiguration]
|
||||
init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
|
||||
self.interface = interface
|
||||
self.peers = peers
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,10 +28,6 @@ struct InterfaceConfiguration: Codable {
|
||||
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
|
@ -87,4 +87,15 @@ extension Endpoint {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func hostname() -> String? {
|
||||
switch (host) {
|
||||
case .name(let hostname, _):
|
||||
return hostname
|
||||
case .ipv4(_):
|
||||
return nil
|
||||
case .ipv6(_):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
35
WireGuard/Shared/NETunnelProviderProtocol+Extension.swift
Normal file
35
WireGuard/Shared/NETunnelProviderProtocol+Extension.swift
Normal file
@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration) {
|
||||
assert(!tunnelConfiguration.interface.name.isEmpty)
|
||||
guard let serializedTunnelConfiguration = try? JSONEncoder().encode(tunnelConfiguration) else { return nil }
|
||||
|
||||
self.init()
|
||||
|
||||
let appId = Bundle.main.bundleIdentifier!
|
||||
providerBundleIdentifier = "\(appId).network-extension"
|
||||
providerConfiguration = [
|
||||
"tunnelConfiguration": serializedTunnelConfiguration,
|
||||
"tunnelConfigurationVersion": 1
|
||||
]
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint})
|
||||
if endpoints.count == 1 {
|
||||
serverAddress = endpoints.first!.stringRepresentation()
|
||||
} else if endpoints.isEmpty {
|
||||
serverAddress = "Unspecified"
|
||||
} else {
|
||||
serverAddress = "Multiple endpoints"
|
||||
}
|
||||
username = tunnelConfiguration.interface.name
|
||||
}
|
||||
|
||||
func tunnelConfiguration() -> TunnelConfiguration? {
|
||||
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return nil }
|
||||
return try? JSONDecoder().decode(TunnelConfiguration.self, from: serializedTunnelConfiguration)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -9,12 +9,12 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; };
|
||||
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */; };
|
||||
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
|
||||
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
|
||||
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
|
||||
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; };
|
||||
6F5D0C22218352EF000F85AD /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
6F5D0C452183BCDA000F85AD /* PacketTunnelOptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */; };
|
||||
6F5D0C462183C0B4000F85AD /* PacketTunnelOptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */; };
|
||||
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */; };
|
||||
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1E821B932F700483816 /* WireGuardAppError.swift */; };
|
||||
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */; };
|
||||
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; };
|
||||
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
|
||||
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */; };
|
||||
@ -42,8 +42,21 @@
|
||||
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; };
|
||||
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; };
|
||||
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; };
|
||||
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; };
|
||||
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FE219C60290028284D /* ZipExporter.swift */; };
|
||||
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; };
|
||||
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; };
|
||||
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF717E421B2CB1E0045A474 /* InternetReachability.swift */; };
|
||||
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */; };
|
||||
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; };
|
||||
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
|
||||
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
|
||||
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
|
||||
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
|
||||
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
|
||||
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
|
||||
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */; };
|
||||
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -80,14 +93,16 @@
|
||||
/* Begin PBXFileReference section */
|
||||
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; };
|
||||
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = "<group>"; };
|
||||
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = "<group>"; };
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
|
||||
6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
||||
6F5D0C1E218352EF000F85AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuardNetworkExtension.entitlements; sourceTree = "<group>"; };
|
||||
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WireGuardNetworkExtension-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptionKey.swift; sourceTree = "<group>"; };
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptionsGenerator.swift; sourceTree = "<group>"; };
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; };
|
||||
6F61F1E821B932F700483816 /* WireGuardAppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardAppError.swift; sourceTree = "<group>"; };
|
||||
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardResult.swift; sourceTree = "<group>"; };
|
||||
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewModel.swift; sourceTree = "<group>"; };
|
||||
6F628C3E217F3413003482A3 /* DNSServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSServer.swift; sourceTree = "<group>"; };
|
||||
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = "<group>"; };
|
||||
@ -120,6 +135,8 @@
|
||||
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = "<group>"; };
|
||||
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; };
|
||||
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = "<group>"; };
|
||||
6FE254FA219C10800028284D /* ZipImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipImporter.swift; sourceTree = "<group>"; };
|
||||
6FE254FE219C60290028284D /* ZipExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipExporter.swift; sourceTree = "<group>"; };
|
||||
6FF4AC14211EC46F002C96EB /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
@ -127,6 +144,10 @@
|
||||
6FF4AC2B211EC776002C96EB /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Config.xcconfig; path = Config/Config.xcconfig; sourceTree = "<group>"; };
|
||||
6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
|
||||
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; };
|
||||
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetReachability.swift; sourceTree = "<group>"; };
|
||||
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; };
|
||||
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = "<group>"; };
|
||||
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandSetting.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -152,9 +173,12 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */,
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */,
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */,
|
||||
6F5D0C1E218352EF000F85AD /* Info.plist */,
|
||||
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */,
|
||||
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */,
|
||||
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */,
|
||||
);
|
||||
path = WireGuardNetworkExtension;
|
||||
sourceTree = "<group>";
|
||||
@ -162,7 +186,9 @@
|
||||
6F5D0C432183B4A4000F85AD /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */,
|
||||
6F7774E6217201E0006A79B3 /* Model */,
|
||||
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
|
||||
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
@ -227,8 +253,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */,
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */,
|
||||
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
|
||||
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */,
|
||||
);
|
||||
path = VPN;
|
||||
sourceTree = "<group>";
|
||||
@ -255,6 +281,8 @@
|
||||
6FDEF7E72186320E00D8FBF6 /* ZipArchive */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6FE254FA219C10800028284D /* ZipImporter.swift */,
|
||||
6FE254FE219C60290028284D /* ZipExporter.swift */,
|
||||
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */,
|
||||
6FDEF7F421863B6100D8FBF6 /* 3rdparty */,
|
||||
);
|
||||
@ -308,10 +336,11 @@
|
||||
6F919ED3218C65C50023B400 /* Resources */,
|
||||
6F6899A32180445A0012E523 /* Crypto */,
|
||||
6F6899AA218099D00012E523 /* ConfigFile */,
|
||||
6F7774E6217201E0006A79B3 /* Model */,
|
||||
6F7774DD217181B1006A79B3 /* UI */,
|
||||
6F7774ED21722D0C006A79B3 /* VPN */,
|
||||
6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
|
||||
6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
|
||||
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
|
||||
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */,
|
||||
6FF4AC1E211EC472002C96EB /* Assets.xcassets */,
|
||||
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */,
|
||||
@ -355,6 +384,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */;
|
||||
buildPhases = (
|
||||
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */,
|
||||
6F5D0C16218352EF000F85AD /* Sources */,
|
||||
6F5D0C17218352EF000F85AD /* Frameworks */,
|
||||
6F5D0C18218352EF000F85AD /* Resources */,
|
||||
@ -410,6 +440,9 @@
|
||||
CreatedOnToolsVersion = 9.4.1;
|
||||
LastSwiftMigration = 1000;
|
||||
SystemCapabilities = {
|
||||
com.apple.ApplicationGroups.iOS = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.NetworkExtensions.iOS = {
|
||||
enabled = 1;
|
||||
};
|
||||
@ -480,6 +513,25 @@
|
||||
shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Extract wireguard-go Version";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@ -487,7 +539,15 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6F5D0C462183C0B4000F85AD /* PacketTunnelOptionKey.swift in Sources */,
|
||||
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */,
|
||||
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */,
|
||||
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
|
||||
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */,
|
||||
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */,
|
||||
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */,
|
||||
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */,
|
||||
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */,
|
||||
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */,
|
||||
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -499,30 +559,35 @@
|
||||
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */,
|
||||
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */,
|
||||
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
|
||||
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */,
|
||||
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */,
|
||||
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
|
||||
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
|
||||
6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
|
||||
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */,
|
||||
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
|
||||
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
|
||||
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
|
||||
6F6899A62180447E0012E523 /* x25519.c in Sources */,
|
||||
6F5D0C452183BCDA000F85AD /* PacketTunnelOptionKey.swift in Sources */,
|
||||
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
|
||||
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */,
|
||||
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
|
||||
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
|
||||
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
|
||||
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
|
||||
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
|
||||
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
|
||||
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
|
||||
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
|
||||
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
|
||||
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
|
||||
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
|
||||
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
|
||||
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */,
|
||||
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
|
||||
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
|
||||
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
|
||||
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
|
||||
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
|
||||
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1,2 +1,2 @@
|
||||
VERSION_NAME = 0.0.20181104
|
||||
VERSION_ID = 3
|
||||
VERSION_ID = 5
|
||||
|
@ -26,16 +26,16 @@ class WgQuickConfigFileParser {
|
||||
|
||||
func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? {
|
||||
// required wg fields
|
||||
guard let privateKeyString = attributes["PrivateKey"] else { return nil }
|
||||
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"] {
|
||||
if let listenPortString = attributes["listenport"] {
|
||||
guard let listenPort = UInt16(listenPortString) else { return nil }
|
||||
interface.listenPort = listenPort
|
||||
}
|
||||
// wg-quick fields
|
||||
if let addressesString = attributes["Address"] {
|
||||
if let addressesString = attributes["address"] {
|
||||
var addresses: [IPAddressRange] = []
|
||||
for addressString in addressesString.split(separator: ",") {
|
||||
let trimmedString = addressString.trimmingCharacters(in: .whitespaces)
|
||||
@ -44,7 +44,7 @@ class WgQuickConfigFileParser {
|
||||
}
|
||||
interface.addresses = addresses
|
||||
}
|
||||
if let dnsString = attributes["DNS"] {
|
||||
if let dnsString = attributes["dns"] {
|
||||
var dnsServers: [DNSServer] = []
|
||||
for dnsServerString in dnsString.split(separator: ",") {
|
||||
let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces)
|
||||
@ -53,7 +53,7 @@ class WgQuickConfigFileParser {
|
||||
}
|
||||
interface.dns = dnsServers
|
||||
}
|
||||
if let mtuString = attributes["MTU"] {
|
||||
if let mtuString = attributes["mtu"] {
|
||||
guard let mtu = UInt16(mtuString) else { return nil }
|
||||
interface.mtu = mtu
|
||||
}
|
||||
@ -62,15 +62,15 @@ class WgQuickConfigFileParser {
|
||||
|
||||
func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
|
||||
// required wg fields
|
||||
guard let publicKeyString = attributes["PublicKey"] else { return nil }
|
||||
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"] {
|
||||
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"] {
|
||||
if let allowedIPsString = attributes["allowedips"] {
|
||||
var allowedIPs: [IPAddressRange] = []
|
||||
for allowedIPString in allowedIPsString.split(separator: ",") {
|
||||
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
@ -79,11 +79,11 @@ class WgQuickConfigFileParser {
|
||||
}
|
||||
peer.allowedIPs = allowedIPs
|
||||
}
|
||||
if let endpointString = attributes["Endpoint"] {
|
||||
if let endpointString = attributes["endpoint"] {
|
||||
guard let endpoint = Endpoint(from: endpointString) else { return nil }
|
||||
peer.endpoint = endpoint
|
||||
}
|
||||
if let persistentKeepAliveString = attributes["PersistentKeepalive"] {
|
||||
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
|
||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
|
||||
peer.persistentKeepAlive = persistentKeepAlive
|
||||
}
|
||||
@ -113,9 +113,9 @@ class WgQuickConfigFileParser {
|
||||
|
||||
if let equalsIndex = line.firstIndex(of: "=") {
|
||||
// Line contains an attribute
|
||||
let key = line[..<equalsIndex].trimmingCharacters(in: .whitespaces)
|
||||
let key = line[..<equalsIndex].trimmingCharacters(in: .whitespaces).lowercased()
|
||||
let value = line[line.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespaces)
|
||||
let keysWithMultipleEntriesAllowed: Set<String> = ["Address", "AllowedIPs", "DNS"]
|
||||
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
|
||||
if let presentValue = attributes[key], keysWithMultipleEntriesAllowed.contains(key) {
|
||||
attributes[key] = presentValue + "," + value
|
||||
} else {
|
||||
@ -157,8 +157,7 @@ class WgQuickConfigFileParser {
|
||||
}
|
||||
|
||||
if let interfaceConfiguration = interfaceConfiguration {
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration)
|
||||
tunnelConfiguration.peers = peerConfigurations
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
|
||||
return tunnelConfiguration
|
||||
} else {
|
||||
throw ParseError.noInterface
|
||||
|
@ -25,3 +25,9 @@ struct Curve25519 {
|
||||
return publicKey
|
||||
}
|
||||
}
|
||||
|
||||
extension InterfaceConfiguration {
|
||||
var publicKey: Data {
|
||||
return Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
@ -41,6 +43,20 @@
|
||||
<string>com.pkware.zip-archive</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Text file</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.text</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
@ -106,5 +122,7 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -361,6 +361,7 @@ class TunnelViewModel {
|
||||
[TunnelViewModel.PeerData.ipv4DefaultRouteString]
|
||||
}
|
||||
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
|
||||
validatedConfiguration = nil // The configuration has been modified, and needs to be saved
|
||||
excludePrivateIPsValue = isOn
|
||||
}
|
||||
}
|
||||
@ -441,9 +442,42 @@ class TunnelViewModel {
|
||||
return .error("Two or more peers cannot have the same public key")
|
||||
}
|
||||
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration)
|
||||
tunnelConfiguration.peers = peerConfigurations
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
|
||||
return .saved(tunnelConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Activate on demand
|
||||
|
||||
extension TunnelViewModel {
|
||||
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
|
||||
switch (activateOnDemandOption) {
|
||||
case .none:
|
||||
return "Off"
|
||||
case .useOnDemandOverWiFiOrCellular:
|
||||
return "Wi-Fi or cellular"
|
||||
case .useOnDemandOverWiFiOnly:
|
||||
return "Wi-Fi only"
|
||||
case .useOnDemandOverCellularOnly:
|
||||
return "Cellular only"
|
||||
}
|
||||
}
|
||||
|
||||
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
|
||||
if let activateOnDemandSetting = activateOnDemandSetting {
|
||||
if (activateOnDemandSetting.isActivateOnDemandEnabled) {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
|
||||
} else {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
||||
}
|
||||
} else {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
||||
}
|
||||
}
|
||||
|
||||
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
|
||||
return .useOnDemandOverWiFiOrCellular
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var mainVC: MainViewController?
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.backgroundColor = UIColor.white
|
||||
@ -27,14 +27,47 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
defer {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {
|
||||
os_log("Failed to remove item from Inbox: %{public}@", log: OSLog.default, type: .debug, url.absoluteString)
|
||||
}
|
||||
}
|
||||
mainVC?.tunnelsListVC?.importFromFile(url: url)
|
||||
_ = FileManager.deleteFile(at: url)
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
mainVC?.refreshTunnelConnectionStatuses()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: State restoration
|
||||
|
||||
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 == "MainVC") {
|
||||
return MainViewController()
|
||||
} else if (vcIdentifier == "TunnelsListVC") {
|
||||
return TunnelsListTableViewController()
|
||||
} else 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
|
||||
}
|
||||
}
|
||||
|
@ -5,54 +5,10 @@ import UIKit
|
||||
import os.log
|
||||
|
||||
class ErrorPresenter {
|
||||
static func errorMessage(for error: Error) -> (String, String)? {
|
||||
switch (error) {
|
||||
|
||||
// TunnelManagementError
|
||||
case TunnelManagementError.tunnelAlreadyExistsWithThatName:
|
||||
return ("Name already exists", "A tunnel with that name already exists")
|
||||
case TunnelManagementError.tunnelInvalidName:
|
||||
return ("Name already exists", "The tunnel name is invalid")
|
||||
case TunnelManagementError.vpnSystemErrorOnAddTunnel:
|
||||
return ("Unable to create tunnel", "Internal error")
|
||||
case TunnelManagementError.vpnSystemErrorOnModifyTunnel:
|
||||
return ("Unable to modify tunnel", "Internal error")
|
||||
case TunnelManagementError.vpnSystemErrorOnRemoveTunnel:
|
||||
return ("Unable to remove tunnel", "Internal error")
|
||||
|
||||
// TunnelActivationError
|
||||
case TunnelActivationError.dnsResolutionFailed:
|
||||
return ("DNS resolution failure", "One or more endpoint domains could not be resolved")
|
||||
case TunnelActivationError.tunnelActivationFailed:
|
||||
return ("Activation failure", "The tunnel could not be activated due to an internal error")
|
||||
case TunnelActivationError.attemptingActivationWhenAnotherTunnelIsBusy(let otherTunnelStatus):
|
||||
let statusString: String = {
|
||||
switch (otherTunnelStatus) {
|
||||
case .active: fallthrough
|
||||
case .reasserting: fallthrough
|
||||
case .restarting:
|
||||
return "active"
|
||||
case .activating: fallthrough
|
||||
case .resolvingEndpointDomains:
|
||||
return "being activated"
|
||||
case .deactivating:
|
||||
return "being deactivated"
|
||||
case .inactive:
|
||||
fatalError()
|
||||
}
|
||||
}()
|
||||
return ("Activation failure", "Another tunnel is currently \(statusString)")
|
||||
|
||||
default:
|
||||
os_log("ErrorPresenter: Error not presented: %{public}@", log: OSLog.default, type: .error, "\(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func showErrorAlert(error: Error, from sourceVC: UIViewController?,
|
||||
static func showErrorAlert(error: WireGuardAppError, 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 (title, message) = error.alertText()
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
|
||||
onDismissal?()
|
||||
}
|
||||
|
@ -4,20 +4,34 @@
|
||||
import UIKit
|
||||
|
||||
class MainViewController: UISplitViewController {
|
||||
|
||||
var tunnelsManager: TunnelsManager?
|
||||
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
|
||||
|
||||
var tunnelsListVC: TunnelsListTableViewController?
|
||||
|
||||
override func loadView() {
|
||||
init() {
|
||||
let detailVC = UIViewController()
|
||||
detailVC.view.backgroundColor = UIColor.white
|
||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||
|
||||
let masterVC = TunnelsListTableViewController()
|
||||
let masterNC = UINavigationController(rootViewController: masterVC)
|
||||
|
||||
self.tunnelsListVC = masterVC
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.viewControllers = [ masterNC, detailNC ]
|
||||
|
||||
super.loadView()
|
||||
// State restoration
|
||||
self.restorationIdentifier = "MainVC"
|
||||
masterNC.restorationIdentifier = "MasterNC"
|
||||
detailNC.restorationIdentifier = "DetailNC"
|
||||
}
|
||||
|
||||
tunnelsListVC = masterVC
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
@ -25,10 +39,62 @@ class MainViewController: UISplitViewController {
|
||||
|
||||
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
|
||||
self.preferredDisplayMode = .allVisible
|
||||
|
||||
// Create the tunnels manager, and when it's ready, inform tunnelsListVC
|
||||
TunnelsManager.create { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
let tunnelsManager: TunnelsManager = result.value!
|
||||
guard let s = self else { return }
|
||||
|
||||
s.tunnelsManager = tunnelsManager
|
||||
s.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
|
||||
|
||||
tunnelsManager.activationDelegate = s
|
||||
|
||||
s.onTunnelsManagerReady?(tunnelsManager)
|
||||
s.onTunnelsManagerReady = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController: TunnelsManagerActivationDelegate {
|
||||
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError) {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController {
|
||||
func refreshTunnelConnectionStatuses() {
|
||||
if let tunnelsManager = tunnelsManager {
|
||||
tunnelsManager.refreshStatuses()
|
||||
}
|
||||
}
|
||||
|
||||
func openForEditing(configFileURL: URL) {
|
||||
tunnelsListVC?.openForEditing(configFileURL: configFileURL)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,11 +10,13 @@ class SettingsTableViewController: UITableViewController {
|
||||
case iosAppVersion = "WireGuard for iOS"
|
||||
case goBackendVersion = "WireGuard Go Backend"
|
||||
case exportZipArchive = "Export zip archive"
|
||||
case exportLogFile = "Export log file"
|
||||
}
|
||||
|
||||
let settingsFieldsBySection: [[SettingsFields]] = [
|
||||
[.iosAppVersion, .goBackendVersion],
|
||||
[.exportZipArchive]
|
||||
[.exportZipArchive],
|
||||
[.exportLogFile]
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager?
|
||||
@ -63,55 +65,73 @@ class SettingsTableViewController: UITableViewController {
|
||||
}
|
||||
|
||||
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 tunnelsManager = tunnelsManager else { return }
|
||||
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)
|
||||
}
|
||||
_ = FileManager.deleteFile(at: destinationURL)
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
} catch (let error) {
|
||||
showErrorAlert(title: "Unable to export", message: "There was an error exporting the tunnel configuration archive: \(String(describing: error))")
|
||||
let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
|
||||
self?.present(fileExportVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
func exportLogForLastActivatedTunnel(sourceView: UIView) {
|
||||
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
return
|
||||
}
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
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: "No log available", message: "The pre-existing log could not be cleared", from: self)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let networkExtensionLogFileURL = FileManager.networkExtensionLogFileURL,
|
||||
FileManager.default.fileExists(atPath: networkExtensionLogFileURL.path) else {
|
||||
ErrorPresenter.showErrorAlert(title: "No log available", message: "Please activate a tunnel and then export the log", from: self)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.copyItem(at: networkExtensionLogFileURL, to: destinationURL)
|
||||
} catch {
|
||||
os_log("Failed to copy file: %{public}@ to %{public}@: %{public}@", log: OSLog.default, type: .error, networkExtensionLogFileURL.absoluteString, destinationURL.absoluteString, error.localizedDescription)
|
||||
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "The log could not be copied", from: self)
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
|
||||
// popoverPresentationController shall be non-nil on the iPad
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,6 +152,8 @@ extension SettingsTableViewController {
|
||||
return "About"
|
||||
case 1:
|
||||
return "Export configurations"
|
||||
case 2:
|
||||
return "Tunnel log"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@ -143,20 +165,30 @@ extension SettingsTableViewController {
|
||||
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"
|
||||
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 {
|
||||
assert(field == .exportZipArchive)
|
||||
} else if (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
|
||||
} else {
|
||||
assert(field == .exportLogFile)
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = { [weak self] in
|
||||
self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,10 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
self.tableView.register(TunnelDetailTableViewStatusCell.self, forCellReuseIdentifier: TunnelDetailTableViewStatusCell.id)
|
||||
self.tableView.register(TunnelDetailTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
|
||||
self.tableView.register(TunnelDetailTableViewButtonCell.self, forCellReuseIdentifier: TunnelDetailTableViewButtonCell.id)
|
||||
self.tableView.register(TunnelDetailTableViewActivateOnDemandCell.self, forCellReuseIdentifier: TunnelDetailTableViewActivateOnDemandCell.id)
|
||||
|
||||
// State restoration
|
||||
self.restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
|
||||
}
|
||||
|
||||
@objc func editTapped() {
|
||||
@ -52,14 +56,6 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
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
|
||||
@ -72,6 +68,7 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
|
||||
// popoverPresentationController will be nil on iPhone and non-nil on iPad
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
alert.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
@ -94,7 +91,7 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate
|
||||
|
||||
extension TunnelDetailTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 3 + tunnelViewModel.peersData.count
|
||||
return 4 + tunnelViewModel.peersData.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
@ -111,7 +108,11 @@ extension TunnelDetailTableViewController {
|
||||
// Peer
|
||||
let peerData = tunnelViewModel.peersData[section - 2]
|
||||
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
|
||||
} else if (section < (3 + numberOfPeerSections)) {
|
||||
// Activate on demand
|
||||
return 1
|
||||
} else {
|
||||
assert(section == (3 + numberOfPeerSections))
|
||||
// Delete tunnel
|
||||
return 1
|
||||
}
|
||||
@ -129,6 +130,9 @@ extension TunnelDetailTableViewController {
|
||||
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
|
||||
// Peer
|
||||
return "Peer"
|
||||
} else if (section < (3 + numberOfPeerSections)) {
|
||||
// On-Demand Activation
|
||||
return "On-Demand Activation"
|
||||
} else {
|
||||
// Delete tunnel
|
||||
return nil
|
||||
@ -151,16 +155,15 @@ extension TunnelDetailTableViewController {
|
||||
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
|
||||
}
|
||||
ErrorPresenter.showErrorAlert(error: error, from: s, onPresented: {
|
||||
DispatchQueue.main.async {
|
||||
cell.statusSwitch.isOn = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.tunnelsManager.startDeactivation(of: s.tunnel) { error in
|
||||
print("Error while deactivating: \(String(describing: error))")
|
||||
}
|
||||
s.tunnelsManager.startDeactivation(of: s.tunnel)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
@ -182,8 +185,13 @@ extension TunnelDetailTableViewController {
|
||||
cell.key = field.rawValue
|
||||
cell.value = peerData[field]
|
||||
return cell
|
||||
} else if (section < (3 + numberOfPeerSections)) {
|
||||
// On-Demand Activation
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewActivateOnDemandCell.id, for: indexPath) as! TunnelDetailTableViewActivateOnDemandCell
|
||||
cell.tunnel = self.tunnel
|
||||
return cell
|
||||
} else {
|
||||
assert(section == (2 + numberOfPeerSections))
|
||||
assert(section == (3 + numberOfPeerSections))
|
||||
// Delete configuration
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
|
||||
cell.buttonText = "Delete tunnel"
|
||||
@ -258,10 +266,10 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
|
||||
text = "Deactivating"
|
||||
case .reasserting:
|
||||
text = "Reactivating"
|
||||
case .resolvingEndpointDomains:
|
||||
text = "Resolving domains"
|
||||
case .restarting:
|
||||
text = "Restarting"
|
||||
case .waiting:
|
||||
text = "Waiting"
|
||||
}
|
||||
textLabel?.text = text
|
||||
DispatchQueue.main.async { [weak statusSwitch] in
|
||||
@ -393,3 +401,36 @@ class TunnelDetailTableViewButtonCell: UITableViewCell {
|
||||
hasDestructiveAction = false
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelDetailTableViewActivateOnDemandCell: UITableViewCell {
|
||||
static let id: String = "TunnelDetailTableViewActivateOnDemandCell"
|
||||
|
||||
var tunnel: TunnelContainer? {
|
||||
didSet(value) {
|
||||
update(from: tunnel?.activateOnDemandSetting())
|
||||
onDemandStatusObservervationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] (tunnel, _) in
|
||||
self?.update(from: tunnel.activateOnDemandSetting())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onDemandStatusObservervationToken: AnyObject?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
|
||||
textLabel?.text = "Activate on demand"
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(from activateOnDemandSetting: ActivateOnDemandSetting?) {
|
||||
detailTextLabel?.text = TunnelViewModel.activateOnDemandDetailText(for: activateOnDemandSetting)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
detailTextLabel?.text = ""
|
||||
}
|
||||
}
|
||||
|
@ -26,15 +26,23 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
.deletePeer
|
||||
]
|
||||
|
||||
let activateOnDemandOptions: [ActivateOnDemandOption] = [
|
||||
.useOnDemandOverWiFiOrCellular,
|
||||
.useOnDemandOverWiFiOnly,
|
||||
.useOnDemandOverCellularOnly
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager
|
||||
let tunnel: TunnelContainer?
|
||||
let tunnelViewModel: TunnelViewModel
|
||||
var activateOnDemandSetting: ActivateOnDemandSetting
|
||||
|
||||
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
|
||||
// Use this initializer to edit an existing tunnel.
|
||||
tunnelsManager = tm
|
||||
tunnel = t
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
|
||||
activateOnDemandSetting = t.activateOnDemandSetting()
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
@ -44,6 +52,7 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
tunnelsManager = tm
|
||||
tunnel = nil
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
|
||||
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
@ -58,12 +67,12 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
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(TunnelEditTableViewReadOnlyKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.id)
|
||||
self.tableView.register(TunnelEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelEditTableViewButtonCell.id)
|
||||
self.tableView.register(TunnelEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelEditTableViewSwitchCell.id)
|
||||
self.tableView.register(TunnelEditTableViewSelectionListCell.self, forCellReuseIdentifier: TunnelEditTableViewSelectionListCell.id)
|
||||
}
|
||||
|
||||
@objc func saveTapped() {
|
||||
@ -77,7 +86,9 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
case .saved(let tunnelConfiguration):
|
||||
if let tunnel = tunnel {
|
||||
// We're modifying an existing tunnel
|
||||
tunnelsManager.modify(tunnel: tunnel, with: tunnelConfiguration) { [weak self] (error) in
|
||||
tunnelsManager.modify(tunnel: tunnel,
|
||||
tunnelConfiguration: tunnelConfiguration,
|
||||
activateOnDemandSetting: activateOnDemandSetting) { [weak self] (error) in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
@ -87,14 +98,14 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
}
|
||||
} else {
|
||||
// We're adding a new tunnel
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] (tunnel, error) in
|
||||
if let error = error {
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration,
|
||||
activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
let tunnel: TunnelContainer = result.value!
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
if let tunnel = tunnel {
|
||||
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
||||
}
|
||||
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -105,14 +116,6 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
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
|
||||
@ -122,7 +125,7 @@ extension TunnelEditTableViewController {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
return numberOfInterfaceSections + numberOfPeerSections + 1
|
||||
return numberOfInterfaceSections + numberOfPeerSections + 1 /* Add Peer */ + 1 /* On-Demand */
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
@ -138,9 +141,16 @@ extension TunnelEditTableViewController {
|
||||
let peerData = tunnelViewModel.peersData[peerIndex]
|
||||
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
|
||||
return peerFieldsToShow.count
|
||||
} else {
|
||||
} else if (section < (numberOfInterfaceSections + numberOfPeerSections + 1)) {
|
||||
// Add peer
|
||||
return 1
|
||||
} else {
|
||||
// On-Demand Rules
|
||||
if (activateOnDemandSetting.isActivateOnDemandEnabled) {
|
||||
return 4
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,9 +164,12 @@ extension TunnelEditTableViewController {
|
||||
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
|
||||
// Peer
|
||||
return "Peer"
|
||||
} else {
|
||||
} else if (section == (numberOfInterfaceSections + numberOfPeerSections)) {
|
||||
// Add peer
|
||||
return nil
|
||||
} else {
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
|
||||
return "On-Demand Activation"
|
||||
}
|
||||
}
|
||||
|
||||
@ -344,8 +357,7 @@ extension TunnelEditTableViewController {
|
||||
}
|
||||
return cell
|
||||
}
|
||||
} else {
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeerSections))
|
||||
} else if (section == (numberOfInterfaceSections + numberOfPeerSections)) {
|
||||
// Add peer
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
|
||||
cell.buttonText = "Add peer"
|
||||
@ -365,6 +377,37 @@ extension TunnelEditTableViewController {
|
||||
}, completion: nil)
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
|
||||
if (row == 0) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSwitchCell.id, for: indexPath) as! TunnelEditTableViewSwitchCell
|
||||
cell.message = "Activate on demand"
|
||||
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
|
||||
cell.onSwitchToggled = { [weak self] (isOn) in
|
||||
guard let s = self else { return }
|
||||
let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: section) }
|
||||
if (isOn) {
|
||||
s.activateOnDemandSetting.isActivateOnDemandEnabled = true
|
||||
if (s.activateOnDemandSetting.activateOnDemandOption == .none) {
|
||||
s.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
|
||||
}
|
||||
s.tableView.insertRows(at: indexPaths, with: .automatic)
|
||||
} else {
|
||||
s.activateOnDemandSetting.isActivateOnDemandEnabled = false
|
||||
s.tableView.deleteRows(at: indexPaths, with: .automatic)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
assert(row < 4)
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSelectionListCell.id, for: indexPath) as! TunnelEditTableViewSelectionListCell
|
||||
let rowOption = activateOnDemandOptions[row - 1]
|
||||
let selectedOption = activateOnDemandSetting.activateOnDemandOption
|
||||
assert(selectedOption != .none)
|
||||
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
|
||||
cell.isChecked = (selectedOption == rowOption)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,11 +443,48 @@ extension TunnelEditTableViewController {
|
||||
|
||||
// popoverPresentationController will be nil on iPhone and non-nil on iPad
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
alert.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDelegate
|
||||
|
||||
extension TunnelEditTableViewController {
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
let section = indexPath.section
|
||||
let row = indexPath.row
|
||||
|
||||
if (section == (numberOfInterfaceSections + numberOfPeerSections + 1)) {
|
||||
return (row > 0) ? indexPath : nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
let section = indexPath.section
|
||||
let row = indexPath.row
|
||||
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
|
||||
assert(row > 0)
|
||||
|
||||
let option = activateOnDemandOptions[row - 1]
|
||||
assert(option != .none)
|
||||
activateOnDemandSetting.activateOnDemandOption = option
|
||||
|
||||
let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: section) }
|
||||
tableView.reloadRows(at: indexPaths, with: .automatic)
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelEditTableViewKeyValueCell: UITableViewCell {
|
||||
static let id: String = "TunnelEditTableViewKeyValueCell"
|
||||
var key: String {
|
||||
@ -487,6 +567,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell {
|
||||
isValueValid = true
|
||||
keyboardType = .default
|
||||
onValueChanged = nil
|
||||
onValueBeingEdited = nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -663,3 +744,30 @@ class TunnelEditTableViewSwitchCell: UITableViewCell {
|
||||
isOn = false
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelEditTableViewSelectionListCell: UITableViewCell {
|
||||
static let id: String = "TunnelEditTableViewSelectionListCell"
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,11 @@
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import UserNotifications
|
||||
|
||||
class TunnelsListTableViewController: UIViewController {
|
||||
|
||||
var tunnelsManager: TunnelsManager?
|
||||
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
|
||||
|
||||
var busyIndicator: UIActivityIndicatorView?
|
||||
var centeredAddButton: BorderedTextButton?
|
||||
@ -38,50 +38,66 @@ class TunnelsListTableViewController: UIViewController {
|
||||
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 }
|
||||
// State restoration
|
||||
self.restorationIdentifier = "TunnelsListVC"
|
||||
}
|
||||
|
||||
let tableView = UITableView(frame: CGRect.zero, style: .plain)
|
||||
tableView.rowHeight = 60
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id)
|
||||
func setTunnelsManager(tunnelsManager: TunnelsManager) {
|
||||
if (self.tunnelsManager != nil) {
|
||||
// If a tunnels manager is already set, do nothing
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
// Create the table view
|
||||
|
||||
// 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
|
||||
let tableView = UITableView(frame: CGRect.zero, style: .plain)
|
||||
tableView.rowHeight = 60
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id)
|
||||
|
||||
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
|
||||
busyIndicator.stopAnimating()
|
||||
self.view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
|
||||
tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
|
||||
tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
|
||||
])
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
self.tableView = tableView
|
||||
|
||||
tunnelsManager.delegate = s
|
||||
s.tunnelsManager = tunnelsManager
|
||||
s.onTunnelsManagerReady?(tunnelsManager)
|
||||
s.onTunnelsManagerReady = nil
|
||||
// Add button at the center
|
||||
|
||||
let centeredAddButton = BorderedTextButton()
|
||||
centeredAddButton.title = "Add a tunnel"
|
||||
centeredAddButton.isHidden = true
|
||||
self.view.addSubview(centeredAddButton)
|
||||
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
centeredAddButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
centeredAddButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
|
||||
])
|
||||
centeredAddButton.onTapped = { [weak self] in
|
||||
self?.addButtonTapped(sender: centeredAddButton)
|
||||
}
|
||||
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
|
||||
self.centeredAddButton = centeredAddButton
|
||||
|
||||
// Hide the busy indicator
|
||||
|
||||
self.busyIndicator?.stopAnimating()
|
||||
|
||||
// Keep track of the tunnels manager
|
||||
|
||||
self.tunnelsManager = tunnelsManager
|
||||
tunnelsManager.tunnelsListDelegate = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_: Bool) {
|
||||
// Remove selection when getting back to the list view on iPhone
|
||||
if let tableView = self.tableView, let selectedRowIndexPath = tableView.indexPathForSelectedRow {
|
||||
tableView.deselectRow(at: selectedRowIndexPath, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +129,7 @@ class TunnelsListTableViewController: UIViewController {
|
||||
alert.popoverPresentationController?.barButtonItem = sender
|
||||
} else if let sender = sender as? UIView {
|
||||
alert.popoverPresentationController?.sourceView = sender
|
||||
alert.popoverPresentationController?.sourceRect = sender.bounds
|
||||
}
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
@ -125,26 +142,6 @@ class TunnelsListTableViewController: UIViewController {
|
||||
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)
|
||||
@ -153,7 +150,7 @@ class TunnelsListTableViewController: UIViewController {
|
||||
}
|
||||
|
||||
func presentViewControllerForFileImport() {
|
||||
let documentTypes = ["com.wireguard.config.quick", String(kUTTypeZipArchive)]
|
||||
let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)]
|
||||
let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
|
||||
filePicker.delegate = self
|
||||
self.present(filePicker, animated: true)
|
||||
@ -167,77 +164,37 @@ class TunnelsListTableViewController: UIViewController {
|
||||
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) {
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
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 {
|
||||
ZipImporter.importConfigFiles(from: url) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
self?.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
|
||||
message: "Created \(numberSuccessful) of \(unarchivedFiles.count) tunnels from zip archive")
|
||||
let configs: [TunnelConfiguration?] = result.value!
|
||||
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] (numberSuccessful) in
|
||||
if numberSuccessful == configs.count {
|
||||
return
|
||||
}
|
||||
ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
|
||||
message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive",
|
||||
from: self)
|
||||
}
|
||||
}
|
||||
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
|
||||
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let fileContents = try? String(contentsOf: url),
|
||||
let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) {
|
||||
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in
|
||||
if let error = error {
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showErrorAlert(title: "Unable to import tunnel", message: "An error occured when importing the tunnel configuration.")
|
||||
ErrorPresenter.showErrorAlert(title: "Unable to import tunnel",
|
||||
message: "An error occured when importing the tunnel configuration.",
|
||||
from: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,8 +213,8 @@ extension TunnelsListTableViewController: UIDocumentPickerDelegate {
|
||||
extension TunnelsListTableViewController: QRScanViewControllerDelegate {
|
||||
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
|
||||
completionHandler: (() -> Void)?) {
|
||||
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in
|
||||
if let error = error {
|
||||
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
|
||||
} else {
|
||||
completionHandler?()
|
||||
@ -295,9 +252,7 @@ extension TunnelsListTableViewController: UITableViewDataSource {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tunnelsManager.startDeactivation(of: tunnel) { [weak s] error in
|
||||
s?.showErrorAlert(title: "Deactivation error", message: "Error while bringing down tunnel: \(String(describing: error))")
|
||||
}
|
||||
tunnelsManager.startDeactivation(of: tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -309,12 +264,12 @@ extension TunnelsListTableViewController: UITableViewDataSource {
|
||||
|
||||
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)
|
||||
tunnelDetailNC.restorationIdentifier = "DetailNC"
|
||||
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
|
||||
}
|
||||
|
||||
@ -338,7 +293,7 @@ extension TunnelsListTableViewController: UITableViewDelegate {
|
||||
|
||||
// MARK: TunnelsManagerDelegate
|
||||
|
||||
extension TunnelsListTableViewController: TunnelsManagerDelegate {
|
||||
extension TunnelsListTableViewController: TunnelsManagerListDelegate {
|
||||
func tunnelAdded(at index: Int) {
|
||||
tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
|
||||
|
75
WireGuard/WireGuard/VPN/ActivateOnDemandSetting.swift
Normal file
75
WireGuard/WireGuard/VPN/ActivateOnDemandSetting.swift
Normal file
@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
struct ActivateOnDemandSetting {
|
||||
var isActivateOnDemandEnabled: Bool
|
||||
var activateOnDemandOption: ActivateOnDemandOption
|
||||
}
|
||||
|
||||
enum ActivateOnDemandOption {
|
||||
case none // Valid only when isActivateOnDemandEnabled is false
|
||||
case useOnDemandOverWiFiOrCellular
|
||||
case useOnDemandOverWiFiOnly
|
||||
case useOnDemandOverCellularOnly
|
||||
}
|
||||
|
||||
extension ActivateOnDemandSetting {
|
||||
func apply(on tunnelProviderManager: NETunnelProviderManager) {
|
||||
tunnelProviderManager.isOnDemandEnabled = isActivateOnDemandEnabled
|
||||
let rules: [NEOnDemandRule]?
|
||||
let connectRule = NEOnDemandRuleConnect()
|
||||
let disconnectRule = NEOnDemandRuleDisconnect()
|
||||
switch (activateOnDemandOption) {
|
||||
case .none:
|
||||
rules = nil
|
||||
case .useOnDemandOverWiFiOrCellular:
|
||||
rules = [connectRule]
|
||||
case .useOnDemandOverWiFiOnly:
|
||||
connectRule.interfaceTypeMatch = .wiFi
|
||||
disconnectRule.interfaceTypeMatch = .cellular
|
||||
rules = [connectRule, disconnectRule]
|
||||
case .useOnDemandOverCellularOnly:
|
||||
connectRule.interfaceTypeMatch = .cellular
|
||||
disconnectRule.interfaceTypeMatch = .wiFi
|
||||
rules = [connectRule, disconnectRule]
|
||||
}
|
||||
tunnelProviderManager.onDemandRules = rules
|
||||
}
|
||||
|
||||
init(from tunnelProviderManager: NETunnelProviderManager) {
|
||||
let rules = tunnelProviderManager.onDemandRules ?? []
|
||||
let activateOnDemandOption: ActivateOnDemandOption
|
||||
switch (rules.count) {
|
||||
case 0:
|
||||
activateOnDemandOption = .none
|
||||
case 1:
|
||||
let rule = rules[0]
|
||||
precondition(rule.action == .connect)
|
||||
activateOnDemandOption = .useOnDemandOverWiFiOrCellular
|
||||
case 2:
|
||||
let connectRule = rules.first(where: { $0.action == .connect })!
|
||||
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
|
||||
if (connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular) {
|
||||
activateOnDemandOption = .useOnDemandOverWiFiOnly
|
||||
} else if (connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi) {
|
||||
activateOnDemandOption = .useOnDemandOverCellularOnly
|
||||
} else {
|
||||
fatalError("Unexpected onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
default:
|
||||
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
self.activateOnDemandOption = activateOnDemandOption
|
||||
if (activateOnDemandOption == .none) {
|
||||
self.isActivateOnDemandEnabled = false
|
||||
} else {
|
||||
self.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActivateOnDemandSetting {
|
||||
static var defaultSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: false, activateOnDemandOption: .none)
|
||||
}
|
51
WireGuard/WireGuard/VPN/InternetReachability.swift
Normal file
51
WireGuard/WireGuard/VPN/InternetReachability.swift
Normal file
@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import SystemConfiguration
|
||||
|
||||
class InternetReachability {
|
||||
|
||||
enum Status {
|
||||
case unknown
|
||||
case notReachable
|
||||
case reachableOverWiFi
|
||||
case reachableOverCellular
|
||||
}
|
||||
|
||||
static func currentStatus() -> Status {
|
||||
var status: Status = .unknown
|
||||
if let reachabilityRef = InternetReachability.reachabilityRef() {
|
||||
var flags = SCNetworkReachabilityFlags(rawValue: 0)
|
||||
SCNetworkReachabilityGetFlags(reachabilityRef, &flags)
|
||||
status = Status(reachabilityFlags: flags)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
private static func reachabilityRef() -> SCNetworkReachability? {
|
||||
let addrIn = sockaddr_in(sin_len: UInt8(MemoryLayout<sockaddr_in>.size),
|
||||
sin_family: sa_family_t(AF_INET),
|
||||
sin_port: 0,
|
||||
sin_addr: in_addr(s_addr: 0),
|
||||
sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
|
||||
return withUnsafePointer(to: addrIn) { (addrInPtr) -> SCNetworkReachability? in
|
||||
addrInPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { (addrPtr) -> SCNetworkReachability? in
|
||||
return SCNetworkReachabilityCreateWithAddress(nil, addrPtr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InternetReachability.Status {
|
||||
init(reachabilityFlags flags: SCNetworkReachabilityFlags) {
|
||||
var status: InternetReachability.Status = .notReachable
|
||||
if (flags.contains(.reachable)) {
|
||||
if (flags.contains(.isWWAN)) {
|
||||
status = .reachableOverCellular
|
||||
} else {
|
||||
status = .reachableOverWiFi
|
||||
}
|
||||
}
|
||||
self = status
|
||||
}
|
||||
}
|
@ -5,33 +5,61 @@ import Foundation
|
||||
import NetworkExtension
|
||||
import os.log
|
||||
|
||||
protocol TunnelsManagerDelegate: class {
|
||||
protocol TunnelsManagerListDelegate: class {
|
||||
func tunnelAdded(at: Int)
|
||||
func tunnelModified(at: Int)
|
||||
func tunnelMoved(at oldIndex: Int, to newIndex: Int)
|
||||
func tunnelRemoved(at: Int)
|
||||
}
|
||||
|
||||
enum TunnelActivationError: Error {
|
||||
case dnsResolutionFailed
|
||||
case tunnelActivationFailed
|
||||
case attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: TunnelStatus)
|
||||
case attemptingActivationWhenTunnelIsNotInactive
|
||||
case attemptingDeactivationWhenTunnelIsInactive
|
||||
protocol TunnelsManagerActivationDelegate: class {
|
||||
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError)
|
||||
}
|
||||
|
||||
enum TunnelManagementError: Error {
|
||||
enum TunnelsManagerError: WireGuardAppError {
|
||||
// Tunnels list management
|
||||
case tunnelNameEmpty
|
||||
case tunnelAlreadyExistsWithThatName
|
||||
case tunnelInvalidName
|
||||
case vpnSystemErrorOnListingTunnels
|
||||
case vpnSystemErrorOnAddTunnel
|
||||
case vpnSystemErrorOnModifyTunnel
|
||||
case vpnSystemErrorOnRemoveTunnel
|
||||
|
||||
// Tunnel activation
|
||||
case tunnelActivationAttemptFailed // startTunnel() throwed
|
||||
case tunnelActivationFailedInternalError // startTunnel() succeeded, but activation failed
|
||||
case tunnelActivationFailedNoInternetConnection // startTunnel() succeeded, but activation failed since no internet
|
||||
|
||||
func alertText() -> (String, String) {
|
||||
switch (self) {
|
||||
case .tunnelNameEmpty:
|
||||
return ("No name provided", "Can't create tunnel with an empty name")
|
||||
case .tunnelAlreadyExistsWithThatName:
|
||||
return ("Name already exists", "A tunnel with that name already exists")
|
||||
case .vpnSystemErrorOnListingTunnels:
|
||||
return ("Unable to list tunnels", "Internal error")
|
||||
case .vpnSystemErrorOnAddTunnel:
|
||||
return ("Unable to create tunnel", "Internal error")
|
||||
case .vpnSystemErrorOnModifyTunnel:
|
||||
return ("Unable to modify tunnel", "Internal error")
|
||||
case .vpnSystemErrorOnRemoveTunnel:
|
||||
return ("Unable to remove tunnel", "Internal error")
|
||||
|
||||
case .tunnelActivationAttemptFailed:
|
||||
return ("Activation failure", "The tunnel could not be activated due to an internal error")
|
||||
case .tunnelActivationFailedInternalError:
|
||||
return ("Activation failure", "The tunnel could not be activated due to an internal error")
|
||||
case .tunnelActivationFailedNoInternetConnection:
|
||||
return ("Activation failure", "No internet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelsManager {
|
||||
|
||||
private var tunnels: [TunnelContainer]
|
||||
weak var delegate: TunnelsManagerDelegate?
|
||||
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
|
||||
weak var activationDelegate: TunnelsManagerActivationDelegate?
|
||||
|
||||
private var isAddingTunnel: Bool = false
|
||||
private var isModifyingTunnel: Bool = false
|
||||
@ -41,25 +69,33 @@ class TunnelsManager {
|
||||
self.tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
|
||||
}
|
||||
|
||||
static func create(completionHandler: @escaping (TunnelsManager?) -> Void) {
|
||||
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) {
|
||||
#if targetEnvironment(simulator)
|
||||
// NETunnelProviderManager APIs don't work on the simulator
|
||||
completionHandler(.success(TunnelsManager(tunnelProviders: [])))
|
||||
#else
|
||||
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
|
||||
if let error = error {
|
||||
os_log("Failed to load tunnel provider managers: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
completionHandler(.failure(TunnelsManagerError.vpnSystemErrorOnListingTunnels))
|
||||
return
|
||||
}
|
||||
completionHandler(TunnelsManager(tunnelProviders: managers ?? []))
|
||||
completionHandler(.success(TunnelsManager(tunnelProviders: managers ?? [])))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func add(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelContainer?, TunnelManagementError?) -> Void) {
|
||||
func add(tunnelConfiguration: TunnelConfiguration,
|
||||
activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting,
|
||||
completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.interface.name
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
|
||||
return
|
||||
}
|
||||
|
||||
if self.tunnels.contains(where: { $0.name == tunnelName }) {
|
||||
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
|
||||
return
|
||||
}
|
||||
|
||||
@ -69,19 +105,21 @@ class TunnelsManager {
|
||||
tunnelProviderManager.localizedDescription = tunnelName
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] (error) in
|
||||
defer { self?.isAddingTunnel = false }
|
||||
guard (error == nil) else {
|
||||
os_log("Add: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(nil, TunnelManagementError.vpnSystemErrorOnAddTunnel)
|
||||
completionHandler(.failure(TunnelsManagerError.vpnSystemErrorOnAddTunnel))
|
||||
return
|
||||
}
|
||||
if let s = self {
|
||||
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
|
||||
s.tunnels.append(tunnel)
|
||||
s.tunnels.sort { $0.name < $1.name }
|
||||
s.delegate?.tunnelAdded(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
completionHandler(tunnel, nil)
|
||||
s.tunnelsListDelegate?.tunnelAdded(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
completionHandler(.success(tunnel))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,17 +134,18 @@ class TunnelsManager {
|
||||
return
|
||||
}
|
||||
let tail = tunnelConfigurations.dropFirst()
|
||||
self.add(tunnelConfiguration: head) { [weak self, tail] (_, error) in
|
||||
self.add(tunnelConfiguration: head) { [weak self, tail] (result) in
|
||||
DispatchQueue.main.async {
|
||||
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (error == nil ? 1 : 0), completionHandler: completionHandler)
|
||||
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (result.isSuccess ? 1 : 0), completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modify(tunnel: TunnelContainer, with tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelManagementError?) -> Void) {
|
||||
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration,
|
||||
activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.interface.name
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
completionHandler(TunnelsManagerError.tunnelNameEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
@ -117,7 +156,7 @@ class TunnelsManager {
|
||||
var oldName: String?
|
||||
if (isNameChanged) {
|
||||
if self.tunnels.contains(where: { $0.name == tunnelName }) {
|
||||
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
|
||||
return
|
||||
}
|
||||
oldName = tunnel.name
|
||||
@ -127,11 +166,14 @@ class TunnelsManager {
|
||||
tunnelProviderManager.localizedDescription = tunnelName
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
let isActivatingOnDemand = (!tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled)
|
||||
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] (error) in
|
||||
defer { self?.isModifyingTunnel = false }
|
||||
guard (error == nil) else {
|
||||
os_log("Modify: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(TunnelManagementError.vpnSystemErrorOnModifyTunnel)
|
||||
completionHandler(TunnelsManagerError.vpnSystemErrorOnModifyTunnel)
|
||||
return
|
||||
}
|
||||
if let s = self {
|
||||
@ -139,21 +181,35 @@ class TunnelsManager {
|
||||
let oldIndex = s.tunnels.firstIndex(of: tunnel)!
|
||||
s.tunnels.sort { $0.name < $1.name }
|
||||
let newIndex = s.tunnels.firstIndex(of: tunnel)!
|
||||
s.delegate?.tunnelMoved(at: oldIndex, to: newIndex)
|
||||
s.tunnelsListDelegate?.tunnelMoved(at: oldIndex, to: newIndex)
|
||||
}
|
||||
s.delegate?.tunnelModified(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
s.tunnelsListDelegate?.tunnelModified(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
|
||||
if (tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting) {
|
||||
// Turn off the tunnel, and then turn it back on, so the changes are made effective
|
||||
tunnel.beginRestart()
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
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 {
|
||||
os_log("Modify: Re-loading after saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(TunnelsManagerError.vpnSystemErrorOnModifyTunnel)
|
||||
return
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelManagementError?) -> Void) {
|
||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelProviderManager = tunnel.tunnelProvider
|
||||
|
||||
isDeletingTunnel = true
|
||||
@ -162,13 +218,13 @@ class TunnelsManager {
|
||||
defer { self?.isDeletingTunnel = false }
|
||||
guard (error == nil) else {
|
||||
os_log("Remove: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(TunnelManagementError.vpnSystemErrorOnRemoveTunnel)
|
||||
completionHandler(TunnelsManagerError.vpnSystemErrorOnRemoveTunnel)
|
||||
return
|
||||
}
|
||||
if let s = self {
|
||||
let index = s.tunnels.firstIndex(of: tunnel)!
|
||||
s.tunnels.remove(at: index)
|
||||
s.delegate?.tunnelRemoved(at: index)
|
||||
s.tunnelsListDelegate?.tunnelRemoved(at: index)
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
@ -182,57 +238,49 @@ class TunnelsManager {
|
||||
return tunnels[index]
|
||||
}
|
||||
|
||||
func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (Error?) -> Void) {
|
||||
guard (tunnel.status == .inactive) else {
|
||||
completionHandler(TunnelActivationError.attemptingActivationWhenTunnelIsNotInactive)
|
||||
return
|
||||
}
|
||||
for t in tunnels {
|
||||
if t.status != .inactive {
|
||||
completionHandler(TunnelActivationError.attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: t.status))
|
||||
return
|
||||
}
|
||||
}
|
||||
tunnel.startActivation(completionHandler: completionHandler)
|
||||
func tunnel(named tunnelName: String) -> TunnelContainer? {
|
||||
return self.tunnels.first(where: { $0.name == tunnelName })
|
||||
}
|
||||
|
||||
func startDeactivation(of tunnel: TunnelContainer, completionHandler: @escaping (Error?) -> Void) {
|
||||
func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
guard (tunnel.status == .inactive) else {
|
||||
return
|
||||
}
|
||||
|
||||
func _startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
tunnel.onActivationCommitted = { [weak self] (success) in
|
||||
if (!success) {
|
||||
let error = (InternetReachability.currentStatus() == .notReachable ?
|
||||
TunnelsManagerError.tunnelActivationFailedNoInternetConnection :
|
||||
TunnelsManagerError.tunnelActivationFailedInternalError)
|
||||
self?.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: error)
|
||||
}
|
||||
}
|
||||
tunnel.startActivation(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
|
||||
tunnel.status = .waiting
|
||||
tunnelInOperation.onDeactivationComplete = {
|
||||
_startActivation(of: tunnel, completionHandler: completionHandler)
|
||||
}
|
||||
startDeactivation(of: tunnelInOperation)
|
||||
} else {
|
||||
_startActivation(of: tunnel, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
func startDeactivation(of tunnel: TunnelContainer) {
|
||||
if (tunnel.status == .inactive) {
|
||||
completionHandler(TunnelActivationError.attemptingDeactivationWhenTunnelIsInactive)
|
||||
return
|
||||
}
|
||||
tunnel.startDeactivation()
|
||||
}
|
||||
}
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration) {
|
||||
assert(!tunnelConfiguration.interface.name.isEmpty)
|
||||
guard let serializedTunnelConfiguration = try? JSONEncoder().encode(tunnelConfiguration) else { return nil }
|
||||
|
||||
self.init()
|
||||
|
||||
let appId = Bundle.main.bundleIdentifier!
|
||||
providerBundleIdentifier = "\(appId).network-extension"
|
||||
providerConfiguration = [
|
||||
"tunnelConfiguration": serializedTunnelConfiguration,
|
||||
"tunnelConfigurationVersion": 1
|
||||
]
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint})
|
||||
if endpoints.count == 1 {
|
||||
serverAddress = endpoints.first!.stringRepresentation()
|
||||
} else if endpoints.isEmpty {
|
||||
serverAddress = "Unspecified"
|
||||
} else {
|
||||
serverAddress = "Multiple endpoints"
|
||||
func refreshStatuses() {
|
||||
for t in tunnels {
|
||||
t.refreshStatus()
|
||||
}
|
||||
username = tunnelConfiguration.interface.name
|
||||
}
|
||||
|
||||
func tunnelConfiguration() -> TunnelConfiguration? {
|
||||
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return nil }
|
||||
return try? JSONDecoder().decode(TunnelConfiguration.self, from: serializedTunnelConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,18 +288,29 @@ class TunnelContainer: NSObject {
|
||||
@objc dynamic var name: String
|
||||
@objc dynamic var status: TunnelStatus
|
||||
|
||||
@objc dynamic var isActivateOnDemandEnabled: Bool {
|
||||
didSet {
|
||||
if (isActivateOnDemandEnabled) {
|
||||
startObservingTunnelStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isAttemptingActivation: Bool = false
|
||||
var onActivationCommitted: ((Bool) -> Void)?
|
||||
var onDeactivationComplete: (() -> Void)?
|
||||
|
||||
fileprivate let tunnelProvider: NETunnelProviderManager
|
||||
private var statusObservationToken: AnyObject?
|
||||
|
||||
private var dnsResolver: DNSResolver?
|
||||
|
||||
init(tunnel: NETunnelProviderManager) {
|
||||
self.name = tunnel.localizedDescription ?? "Unnamed"
|
||||
let status = TunnelStatus(from: tunnel.connection.status)
|
||||
self.status = status
|
||||
self.isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
|
||||
self.tunnelProvider = tunnel
|
||||
super.init()
|
||||
if (status != .inactive) {
|
||||
if (status != .inactive || isActivateOnDemandEnabled) {
|
||||
startObservingTunnelStatus()
|
||||
}
|
||||
}
|
||||
@ -260,62 +319,39 @@ class TunnelContainer: NSObject {
|
||||
return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration()
|
||||
}
|
||||
|
||||
fileprivate func startActivation(completionHandler: @escaping (Error?) -> Void) {
|
||||
assert(status == .inactive || status == .restarting)
|
||||
assert(self.dnsResolver == nil)
|
||||
func activateOnDemandSetting() -> ActivateOnDemandSetting {
|
||||
return ActivateOnDemandSetting(from: tunnelProvider)
|
||||
}
|
||||
|
||||
func refreshStatus() {
|
||||
let status = TunnelStatus(from: self.tunnelProvider.connection.status)
|
||||
self.status = status
|
||||
self.isActivateOnDemandEnabled = self.tunnelProvider.isOnDemandEnabled
|
||||
if (status != .inactive || isActivateOnDemandEnabled) {
|
||||
startObservingTunnelStatus()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func startActivation(completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
assert(status == .inactive || status == .restarting || status == .waiting)
|
||||
|
||||
guard let tunnelConfiguration = tunnelConfiguration() else { fatalError() }
|
||||
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
|
||||
|
||||
// Resolve DNS and start the tunnel
|
||||
let dnsResolver = DNSResolver(endpoints: endpoints)
|
||||
let resolvedEndpoints = dnsResolver.resolveWithoutNetworkRequests()
|
||||
if let resolvedEndpoints = resolvedEndpoints {
|
||||
// If we don't have to make a DNS network request, we never
|
||||
// change the status to .resolvingEndpointDomains
|
||||
startActivation(tunnelConfiguration: tunnelConfiguration,
|
||||
resolvedEndpoints: resolvedEndpoints,
|
||||
completionHandler: completionHandler)
|
||||
} else {
|
||||
status = .resolvingEndpointDomains
|
||||
self.dnsResolver = dnsResolver
|
||||
dnsResolver.resolve { [weak self] resolvedEndpoints in
|
||||
guard let s = self else { return }
|
||||
assert(s.status == .resolvingEndpointDomains)
|
||||
s.dnsResolver = nil
|
||||
guard let resolvedEndpoints = resolvedEndpoints else {
|
||||
s.status = .inactive
|
||||
completionHandler(TunnelActivationError.dnsResolutionFailed)
|
||||
return
|
||||
}
|
||||
s.startActivation(tunnelConfiguration: tunnelConfiguration,
|
||||
resolvedEndpoints: resolvedEndpoints,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
onDeactivationComplete = nil
|
||||
isAttemptingActivation = true
|
||||
startActivation(tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate func startActivation(recursionCount: UInt = 0,
|
||||
lastError: Error? = nil,
|
||||
tunnelConfiguration: TunnelConfiguration,
|
||||
resolvedEndpoints: [Endpoint?],
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
if (recursionCount >= 8) {
|
||||
os_log("startActivation: Failed after 8 attempts. Giving up with %{public}@", log: OSLog.default, type: .error, "\(lastError!)")
|
||||
completionHandler(TunnelActivationError.tunnelActivationFailed)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// resolvedEndpoints should contain only IP addresses, not any named endpoints
|
||||
assert(resolvedEndpoints.allSatisfy { (resolvedEndpoint) in
|
||||
guard let resolvedEndpoint = resolvedEndpoint else { return true }
|
||||
switch (resolvedEndpoint.host) {
|
||||
case .ipv4: return true
|
||||
case .ipv6: return true
|
||||
case .name: return false
|
||||
}
|
||||
})
|
||||
|
||||
os_log("startActivation: Entering", log: OSLog.default, type: .debug)
|
||||
|
||||
guard (tunnelProvider.isEnabled) else {
|
||||
@ -326,12 +362,12 @@ class TunnelContainer: NSObject {
|
||||
tunnelProvider.saveToPreferences { [weak self] (error) in
|
||||
if (error != nil) {
|
||||
os_log("Error saving tunnel after re-enabling: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(error)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
os_log("startActivation: Tunnel saved after re-enabling", log: OSLog.default, type: .info)
|
||||
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints, completionHandler: completionHandler)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -340,41 +376,35 @@ class TunnelContainer: NSObject {
|
||||
startObservingTunnelStatus()
|
||||
let session = (tunnelProvider.connection as! NETunnelProviderSession)
|
||||
do {
|
||||
os_log("startActivation: Generating options", log: OSLog.default, type: .debug)
|
||||
let tunnelOptions = PacketTunnelOptionsGenerator.generateOptions(
|
||||
from: tunnelConfiguration, withResolvedEndpoints: resolvedEndpoints)
|
||||
os_log("startActivation: Starting tunnel", log: OSLog.default, type: .debug)
|
||||
try session.startTunnel(options: tunnelOptions)
|
||||
try session.startTunnel()
|
||||
os_log("startActivation: Success", log: OSLog.default, type: .debug)
|
||||
completionHandler(nil)
|
||||
} catch (let error) {
|
||||
os_log("startActivation: Error starting tunnel. Examining error", log: OSLog.default, type: .debug)
|
||||
guard let vpnError = error as? NEVPNError else {
|
||||
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
os_log("Failed to activate tunnel: Error: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
status = .inactive
|
||||
completionHandler(error)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
guard (vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale) else {
|
||||
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
os_log("Failed to activate tunnel: VPN Error: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
status = .inactive
|
||||
completionHandler(error)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
assert(vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale)
|
||||
os_log("startActivation: Error says: %{public}@", log: OSLog.default, type: .debug,
|
||||
vpnError.code == NEVPNError.configurationInvalid ? "Configuration invalid" : "Configuration stale")
|
||||
os_log("startActivation: Will reload tunnel and then try to start it. ", log: OSLog.default, type: .info)
|
||||
tunnelProvider.loadFromPreferences { [weak self] (error) in
|
||||
if (error != nil) {
|
||||
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error!)")
|
||||
os_log("startActivation: Error reloading tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error!)")
|
||||
self?.status = .inactive
|
||||
completionHandler(error)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
os_log("startActivation: Tunnel reloaded", log: OSLog.default, type: .info)
|
||||
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: vpnError, tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints, completionHandler: completionHandler)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: vpnError, tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -402,6 +432,18 @@ class TunnelContainer: NSObject {
|
||||
object: connection,
|
||||
queue: nil) { [weak self] (_) in
|
||||
guard let s = self else { return }
|
||||
if (s.isAttemptingActivation) {
|
||||
if (connection.status == .connecting || connection.status == .connected) {
|
||||
// We tried to start the tunnel, and that attempt is on track to become succeessful
|
||||
s.onActivationCommitted?(true)
|
||||
s.onActivationCommitted = nil
|
||||
} else if (connection.status == .disconnecting || connection.status == .disconnected) {
|
||||
// We tried to start the tunnel, but that attempt didn't succeed
|
||||
s.onActivationCommitted?(false)
|
||||
s.onActivationCommitted = nil
|
||||
}
|
||||
s.isAttemptingActivation = false
|
||||
}
|
||||
if ((s.status == .restarting) && (connection.status == .disconnected || connection.status == .disconnecting)) {
|
||||
// Don't change s.status when disconnecting for a restart
|
||||
if (connection.status == .disconnected) {
|
||||
@ -409,13 +451,13 @@ class TunnelContainer: NSObject {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (s.status == .resolvingEndpointDomains && connection.status == .disconnected) {
|
||||
// Don't change to .inactive if we're still resolving endpoints
|
||||
return
|
||||
}
|
||||
s.status = TunnelStatus(from: connection.status)
|
||||
if (s.status == .inactive) {
|
||||
s.statusObservationToken = nil
|
||||
s.onDeactivationComplete?()
|
||||
s.onDeactivationComplete = nil
|
||||
if (!s.isActivateOnDemandEnabled) {
|
||||
s.statusObservationToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -429,7 +471,7 @@ class TunnelContainer: NSObject {
|
||||
case reasserting // Not a possible state at present
|
||||
|
||||
case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
|
||||
case resolvingEndpointDomains // DNS resolution in progress
|
||||
case waiting // Waiting for another tunnel to be brought down
|
||||
|
||||
init(from vpnStatus: NEVPNStatus) {
|
||||
switch (vpnStatus) {
|
||||
|
@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>packet-tunnel-provider</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
6
WireGuard/WireGuard/WireGuardAppError.swift
Normal file
6
WireGuard/WireGuard/WireGuardAppError.swift
Normal file
@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
protocol WireGuardAppError: Error {
|
||||
func alertText() -> (/* title */ String, /* message */ String)
|
||||
}
|
28
WireGuard/WireGuard/WireGuardResult.swift
Normal file
28
WireGuard/WireGuard/WireGuardResult.swift
Normal file
@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
enum WireGuardResult<T> {
|
||||
case success(T)
|
||||
case failure(WireGuardAppError)
|
||||
|
||||
var value: T? {
|
||||
switch (self) {
|
||||
case .success(let v): return v
|
||||
case .failure(_): return nil
|
||||
}
|
||||
}
|
||||
|
||||
var error: WireGuardAppError? {
|
||||
switch (self) {
|
||||
case .success(_): return nil
|
||||
case .failure(let e): return e
|
||||
}
|
||||
}
|
||||
|
||||
var isSuccess: Bool {
|
||||
switch (self) {
|
||||
case .success(_): return true
|
||||
case .failure(_): return false
|
||||
}
|
||||
}
|
||||
}
|
@ -3,10 +3,21 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ZipArchiveError: Error {
|
||||
enum ZipArchiveError: WireGuardAppError {
|
||||
case cantOpenInputZipFile
|
||||
case cantOpenOutputZipFileForWriting
|
||||
case badArchive
|
||||
|
||||
func alertText() -> (String, String) {
|
||||
switch (self) {
|
||||
case .cantOpenInputZipFile:
|
||||
return ("Unable to read zip archive", "The zip archive could not be read.")
|
||||
case .cantOpenOutputZipFileForWriting:
|
||||
return ("Unable to create zip archive", "Could not open zip file for writing.")
|
||||
case .badArchive:
|
||||
return ("Unable to read zip archive", "Bad or corrupt zip archive.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZipArchive {
|
||||
@ -48,7 +59,7 @@ class ZipArchive {
|
||||
throw ZipArchiveError.badArchive
|
||||
}
|
||||
|
||||
let bufferSize = 1024
|
||||
let bufferSize = 16384 // 16 kb
|
||||
var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
|
||||
var dataBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
|
||||
|
||||
|
47
WireGuard/WireGuard/ZipArchive/ZipExporter.swift
Normal file
47
WireGuard/WireGuard/ZipArchive/ZipExporter.swift
Normal file
@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
enum ZipExporterError: WireGuardAppError {
|
||||
case noTunnelsToExport
|
||||
|
||||
func alertText() -> (String, String) {
|
||||
switch (self) {
|
||||
case .noTunnelsToExport:
|
||||
return ("Nothing to export", "There are no tunnels to export")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZipExporter {
|
||||
static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL,
|
||||
completion: @escaping (WireGuardAppError?) -> Void) {
|
||||
|
||||
guard (!tunnelConfigurations.isEmpty) else {
|
||||
completion(ZipExporterError.noTunnelsToExport)
|
||||
return
|
||||
}
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
var inputsToArchiver: [(fileName: String, contents: Data)] = []
|
||||
var lastTunnelName: String = ""
|
||||
for tunnelConfiguration in tunnelConfigurations {
|
||||
if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) {
|
||||
let name = tunnelConfiguration.interface.name
|
||||
if (name.isEmpty || name == lastTunnelName) { continue }
|
||||
inputsToArchiver.append((fileName: "\(name).conf", contents: contents))
|
||||
lastTunnelName = name
|
||||
}
|
||||
}
|
||||
do {
|
||||
try ZipArchive.archive(inputs: inputsToArchiver, to: url)
|
||||
} catch (let error as WireGuardAppError) {
|
||||
DispatchQueue.main.async { completion(error) }
|
||||
return
|
||||
} catch {
|
||||
fatalError()
|
||||
}
|
||||
DispatchQueue.main.async { completion(nil) }
|
||||
}
|
||||
}
|
||||
}
|
60
WireGuard/WireGuard/ZipArchive/ZipImporter.swift
Normal file
60
WireGuard/WireGuard/ZipArchive/ZipImporter.swift
Normal file
@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
enum ZipImporterError: WireGuardAppError {
|
||||
case noTunnelsInZipArchive
|
||||
|
||||
func alertText() -> (String, String) {
|
||||
switch (self) {
|
||||
case .noTunnelsInZipArchive:
|
||||
return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZipImporter {
|
||||
static func importConfigFiles(from url: URL, completion: @escaping (WireGuardResult<[TunnelConfiguration?]>) -> Void) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
var unarchivedFiles: [(fileName: String, contents: Data)]
|
||||
do {
|
||||
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
|
||||
|
||||
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) {
|
||||
throw ZipImporterError.noTunnelsInZipArchive
|
||||
}
|
||||
} catch (let error as WireGuardAppError) {
|
||||
DispatchQueue.main.async { completion(.failure(error)) }
|
||||
return
|
||||
} catch {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
unarchivedFiles.sort { $0.fileName < $1.fileName }
|
||||
var configs = Array<TunnelConfiguration?>(repeating: nil, count: unarchivedFiles.count)
|
||||
for (i, file) in unarchivedFiles.enumerated() {
|
||||
if (i > 0 && file == unarchivedFiles[i - 1]) {
|
||||
continue
|
||||
}
|
||||
guard let fileContents = String(data: file.contents, encoding: .utf8) else {
|
||||
continue
|
||||
}
|
||||
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileName) else {
|
||||
continue
|
||||
}
|
||||
configs[i] = tunnelConfig
|
||||
}
|
||||
DispatchQueue.main.async { completion(.success(configs)) }
|
||||
}
|
||||
}
|
||||
}
|
@ -4,86 +4,61 @@
|
||||
import Network
|
||||
import Foundation
|
||||
|
||||
enum DNSResolverError: Error {
|
||||
case dnsResolutionFailed(hostnames: [String])
|
||||
}
|
||||
|
||||
class DNSResolver {
|
||||
let endpoints: [Endpoint?]
|
||||
let dispatchGroup: DispatchGroup
|
||||
var dispatchWorkItems: [DispatchWorkItem]
|
||||
static var cache = NSCache<NSString, NSString>()
|
||||
|
||||
init(endpoints: [Endpoint?]) {
|
||||
self.endpoints = endpoints
|
||||
self.dispatchWorkItems = []
|
||||
self.dispatchGroup = DispatchGroup()
|
||||
}
|
||||
|
||||
func resolveWithoutNetworkRequests() -> [Endpoint?]? {
|
||||
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
|
||||
for (i, endpoint) in self.endpoints.enumerated() {
|
||||
static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool {
|
||||
for endpoint in endpoints {
|
||||
guard let endpoint = endpoint else { continue }
|
||||
if (endpoint.hasHostAsIPAddress()) {
|
||||
resolvedEndpoints[i] = endpoint
|
||||
} else if let resolvedEndpointStringInCache = DNSResolver.cache.object(forKey: endpoint.stringRepresentation() as NSString),
|
||||
let resolvedEndpointInCache = Endpoint(from: resolvedEndpointStringInCache as String) {
|
||||
resolvedEndpoints[i] = resolvedEndpointInCache
|
||||
} else {
|
||||
return nil
|
||||
if (!endpoint.hasHostAsIPAddress()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return resolvedEndpoints
|
||||
return true
|
||||
}
|
||||
|
||||
func resolve(completionHandler: @escaping ([Endpoint?]?) -> Void) {
|
||||
let endpoints = self.endpoints
|
||||
let dispatchGroup = self.dispatchGroup
|
||||
dispatchWorkItems = []
|
||||
static func resolveSync(endpoints: [Endpoint?]) throws -> [Endpoint?] {
|
||||
let dispatchGroup: DispatchGroup = DispatchGroup()
|
||||
|
||||
if (isAllEndpointsAlreadyResolved(endpoints: endpoints)) {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
|
||||
var isResolvedByDNSRequest: [Bool] = Array<Bool>(repeating: false, count: endpoints.count)
|
||||
for (i, endpoint) in self.endpoints.enumerated() {
|
||||
for (i, endpoint) in endpoints.enumerated() {
|
||||
guard let endpoint = endpoint else { continue }
|
||||
if (endpoint.hasHostAsIPAddress()) {
|
||||
resolvedEndpoints[i] = endpoint
|
||||
} else if let resolvedEndpointStringInCache = DNSResolver.cache.object(forKey: endpoint.stringRepresentation() as NSString),
|
||||
let resolvedEndpointInCache = Endpoint(from: resolvedEndpointStringInCache as String) {
|
||||
resolvedEndpoints[i] = resolvedEndpointInCache
|
||||
} else {
|
||||
let workItem = DispatchWorkItem {
|
||||
resolvedEndpoints[i] = DNSResolver.resolveSync(endpoint: endpoint)
|
||||
isResolvedByDNSRequest[i] = true
|
||||
}
|
||||
dispatchWorkItems.append(workItem)
|
||||
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
|
||||
}
|
||||
}
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
assert(endpoints.count == resolvedEndpoints.count)
|
||||
for (i, endpoint) in endpoints.enumerated() {
|
||||
guard let endpoint = endpoint, let resolvedEndpoint = resolvedEndpoints[i] else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
if (isResolvedByDNSRequest[i]) {
|
||||
DNSResolver.cache.setObject(resolvedEndpoint.stringRepresentation() as NSString,
|
||||
forKey: endpoint.stringRepresentation() as NSString)
|
||||
|
||||
dispatchGroup.wait() // TODO: Timeout?
|
||||
|
||||
var hostnamesWithDnsResolutionFailure: [String] = []
|
||||
assert(endpoints.count == resolvedEndpoints.count)
|
||||
for tuple in zip(endpoints, resolvedEndpoints) {
|
||||
let endpoint = tuple.0
|
||||
let resolvedEndpoint = tuple.1
|
||||
if let endpoint = endpoint {
|
||||
if (resolvedEndpoint == nil) {
|
||||
// DNS resolution failed
|
||||
guard let hostname = endpoint.hostname() else { fatalError() }
|
||||
hostnamesWithDnsResolutionFailure.append(hostname)
|
||||
}
|
||||
}
|
||||
let numberOfEndpointsToResolve = endpoints.compactMap { $0 }.count
|
||||
let numberOfResolvedEndpoints = resolvedEndpoints.compactMap { $0 }.count
|
||||
if (numberOfResolvedEndpoints < numberOfEndpointsToResolve) {
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
completionHandler(resolvedEndpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
for workItem in dispatchWorkItems {
|
||||
workItem.cancel()
|
||||
if (!hostnamesWithDnsResolutionFailure.isEmpty) {
|
||||
throw DNSResolverError.dnsResolutionFailed(hostnames: hostnamesWithDnsResolutionFailure)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
cancel()
|
||||
return resolvedEndpoints
|
||||
}
|
||||
}
|
||||
|
25
WireGuard/WireGuardNetworkExtension/ErrorNotifier.swift
Normal file
25
WireGuard/WireGuardNetworkExtension/ErrorNotifier.swift
Normal file
@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
class ErrorNotifier {
|
||||
static func errorMessage(for error: PacketTunnelProviderError) -> (String, String)? {
|
||||
switch (error) {
|
||||
case .savedProtocolConfigurationIsInvalid:
|
||||
return ("Activation failure", "Could not retrieve tunnel information from the saved configuration")
|
||||
case .dnsResolutionFailure(_):
|
||||
return ("DNS resolution failure", "One or more endpoint domains could not be resolved")
|
||||
case .couldNotStartWireGuard:
|
||||
return ("Activation failure", "WireGuard backend could not be started")
|
||||
case .coultNotSetNetworkSettings:
|
||||
return ("Activation failure", "Error applying network settings on the tunnel")
|
||||
}
|
||||
}
|
||||
|
||||
static func notify(_ error: PacketTunnelProviderError, from tunnelProvider: NEPacketTunnelProvider) {
|
||||
guard let (title, message) = ErrorNotifier.errorMessage(for: error) else { return }
|
||||
// displayMessage() is deprecated, but there's no better alternative to show the error to the user
|
||||
tunnelProvider.displayMessage("\(title): \(message)", completionHandler: { (_) in })
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@ -27,5 +29,7 @@
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
|
||||
</dict>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -2,14 +2,18 @@
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
enum PacketTunnelProviderError: Error {
|
||||
case invalidOptions
|
||||
case savedProtocolConfigurationIsInvalid
|
||||
case dnsResolutionFailure(hostnames: [String])
|
||||
case couldNotStartWireGuard
|
||||
case coultNotSetNetworkSettings
|
||||
}
|
||||
|
||||
private var logFileHandle: FileHandle?
|
||||
|
||||
/// A packet tunnel provider object.
|
||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
@ -22,110 +26,81 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
/// Begin the process of establishing the tunnel.
|
||||
override func startTunnel(options: [String: NSObject]?,
|
||||
completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
|
||||
os_log("Starting tunnel", log: OSLog.default, type: .info)
|
||||
|
||||
guard let options = options else {
|
||||
os_log("Starting tunnel failed: No options passed. Possible connection request from preferences", log: OSLog.default, type: .error)
|
||||
// displayMessage is deprecated API
|
||||
displayMessage("Please use the WireGuard app to start WireGuard tunnels") { (_) in
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.invalidOptions)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let interfaceName = options[.interfaceName] as? String,
|
||||
let wireguardSettings = options[.wireguardSettings] as? String,
|
||||
let remoteAddress = options[.remoteAddress] as? String,
|
||||
let dnsServers = options[.dnsServers] as? [String],
|
||||
let mtu = options[.mtu] as? NSNumber,
|
||||
|
||||
// IPv4 settings
|
||||
let ipv4Addresses = options[.ipv4Addresses] as? [String],
|
||||
let ipv4SubnetMasks = options[.ipv4SubnetMasks] as? [String],
|
||||
let ipv4IncludedRouteAddresses = options[.ipv4IncludedRouteAddresses] as? [String],
|
||||
let ipv4IncludedRouteSubnetMasks = options[.ipv4IncludedRouteSubnetMasks] as? [String],
|
||||
let ipv4ExcludedRouteAddresses = options[.ipv4ExcludedRouteAddresses] as? [String],
|
||||
let ipv4ExcludedRouteSubnetMasks = options[.ipv4ExcludedRouteSubnetMasks] as? [String],
|
||||
|
||||
// IPv6 settings
|
||||
let ipv6Addresses = options[.ipv6Addresses] as? [String],
|
||||
let ipv6NetworkPrefixLengths = options[.ipv6NetworkPrefixLengths] as? [NSNumber],
|
||||
let ipv6IncludedRouteAddresses = options[.ipv6IncludedRouteAddresses] as? [String],
|
||||
let ipv6IncludedRouteNetworkPrefixLengths = options[.ipv6IncludedRouteNetworkPrefixLengths] as? [NSNumber],
|
||||
let ipv6ExcludedRouteAddresses = options[.ipv6ExcludedRouteAddresses] as? [String],
|
||||
let ipv6ExcludedRouteNetworkPrefixLengths = options[.ipv6ExcludedRouteNetworkPrefixLengths] as? [NSNumber]
|
||||
|
||||
else {
|
||||
os_log("Starting tunnel failed: Invalid options passed", log: OSLog.default, type: .error)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.invalidOptions)
|
||||
guard let tunnelProviderProtocol = self.protocolConfiguration as? NETunnelProviderProtocol,
|
||||
let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else {
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid, from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler)
|
||||
}
|
||||
|
||||
func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
|
||||
|
||||
// Configure logging
|
||||
configureLogger()
|
||||
|
||||
wg_log(.info, message: "WireGuard for iOS version \(appVersion())")
|
||||
wg_log(.info, message: "WireGuard Go backend version \(goBackendVersion())")
|
||||
wg_log(.info, message: "Tunnel interface name: \(tunnelConfiguration.interface.name)")
|
||||
|
||||
wg_log(.info, staticMessage: "Starting tunnel")
|
||||
|
||||
// Resolve endpoint domains
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
|
||||
var resolvedEndpoints: [Endpoint?] = []
|
||||
do {
|
||||
resolvedEndpoints = try DNSResolver.resolveSync(endpoints: endpoints)
|
||||
} catch DNSResolverError.dnsResolutionFailed(let hostnames) {
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: DNS resolution failure")
|
||||
wg_log(.error, message: "Hostnames for which DNS resolution failed: \(hostnames.joined(separator: ", "))")
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames), from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
|
||||
return
|
||||
} catch {
|
||||
// There can be no other errors from DNSResolver.resolveSync()
|
||||
fatalError()
|
||||
}
|
||||
assert(endpoints.count == resolvedEndpoints.count)
|
||||
|
||||
// Setup packetTunnelSettingsGenerator
|
||||
|
||||
let packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration,
|
||||
resolvedEndpoints: resolvedEndpoints)
|
||||
|
||||
// Bring up wireguard-go backend
|
||||
|
||||
let fd = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32
|
||||
if fd < 0 {
|
||||
os_log("Starting tunnel failed: Could not determine file descriptor", log: OSLog.default, type: .error)
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
|
||||
return
|
||||
}
|
||||
let handle = connect(interfaceName: interfaceName, settings: wireguardSettings, fd: fd)
|
||||
|
||||
let wireguardSettings = packetTunnelSettingsGenerator.generateWireGuardSettings()
|
||||
let handle = connect(interfaceName: tunnelConfiguration.interface.name, settings: wireguardSettings, fd: fd)
|
||||
|
||||
if handle < 0 {
|
||||
os_log("Starting tunnel failed: Could not start WireGuard", log: OSLog.default, type: .error)
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard")
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
|
||||
return
|
||||
}
|
||||
|
||||
wgHandle = handle
|
||||
|
||||
// Network settings
|
||||
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
|
||||
|
||||
// IPv4 settings
|
||||
let ipv4Settings = NEIPv4Settings(addresses: ipv4Addresses, subnetMasks: ipv4SubnetMasks)
|
||||
assert(ipv4IncludedRouteAddresses.count == ipv4IncludedRouteSubnetMasks.count)
|
||||
ipv4Settings.includedRoutes = zip(ipv4IncludedRouteAddresses, ipv4IncludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
assert(ipv4ExcludedRouteAddresses.count == ipv4ExcludedRouteSubnetMasks.count)
|
||||
ipv4Settings.excludedRoutes = zip(ipv4ExcludedRouteAddresses, ipv4ExcludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
networkSettings.ipv4Settings = ipv4Settings
|
||||
|
||||
// IPv6 settings
|
||||
|
||||
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
|
||||
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
|
||||
* very bad, if various network parameters were actually relying on that subnet being
|
||||
* intentionally small. TODO: talk about this with upstream iOS devs.
|
||||
*/
|
||||
let ipv6Settings = NEIPv6Settings(addresses: ipv6Addresses, networkPrefixLengths: ipv6NetworkPrefixLengths.map { NSNumber(value: min(120, $0.intValue)) })
|
||||
assert(ipv6IncludedRouteAddresses.count == ipv6IncludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.includedRoutes = zip(ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
assert(ipv6ExcludedRouteAddresses.count == ipv6ExcludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.excludedRoutes = zip(ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
networkSettings.ipv6Settings = ipv6Settings
|
||||
|
||||
// DNS
|
||||
networkSettings.dnsSettings = NEDNSSettings(servers: dnsServers)
|
||||
|
||||
// MTU
|
||||
if (mtu == 0) {
|
||||
// 0 imples automatic MTU, where we set overhead as 80 bytes, which is the worst case for WireGuard
|
||||
networkSettings.tunnelOverheadBytes = 80
|
||||
} else {
|
||||
networkSettings.mtu = mtu
|
||||
}
|
||||
// Apply network settings
|
||||
|
||||
let networkSettings: NEPacketTunnelNetworkSettings = packetTunnelSettingsGenerator.generateNetworkSettings()
|
||||
setTunnelNetworkSettings(networkSettings) { (error) in
|
||||
if let error = error {
|
||||
os_log("Starting tunnel failed: Error setting network settings: %s", log: OSLog.default, type: .error, error.localizedDescription)
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: Error setting network settings.")
|
||||
wg_log(.error, message: "Error from setTunnelNetworkSettings: \(error.localizedDescription)")
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings, from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
|
||||
} else {
|
||||
startTunnelCompletionHandler(nil /* No errors */)
|
||||
@ -135,14 +110,34 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
/// Begin the process of stopping the tunnel.
|
||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
os_log("Stopping tunnel", log: OSLog.default, type: .info)
|
||||
wg_log(.info, staticMessage: "Stopping tunnel")
|
||||
if let handle = wgHandle {
|
||||
wgTurnOff(handle)
|
||||
}
|
||||
if let fileHandle = logFileHandle {
|
||||
fileHandle.closeFile()
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
private func configureLogger() {
|
||||
|
||||
// Setup writing the log to a file
|
||||
if let networkExtensionLogFileURL = FileManager.networkExtensionLogFileURL {
|
||||
let fileManager = FileManager.default
|
||||
let filePath = networkExtensionLogFileURL.path
|
||||
fileManager.createFile(atPath: filePath, contents: nil) // Create the file if it doesn't already exist
|
||||
if let fileHandle = FileHandle(forWritingAtPath: filePath) {
|
||||
logFileHandle = fileHandle
|
||||
} else {
|
||||
os_log("Can't open log file for writing. Log is not saved to file.", log: OSLog.default, type: .error)
|
||||
logFileHandle = nil
|
||||
}
|
||||
} else {
|
||||
os_log("Can't obtain log file URL. Log is not saved to file.", log: OSLog.default, type: .error)
|
||||
}
|
||||
|
||||
// Setup WireGuard logger
|
||||
wgSetLogger { (level, msgCStr) in
|
||||
let logType: OSLogType
|
||||
switch level {
|
||||
@ -156,7 +151,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
logType = .default
|
||||
}
|
||||
let msg = (msgCStr != nil) ? String(cString: msgCStr!) : ""
|
||||
os_log("%{public}s", log: OSLog.default, type: logType, msg)
|
||||
wg_log(logType, message: msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,6 +160,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
return wgTurnOn(nameGoStr, settingsGoStr, fd)
|
||||
}
|
||||
}
|
||||
|
||||
func appVersion() -> String {
|
||||
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
|
||||
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
appVersion += " (\(appBuild))"
|
||||
}
|
||||
return appVersion
|
||||
}
|
||||
|
||||
func goBackendVersion() -> String {
|
||||
return WIREGUARD_GO_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
private func withStringsAsGoStrings<R>(_ str1: String, _ str2: String, closure: (gostring_t, gostring_t) -> R) -> R {
|
||||
@ -176,3 +183,34 @@ private func withStringsAsGoStrings<R>(_ str1: String, _ str2: String, closure:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func wg_log(_ type: OSLogType, staticMessage msg: StaticString) {
|
||||
// Write to os log
|
||||
os_log(msg, log: OSLog.default, type: type)
|
||||
// Write to file log
|
||||
let msgString: String = msg.withUTF8Buffer { (ptr: UnsafeBufferPointer<UInt8>) -> String in
|
||||
return String(decoding: ptr, as: UTF8.self)
|
||||
}
|
||||
file_log(type: type, message: msgString)
|
||||
}
|
||||
|
||||
private func wg_log(_ type: OSLogType, message msg: String) {
|
||||
// Write to os log
|
||||
os_log("%{public}s", log: OSLog.default, type: type, msg)
|
||||
// Write to file log
|
||||
file_log(type: type, message: msg)
|
||||
}
|
||||
|
||||
private func file_log(type: OSLogType, message: String) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS: "
|
||||
var msgLine = formatter.string(from: Date()) + message
|
||||
if (msgLine.last! != "\n") {
|
||||
msgLine.append("\n")
|
||||
}
|
||||
let data = msgLine.data(using: .utf8)
|
||||
if let data = data, let logFileHandle = logFileHandle {
|
||||
logFileHandle.write(data)
|
||||
logFileHandle.synchronizeFile()
|
||||
}
|
||||
}
|
||||
|
@ -3,29 +3,30 @@
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import NetworkExtension
|
||||
|
||||
class PacketTunnelOptionsGenerator {
|
||||
static func generateOptions(from tc: TunnelConfiguration,
|
||||
withResolvedEndpoints resolvedEndpoints: [Endpoint?]) -> [String: NSObject] {
|
||||
var options: [String: NSObject] = [:]
|
||||
class PacketTunnelSettingsGenerator {
|
||||
|
||||
// Interface name
|
||||
let tunnelConfiguration: TunnelConfiguration
|
||||
let resolvedEndpoints: [Endpoint?]
|
||||
|
||||
options[.interfaceName] = tc.interface.name as NSObject
|
||||
|
||||
// WireGuard settings
|
||||
init(tunnelConfiguration: TunnelConfiguration, resolvedEndpoints: [Endpoint?]) {
|
||||
self.tunnelConfiguration = tunnelConfiguration
|
||||
self.resolvedEndpoints = resolvedEndpoints
|
||||
}
|
||||
|
||||
func generateWireGuardSettings() -> String {
|
||||
var wgSettings = ""
|
||||
let privateKey = tc.interface.privateKey.hexEncodedString()
|
||||
let privateKey = tunnelConfiguration.interface.privateKey.hexEncodedString()
|
||||
wgSettings.append("private_key=\(privateKey)\n")
|
||||
if let listenPort = tc.interface.listenPort {
|
||||
if let listenPort = tunnelConfiguration.interface.listenPort {
|
||||
wgSettings.append("listen_port=\(listenPort)\n")
|
||||
}
|
||||
if (tc.peers.count > 0) {
|
||||
if (tunnelConfiguration.peers.count > 0) {
|
||||
wgSettings.append("replace_peers=true\n")
|
||||
}
|
||||
assert(tc.peers.count == resolvedEndpoints.count)
|
||||
for (i, peer) in tc.peers.enumerated() {
|
||||
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
|
||||
for (i, peer) in tunnelConfiguration.peers.enumerated() {
|
||||
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
|
||||
if let preSharedKey = peer.preSharedKey {
|
||||
wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n")
|
||||
@ -43,8 +44,10 @@ class PacketTunnelOptionsGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
return wgSettings
|
||||
}
|
||||
|
||||
options[.wireguardSettings] = wgSettings as NSObject
|
||||
func generateNetworkSettings() -> NEPacketTunnelNetworkSettings {
|
||||
|
||||
// Remote address
|
||||
|
||||
@ -67,15 +70,24 @@ class PacketTunnelOptionsGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
options[.remoteAddress] = remoteAddress as NSObject
|
||||
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
|
||||
|
||||
// DNS
|
||||
|
||||
options[.dnsServers] = tc.interface.dns.map { $0.stringRepresentation() } as NSObject
|
||||
let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation() }
|
||||
let dnsSettings = NEDNSSettings(servers: dnsServerStrings)
|
||||
dnsSettings.matchDomains = [""] // All DNS queries must first go through the VPN's DNS
|
||||
networkSettings.dnsSettings = dnsSettings
|
||||
|
||||
// MTU
|
||||
|
||||
options[.mtu] = NSNumber(value: tc.interface.mtu ?? 0) // 0 implies auto-MTU
|
||||
let mtu = tunnelConfiguration.interface.mtu ?? 0
|
||||
if (mtu == 0) {
|
||||
// 0 imples automatic MTU, where we set overhead as 80 bytes, which is the worst case for WireGuard
|
||||
networkSettings.tunnelOverheadBytes = 80
|
||||
} else {
|
||||
networkSettings.mtu = NSNumber(value: mtu)
|
||||
}
|
||||
|
||||
// Addresses from interface addresses
|
||||
|
||||
@ -85,22 +97,16 @@ class PacketTunnelOptionsGenerator {
|
||||
var ipv6Addresses: [String] = []
|
||||
var ipv6NetworkPrefixLengths: [NSNumber] = []
|
||||
|
||||
for addressRange in tc.interface.addresses {
|
||||
for addressRange in tunnelConfiguration.interface.addresses {
|
||||
if (addressRange.address is IPv4Address) {
|
||||
ipv4Addresses.append("\(addressRange.address)")
|
||||
ipv4SubnetMasks.append(ipv4SubnetMaskString(of: addressRange))
|
||||
ipv4SubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
|
||||
} else if (addressRange.address is IPv6Address) {
|
||||
ipv6Addresses.append("\(addressRange.address)")
|
||||
ipv6NetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
|
||||
}
|
||||
}
|
||||
|
||||
options[.ipv4Addresses] = ipv4Addresses as NSObject
|
||||
options[.ipv4SubnetMasks] = ipv4SubnetMasks as NSObject
|
||||
|
||||
options[.ipv6Addresses] = ipv6Addresses as NSObject
|
||||
options[.ipv6NetworkPrefixLengths] = ipv6NetworkPrefixLengths as NSObject
|
||||
|
||||
// Included routes from AllowedIPs
|
||||
|
||||
var ipv4IncludedRouteAddresses: [String] = []
|
||||
@ -109,11 +115,11 @@ class PacketTunnelOptionsGenerator {
|
||||
var ipv6IncludedRouteAddresses: [String] = []
|
||||
var ipv6IncludedRouteNetworkPrefixLengths: [NSNumber] = []
|
||||
|
||||
for peer in tc.peers {
|
||||
for peer in tunnelConfiguration.peers {
|
||||
for addressRange in peer.allowedIPs {
|
||||
if (addressRange.address is IPv4Address) {
|
||||
ipv4IncludedRouteAddresses.append("\(addressRange.address)")
|
||||
ipv4IncludedRouteSubnetMasks.append(ipv4SubnetMaskString(of: addressRange))
|
||||
ipv4IncludedRouteSubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
|
||||
} else if (addressRange.address is IPv6Address) {
|
||||
ipv6IncludedRouteAddresses.append("\(addressRange.address)")
|
||||
ipv6IncludedRouteNetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
|
||||
@ -121,12 +127,6 @@ class PacketTunnelOptionsGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
options[.ipv4IncludedRouteAddresses] = ipv4IncludedRouteAddresses as NSObject
|
||||
options[.ipv4IncludedRouteSubnetMasks] = ipv4IncludedRouteSubnetMasks as NSObject
|
||||
|
||||
options[.ipv6IncludedRouteAddresses] = ipv6IncludedRouteAddresses as NSObject
|
||||
options[.ipv6IncludedRouteNetworkPrefixLengths] = ipv6IncludedRouteNetworkPrefixLengths as NSObject
|
||||
|
||||
// Excluded routes from endpoints
|
||||
|
||||
var ipv4ExcludedRouteAddresses: [String] = []
|
||||
@ -149,13 +149,40 @@ class PacketTunnelOptionsGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
options[.ipv4ExcludedRouteAddresses] = ipv4ExcludedRouteAddresses as NSObject
|
||||
options[.ipv4ExcludedRouteSubnetMasks] = ipv4ExcludedRouteSubnetMasks as NSObject
|
||||
// Apply IPv4 settings
|
||||
|
||||
options[.ipv6ExcludedRouteAddresses] = ipv6ExcludedRouteAddresses as NSObject
|
||||
options[.ipv6ExcludedRouteNetworkPrefixLengths] = ipv6ExcludedRouteNetworkPrefixLengths as NSObject
|
||||
let ipv4Settings = NEIPv4Settings(addresses: ipv4Addresses, subnetMasks: ipv4SubnetMasks)
|
||||
assert(ipv4IncludedRouteAddresses.count == ipv4IncludedRouteSubnetMasks.count)
|
||||
ipv4Settings.includedRoutes = zip(ipv4IncludedRouteAddresses, ipv4IncludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
assert(ipv4ExcludedRouteAddresses.count == ipv4ExcludedRouteSubnetMasks.count)
|
||||
ipv4Settings.excludedRoutes = zip(ipv4ExcludedRouteAddresses, ipv4ExcludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
networkSettings.ipv4Settings = ipv4Settings
|
||||
|
||||
return options
|
||||
// Apply IPv6 settings
|
||||
|
||||
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
|
||||
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
|
||||
* very bad, if various network parameters were actually relying on that subnet being
|
||||
* intentionally small. TODO: talk about this with upstream iOS devs.
|
||||
*/
|
||||
let ipv6Settings = NEIPv6Settings(addresses: ipv6Addresses, networkPrefixLengths: ipv6NetworkPrefixLengths.map { NSNumber(value: min(120, $0.intValue)) })
|
||||
assert(ipv6IncludedRouteAddresses.count == ipv6IncludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.includedRoutes = zip(ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
assert(ipv6ExcludedRouteAddresses.count == ipv6ExcludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.excludedRoutes = zip(ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
networkSettings.ipv6Settings = ipv6Settings
|
||||
|
||||
// Done
|
||||
|
||||
return networkSettings
|
||||
}
|
||||
|
||||
static func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
|
@ -1 +1,2 @@
|
||||
#include "../../wireguard-go-bridge/wireguard.h"
|
||||
#include "wireguard-go-version.h"
|
||||
|
@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>packet-tunnel-provider</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -25,6 +25,8 @@ import (
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@ -46,12 +48,54 @@ func (l *CLogger) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
var tunnelHandles map[int32]*Device
|
||||
type DeviceState struct {
|
||||
device *Device
|
||||
logger *Logger
|
||||
endpointsTimer *time.Timer
|
||||
endpointsSettings string
|
||||
}
|
||||
|
||||
var tunnelHandles map[int32]*DeviceState
|
||||
|
||||
func listenForRouteChanges() {
|
||||
//TODO: replace with NWPathMonitor
|
||||
data := make([]byte, os.Getpagesize())
|
||||
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
n, err := unix.Read(routeSocket, data)
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EINTR {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if n < 4 {
|
||||
continue
|
||||
}
|
||||
for _, deviceState := range tunnelHandles {
|
||||
if deviceState.endpointsTimer == nil {
|
||||
deviceState.endpointsTimer = time.AfterFunc(time.Second, func() {
|
||||
deviceState.endpointsTimer = nil
|
||||
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(deviceState.endpointsSettings)), bufio.NewWriter(ioutil.Discard))
|
||||
deviceState.logger.Info.Println("Setting endpoints for re-resolution due to network change")
|
||||
err := ipcSetOperation(deviceState.device, bufferedSettings)
|
||||
if err != nil {
|
||||
deviceState.logger.Error.Println(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionString = C.CString(WireGuardGoVersion)
|
||||
roamingDisabled = true
|
||||
tunnelHandles = make(map[int32]*Device)
|
||||
tunnelHandles = make(map[int32]*DeviceState)
|
||||
signals := make(chan os.Signal)
|
||||
signal.Notify(signals, unix.SIGUSR2)
|
||||
go func() {
|
||||
@ -67,6 +111,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
}()
|
||||
go listenForRouteChanges()
|
||||
}
|
||||
|
||||
//export wgSetLogger
|
||||
@ -74,6 +119,32 @@ func wgSetLogger(loggerFn uintptr) {
|
||||
loggerFunc = unsafe.Pointer(loggerFn)
|
||||
}
|
||||
|
||||
func extractEndpointFromSettings(settings string) string {
|
||||
var b strings.Builder
|
||||
pubkey := ""
|
||||
endpoint := ""
|
||||
listenPort := "listen_port=0"
|
||||
for _, line := range strings.Split(settings, "\n") {
|
||||
if strings.HasPrefix(line, "listen_port=") {
|
||||
listenPort = line
|
||||
} else if strings.HasPrefix(line, "public_key=") {
|
||||
if pubkey != "" && endpoint != "" {
|
||||
b.WriteString(pubkey + "\n" + endpoint + "\n")
|
||||
}
|
||||
pubkey = line
|
||||
} else if strings.HasPrefix(line, "endpoint=") {
|
||||
endpoint = line
|
||||
} else if line == "remove=true" {
|
||||
pubkey = ""
|
||||
endpoint = ""
|
||||
}
|
||||
}
|
||||
if pubkey != "" && endpoint != "" {
|
||||
b.WriteString(pubkey + "\n" + endpoint + "\n")
|
||||
}
|
||||
return listenPort + "\n" + b.String()
|
||||
}
|
||||
|
||||
//export wgTurnOn
|
||||
func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
|
||||
interfaceName := string([]byte(ifnameRef))
|
||||
@ -113,18 +184,27 @@ func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
|
||||
if i == math.MaxInt32 {
|
||||
return -1
|
||||
}
|
||||
tunnelHandles[i] = device
|
||||
tunnelHandles[i] = &DeviceState{
|
||||
device: device,
|
||||
logger: logger,
|
||||
endpointsSettings: extractEndpointFromSettings(settings),
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
//export wgTurnOff
|
||||
func wgTurnOff(tunnelHandle int32) {
|
||||
device, ok := tunnelHandles[tunnelHandle]
|
||||
deviceState, ok := tunnelHandles[tunnelHandle]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(tunnelHandles, tunnelHandle)
|
||||
device.Close()
|
||||
t := deviceState.endpointsTimer
|
||||
if t != nil {
|
||||
deviceState.endpointsTimer = nil
|
||||
t.Stop()
|
||||
}
|
||||
deviceState.device.Close()
|
||||
}
|
||||
|
||||
//export wgVersion
|
||||
|
Reference in New Issue
Block a user