diff --git a/.gitignore b/.gitignore
index 6d52644..51965fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -290,4 +290,8 @@ xcuserdata
 /*.gcno
 **/xcshareddata/WorkspaceSettings.xcsettings
 
+### VSCode & Sweetpad ###
+.vscode/**
+buildServer.json
+
 # End of https://www.toptal.com/developers/gitignore/api/xcode,jetbrains,macos,direnv,swift,swiftpm,objective-c
diff --git a/Coder Desktop/.swiftformat b/Coder Desktop/.swiftformat
new file mode 100644
index 0000000..cb200b4
--- /dev/null
+++ b/Coder Desktop/.swiftformat	
@@ -0,0 +1,3 @@
+--selfrequired log,info,error,debug,critical,fault
+--exclude **.pb.swift
+--condassignment always
\ No newline at end of file
diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
index bfb01ce..4bec8d2 100644
--- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift	
+++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift	
@@ -16,9 +16,10 @@ struct DesktopApp: App {
                 .environmentObject(appDelegate.settings)
         }
         .windowResizability(.contentSize)
-        SwiftUI.Settings { SettingsView<CoderVPNService>()
-            .environmentObject(appDelegate.vpn)
-            .environmentObject(appDelegate.settings)
+        SwiftUI.Settings {
+            SettingsView<CoderVPNService>()
+                .environmentObject(appDelegate.vpn)
+                .environmentObject(appDelegate.settings)
         }
         .windowResizability(.contentSize)
     }
@@ -32,7 +33,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
     let settings: Settings
 
     override init() {
-        // TODO: Replace with real implementation
         vpn = CoderVPNService()
         settings = Settings()
         session = SecureSession(onChange: vpn.configureTunnelProviderProtocol)
diff --git a/Coder Desktop/Coder Desktop/State.swift b/Coder Desktop/Coder Desktop/State.swift
index 01ef7c2..cb51450 100644
--- a/Coder Desktop/Coder Desktop/State.swift	
+++ b/Coder Desktop/Coder Desktop/State.swift	
@@ -41,7 +41,9 @@ class SecureSession: ObservableObject, Session {
         if !hasSession { return nil }
         let proto = NETunnelProviderProtocol()
         proto.providerBundleIdentifier = "\(appId).VPN"
-        proto.passwordReference = keychain[attributes: Keys.sessionToken]?.persistentRef
+        // HACK: We can't write to the system keychain, and the user keychain
+        // isn't accessible, so we'll use providerConfiguration, which is over XPC.
+        proto.providerConfiguration = ["token": sessionToken!]
         proto.serverAddress = baseAccessURL!.absoluteString
         return proto
     }
diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift
index 3ca0004..9a3e35c 100644
--- a/Coder Desktop/VPN/Manager.swift	
+++ b/Coder Desktop/VPN/Manager.swift	
@@ -79,11 +79,11 @@ actor Manager {
                 case let .message(msg):
                     handleMessage(msg)
                 case let .RPC(rpc):
-                    handleRPC(rpc)
+                    await handleRPC(rpc)
                 }
             }
         } catch {
-            logger.error("tunnel read loop failed: \(error)")
+            logger.error("tunnel read loop failed: \(error.localizedDescription, privacy: .public)")
             try await tunnelHandle.close()
             // TODO: Notify app over XPC
             return
@@ -108,21 +108,33 @@ actor Manager {
         }
     }
 
-    func handleRPC(_ rpc: RPCRequest<Vpn_ManagerMessage, Vpn_TunnelMessage>) {
+    func handleRPC(_ rpc: RPCRequest<Vpn_ManagerMessage, Vpn_TunnelMessage>) async {
         guard let msgType = rpc.msg.msg else {
             logger.critical("received rpc with no type")
             return
         }
         switch msgType {
         case let .networkSettings(ns):
-            let neSettings = convertNetworkSettingsRequest(ns)
-            ptp.setTunnelNetworkSettings(neSettings)
+            do {
+                try await ptp.applyTunnelNetworkSettings(ns)
+                try? await rpc.sendReply(.with { resp in
+                    resp.networkSettings = .with { settings in
+                        settings.success = true
+                    }
+                })
+            } catch {
+                try? await rpc.sendReply(.with { resp in
+                    resp.networkSettings = .with { settings in
+                        settings.success = false
+                        settings.errorMessage = error.localizedDescription
+                    }
+                })
+            }
         case .log, .peerUpdate, .start, .stop:
             logger.critical("received unexpected rpc: `\(String(describing: msgType))`")
         }
     }
 
-    // TODO: Call via XPC
     func startVPN() async throws(ManagerError) {
         logger.info("sending start rpc")
         guard let tunFd = ptp.tunnelFileDescriptor else {
@@ -149,7 +161,6 @@ actor Manager {
         // TODO: notify app over XPC
     }
 
-    // TODO: Call via XPC
     func stopVPN() async throws(ManagerError) {
         logger.info("sending stop rpc")
         let resp: Vpn_TunnelMessage
@@ -246,5 +257,5 @@ func writeVpnLog(_ log: Vpn_Log) {
         category: log.loggerNames.joined(separator: ".")
     )
     let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
-    logger.log(level: level, "\(log.message): \(fields)")
+    logger.log(level: level, "\(log.message, privacy: .public): \(fields, privacy: .public)")
 }
diff --git a/Coder Desktop/VPN/PacketTunnelProvider.swift b/Coder Desktop/VPN/PacketTunnelProvider.swift
index 308882c..e548d8c 100644
--- a/Coder Desktop/VPN/PacketTunnelProvider.swift	
+++ b/Coder Desktop/VPN/PacketTunnelProvider.swift	
@@ -8,6 +8,8 @@ let CTLIOCGINFO: UInt = 0xC064_4E03
 class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
     private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "provider")
     private var manager: Manager?
+    // a `tunnelRemoteAddress` is required, but not currently used.
+    private var currentSettings: NEPacketTunnelNetworkSettings = .init(tunnelRemoteAddress: "127.0.0.1")
 
     var tunnelFileDescriptor: Int32? {
         var ctlInfo = ctl_info()
@@ -41,21 +43,42 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
         return nil
     }
 
-    override func startTunnel(options _: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
+    override func startTunnel(
+        options _: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void
+    ) {
         logger.debug("startTunnel called")
         guard manager == nil else {
             logger.error("startTunnel called with non-nil Manager")
-            completionHandler(nil)
+            completionHandler(PTPError.alreadyRunning)
             return
         }
+        guard let proto = protocolConfiguration as? NETunnelProviderProtocol,
+              let baseAccessURL = proto.serverAddress
+        else {
+            logger.error("startTunnel called with nil protocolConfiguration")
+            completionHandler(PTPError.missingConfiguration)
+            return
+        }
+        // HACK: We can't write to the system keychain, and the NE can't read the user keychain.
+        guard let token = proto.providerConfiguration?["token"] as? String else {
+            logger.error("startTunnel called with nil token")
+            completionHandler(PTPError.missingToken)
+            return
+        }
+        logger.debug("retrieved token & access URL")
         let completionHandler = CallbackWrapper(completionHandler)
         Task {
-            // TODO: Retrieve access URL & Token via Keychain
             do throws(ManagerError) {
+                logger.debug("creating manager")
                 manager = try await Manager(
                     with: self,
-                    cfg: .init(apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")!)
+                    cfg: .init(
+                        apiToken: token, serverUrl: .init(string: baseAccessURL)!
+                    )
                 )
+                logger.debug("starting vpn")
+                try await manager!.startVPN()
+                logger.info("vpn started")
                 completionHandler(nil)
             } catch {
                 completionHandler(error)
@@ -64,15 +87,26 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
         }
     }
 
-    override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
+    override func stopTunnel(
+        with _: NEProviderStopReason, completionHandler: @escaping () -> Void
+    ) {
         logger.debug("stopTunnel called")
-        guard manager != nil else {
+        guard let manager else {
             logger.error("stopTunnel called with nil Manager")
             completionHandler()
             return
         }
-        manager = nil
-        completionHandler()
+
+        let completionHandler = CompletionWrapper(completionHandler)
+        Task { [manager] in
+            do throws(ManagerError) {
+                try await manager.stopVPN()
+            } catch {
+                logger.error("error stopping manager: \(error.description, privacy: .public)")
+            }
+            completionHandler()
+        }
+        self.manager = nil
     }
 
     override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
@@ -92,4 +126,33 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
         // Add code here to wake up.
         logger.debug("wake called")
     }
+
+    // Wrapper around `setTunnelNetworkSettings` that supports merging updates
+    func applyTunnelNetworkSettings(_ diff: Vpn_NetworkSettingsRequest) async throws {
+        logger.debug("applying settings diff: \(diff.debugDescription, privacy: .public)")
+
+        if diff.hasDnsSettings {
+            currentSettings.dnsSettings = convertDnsSettings(diff.dnsSettings)
+        }
+
+        if diff.mtu != 0 {
+            currentSettings.mtu = NSNumber(value: diff.mtu)
+        }
+
+        if diff.hasIpv4Settings {
+            currentSettings.ipv4Settings = convertIPv4Settings(diff.ipv4Settings)
+        }
+        if diff.hasIpv6Settings {
+            currentSettings.ipv6Settings = convertIPv6Settings(diff.ipv6Settings)
+        }
+
+        logger.info("applying settings: \(self.currentSettings.debugDescription, privacy: .public)")
+        try await setTunnelNetworkSettings(currentSettings)
+    }
+}
+
+enum PTPError: Error {
+    case alreadyRunning
+    case missingConfiguration
+    case missingToken
 }
diff --git a/Coder Desktop/VPNLib/Convert.swift b/Coder Desktop/VPNLib/Convert.swift
index c3e9401..6784693 100644
--- a/Coder Desktop/VPNLib/Convert.swift	
+++ b/Coder Desktop/VPNLib/Convert.swift	
@@ -1,60 +1,61 @@
 import NetworkExtension
 import os
 
-// swiftlint:disable:next function_body_length
-public func convertNetworkSettingsRequest(_ req: Vpn_NetworkSettingsRequest) -> NEPacketTunnelNetworkSettings {
-    let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: req.tunnelRemoteAddress)
-    networkSettings.tunnelOverheadBytes = NSNumber(value: req.tunnelOverheadBytes)
-    networkSettings.mtu = NSNumber(value: req.mtu)
+public func convertDnsSettings(_ req: Vpn_NetworkSettingsRequest.DNSSettings) -> NEDNSSettings {
+    let dnsSettings = NEDNSSettings(servers: req.servers)
+    dnsSettings.searchDomains = req.searchDomains
+    dnsSettings.domainName = req.domainName
+    dnsSettings.matchDomains = req.matchDomains
+    dnsSettings.matchDomainsNoSearch = req.matchDomainsNoSearch
+    return dnsSettings
+}
 
-    if req.hasDnsSettings {
-        let dnsSettings = NEDNSSettings(servers: req.dnsSettings.servers)
-        dnsSettings.searchDomains = req.dnsSettings.searchDomains
-        dnsSettings.domainName = req.dnsSettings.domainName
-        dnsSettings.matchDomains = req.dnsSettings.matchDomains
-        dnsSettings.matchDomainsNoSearch = req.dnsSettings.matchDomainsNoSearch
-        networkSettings.dnsSettings = dnsSettings
+public func convertIPv4Settings(_ req: Vpn_NetworkSettingsRequest.IPv4Settings) -> NEIPv4Settings {
+    let ipv4Settings = NEIPv4Settings(addresses: req.addrs, subnetMasks: req.subnetMasks)
+    if !req.router.isEmpty {
+        ipv4Settings.router = req.router
     }
-
-    if req.hasIpv4Settings {
-        let ipv4Settings = NEIPv4Settings(addresses: req.ipv4Settings.addrs, subnetMasks: req.ipv4Settings.subnetMasks)
-        ipv4Settings.router = req.ipv4Settings.router
-        ipv4Settings.includedRoutes = req.ipv4Settings.includedRoutes.map {
-            let route = NEIPv4Route(destinationAddress: $0.destination, subnetMask: $0.mask)
+    ipv4Settings.includedRoutes = req.includedRoutes.map {
+        let route = NEIPv4Route(destinationAddress: $0.destination, subnetMask: $0.mask)
+        if !$0.router.isEmpty {
             route.gatewayAddress = $0.router
-            return route
         }
-        ipv4Settings.excludedRoutes = req.ipv4Settings.excludedRoutes.map {
-            let route = NEIPv4Route(destinationAddress: $0.destination, subnetMask: $0.mask)
+        return route
+    }
+    ipv4Settings.excludedRoutes = req.excludedRoutes.map {
+        let route = NEIPv4Route(destinationAddress: $0.destination, subnetMask: $0.mask)
+        if !$0.router.isEmpty {
             route.gatewayAddress = $0.router
-            return route
         }
-        networkSettings.ipv4Settings = ipv4Settings
+        return route
     }
+    return ipv4Settings
+}
 
-    if req.hasIpv6Settings {
-        let ipv6Settings = NEIPv6Settings(
-            addresses: req.ipv6Settings.addrs,
-            networkPrefixLengths: req.ipv6Settings.prefixLengths.map { NSNumber(value: $0)
-            }
+public func convertIPv6Settings(_ req: Vpn_NetworkSettingsRequest.IPv6Settings) -> NEIPv6Settings {
+    let ipv6Settings = NEIPv6Settings(
+        addresses: req.addrs,
+        networkPrefixLengths: req.prefixLengths.map { NSNumber(value: $0) }
+    )
+    ipv6Settings.includedRoutes = req.includedRoutes.map {
+        let route = NEIPv6Route(
+            destinationAddress: $0.destination,
+            networkPrefixLength: NSNumber(value: $0.prefixLength)
         )
-        ipv6Settings.includedRoutes = req.ipv6Settings.includedRoutes.map {
-            let route = NEIPv6Route(
-                destinationAddress: $0.destination,
-                networkPrefixLength: NSNumber(value: $0.prefixLength)
-            )
+        if !$0.router.isEmpty {
             route.gatewayAddress = $0.router
-            return route
         }
-        ipv6Settings.excludedRoutes = req.ipv6Settings.excludedRoutes.map {
-            let route = NEIPv6Route(
-                destinationAddress: $0.destination,
-                networkPrefixLength: NSNumber(value: $0.prefixLength)
-            )
+        return route
+    }
+    ipv6Settings.excludedRoutes = req.excludedRoutes.map {
+        let route = NEIPv6Route(
+            destinationAddress: $0.destination,
+            networkPrefixLength: NSNumber(value: $0.prefixLength)
+        )
+        if !$0.router.isEmpty {
             route.gatewayAddress = $0.router
-            return route
         }
-        networkSettings.ipv6Settings = ipv6Settings
+        return route
     }
-    return networkSettings
+    return ipv6Settings
 }
diff --git a/Coder Desktop/VPNLib/Receiver.swift b/Coder Desktop/VPNLib/Receiver.swift
index b139283..8151c3c 100644
--- a/Coder Desktop/VPNLib/Receiver.swift	
+++ b/Coder Desktop/VPNLib/Receiver.swift	
@@ -6,7 +6,6 @@ import SwiftProtobuf
 actor Receiver<RecvMsg: Message> {
     private let dispatch: DispatchIO
     private let queue: DispatchQueue
-    private var running = false
     private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "proto")
 
     /// Creates an instance using the given `DispatchIO` channel and queue.
@@ -58,11 +57,7 @@ actor Receiver<RecvMsg: Message> {
     /// Starts reading protocol messages from the `DispatchIO` channel and returns them as an `AsyncStream` of messages.
     /// On read or decoding error, it logs and closes the stream.
     func messages() throws(ReceiveError) -> AsyncStream<RecvMsg> {
-        if running {
-            throw .alreadyRunning
-        }
-        running = true
-        return AsyncStream(
+        AsyncStream(
             unfolding: {
                 do {
                     let length = try await self.readLen()
@@ -83,7 +78,6 @@ actor Receiver<RecvMsg: Message> {
 enum ReceiveError: Error {
     case readError(String)
     case invalidLength
-    case alreadyRunning
 }
 
 func deserializeLen(_ data: Data) throws -> UInt32 {
diff --git a/Coder Desktop/VPNLib/Speaker.swift b/Coder Desktop/VPNLib/Speaker.swift
index 384a8a7..27dbf2b 100644
--- a/Coder Desktop/VPNLib/Speaker.swift	
+++ b/Coder Desktop/VPNLib/Speaker.swift	
@@ -79,10 +79,10 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag
             }
         )
         receiver = Receiver(dispatch: dispatch, queue: queue)
-        if SendMsg.self == Vpn_TunnelMessage.self {
-            role = .tunnel
+        role = if SendMsg.self == Vpn_TunnelMessage.self {
+            .tunnel
         } else {
-            role = .manager
+            .manager
         }
     }
 
diff --git a/Coder Desktop/VPNLib/Util.swift b/Coder Desktop/VPNLib/Util.swift
index 9dbfbc7..ff31e4f 100644
--- a/Coder Desktop/VPNLib/Util.swift	
+++ b/Coder Desktop/VPNLib/Util.swift	
@@ -1,4 +1,4 @@
-public final class CallbackWrapper<T, U>: @unchecked Sendable {
+public struct CallbackWrapper<T, U>: @unchecked Sendable {
     private let block: (T?) -> U
 
     public init(_ block: @escaping (T?) -> U) {
@@ -6,7 +6,18 @@ public final class CallbackWrapper<T, U>: @unchecked Sendable {
     }
 
     public func callAsFunction(_ error: T?) -> U {
-        // Just forward to the original block
         block(error)
     }
 }
+
+public struct CompletionWrapper<T>: @unchecked Sendable {
+    private let block: () -> T
+
+    public init(_ block: @escaping () -> T) {
+        self.block = block
+    }
+
+    public func callAsFunction() -> T {
+        block()
+    }
+}
diff --git a/Coder Desktop/VPNLibTests/ConvertTests.swift b/Coder Desktop/VPNLibTests/ConvertTests.swift
index b61a28b..9bf35f4 100644
--- a/Coder Desktop/VPNLibTests/ConvertTests.swift	
+++ b/Coder Desktop/VPNLibTests/ConvertTests.swift	
@@ -2,98 +2,107 @@ import Testing
 @testable import VPNLib
 
 @Suite(.timeLimit(.minutes(1)))
-struct ConvertTests {
+struct ConvertNetworkSettingsTests {
     @Test
-    // swiftlint:disable:next function_body_length
-    func convertProtoNetworkSettingsRequest() async throws {
-        let req: Vpn_NetworkSettingsRequest = .with { req in
-            req.tunnelRemoteAddress = "10.0.0.1"
-            req.tunnelOverheadBytes = 20
-            req.mtu = 1400
+    func testConvertDnsSettings() async throws {
+        let req: Vpn_NetworkSettingsRequest.DNSSettings = .with { dns in
+            dns.servers = ["8.8.8.8"]
+            dns.searchDomains = ["example.com"]
+            dns.domainName = "example.com"
+            dns.matchDomains = ["example.com"]
+            dns.matchDomainsNoSearch = false
+        }
 
-            req.dnsSettings = .with { dns in
-                dns.servers = ["8.8.8.8"]
-                dns.searchDomains = ["example.com"]
-                dns.domainName = "example.com"
-                dns.matchDomains = ["example.com"]
-                dns.matchDomainsNoSearch = false
-            }
+        let result = convertDnsSettings(req)
 
-            req.ipv4Settings = .with { ipv4 in
-                ipv4.addrs = ["192.168.1.1"]
-                ipv4.subnetMasks = ["255.255.255.0"]
-                ipv4.router = "192.168.1.254"
-                ipv4.includedRoutes = [
-                    .with { route in
-                        route.destination = "10.0.0.0"
-                        route.mask = "255.0.0.0"
-                        route.router = "192.168.1.254"
-                    },
-                ]
-                ipv4.excludedRoutes = [
-                    .with { route in
-                        route.destination = "172.16.0.0"
-                        route.mask = "255.240.0.0"
-                        route.router = "192.168.1.254"
-                    },
-                ]
-            }
+        #expect(result.servers == req.servers)
+        #expect(result.searchDomains == req.searchDomains)
+        #expect(result.domainName == req.domainName)
+        #expect(result.matchDomains == req.matchDomains)
+        #expect(result.matchDomainsNoSearch == req.matchDomainsNoSearch)
+    }
 
-            req.ipv6Settings = .with { ipv6 in
-                ipv6.addrs = ["2001:db8::1"]
-                ipv6.prefixLengths = [64]
-                ipv6.includedRoutes = [
-                    .with { route in
-                        route.destination = "2001:db8::"
-                        route.router = "2001:db8::1"
-                        route.prefixLength = 64
-                    },
-                ]
-                ipv6.excludedRoutes = [
-                    .with { route in
-                        route.destination = "2001:0db8:85a3::"
-                        route.router = "2001:db8::1"
-                        route.prefixLength = 128
-                    },
-                ]
-            }
+    @Test
+    func testConvertIPv4Settings() async throws {
+        let req: Vpn_NetworkSettingsRequest.IPv4Settings = .with { ipv4 in
+            ipv4.addrs = ["192.168.1.1"]
+            ipv4.subnetMasks = ["255.255.255.0"]
+            ipv4.router = "192.168.1.254"
+            ipv4.includedRoutes = [
+                .with { route in
+                    route.destination = "10.0.0.0"
+                    route.mask = "255.0.0.0"
+                    route.router = "192.168.1.254"
+                },
+            ]
+            ipv4.excludedRoutes = [
+                .with { route in
+                    route.destination = "172.16.0.0"
+                    route.mask = "255.240.0.0"
+                    route.router = "192.168.1.254"
+                },
+            ]
         }
 
-        let result = convertNetworkSettingsRequest(req)
-        #expect(result.tunnelRemoteAddress == req.tunnelRemoteAddress)
-        #expect(result.dnsSettings!.servers == req.dnsSettings.servers)
-        #expect(result.dnsSettings!.domainName == req.dnsSettings.domainName)
-        #expect(result.ipv4Settings!.addresses == req.ipv4Settings.addrs)
-        #expect(result.ipv4Settings!.subnetMasks == req.ipv4Settings.subnetMasks)
-        #expect(result.ipv6Settings!.addresses == req.ipv6Settings.addrs)
-        #expect(result.ipv6Settings!.networkPrefixLengths == [64])
+        let result = convertIPv4Settings(req)
+
+        #expect(result.addresses == req.addrs)
+        #expect(result.subnetMasks == req.subnetMasks)
+        #expect(result.router == req.router)
+
+        try #require(result.includedRoutes?.count == req.includedRoutes.count)
+        let includedRoute = result.includedRoutes![0]
+        let expectedIncludedRoute = req.includedRoutes[0]
+        #expect(includedRoute.destinationAddress == expectedIncludedRoute.destination)
+        #expect(includedRoute.destinationSubnetMask == expectedIncludedRoute.mask)
+        #expect(includedRoute.gatewayAddress == expectedIncludedRoute.router)
+
+        try #require(result.excludedRoutes?.count == req.excludedRoutes.count)
+        let excludedRoute = result.excludedRoutes![0]
+        let expectedExcludedRoute = req.excludedRoutes[0]
+        #expect(excludedRoute.destinationAddress == expectedExcludedRoute.destination)
+        #expect(excludedRoute.destinationSubnetMask == expectedExcludedRoute.mask)
+        #expect(excludedRoute.gatewayAddress == expectedExcludedRoute.router)
+    }
+
+    @Test
+    func testConvertIPv6Settings() async throws {
+        let req: Vpn_NetworkSettingsRequest.IPv6Settings = .with { ipv6 in
+            ipv6.addrs = ["2001:db8::1"]
+            ipv6.prefixLengths = [64]
+            ipv6.includedRoutes = [
+                .with { route in
+                    route.destination = "2001:db8::"
+                    route.router = "2001:db8::1"
+                    route.prefixLength = 64
+                },
+            ]
+            ipv6.excludedRoutes = [
+                .with { route in
+                    route.destination = "2001:0db8:85a3::"
+                    route.router = "2001:db8::1"
+                    route.prefixLength = 128
+                },
+            ]
+        }
 
-        try #require(result.ipv4Settings!.includedRoutes?.count == req.ipv4Settings.includedRoutes.count)
-        let ipv4IncludedRoute = result.ipv4Settings!.includedRoutes![0]
-        let expectedIpv4IncludedRoute = req.ipv4Settings.includedRoutes[0]
-        #expect(ipv4IncludedRoute.destinationAddress == expectedIpv4IncludedRoute.destination)
-        #expect(ipv4IncludedRoute.destinationSubnetMask == expectedIpv4IncludedRoute.mask)
-        #expect(ipv4IncludedRoute.gatewayAddress == expectedIpv4IncludedRoute.router)
+        let result = convertIPv6Settings(req)
 
-        try #require(result.ipv4Settings!.excludedRoutes?.count == req.ipv4Settings.excludedRoutes.count)
-        let ipv4ExcludedRoute = result.ipv4Settings!.excludedRoutes![0]
-        let expectedIpv4ExcludedRoute = req.ipv4Settings.excludedRoutes[0]
-        #expect(ipv4ExcludedRoute.destinationAddress == expectedIpv4ExcludedRoute.destination)
-        #expect(ipv4ExcludedRoute.destinationSubnetMask == expectedIpv4ExcludedRoute.mask)
-        #expect(ipv4ExcludedRoute.gatewayAddress == expectedIpv4ExcludedRoute.router)
+        #expect(result.addresses == req.addrs)
+        #expect(result.networkPrefixLengths == req.prefixLengths.map { NSNumber(value: $0) })
 
-        try #require(result.ipv6Settings!.includedRoutes?.count == req.ipv6Settings.includedRoutes.count)
-        let ipv6IncludedRoute = result.ipv6Settings!.includedRoutes![0]
-        let expectedIpv6IncludedRoute = req.ipv6Settings.includedRoutes[0]
-        #expect(ipv6IncludedRoute.destinationAddress == expectedIpv6IncludedRoute.destination)
-        #expect(ipv6IncludedRoute.destinationNetworkPrefixLength == 64)
-        #expect(ipv6IncludedRoute.gatewayAddress == expectedIpv6IncludedRoute.router)
+        try #require(result.includedRoutes?.count == req.includedRoutes.count)
+        let includedRoute = result.includedRoutes![0]
+        let expectedIncludedRoute = req.includedRoutes[0]
+        #expect(includedRoute.destinationAddress == expectedIncludedRoute.destination)
+        #expect(includedRoute.destinationNetworkPrefixLength == NSNumber(value: 64))
+        #expect(includedRoute.gatewayAddress == expectedIncludedRoute.router)
 
-        try #require(result.ipv6Settings!.excludedRoutes?.count == req.ipv6Settings.excludedRoutes.count)
-        let ipv6ExcludedRoute = result.ipv6Settings!.excludedRoutes![0]
-        let expectedIpv6ExcludedRoute = req.ipv6Settings.excludedRoutes[0]
-        #expect(ipv6ExcludedRoute.destinationAddress == expectedIpv6ExcludedRoute.destination)
-        #expect(ipv6ExcludedRoute.destinationNetworkPrefixLength == 128)
-        #expect(ipv6ExcludedRoute.gatewayAddress == expectedIpv6ExcludedRoute.router)
+        try #require(result.excludedRoutes?.count == req.excludedRoutes.count)
+        let excludedRoute = result.excludedRoutes![0]
+        let expectedExcludedRoute = req.excludedRoutes[0]
+        #expect(excludedRoute.destinationAddress == expectedExcludedRoute.destination)
+        #expect(excludedRoute.destinationNetworkPrefixLength == NSNumber(value: 128))
+        #expect(excludedRoute.gatewayAddress == expectedExcludedRoute.router)
     }
 }
diff --git a/Makefile b/Makefile
index e66eb59..7e078de 100644
--- a/Makefile
+++ b/Makefile
@@ -26,7 +26,6 @@ $(PROJECT)/VPNLib/vpn.pb.swift: $(PROJECT)/VPNLib/vpn.proto
 .PHONY: fmt
 fmt: ## Run Swift file formatter
 	swiftformat \
-		--exclude '**.pb.swift' \
 		--swiftversion $(SWIFT_VERSION) \
 		$(FMTFLAGS) .