Make the server settings customizable

This commit is contained in:
Kenta Kubo 2020-09-25 18:42:31 +09:00
parent e640e3da14
commit 539ad6a3bb
8 changed files with 515 additions and 53 deletions

View file

@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
890B80D5251DC3A20046BAA0 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890B80D4251DC3A20046BAA0 /* DetailView.swift */; };
890B80DF251DC6B50046BAA0 /* Presets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890B80DE251DC6B50046BAA0 /* Presets.swift */; };
8940023C24ACBD2700EBE74B /* CustomDNSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940023B24ACBD2700EBE74B /* CustomDNSApp.swift */; };
8940023E24ACBD2700EBE74B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940023D24ACBD2700EBE74B /* ContentView.swift */; };
8940024024ACBD2800EBE74B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8940023F24ACBD2800EBE74B /* Assets.xcassets */; };
@ -14,6 +16,8 @@
8940024E24ACBD2800EBE74B /* CustomDNSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940024D24ACBD2800EBE74B /* CustomDNSTests.swift */; };
8940025924ACBD2800EBE74B /* CustomDNSUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940025824ACBD2800EBE74B /* CustomDNSUITests.swift */; };
8940026924ACBE4900EBE74B /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8940026824ACBE4900EBE74B /* NetworkExtension.framework */; };
8963FDFB251DF1BC00E3DFE7 /* BundleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8963FDFA251DF1BC00E3DFE7 /* BundleExtensions.swift */; };
8986CDCF251D9B3400D947CD /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8986CDCE251D9B3400D947CD /* Resolver.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -34,6 +38,8 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
890B80D4251DC3A20046BAA0 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = "<group>"; };
890B80DE251DC6B50046BAA0 /* Presets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presets.swift; sourceTree = "<group>"; };
8924EDF324C9CDF1004AF871 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
8940023824ACBD2700EBE74B /* CustomDNS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CustomDNS.app; sourceTree = BUILT_PRODUCTS_DIR; };
8940023B24ACBD2700EBE74B /* CustomDNSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDNSApp.swift; sourceTree = "<group>"; };
@ -49,6 +55,8 @@
8940025A24ACBD2800EBE74B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8940026624ACBE4900EBE74B /* CustomDNS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CustomDNS.entitlements; sourceTree = "<group>"; };
8940026824ACBE4900EBE74B /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
8963FDFA251DF1BC00E3DFE7 /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = "<group>"; };
8986CDCE251D9B3400D947CD /* Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -105,9 +113,13 @@
8940026624ACBE4900EBE74B /* CustomDNS.entitlements */,
8940023B24ACBD2700EBE74B /* CustomDNSApp.swift */,
8940023D24ACBD2700EBE74B /* ContentView.swift */,
8963FDFA251DF1BC00E3DFE7 /* BundleExtensions.swift */,
890B80DE251DC6B50046BAA0 /* Presets.swift */,
890B80D4251DC3A20046BAA0 /* DetailView.swift */,
8940023F24ACBD2800EBE74B /* Assets.xcassets */,
8940024424ACBD2800EBE74B /* Info.plist */,
8940024124ACBD2800EBE74B /* Preview Content */,
8986CDCE251D9B3400D947CD /* Resolver.swift */,
);
path = CustomDNS;
sourceTree = "<group>";
@ -275,8 +287,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8986CDCF251D9B3400D947CD /* Resolver.swift in Sources */,
890B80D5251DC3A20046BAA0 /* DetailView.swift in Sources */,
8940023E24ACBD2700EBE74B /* ContentView.swift in Sources */,
890B80DF251DC6B50046BAA0 /* Presets.swift in Sources */,
8940023C24ACBD2700EBE74B /* CustomDNSApp.swift in Sources */,
8963FDFB251DF1BC00E3DFE7 /* BundleExtensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -446,9 +462,9 @@
);
PRODUCT_BUNDLE_IDENTIFIER = xyz.kebo.CustomDNS;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@ -470,9 +486,9 @@
);
PRODUCT_BUNDLE_IDENTIFIER = xyz.kebo.CustomDNS;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};

View file

@ -0,0 +1,15 @@
//
// BundleExtensions.swift
// CustomDNS
//
// Created by Kenta Kubo on 9/25/20.
//
import Foundation
extension Bundle {
var displayName: String? {
self.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
?? self.object(forInfoDictionaryKey: "CFBundleName") as? String
}
}

View file

@ -5,38 +5,139 @@
// Created by Kenta Kubo on 7/1/20.
//
import Combine
import NetworkExtension
import SwiftUI
struct ContentView {
@AppStorage("servers") var servers = Presets.servers
@AppStorage("usedID") var usedID: String?
@State var isEnabled = false
var cancellable: Cancellable?
init() {
self.cancellable = Future<Bool, Error> { resolve in
let manager = NEDNSSettingsManager.shared()
manager.loadFromPreferences {
if let err = $0 {
resolve(.failure(err))
} else {
resolve(.success(manager.isEnabled))
}
func addNewDoTServer() {
self.servers.append(
.init(
name: "New",
configuration: .dnsOverTLS(DoTConfiguration())
)
)
}
func addNewDoHServer() {
self.servers.append(
.init(
name: "New",
configuration: .dnsOverHTTPS(DoHConfiguration())
)
)
}
func updateStatus() {
let manager = NEDNSSettingsManager.shared()
manager.loadFromPreferences {
if let err = $0 {
print("\(err.localizedDescription)")
} else {
self.isEnabled = manager.isEnabled
}
}
}
func syncSettings() {
let manager = NEDNSSettingsManager.shared()
manager.loadFromPreferences { loadError in
if let loadError = loadError {
print("\(loadError.localizedDescription)")
return
}
manager.dnsSettings = self.usedID
.flatMap(UUID.init)
.flatMap(self.servers.find)
.map(\.configuration)
.map { $0.toDNSSettings() }
manager.saveToPreferences { saveError in
if let saveError = saveError {
print("\(saveError.localizedDescription)")
return
}
print("saved")
}
}
.mapError { fatalError("\($0.localizedDescription)") }
.assign(to: \.isEnabled, on: self)
}
}
extension ContentView: View {
@ViewBuilder var body: some View {
if self.isEnabled {
Text("Enabled")
.foregroundColor(.green)
} else {
Text("Disabled")
.foregroundColor(.secondary)
var body: some View {
NavigationView {
List {
Section(header: Text("Servers")) {
ForEach(0..<self.servers.count, id: \.self) { i in
NavigationLink(
destination: DetailView(
server: .init(
get: { self.servers[i] },
set: { self.servers[i] = $0 }
),
isOn: .init(
get: {
self.usedID == self.servers[i].id.uuidString
},
set: {
if $0 {
self.usedID = self.servers[i].id.uuidString
} else {
self.usedID = nil
}
self.syncSettings()
}
)
)
) {
VStack(alignment: .leading) {
Text(self.servers[i].name)
Text(self.servers[i].configuration.description)
.foregroundColor(.secondary)
}
if self.usedID == self.servers[i].id.uuidString {
Spacer()
Image(systemName: "checkmark")
}
}
}
.onDelete { indexSet in
self.servers.remove(atOffsets: indexSet)
}
.onMove { src, dst in
self.servers.move(fromOffsets: src, toOffset: dst)
}
}
}
.listStyle(SidebarListStyle())
.navigationTitle(Bundle.main.displayName!)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Menu {
Button("DNS-over-TLS", action: self.addNewDoTServer)
Button("DNS-over-HTTPS", action: self.addNewDoHServer)
} label: {
Image(systemName: "plus")
}
EditButton()
}
ToolbarItem(placement: .status) {
VStack {
HStack {
Circle()
.frame(width: 10, height: 10)
.foregroundColor(self.isEnabled ? .green : .secondary)
Text(self.isEnabled ? "Active" : "Inactive")
}
.onAppear(perform: self.updateStatus)
if !self.isEnabled {
Link("Activate", destination: URL(string: "App-prefs:root=General&path=Network/VPN")!)
}
}
}
}
}
}
}

View file

@ -5,37 +5,10 @@
// Created by Kenta Kubo on 7/1/20.
//
import NetworkExtension
import SwiftUI
@main
struct CustomDNSApp {
init() {
// Create a DNS configuration
let manager = NEDNSSettingsManager.shared()
manager.loadFromPreferences { loadError in
if let loadError = loadError {
print(loadError)
return
}
let dotSettings = NEDNSOverTLSSettings(servers: [
"1.1.1.1",
"1.0.0.1",
"2606:4700:4700::64",
"2606:4700:4700::6400",
])
dotSettings.serverName = "cloudflare-dns.com"
manager.dnsSettings = dotSettings
manager.saveToPreferences { saveError in
if let saveError = saveError {
print(saveError)
return
}
print("saved")
}
}
}
}
struct CustomDNSApp {}
extension CustomDNSApp: App {
var body: some Scene {

155
CustomDNS/DetailView.swift Normal file
View file

@ -0,0 +1,155 @@
//
// DetailView.swift
// CustomDNS
//
// Created by Kenta Kubo on 9/25/20.
//
import SwiftUI
struct DetailView {
@Binding var server: Resolver
@Binding var isOn: Bool
}
extension DetailView: View {
var body: some View {
Form {
Section {
Toggle("Use This Server", isOn: self.$isOn)
}
Section {
HStack {
Text("Name")
TextField("Name", text: self.$server.name)
.multilineTextAlignment(.trailing)
}
}
switch self.server.configuration {
case var .dnsOverTLS(configuration):
Section(
header: Text("Servers"),
footer: Text("The DNS server IP addresses.")
) {
ForEach(0..<configuration.servers.count, id: \.self) { i in
TextField(
"IP address",
text: .init(
get: { configuration.servers[i] },
set: { configuration.servers[i] = $0 }
),
onCommit: {
self.server.configuration = .dnsOverTLS(configuration)
}
)
.textContentType(.URL)
.keyboardType(.asciiCapableNumberPad)
.autocapitalization(.none)
.disableAutocorrection(true)
}
Button("Add New Server") {
configuration.servers.append("")
self.server.configuration = .dnsOverTLS(configuration)
}
}
Section(
header: Text("DNS-over-TLS Settings"),
footer: Text("The TLS name of a DNS-over-TLS server.")
) {
HStack {
Text("Server Name")
Spacer()
TextField(
"Server Name",
text: .init(
get: {
configuration.serverName ?? ""
},
set: {
configuration.serverName = $0
}
),
onCommit: {
self.server.configuration = .dnsOverTLS(configuration)
}
)
.multilineTextAlignment(.trailing)
.textContentType(.URL)
.keyboardType(.URL)
.autocapitalization(.none)
.disableAutocorrection(true)
}
}
case var .dnsOverHTTPS(configuration):
Section(
header: Text("Servers"),
footer: Text("The DNS server IP addresses.")
) {
ForEach(0..<configuration.servers.count, id: \.self) { i in
TextField(
"IP address",
text: .init(
get: { configuration.servers[i] },
set: { configuration.servers[i] = $0 }
),
onCommit: {
self.server.configuration = .dnsOverHTTPS(configuration)
}
)
.textContentType(.URL)
.keyboardType(.asciiCapableNumberPad)
.autocapitalization(.none)
.disableAutocorrection(true)
}
Button("Add New Server") {
configuration.servers.append("")
self.server.configuration = .dnsOverHTTPS(configuration)
}
}
Section(
header: Text("DNS over HTTPS Settings"),
footer: Text("The URL of a DNS-over-HTTPS server.")
) {
HStack {
Text("Server URL")
Spacer()
TextField(
"Server URL",
text: .init(
get: {
configuration.serverURL?.absoluteString ?? ""
},
set: {
configuration.serverURL = URL(string: $0)
}
),
onCommit: {
self.server.configuration = .dnsOverHTTPS(configuration)
}
)
.multilineTextAlignment(.trailing)
.textContentType(.URL)
.keyboardType(.URL)
.autocapitalization(.none)
.disableAutocorrection(true)
}
}
}
}
.navigationTitle(self.server.name)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(
server: .constant(
.init(
name: "My Server",
configuration: .dnsOverTLS(DoTConfiguration())
)
),
isOn: .constant(true)
)
}
}

View file

@ -25,7 +25,7 @@
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<false/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>

69
CustomDNS/Presets.swift Normal file
View file

@ -0,0 +1,69 @@
//
// Presets.swift
// CustomDNS
//
// Created by Kenta Kubo on 9/25/20.
//
import Foundation
enum Presets {
static let servers: Resolvers = [
.init(
name: "Google Public DNS",
configuration: .dnsOverTLS(
DoTConfiguration(
servers: [
"8.8.8.8",
"8.8.4.4",
"2001:4860:4860::8888",
"2001:4860:4860::8844",
],
serverName: "dns.google"
)
)
),
.init(
name: "Google Public DNS",
configuration: .dnsOverHTTPS(
DoHConfiguration(
servers: [
"8.8.8.8",
"8.8.4.4",
"2001:4860:4860::8888",
"2001:4860:4860::8844",
],
serverURL: URL(string: "https://dns.google/dns-query")
)
)
),
.init(
name: "1.1.1.1",
configuration: .dnsOverTLS(
DoTConfiguration(
servers: [
"1.1.1.1",
"1.0.0.1",
"2606:4700:4700::64",
"2606:4700:4700::6400",
],
serverName: "cloudflare-dns.com"
)
)
),
.init(
name: "1.1.1.1",
configuration: .dnsOverHTTPS(
DoHConfiguration(
servers: [
"1.1.1.1",
"1.0.0.1",
"2606:4700:4700::64",
"2606:4700:4700::6400",
],
serverURL: URL(string: "https://cloudflare-dns.com/dns-query")
)
)
),
]
}

133
CustomDNS/Resolver.swift Normal file
View file

@ -0,0 +1,133 @@
//
// Resolver.swift
// CustomDNS
//
// Created by Kenta Kubo on 9/25/20.
//
import Foundation
import NetworkExtension
struct DoTConfiguration {
var servers: [String] = []
var serverName: String? = nil
func toDNSSettings() -> NEDNSOverTLSSettings {
let settings = NEDNSOverTLSSettings(servers: self.servers)
settings.serverName = self.serverName
return settings
}
}
extension DoTConfiguration: Codable {}
struct DoHConfiguration {
var servers: [String] = []
var serverURL: URL? = nil
func toDNSSettings() -> NEDNSOverHTTPSSettings {
let settings = NEDNSOverHTTPSSettings(servers: self.servers)
settings.serverURL = self.serverURL
return settings
}
}
extension DoHConfiguration: Codable {}
enum Configuration {
case dnsOverTLS(DoTConfiguration)
case dnsOverHTTPS(DoHConfiguration)
func toDNSSettings() -> NEDNSSettings {
switch self {
case let .dnsOverTLS(configuration):
return configuration.toDNSSettings()
case let .dnsOverHTTPS(configuration):
return configuration.toDNSSettings()
}
}
}
extension Configuration: Codable {
enum CodingKeys: String, CodingKey {
case base, dotConfiguration, dohConfiguration
}
enum Base: String, Codable {
case dnsOverTLS, dnsOverHTTPS
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Self.CodingKeys.self)
let base = try container.decode(Self.Base.self, forKey: .base)
switch base {
case .dnsOverTLS:
let configuration = try container.decode(DoTConfiguration.self, forKey: .dotConfiguration)
self = .dnsOverTLS(configuration)
case .dnsOverHTTPS:
let configuration = try container.decode(DoHConfiguration.self, forKey: .dohConfiguration)
self = .dnsOverHTTPS(configuration)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Self.CodingKeys.self)
switch self {
case let .dnsOverTLS(configuration):
try container.encode(Self.Base.dnsOverTLS, forKey: .base)
try container.encode(configuration, forKey: .dotConfiguration)
case let .dnsOverHTTPS(configuration):
try container.encode(Self.Base.dnsOverHTTPS, forKey: .base)
try container.encode(configuration, forKey: .dohConfiguration)
}
}
}
extension Configuration: CustomStringConvertible {
var description: String {
switch self {
case .dnsOverTLS: return "DNS-over-TLS"
case .dnsOverHTTPS: return "DNS-over-HTTPS"
}
}
}
struct Resolver {
var id = UUID()
var name: String
var configuration: Configuration
}
extension Resolver: Identifiable {}
extension Resolver: Codable {}
typealias Resolvers = [Resolver]
extension Resolvers {
func find(by id: UUID) -> Self.Element? {
self.first { $0.id == id }
}
}
extension Resolvers: RawRepresentable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Self.self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}