Incorrect VPN Data Statistics in MacOS Swift App

226 views Asked by At

I'm trying to trying to get Rx (received packet byte count) and Tx (transmitted packet byte count) in MacOS Swift app that uses TunnelKit to create a WireGuard connection to a VPN.

I've verified that I am connected to my VPN and am getting data. However, I'm not getting the Rx and Tx values back at all - here's my code; I'm using the SystemConfiguration framework to try get the data for the connected VPN.

Here is where I am trying to get the VPN statistics data from via the SystemConfiguration framework

#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>

NS_ASSUME_NONNULL_BEGIN

@interface VPNConnectionStatisticsManager : NSObject

- (SCNetworkConnectionRef _Nullable)initializeVPNConnectionWithServiceID:(NSString *)serviceID;

@end

NS_ASSUME_NONNULL_END


#import <Foundation/Foundation.h>
#import "VPNConnectionStatisticsManager.h"

@implementation VPNConnectionStatisticsManager

- (SCNetworkConnectionRef _Nullable)initializeVPNConnectionWithServiceID:(NSString *)serviceID {
    SCNetworkConnectionContext context = {0, NULL, NULL, NULL, NULL};
    SCNetworkConnectionRef connection = SCNetworkConnectionCreateWithServiceID(NULL, (__bridge CFStringRef)serviceID, ConnectionStatusChanged, &context);
    return connection;
}


static void ConnectionStatusChanged(SCNetworkConnectionRef connection, SCNetworkConnectionStatus status, void *info) {
    NSLog(@"VPN status changed: %d", (int)status);
}

@end

I changed my code to make sure that VPNConnectionStatisticsManager is not being recreated on every call. Have also included my view code that shows this in context:

class VPNStatusViewModel: ObservableObject {
    var vpnStatusRefreshTimer: Timer?
    var vpnConnection: SCNetworkConnection?
    private let vpnConnectionStatisticsManager = VPNConnectionStatisticsManager()

    func initializeVPNConnection() {
        if let serviceID = findMeterVPNServiceID(),
           let vpnConnectionRef = vpnConnectionStatisticsManager.initializeVPNConnection(withServiceID: serviceID) {
            vpnConnection = vpnConnectionRef.takeRetainedValue()
        }
    }

    func getVPNData() -> (rx: String, tx: String) {
        var rx: String = "N/A"
        var tx: String = "N/A"
        
        if let vpnConnection = vpnConnection,
           let statsDict = SCNetworkConnectionCopyStatistics(vpnConnection) as? [String: Any],
           let vpnDict = statsDict["VPN"] as? [String: Any] {
            if let rxInt = vpnDict["BytesIn"] as? Int, let txInt = vpnDict["BytesOut"] as? Int {
                let formatter = ByteCountFormatter()
                formatter.allowedUnits = [.useBytes, .useKB, .useMB]
                rx = formatter.string(fromByteCount: Int64(rxInt))
                tx = formatter.string(fromByteCount: Int64(txInt))
            }
        }
        
        return (rx, tx)
    }

    func stopVPNStatusTimer() {
        vpnStatusRefreshTimer?.invalidate()
        vpnStatusRefreshTimer = nil
    }

    func releaseVPNConnection() {
        vpnConnection = nil
    }
}

struct VPNStatusPane: View {
    @ObservedObject var appDelegate: AppDelegate
    @StateObject private var viewModel = VPNStatusViewModel()
    @State var connectedSince: String
    @State var rxBytes: String = "0"
    @State var txBytes: String = "0"

    init(appDelegate: AppDelegate) {
        self.appDelegate = appDelegate
        self._connectedSince = State(initialValue: appDelegate.vpnConnectedSince)
    }

    var body: some View {
        VStack(alignment: .leading) {
            VStack(alignment: .leading, spacing: 10) {
                HypeneatedText(label: "Rx bytes", value: rxBytes)
                HypeneatedText(label: "Tx bytes", value: txBytes)
                HypeneatedText(label: "Connected for", value: connectedSince)
            }
            .padding(.top, 15)

            Spacer()
        }
        .onAppear {
            viewModel.initializeVPNConnection()
            updateVPNData()
            startVPNStatusTimer()
        }
        .onDisappear {
            viewModel.stopVPNStatusTimer()
            viewModel.releaseVPNConnection()
        }
        .frame(maxWidth: .infinity, alignment: .topLeading)
    }

    private func startVPNStatusTimer() {
        viewModel.vpnStatusRefreshTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [self] _ in
            if self.appDelegate.vpnIsConnected {
                self.appDelegate.updateConnectionTime()
            }
            self.connectedSince = self.appDelegate.vpnConnectedSince
            self.updateVPNData()
        }
    }

    private func updateVPNData() {
        let vpnData = viewModel.getVPNData()
        rxBytes = vpnData.rx
        txBytes = vpnData.tx
    }
}
1

There are 1 answers

0
Stephen de Jager On

It appears the intention is to discuss or review a piece of Objective-C code related to a VPNConnectionStatisticsManager class; however, the actual code has not been provided in your message. To proceed in a helpful manner, I'll describe how such a class and methods might be implemented based on your description.

In Objective-C, a class named VPNConnectionStatisticsManager might be declared in a header file (.h) like this:

VPNConnectionStatisticsManager.h

#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>

@interface VPNConnectionStatisticsManager : NSObject

- (SCNetworkConnectionRef)initializeVPNConnectionWithServiceID:(NSString \*)serviceID;

@end

And the implementation in an implementation file (.m) might be as follows:

VPNConnectionStatisticsManager.m

#import "VPNConnectionStatisticsManager.h"

@implementation VPNConnectionStatisticsManager

- (SCNetworkConnectionRef)initializeVPNConnectionWithServiceID:(NSString \*)serviceID {
    // Assume that serviceID is already a valid and properly formatted string.

    CFStringRef serviceIDRef = (\__bridge CFStringRef)(serviceID);

    SCNetworkConnectionContext context = {0, (\__bridge void \*)(self), NULL, NULL, NULL};

    SCNetworkConnectionRef connectionRef = SCNetworkConnectionCreateWithServiceID(NULL, serviceIDRef, ConnectionStatusChanged, \&context);

    // Here, you might want to add additional setup if necessary
    // ...

    return connectionRef;
}

// It is important to define the correct signature for the callback function.
// The actual implementation may log the status change or act upon it.

void ConnectionStatusChanged(SCNetworkConnectionRef connection, SCNetworkConnectionStatus status, void \*info) {
    NSLog(@"VPN Connection Status Changed: %d", status);

    // Additional logic to handle status change can be implemented here
    // ...
}

@end

This basic implementation simply demonstrates how you might create a class method to initialize a VPN connection using the SCNetworkConnectionCreateWithServiceID function, and how to log the connection status changes using a callback function.

Keep in mind that:

  • Error checking is vital but omitted here for brevity.
  • You'll need to manage the lifecycle of the returned SCNetworkConnectionRef (e.g., calling CFRelease when done).
  • Actual logging and handling of the VPN status change should be more robust, depending on the requirements.
  • To use this in a real application, you should adhere to the best practices of memory management, especially if you're not using Automatic Reference Counting (ARC).

If you have the actual code or specific concerns or questions about the implementation, please share that for a more accurate and detailed response.