// // EndpointAdvancedView+OpenVPN.swift // Passepartout // // Created by Davide De Rosa on 3/8/22. // Copyright (c) 2023 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn // // This file is part of Passepartout. // // Passepartout is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Passepartout 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 Passepartout. If not, see . // import SwiftUI import TunnelKitOpenVPN import PassepartoutLibrary extension EndpointAdvancedView { struct OpenVPNView: View { @Binding var builder: OpenVPN.ConfigurationBuilder let isReadonly: Bool let isServerPushed: Bool private let fallbackConfiguration = OpenVPN.ConfigurationBuilder(withFallbacks: true).build() var body: some View { List { let cfg = builder.build() if !isServerPushed { pullSection(configuration: cfg) } if builder.ipv4 != nil || builder.routes4 != nil { ipv4Section } if builder.ipv6 != nil || builder.routes6 != nil { ipv6Section } dnsSection(configuration: cfg) proxySection(configuration: cfg) if !isReadonly { communicationEditableSection compressionEditableSection } else { communicationSection(configuration: cfg) compressionSection(configuration: cfg) } if !isServerPushed { tlsSection } otherSection(configuration: cfg) } } } } extension EndpointAdvancedView.OpenVPNView { private func pullSection(configuration: OpenVPN.Configuration) -> some View { configuration.pullMask.map { mask in Section { ForEach(mask, id: \.self) { Text($0.localizedDescription) } } header: { Text(L10n.Endpoint.Advanced.Openvpn.Sections.Pull.header) } } } private var ipv4Section: some View { Section { if let settings = builder.ipv4 { themeLongContentLinkDefault( L10n.Global.Strings.address, content: .constant(settings.localizedAddress) ) themeLongContentLinkDefault( L10n.NetworkSettings.Gateway.title, content: .constant(settings.localizedDefaultGateway) ) } builder.routes4.map { routes in ForEach(routes, id: \.self) { route in themeLongContentLinkDefault( L10n.Endpoint.Advanced.Openvpn.Items.Route.caption, content: .constant(route.localizedDescription) ) } } } header: { Text(Unlocalized.Network.ipv4) } } private var ipv6Section: some View { Section { if let settings = builder.ipv6 { themeLongContentLinkDefault( L10n.Global.Strings.address, content: .constant(settings.localizedAddress) ) themeLongContentLinkDefault( L10n.NetworkSettings.Gateway.title, content: .constant(settings.localizedDefaultGateway) ) } builder.routes6.map { routes in ForEach(routes, id: \.self) { route in themeLongContentLinkDefault( L10n.Endpoint.Advanced.Openvpn.Items.Route.caption, content: .constant(route.localizedDescription) ) } } } header: { Text(Unlocalized.Network.ipv6) } } private func communicationSection(configuration: OpenVPN.Configuration) -> some View { configuration.communicationSettings.map { settings in Section { settings.cipher.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.Cipher.caption) .withTrailingText($0.localizedDescription) } settings.digest.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption) .withTrailingText($0.localizedDescription) } // Text(Unlocalized.VPN.xor) // .withTrailingText(settings.xor.localizedDescription) themeLongContentLink( Unlocalized.VPN.xor, content: .constant(settings.xor.localizedLongDescription), withPreview: settings.xor.localizedDescription ) } header: { Text(L10n.Endpoint.Advanced.Openvpn.Sections.Communication.header) } } } private var communicationEditableSection: some View { Section { themeTextPicker( L10n.Endpoint.Advanced.Openvpn.Items.Cipher.caption, selection: $builder.cipher ?? fallbackCipher, values: OpenVPN.Cipher.available, description: \.localizedDescription ) themeTextPicker( L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption, selection: $builder.digest ?? fallbackDigest, values: OpenVPN.Digest.available, description: \.localizedDescription ) // Text(Unlocalized.VPN.xor) // .withTrailingText(builder.xorMethod.localizedDescription) themeLongContentLink( Unlocalized.VPN.xor, content: .constant(builder.xorMethod.localizedLongDescription), withPreview: builder.xorMethod.localizedDescription ) } header: { Text(L10n.Endpoint.Advanced.Openvpn.Sections.Communication.header) } } private func compressionSection(configuration: OpenVPN.Configuration) -> some View { configuration.compressionSettings.map { settings in Section { settings.framing.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionFraming.caption) .withTrailingText($0.localizedDescription) } settings.algorithm.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionAlgorithm.caption) .withTrailingText($0.localizedDescription) } } header: { Text(L10n.Endpoint.Advanced.Openvpn.Sections.Compression.header) } } } private var compressionEditableSection: some View { Section { themeTextPicker( L10n.Endpoint.Advanced.Openvpn.Items.CompressionFraming.caption, selection: $builder.compressionFraming ?? fallbackCompressionFraming, values: OpenVPN.CompressionFraming.available, description: \.localizedDescription ) themeTextPicker( L10n.Endpoint.Advanced.Openvpn.Items.CompressionAlgorithm.caption, selection: $builder.compressionAlgorithm ?? fallbackCompressionAlgorithm, values: OpenVPN.CompressionAlgorithm.available, description: \.localizedDescription ).disabled(builder.compressionFraming == .disabled) } header: { Text(L10n.Endpoint.Advanced.Openvpn.Sections.Compression.header) } } private func dnsSection(configuration: OpenVPN.Configuration) -> some View { configuration.dnsSettings.map { settings in Section { ForEach(settings.servers, id: \.self) { Text(L10n.Global.Strings.address) .withTrailingText($0, copyOnTap: true) } ForEach(settings.domains, id: \.self) { Text(L10n.Global.Strings.domain) .withTrailingText($0, copyOnTap: true) } } header: { Text(Unlocalized.Network.dns) } } } private func proxySection(configuration: OpenVPN.Configuration) -> some View { configuration.proxySettings.map { settings in Section { settings.proxy.map { Text(L10n.Global.Strings.address) .withTrailingText($0.rawValue, copyOnTap: true) } settings.pac.map { Text(Unlocalized.Network.proxyAutoConfiguration) .withTrailingText($0.absoluteString, copyOnTap: true) } ForEach(settings.bypass, id: \.self) { Text(L10n.NetworkSettings.Items.ProxyBypass.caption) .withTrailingText($0, copyOnTap: true) } } header: { Text(L10n.Global.Strings.proxy) } } } private var tlsSection: some View { Section { builder.ca.map { ca in themeLongContentLink( Unlocalized.VPN.certificateAuthority, content: .constant(ca.pem) ) } builder.clientCertificate.map { cert in themeLongContentLink( L10n.Endpoint.Advanced.Openvpn.Items.Client.caption, content: .constant(cert.pem) ) } builder.clientKey.map { key in themeLongContentLink( L10n.Endpoint.Advanced.Openvpn.Items.ClientKey.caption, content: .constant(key.pem) ) } builder.tlsWrap.map { wrap in themeLongContentLink( L10n.Endpoint.Advanced.Openvpn.Items.TlsWrapping.caption, content: .constant(wrap.key.hexString), withPreview: builder.tlsWrap.localizedDescription ) } Text(L10n.Endpoint.Advanced.Openvpn.Items.Eku.caption) .withTrailingText(builder.checksEKU.localizedDescriptionAsEKU) } header: { Text(Unlocalized.Network.tls) } } private func otherSection(configuration: OpenVPN.Configuration) -> some View { configuration.otherSettings.map { settings in Section { settings.keepAlive.map { Text(L10n.Global.Strings.keepalive) .withTrailingText($0.localizedDescriptionAsKeepAlive) } settings.reneg.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.RenegotiationSeconds.caption) .withTrailingText($0.localizedDescriptionAsRenegotiatesAfter) } settings.randomizeEndpoint.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomEndpoint.caption) .withTrailingText($0.localizedDescriptionAsRandomizeEndpoint) } settings.randomizeHostnames.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomHostname.caption) .withTrailingText($0.localizedDescriptionAsRandomizeHostnames) } } header: { Text(L10n.Endpoint.Advanced.Openvpn.Sections.Other.header) } } } } extension OpenVPN.Configuration { var communicationSettings: (cipher: OpenVPN.Cipher?, digest: OpenVPN.Digest?, xor: OpenVPN.XORMethod?)? { guard cipher != nil || digest != nil || xorMethod != nil else { return nil } return (cipher, digest, xorMethod) } var compressionSettings: (framing: OpenVPN.CompressionFraming?, algorithm: OpenVPN.CompressionAlgorithm?)? { guard compressionFraming != nil || compressionAlgorithm != nil else { return nil } return (compressionFraming, compressionAlgorithm) } var dnsSettings: (servers: [String], domains: [String])? { guard !(dnsServers?.isEmpty ?? true) || !(searchDomains?.isEmpty ?? true) else { return nil } return (dnsServers ?? [], searchDomains ?? []) } var proxySettings: (proxy: Proxy?, pac: URL?, bypass: [String])? { guard httpsProxy != nil || httpProxy != nil || proxyAutoConfigurationURL != nil || !(proxyBypassDomains?.isEmpty ?? true) else { return nil } return (httpsProxy ?? httpProxy, proxyAutoConfigurationURL, proxyBypassDomains ?? []) } var otherSettings: (keepAlive: TimeInterval?, reneg: TimeInterval?, randomizeEndpoint: Bool?, randomizeHostnames: Bool?)? { guard keepAliveInterval != nil || renegotiatesAfter != nil || randomizeEndpoint != nil || randomizeHostnames != nil else { return nil } return (keepAliveInterval, renegotiatesAfter, randomizeEndpoint, randomizeHostnames) } } private extension EndpointAdvancedView.OpenVPNView { var fallbackCipher: OpenVPN.Cipher { fallbackConfiguration.cipher! } var fallbackDigest: OpenVPN.Digest { fallbackConfiguration.digest! } var fallbackCompressionFraming: OpenVPN.CompressionFraming { fallbackConfiguration.compressionFraming! } var fallbackCompressionAlgorithm: OpenVPN.CompressionAlgorithm { fallbackConfiguration.compressionAlgorithm! } }