Skip to content
16 changes: 16 additions & 0 deletions NetBird/Source/App/ViewModels/MainViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class ViewModel: ObservableObject {
}
@Published var forceRelayConnection = true
@Published var showForceRelayAlert = false
@Published var disableIPv6 = false
@Published var connectOnDemand = false
@Published var showOnDemandAlert = false
@Published var showOnDemandConflictAlert = false
Expand Down Expand Up @@ -691,6 +692,21 @@ class ViewModel: ObservableObject {
previousExtensionState = extensionState
}

func setDisableIPv6(disabled: Bool) {
let previous = self.disableIPv6
self.disableIPv6 = disabled
configProvider.disableIPv6 = disabled
if !configProvider.commit() {
print("Failed to update IPv6 settings")
self.disableIPv6 = previous
configProvider.disableIPv6 = previous
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func loadIPv6Settings() {
self.disableIPv6 = configProvider.disableIPv6
}

func setForcedRelayConnection(isEnabled: Bool) {
let userDefaults = UserDefaults(suiteName: GlobalConstants.userPreferencesSuiteName)
userDefaults?.set(isEnabled, forKey: GlobalConstants.keyForceRelayConnection)
Expand Down
6 changes: 6 additions & 0 deletions NetBird/Source/App/Views/AdvancedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,17 @@ struct AdvancedView: View {
viewModel.setForcedRelayConnection(isEnabled: value)
}

Toggle("Disable IPv6", isOn: $viewModel.disableIPv6)
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onChange(of: viewModel.disableIPv6) { value in
viewModel.setDisableIPv6(disabled: value)
}
}
}
.onAppear {
viewModel.loadRosenpassSettings()
viewModel.loadPreSharedKey()
viewModel.loadIPv6Settings()
}
.navigationTitle("Advanced")
.navigationBarTitleDisplayMode(.inline)
Expand Down
6 changes: 6 additions & 0 deletions NetBird/Source/App/Views/Components/PeerCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ struct PeerCard: View {
.font(.subheadline)
.foregroundColor(Color("TextSecondary"))
.lineLimit(1)
if let ipv6 = peer.ipv6, !ipv6.isEmpty {
Text(ipv6)
.font(.subheadline)
.foregroundColor(Color("TextSecondary"))
.lineLimit(1)
}
}
Spacer()
ConnectionIndicator(status: peer.connStatus)
Expand Down
4 changes: 4 additions & 0 deletions NetBird/Source/App/Views/Components/PeerDetailSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ struct PeerDetailSheet: View {
NavigationView {
List {
Section {
detailRow("IPv4", peer.ip)
if let ipv6 = peer.ipv6, !ipv6.isEmpty {
detailRow("IPv6", ipv6)
}
detailRow("Status", peer.connStatus)
detailRow("Last status update", relativeDateText)
detailRow("Connection type", peer.relayed ? "Relayed" : "P2P")
Expand Down
27 changes: 27 additions & 0 deletions NetBird/Source/App/Views/TV/TVSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,32 @@ struct TVSettingsView: View {
)
}

TVSettingsSection(title: "Network") {
TVSettingsToggleRow(
icon: "network",
title: "Disable IPv6",
subtitle: "Disable IPv6 overlay addressing on the tunnel",
isOn: Binding(
get: { viewModel.disableIPv6 },
set: { newValue in
viewModel.setDisableIPv6(disabled: newValue)
}
)
)

TVSettingsToggleRow(
icon: "arrow.triangle.branch",
title: "Force Relay",
subtitle: "Force all connections through relay servers",
isOn: Binding(
get: { viewModel.forceRelayConnection },
set: { newValue in
viewModel.setForcedRelayConnection(isEnabled: newValue)
}
)
)
}

TVSettingsSection(title: "Security") {
TVSettingsRow(
icon: "key.fill",
Expand Down Expand Up @@ -125,6 +151,7 @@ struct TVSettingsView: View {
// Load settings from storage to sync UI with actual values
viewModel.loadRosenpassSettings()
viewModel.loadPreSharedKey()
viewModel.loadIPv6Settings()
}
.sheet(isPresented: $showDocsQRCode) {
TVQRCodeSheet(
Expand Down
4 changes: 3 additions & 1 deletion NetBirdTVNetworkExtension/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {

let peerInfo = PeerInfo(
ip: peer.ip,
ipv6: peer.iPv6,
fqdn: peer.fqdn,
localIceCandidateEndpoint: peer.localIceCandidateEndpoint,
remoteIceCandidateEndpoint: peer.remoteIceCandidateEndpoint,
Expand All @@ -579,6 +580,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let clientState = adapter.clientState
let statusDetails = StatusDetails(
ip: statusDetailsMessage.getIP(),
ipv6: statusDetailsMessage.getIPv6(),
fqdn: statusDetailsMessage.getFQDN(),
managementStatus: clientState,
peerInfo: peerInfoArray
Expand Down Expand Up @@ -608,7 +610,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
do {
let routeSelectionDetailsMessage = try adapter.client.getRoutesSelectionDetails()

let routeSelectionInfo: [RoutesSelectionInfo] = (0..<routeSelectionDetailsMessage.size()).compactMap { index in
let routeSelectionInfo: [RoutesSelectionInfo] = (0..<routeSelectionDetailsMessage.size()).compactMap { index -> RoutesSelectionInfo? in
guard let route = routeSelectionDetailsMessage.get(index) else { return nil }

let domains = (0..<(route.domains?.size() ?? 0)).compactMap { domainIndex -> DomainDetails? in
Expand Down
34 changes: 29 additions & 5 deletions NetbirdKit/ConfigurationProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ protocol ConfigurationProvider {
/// Whether Rosenpass permissive mode is enabled (allows non-Rosenpass peers)
var rosenpassPermissive: Bool { get set }

// MARK: - IPv6

/// Whether IPv6 overlay addressing is disabled
var disableIPv6: Bool { get set }

// MARK: - Pre-Shared Key

/// The current pre-shared key (empty string if not set)
Expand Down Expand Up @@ -86,6 +91,23 @@ final class iOSConfigurationProvider: ConfigurationProvider {
}
}

// MARK: - IPv6

var disableIPv6: Bool {
get {
var result = ObjCBool(false)
do {
try preferences.getDisableIPv6(&result)
} catch {
print("ConfigurationProvider: Failed to read disableIPv6 - \(error)")
}
return result.boolValue
}
set {
preferences.setDisableIPv6(newValue)
}
}

// MARK: - Pre-Shared Key

var preSharedKey: String {
Expand Down Expand Up @@ -150,6 +172,13 @@ final class tvOSConfigurationProvider: ConfigurationProvider {
set { updateJSONField(field: "RosenpassPermissive", value: newValue) }
}

// MARK: - IPv6

var disableIPv6: Bool {
get { extractJSONBool(field: "DisableIPv6") ?? false }
set { updateJSONField(field: "DisableIPv6", value: newValue) }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// MARK: - Pre-Shared Key

var preSharedKey: String {
Expand Down Expand Up @@ -206,11 +235,6 @@ final class tvOSConfigurationProvider: ConfigurationProvider {
return
}

guard dict[field] != nil else {
AppLogger.shared.log("ConfigurationProvider: Field '\(field)' not found in config JSON")
return
}

dict[field] = value

guard let data = try? JSONSerialization.data(withJSONObject: dict, options: [.sortedKeys]),
Expand Down
37 changes: 22 additions & 15 deletions NetbirdKit/NetworkChangeListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,28 @@ class NetworkChangeListener: NSObject, NetBirdSDKNetworkChangeListenerProtocol {
private var tunnelManager: PacketTunnelProviderSettingsManager

var interfaceIP: String?

var interfaceIPv6: String?

init(with tunnelManager: PacketTunnelProviderSettingsManager) {
self.tunnelManager = tunnelManager
}

func setInterfaceIP(_ p0: String?) {
guard let validIP = p0, !validIP.isEmpty else {
return
}

self.interfaceIP = validIP
self.tunnelManager.setInterfaceIP(interfaceIP: validIP)
}

func setInterfaceIPv6(_ p0: String?) {
guard let validIPv6 = p0, !validIPv6.isEmpty else {
return
}
self.interfaceIPv6 = validIPv6
self.tunnelManager.setInterfaceIPv6(interfaceIPv6: validIPv6)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func parseRoutesToNESettings(routesString: String) -> ([NEIPv4Route], [NEIPv6Route], Bool) {
var v4Routes : [NEIPv4Route] = []
Expand Down Expand Up @@ -75,6 +84,9 @@ class NetworkChangeListener: NSObject, NetBirdSDKNetworkChangeListenerProtocol {
if let interfaceIP = self.interfaceIP, let interfaceRoute = createIPv4RouteFromCIDR(cidr: interfaceIP) {
v4Routes.append(interfaceRoute)
}
if let interfaceIPv6 = self.interfaceIPv6, let interfaceRoute = createIPv6RouteFromCIDR(cidr: interfaceIPv6) {
v6Routes.append(interfaceRoute)
}
return (v4Routes, v6Routes, containsDefault)
}

Expand Down Expand Up @@ -102,22 +114,17 @@ class NetworkChangeListener: NSObject, NetBirdSDKNetworkChangeListenerProtocol {
}

func detectIPAddressType(_ address: String) -> IPAddressType {
let ipv4Pattern = "^(\\d{1,3}\\.){3}\\d{1,3}(\\/\\d{1,2})?$"
let ipv6Pattern = "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}(\\/\\d{1,3})?$"
let bare = address.split(separator: "/").first.map(String.init) ?? address

let ipv4Regex = try! NSRegularExpression(pattern: ipv4Pattern, options: [])
let ipv6Regex = try! NSRegularExpression(pattern: ipv6Pattern, options: [])

let ipv4Matches = ipv4Regex.numberOfMatches(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count))
let ipv6Matches = ipv6Regex.numberOfMatches(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count))

if ipv4Matches > 0 {
var v4 = in_addr()
if bare.withCString({ inet_pton(AF_INET, $0, &v4) }) == 1 {
return .ipv4
} else if ipv6Matches > 0 {
}
var v6 = in6_addr()
if bare.withCString({ inet_pton(AF_INET6, $0, &v6) }) == 1 {
return .ipv6
} else {
return .invalid
}
return .invalid
}

func extractIPAddressAndSubnet(from cidr: String) -> (String, String)? {
Expand Down
8 changes: 7 additions & 1 deletion NetbirdKit/StatusDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Combine

struct StatusDetails: Codable {
var ip: String
var ipv6: String?
var fqdn: String
var managementStatus: ClientState
var peerInfo: [PeerInfo]
Expand All @@ -18,6 +19,7 @@ struct StatusDetails: Codable {
extension StatusDetails: Equatable {
static func == (lhs: StatusDetails, rhs: StatusDetails) -> Bool {
return lhs.ip == rhs.ip &&
lhs.ipv6 == rhs.ipv6 &&
lhs.fqdn == rhs.fqdn &&
lhs.managementStatus == rhs.managementStatus &&
lhs.peerInfo == rhs.peerInfo
Expand All @@ -27,6 +29,7 @@ extension StatusDetails: Equatable {
class PeerInfo: ObservableObject, Codable, Identifiable {
var id = UUID()
var ip: String
var ipv6: String?
var fqdn: String
var localIceCandidateEndpoint: String
var remoteIceCandidateEndpoint: String
Expand All @@ -45,11 +48,12 @@ class PeerInfo: ObservableObject, Codable, Identifiable {
var routes: [String]
var selected: Bool = false

init(ip: String, fqdn: String, localIceCandidateEndpoint: String, remoteIceCandidateEndpoint: String,
init(ip: String, ipv6: String? = nil, fqdn: String, localIceCandidateEndpoint: String, remoteIceCandidateEndpoint: String,
localIceCandidateType: String, remoteIceCandidateType: String, pubKey: String, latency: String,
bytesRx: Int64, bytesTx: Int64, connStatus: String, connStatusUpdate: String, direct: Bool,
lastWireguardHandshake: String, relayed: Bool, rosenpassEnabled: Bool, routes: [String]) {
self.ip = ip
self.ipv6 = ipv6
self.fqdn = fqdn
self.localIceCandidateEndpoint = localIceCandidateEndpoint
self.remoteIceCandidateEndpoint = remoteIceCandidateEndpoint
Expand All @@ -73,6 +77,7 @@ extension PeerInfo: Equatable {
static func == (lhs: PeerInfo, rhs: PeerInfo) -> Bool {
return lhs.id == rhs.id &&
lhs.ip == rhs.ip &&
lhs.ipv6 == rhs.ipv6 &&
lhs.fqdn == rhs.fqdn &&
lhs.localIceCandidateEndpoint == rhs.localIceCandidateEndpoint &&
lhs.remoteIceCandidateEndpoint == rhs.remoteIceCandidateEndpoint &&
Expand All @@ -95,6 +100,7 @@ extension PeerInfo: Equatable {
extension PeerInfo {
func update(from newInfo: PeerInfo) {
self.ip = newInfo.ip
self.ipv6 = newInfo.ipv6
self.fqdn = newInfo.fqdn
self.localIceCandidateEndpoint = newInfo.localIceCandidateEndpoint
self.remoteIceCandidateEndpoint = newInfo.remoteIceCandidateEndpoint
Expand Down
4 changes: 3 additions & 1 deletion NetbirdNetworkExtension/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {

let peerInfo = PeerInfo(
ip: peer.ip,
ipv6: peer.iPv6,
fqdn: peer.fqdn,
localIceCandidateEndpoint: peer.localIceCandidateEndpoint,
remoteIceCandidateEndpoint: peer.remoteIceCandidateEndpoint,
Expand All @@ -440,6 +441,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let clientState = adapter.clientState
let statusDetails = StatusDetails(
ip: statusDetailsMessage.getIP(),
ipv6: statusDetailsMessage.getIPv6(),
fqdn: statusDetailsMessage.getFQDN(),
managementStatus: clientState,
peerInfo: peerInfoArray
Expand Down Expand Up @@ -469,7 +471,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
do {
let routeSelectionDetailsMessage = try adapter.client.getRoutesSelectionDetails()

let routeSelectionInfo: [RoutesSelectionInfo] = (0..<routeSelectionDetailsMessage.size()).compactMap { index in
let routeSelectionInfo: [RoutesSelectionInfo] = (0..<routeSelectionDetailsMessage.size()).compactMap { index -> RoutesSelectionInfo? in
guard let route = routeSelectionDetailsMessage.get(index) else { return nil }

let domains = (0..<(route.domains?.size() ?? 0)).compactMap { domainIndex -> DomainDetails? in
Expand Down
Loading
Loading