diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Constants.swift b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Constants.swift
new file mode 100644
index 0000000..58dd76f
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Constants.swift
@@ -0,0 +1,13 @@
+//
+// Constants.swift
+// SelfControl
+//
+// Created by Satendra Singh on 09/10/25.
+//
+
+
+
+enum Constants {
+ /// File name for the JSON file with Safari rules.
+ static let SAFARI_BLOCKER_FILE_NAME = "blockerList.json"
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift
new file mode 100644
index 0000000..b20bd5f
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift
@@ -0,0 +1,92 @@
+//
+// ContentBlockerExtensionRequestHandler.swift
+// SelfControl
+//
+// Created by Satendra Singh on 09/10/25.
+//
+
+import os.log
+import Foundation
+
+public enum ContentBlockerExtensionRequestHandler {
+ /// Handles content blocking extension request for rules.
+ ///
+ /// This method loads the content blocker rules JSON file from the shared container
+ /// and attaches it to the extension context to be used by Safari.
+ ///
+ /// - Parameters:
+ /// - context: The extension context that initiated the request.
+ /// - groupIdentifier: The app group identifier used to access the shared container.
+ public static func handleRequest(with context: NSExtensionContext, groupIdentifier: String) {
+ os_log(.info, "[SC] π] Safari Start loading the content blocker, %{public}@", context.inputItems.description)
+
+ // Get the shared container URL using the provided group identifier
+ guard
+ let appGroupURL = FileManager.default.containerURL(
+ forSecurityApplicationGroupIdentifier: groupIdentifier
+ )
+ else {
+ context.cancelRequest(
+ withError: createError(code: 1001, message: "Failed to access App Group container.")
+ )
+ return
+ }
+
+ // Construct the path to the shared blocker list file
+ let sharedFileURL = appGroupURL.appendingPathComponent(Constants.SAFARI_BLOCKER_FILE_NAME)
+
+ // Determine which blocker list file to use
+ var blockerListFileURL = sharedFileURL
+ if !FileManager.default.fileExists(atPath: sharedFileURL.path) {
+ os_log(.info, "[SC] π] Safari No blocker list file found. Using the default one.")
+
+ // Fall back to the default blocker list included in the bundle
+ guard
+ let defaultURL = Bundle.main.url(forResource: "blockerList", withExtension: "json")
+ else {
+ context.cancelRequest(
+ withError: createError(
+ code: 1002,
+ message: "[SC] π] Safari Failed to find default blocker list."
+ )
+ )
+ return
+ }
+ blockerListFileURL = defaultURL
+ }
+
+ // Create an attachment with the blocker list file
+ guard let attachment = NSItemProvider(contentsOf: blockerListFileURL) else {
+ context.cancelRequest(
+ withError: createError(code: 1003, message: "Failed to create attachment.")
+ )
+ return
+ }
+
+ // Prepare and complete the extension request with the blocker list
+ let item = NSExtensionItem()
+ item.attachments = [attachment]
+// item.attributedTitle = NSAttributedString(string: "Hellow world!")
+// item.attributedContentText = NSAttributedString(string: "Hello Content of the world!")
+
+ context.completeRequest(
+ returningItems: [item]
+ ) { _ in
+ os_log(.info, "[SC] π] Safari Finished loading the content blocker")
+ }
+ }
+
+ /// Creates an NSError with the specified code and message.
+ ///
+ /// - Parameters:
+ /// - code: The error code.
+ /// - message: The error message.
+ /// - Returns: An NSError object with the specified parameters.
+ private static func createError(code: Int, message: String) -> NSError {
+ return NSError(
+ domain: "extension request handler",
+ code: code,
+ userInfo: [NSLocalizedDescriptionKey: message]
+ )
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift
new file mode 100644
index 0000000..e2fb7c4
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift
@@ -0,0 +1,78 @@
+//
+// ContentBlockerRequestHandler.swift
+// SelfControl Safari Extension
+//
+// Created by Satendra Singh on 07/10/25.
+//
+
+import Foundation
+import UniformTypeIdentifiers
+import os.log
+
+class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling {
+ // Must match the App Group used by the host app
+ private let appGroup = "group.com.application.SelfControl.corebits"
+ private let sharedFileName = "blockerList.json"
+
+ func beginRequest(with context: NSExtensionContext) {
+ ContentBlockerExtensionRequestHandler.handleRequest(with: context, groupIdentifier: appGroup)
+ return
+ let item = NSExtensionItem()
+
+ // Try to load rules from the shared App Group container first
+ if let sharedFileURL = sharedRulesFileURL(), FileManager.default.fileExists(atPath: sharedFileURL.path) {
+ let provider = NSItemProvider(contentsOf: sharedFileURL)!
+ item.attachments = [provider]
+ context.completeRequest(returningItems: [item], completionHandler: nil)
+ return
+ }
+
+ // Fallback: load bundled blockerList.json from the extension resources
+ if let bundledURL = Bundle.main.url(forResource: "blockerList", withExtension: "json") {
+ let provider = NSItemProvider(contentsOf: bundledURL)!
+ item.attachments = [provider]
+ context.completeRequest(returningItems: [item], completionHandler: nil)
+ return
+ }
+
+ // If neither exists, return an empty rules array to avoid errors
+// let emptyRulesData = Data("[]".utf8)
+// let tmpURL = writeTempData(emptyRulesData, suggestedName: sharedFileName)
+// let provider = NSItemProvider(contentsOf: tmpURL)!
+// item.attachments = [provider]
+// context.completeRequest(returningItems: [item], completionHandler: nil)
+ }
+
+ // MARK: - Helpers
+
+ private func sharedRulesFileURL() -> URL? {
+ let fileManager = FileManager.default
+ guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
+ NSLog("β App Group container not found for %@", appGroup)
+ os_log("[SC] π] Safari β App Group container not found for: %{public}@", appGroup)
+ return nil
+ }
+ return containerURL.appendingPathComponent(sharedFileName)
+ }
+
+ private func writeTempData(_ data: Data, suggestedName: String) -> URL {
+ let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
+ let url = tmpDir.appendingPathComponent(suggestedName)
+ do {
+ try data.write(to: url, options: .atomic)
+ } catch {
+ NSLog("β Failed to write temporary rules file: \(error.localizedDescription)")
+ os_log("[SC] π] Safari β Failed to write temporary rules file: %{public}@", error.localizedDescription)
+
+ }
+ return url
+ }
+
+
+ func cancelRequest(withError error: any Error) {
+
+ os_log("[SC] π] Safari β cancelRequeste: %{public}@", error.localizedDescription)
+
+ }
+
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Info.plist b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Info.plist
new file mode 100644
index 0000000..0d1cc76
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Info.plist
@@ -0,0 +1,15 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.Safari.content-blocker
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).ContentBlockerRequestHandler
+
+ NSHumanReadableDescription
+ Add a description of what your extension does here.
+
+
diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/SelfControl Safari Extension.entitlements b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/SelfControl Safari Extension.entitlements
new file mode 100644
index 0000000..ab2dad3
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/SelfControl Safari Extension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.application.SelfControl.corebits
+
+
+
diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/block.html b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/block.html
new file mode 100644
index 0000000..77a1572
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/block.html
@@ -0,0 +1,33 @@
+
+
+
+
+Site Blocked
+
+
+
+
+ π« Site Blocked
+ This website has been blocked by your Safari content blocker.
+ You can change this in Safari βΈ Extensions if you want to allow it.
+ Open Safari Extensions
+
+
diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/blockerList.json b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/blockerList.json
new file mode 100644
index 0000000..ea0ed75
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/blockerList.json
@@ -0,0 +1,60 @@
+[
+ {
+ "action": {
+ "type": "block"
+ },
+ "trigger": {
+ "url-filter": "facebook.com/friends"
+ }
+ },
+ {
+ "action": {
+ "type": "block"
+ },
+ "trigger": {
+ "url-filter": "facebook.com/marketplace"
+ }
+ },
+ {
+ "action": {
+ "type": "block"
+ },
+ "trigger": {
+ "url-filter": "facebook.com/watch"
+ }
+ }
+]
+//[
+// {
+// "trigger" : {
+// "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/friends.*"
+// },
+// "action" : {
+// "type" : "block"
+// }
+// },
+// {
+// "trigger" : {
+// "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/mynetwork.*"
+// },
+// "action" : {
+// "type" : "block"
+// }
+// },
+// {
+// "trigger" : {
+// "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/jobs.*"
+// },
+// "action" : {
+// "type" : "block"
+// }
+// },
+// {
+// "trigger" : {
+// "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/marketplace.*"
+// },
+// "action" : {
+// "type" : "block"
+// }
+// }
+//]
diff --git a/Self-Control-Extension/SelfControl/SelfControl-Development-Setup.md b/Self-Control-Extension/SelfControl/SelfControl-Development-Setup.md
new file mode 100644
index 0000000..4d3e988
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl-Development-Setup.md
@@ -0,0 +1,112 @@
+# SelfControl Development Setup Guide
+
+This document outlines the exact setup needed for developing the SelfControl app that uses NetworkExtension for filtering network traffic.
+
+## Prerequisites
+
+- Xcode 16.0 or later
+- macOS (15.0) or later
+- Apple Developer account with Network Extension entitlements
+- Administrator privileges on your Mac
+
+## Development Setup Steps
+
+### 1. Disable System Integrity Protection (SIP)
+
+For NetworkExtension development, SIP needs to be disabled to allow proper installation and uninstallation of system extensions during development:
+
+1. Boot into Recovery Mode (restart holding Cmd+R)
+2. Open Terminal from Utilities menu
+3. Run: `csrutil disable`
+4. Restart your Mac
+
+> β οΈ **IMPORTANT**: Disabling SIP reduces your system's security. Only do this on a development machine, not on your primary production machine.
+
+### 2. Configure Post-Build Actions
+
+To ensure fresh installs of the system extension during development, add two critical post-build actions to your Xcode project:
+
+1. Open your project in Xcode
+2. Select your main app target
+3. Go to "Build Phases"
+4. Add a new "Run Script" phase (click the + button)
+5. Add the following script:
+
+```bash
+# Uninstall the previous system extension first
+systemextensionsctl uninstall A4L93BSQEG com.application.SelfControl.SelfControlExtension
+
+# Copy the app to /Applications
+ditto "${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}" "/Applications/${FULL_PRODUCT_NAME}"
+```
+
+> Note: Replace `A4L93BSQEG` with your actual Team ID and `com.application.SelfControl.SelfControlExtension` with your actual extension bundle ID if different.
+
+### 3. Bundle Identifier Setup
+
+Ensure your bundle identifiers are set up correctly:
+
+- Main App: `com.application.SelfControl`
+- Extension: `com.application.SelfControl.SelfControlExtension`
+
+### 4. Configure Info.plist for Network Extension
+
+In your Network Extension's Info.plist, ensure proper configuration:
+
+```xml
+NetworkExtension
+
+ NEMachServiceName
+ $(TeamIdentifierPrefix)com.application.SelfControl.SelfControlExtension
+ NEProviderClasses
+
+ com.apple.networkextension.filter-data
+ $(PRODUCT_MODULE_NAME).FilterDataProvider
+
+
+```
+
+### 5. Development Workflow
+
+With this setup, your development workflow becomes:
+
+1. Make code changes in Xcode
+2. Build the project (βB)
+ - This will automatically uninstall the previous extension
+ - Then copy the new build to /Applications
+3. Launch the app from /Applications (not from Xcode)
+4. Check Console.app for logs with the prefix `[EADBUG]`
+
+### 6. Troubleshooting
+
+If you encounter issues:
+
+- Verify the app is properly copied to `/Applications`
+- Check that the system extension uninstall command is working correctly
+- Inspect Console.app for any error messages
+- Verify your Team ID is correct in the post-build script
+- Ensure bundle identifiers match between the app, extension, and post-build script
+
+### 7. Re-enabling SIP After Development
+
+When you're done with development and ready to deploy:
+
+1. Boot into Recovery Mode
+2. Run: `csrutil enable`
+3. Restart your Mac
+
+## Known Limitations
+
+- Each build requires a fresh installation due to NetworkExtension constraints
+- The app must run from /Applications to function properly
+- With SIP disabled, your development machine has reduced security protections
+- NetworkExtension development generally requires more manual steps than typical app development
+
+## Conclusion
+
+This setup automates the most tedious parts of NetworkExtension development by:
+1. Ensuring old extensions are uninstalled before new ones are installed
+2. Automatically placing the app in the required location (/Applications)
+3. Allowing for rapid iteration despite NetworkExtension's constraints
+
+By following these specific steps, you should be able to develop your NetworkExtension-based app more efficiently.
\ No newline at end of file
diff --git a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..2882203
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj
@@ -0,0 +1,754 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 77BA53092D9D5E2A00863476 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77BA53082D9D5E2A00863476 /* NetworkExtension.framework */; };
+ 9A441CE32E36024E00A521CC /* com.application.SelfControl.corebits.network.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 9A7FFB602E950837007D4A0D /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A7FFA0B2E91950D007D4A0D /* Cocoa.framework */; };
+ 9A7FFB692E950837007D4A0D /* SelfControl Safari Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9A7FFB5F2E950837007D4A0D /* SelfControl Safari Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 9A441CE12E36024300A521CC /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 77BA52CE2D9D5D7700863476 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 77BA53052D9D5E2A00863476;
+ remoteInfo = SelfControlExtension;
+ };
+ 9A7FFB672E950837007D4A0D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 77BA52CE2D9D5D7700863476 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 9A7FFB5E2E950837007D4A0D;
+ remoteInfo = "SelfControl Safari Extension";
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9A7FFA162E91950D007D4A0D /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ 9A7FFB692E950837007D4A0D /* SelfControl Safari Extension.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 9AFC129E2E355671003B5455 /* Embed System Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "$(SYSTEM_EXTENSIONS_FOLDER_PATH)";
+ dstSubfolderSpec = 16;
+ files = (
+ 9A441CE32E36024E00A521CC /* com.application.SelfControl.corebits.network.systemextension in Embed System Extensions */,
+ );
+ name = "Embed System Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 77BA52D62D9D5D7700863476 /* SelfControl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SelfControl.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */ = {isa = PBXFileReference; explicitFileType = "wrapper.system-extension"; includeInIndex = 0; path = com.application.SelfControl.corebits.network.systemextension; sourceTree = BUILT_PRODUCTS_DIR; };
+ 77BA53082D9D5E2A00863476 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
+ 9A7FFA0B2E91950D007D4A0D /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
+ 9A7FFB5F2E950837007D4A0D /* SelfControl Safari Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SelfControl Safari Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ 77BA53142D9D5E2A00863476 /* Exceptions for "SelfControlExtension" folder in "SelfControlExtension" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ DNSProxyProvider.swift,
+ DNSQuestion.swift,
+ Info.plist,
+ );
+ target = 77BA53052D9D5E2A00863476 /* SelfControlExtension */;
+ };
+ 9A6873742E280E320054AD3F /* Exceptions for "SelfControl" folder in "SelfControlExtension" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Preferences/ProxyPreferences.swift,
+ );
+ target = 77BA53052D9D5E2A00863476 /* SelfControlExtension */;
+ };
+ 9A6873762E2814B30054AD3F /* Exceptions for "SelfControlExtension" folder in "SelfControl" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ BlockOrAllowList.swift,
+ IPCConnection.swift,
+ PlistListner.swift,
+ "String+URL.swift",
+ );
+ target = 77BA52D52D9D5D7700863476 /* SelfControl */;
+ };
+ 9A7FFB6D2E950837007D4A0D /* Exceptions for "SelfControl Safari Extension" folder in "SelfControl Safari Extension" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ 77BA52D82D9D5D7700863476 /* SelfControl */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ 9A6873742E280E320054AD3F /* Exceptions for "SelfControl" folder in "SelfControlExtension" target */,
+ );
+ path = SelfControl;
+ sourceTree = "";
+ };
+ 77BA530A2D9D5E2A00863476 /* SelfControlExtension */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ 9A6873762E2814B30054AD3F /* Exceptions for "SelfControlExtension" folder in "SelfControl" target */,
+ 77BA53142D9D5E2A00863476 /* Exceptions for "SelfControlExtension" folder in "SelfControlExtension" target */,
+ );
+ path = SelfControlExtension;
+ sourceTree = "";
+ };
+ 9A7FFB612E950837007D4A0D /* SelfControl Safari Extension */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ 9A7FFB6D2E950837007D4A0D /* Exceptions for "SelfControl Safari Extension" folder in "SelfControl Safari Extension" target */,
+ );
+ path = "SelfControl Safari Extension";
+ sourceTree = "";
+ };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 77BA52D32D9D5D7700863476 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 77BA53032D9D5E2A00863476 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 77BA53092D9D5E2A00863476 /* NetworkExtension.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 9A7FFB5C2E950837007D4A0D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9A7FFB602E950837007D4A0D /* Cocoa.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 77BA52CD2D9D5D7700863476 = {
+ isa = PBXGroup;
+ children = (
+ 77BA52D82D9D5D7700863476 /* SelfControl */,
+ 77BA530A2D9D5E2A00863476 /* SelfControlExtension */,
+ 9A7FFB612E950837007D4A0D /* SelfControl Safari Extension */,
+ 77BA53072D9D5E2A00863476 /* Frameworks */,
+ 77BA52D72D9D5D7700863476 /* Products */,
+ );
+ sourceTree = "";
+ };
+ 77BA52D72D9D5D7700863476 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 77BA52D62D9D5D7700863476 /* SelfControl.app */,
+ 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */,
+ 9A7FFB5F2E950837007D4A0D /* SelfControl Safari Extension.appex */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 77BA53072D9D5E2A00863476 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 77BA53082D9D5E2A00863476 /* NetworkExtension.framework */,
+ 9A7FFA0B2E91950D007D4A0D /* Cocoa.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 77BA52D52D9D5D7700863476 /* SelfControl */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 77BA52E52D9D5D7A00863476 /* Build configuration list for PBXNativeTarget "SelfControl" */;
+ buildPhases = (
+ 77BA52D22D9D5D7700863476 /* Sources */,
+ 77BA52D32D9D5D7700863476 /* Frameworks */,
+ 77BA52D42D9D5D7700863476 /* Resources */,
+ 9AFC129E2E355671003B5455 /* Embed System Extensions */,
+ 9A7FFA162E91950D007D4A0D /* Embed Foundation Extensions */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 9A441CE22E36024300A521CC /* PBXTargetDependency */,
+ 9A7FFB682E950837007D4A0D /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ 77BA52D82D9D5D7700863476 /* SelfControl */,
+ );
+ name = SelfControl;
+ packageProductDependencies = (
+ );
+ productName = SelfControl;
+ productReference = 77BA52D62D9D5D7700863476 /* SelfControl.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 77BA53052D9D5E2A00863476 /* SelfControlExtension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 77BA53152D9D5E2A00863476 /* Build configuration list for PBXNativeTarget "SelfControlExtension" */;
+ buildPhases = (
+ 77BA53022D9D5E2A00863476 /* Sources */,
+ 77BA53032D9D5E2A00863476 /* Frameworks */,
+ 77BA53042D9D5E2A00863476 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ 77BA530A2D9D5E2A00863476 /* SelfControlExtension */,
+ );
+ name = SelfControlExtension;
+ packageProductDependencies = (
+ );
+ productName = SelfControlExtension;
+ productReference = 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */;
+ productType = "com.apple.product-type.system-extension";
+ };
+ 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 9A7FFB6A2E950837007D4A0D /* Build configuration list for PBXNativeTarget "SelfControl Safari Extension" */;
+ buildPhases = (
+ 9A7FFB5B2E950837007D4A0D /* Sources */,
+ 9A7FFB5C2E950837007D4A0D /* Frameworks */,
+ 9A7FFB5D2E950837007D4A0D /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ 9A7FFB612E950837007D4A0D /* SelfControl Safari Extension */,
+ );
+ name = "SelfControl Safari Extension";
+ packageProductDependencies = (
+ );
+ productName = "SelfControl Safari Extension";
+ productReference = 9A7FFB5F2E950837007D4A0D /* SelfControl Safari Extension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 77BA52CE2D9D5D7700863476 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 2600;
+ LastUpgradeCheck = 1620;
+ TargetAttributes = {
+ 77BA52D52D9D5D7700863476 = {
+ CreatedOnToolsVersion = 16.2;
+ };
+ 77BA53052D9D5E2A00863476 = {
+ CreatedOnToolsVersion = 16.2;
+ };
+ 9A7FFB5E2E950837007D4A0D = {
+ CreatedOnToolsVersion = 26.0;
+ };
+ };
+ };
+ buildConfigurationList = 77BA52D12D9D5D7700863476 /* Build configuration list for PBXProject "SelfControl" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 77BA52CD2D9D5D7700863476;
+ minimizedProjectReferenceProxies = 1;
+ preferredProjectObjectVersion = 77;
+ productRefGroup = 77BA52D72D9D5D7700863476 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 77BA52D52D9D5D7700863476 /* SelfControl */,
+ 77BA53052D9D5E2A00863476 /* SelfControlExtension */,
+ 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 77BA52D42D9D5D7700863476 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 77BA53042D9D5E2A00863476 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 9A7FFB5D2E950837007D4A0D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 77BA52D22D9D5D7700863476 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 77BA53022D9D5E2A00863476 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 9A7FFB5B2E950837007D4A0D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 9A441CE22E36024300A521CC /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 77BA53052D9D5E2A00863476 /* SelfControlExtension */;
+ targetProxy = 9A441CE12E36024300A521CC /* PBXContainerItemProxy */;
+ };
+ 9A7FFB682E950837007D4A0D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */;
+ targetProxy = 9A7FFB672E950837007D4A0D /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 77BA52E32D9D5D7A00863476 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.1;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 77BA52E42D9D5D7A00863476 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.1;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ };
+ name = Release;
+ };
+ 77BA52E62D9D5D7A00863476 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = SelfControl/SelfControl.entitlements;
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
+ CODE_SIGN_STYLE = Manual;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"SelfControl/Preview Content\"";
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK;
+ ENABLE_APP_SANDBOX = NO;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = SelfControl/Info.plist;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 13.5;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Self Control app dev";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 77BA52E72D9D5D7A00863476 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = SelfControl/SelfControl.entitlements;
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
+ CODE_SIGN_STYLE = Manual;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"SelfControl/Preview Content\"";
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK;
+ ENABLE_APP_SANDBOX = NO;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = SelfControl/Info.plist;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 13.5;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Self Control app dev";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 77BA53162D9D5E2A00863476 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = SelfControlExtension/SelfControlExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Developer ID Application";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = X6FQ433AWK;
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK;
+ ENABLE_APP_SANDBOX = NO;
+ ENABLE_HARDENED_RUNTIME = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = SelfControlExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = SelfControlExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "DNS lookup";
+ INFOPLIST_KEY_NSSystemExtensionUsageDescription = "testing extension";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 13.5;
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = "-lbsm";
+ PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits.network;
+ PRODUCT_NAME = "$(inherited)";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Self Control Dev ext";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 77BA53172D9D5E2A00863476 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = SelfControlExtension/SelfControlExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Developer ID Application";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = X6FQ433AWK;
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK;
+ ENABLE_APP_SANDBOX = NO;
+ ENABLE_HARDENED_RUNTIME = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = SelfControlExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = SelfControlExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "DNS lookup";
+ INFOPLIST_KEY_NSSystemExtensionUsageDescription = "testing extension";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 13.5;
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = "-lbsm";
+ PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits.network;
+ PRODUCT_NAME = "$(inherited)";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Self Control Dev ext";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 9A7FFB6B2E950837007D4A0D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = "SelfControl Safari Extension/SelfControl Safari Extension.entitlements";
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = X6FQ433AWK;
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK;
+ ENABLE_APP_SANDBOX = YES;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
+ ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
+ ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
+ ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
+ ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
+ ENABLE_RESOURCE_ACCESS_CAMERA = NO;
+ ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
+ ENABLE_RESOURCE_ACCESS_LOCATION = NO;
+ ENABLE_RESOURCE_ACCESS_PRINTING = NO;
+ ENABLE_RESOURCE_ACCESS_USB = NO;
+ ENABLE_USER_SELECTED_FILES = readonly;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "SelfControl Safari Extension/Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = "SelfControl Safari Extension";
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 13.5;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.application.SelfControl.corebits.SelfControl-Safari-Extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "SelfControl Dev Net Ext";
+ REGISTER_APP_GROUPS = YES;
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 9A7FFB6C2E950837007D4A0D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = "SelfControl Safari Extension/SelfControl Safari Extension.entitlements";
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = X6FQ433AWK;
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK;
+ ENABLE_APP_SANDBOX = YES;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
+ ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
+ ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
+ ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
+ ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
+ ENABLE_RESOURCE_ACCESS_CAMERA = NO;
+ ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
+ ENABLE_RESOURCE_ACCESS_LOCATION = NO;
+ ENABLE_RESOURCE_ACCESS_PRINTING = NO;
+ ENABLE_RESOURCE_ACCESS_USB = NO;
+ ENABLE_USER_SELECTED_FILES = readonly;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "SelfControl Safari Extension/Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = "SelfControl Safari Extension";
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 13.5;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.application.SelfControl.corebits.SelfControl-Safari-Extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "SelfControl Dev Net Ext";
+ REGISTER_APP_GROUPS = YES;
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 77BA52D12D9D5D7700863476 /* Build configuration list for PBXProject "SelfControl" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 77BA52E32D9D5D7A00863476 /* Debug */,
+ 77BA52E42D9D5D7A00863476 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 77BA52E52D9D5D7A00863476 /* Build configuration list for PBXNativeTarget "SelfControl" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 77BA52E62D9D5D7A00863476 /* Debug */,
+ 77BA52E72D9D5D7A00863476 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 77BA53152D9D5E2A00863476 /* Build configuration list for PBXNativeTarget "SelfControlExtension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 77BA53162D9D5E2A00863476 /* Debug */,
+ 77BA53172D9D5E2A00863476 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 9A7FFB6A2E950837007D4A0D /* Build configuration list for PBXNativeTarget "SelfControl Safari Extension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 9A7FFB6B2E950837007D4A0D /* Debug */,
+ 9A7FFB6C2E950837007D4A0D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 77BA52CE2D9D5D7700863476 /* Project object */;
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControl.xcscheme b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControl.xcscheme
new file mode 100644
index 0000000..3780773
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControl.xcscheme
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControlExtension.xcscheme b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControlExtension.xcscheme
new file mode 100644
index 0000000..97fcc93
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControlExtension.xcscheme
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AccentColor.colorset/Contents.json b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AppIcon.appiconset/Contents.json b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..3f00db4
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,58 @@
+{
+ "images" : [
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "512x512"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/Contents.json b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Info.plist b/Self-Control-Extension/SelfControl/SelfControl/Info.plist
new file mode 100644
index 0000000..b0aaa9d
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ Copyright Β© 2025 SelfControl. All rights reserved.
+ NSLocalNetworkUsageDescription
+ DNS lookup
+ NSMainStoryboardFile
+ Main
+ NSPrincipalClass
+ NSApplication
+ NSSupportsAutomaticTermination
+
+ NSSupportsSuddenTermination
+
+
+
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift
new file mode 100644
index 0000000..a8912aa
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift
@@ -0,0 +1,94 @@
+//
+// ContentView.swift
+// SelfControl
+//
+// Created by Egzon Arifi on 02/04/2025.
+//
+
+import SwiftUI
+import NetworkExtension
+import SystemExtensions
+import os.log
+import Cocoa
+
+struct ContentView: View {
+ @EnvironmentObject var viewModel: FilterViewModel
+ @Environment(\.openWindow) private var openWindow
+ @State private var newDomain = ""
+
+ var body: some View {
+ VStack(spacing: 20) {
+ // Status indicator with image and text.
+ Button {
+ viewModel.activateExtension()
+ } label: {
+ Text("Install and Start Block")
+ }
+ HStack {
+ Spacer()
+ Slider(value: $viewModel.delay, in: 1...60) {
+ Text("Time: \(viewModel.delay, specifier: "%.1f") Minutes")
+ }
+ Spacer()
+ }
+ statusView
+ // Show a progress indicator when in the indeterminate state.
+ if viewModel.status == .indeterminate {
+ ProgressView()
+ .progressViewStyle(CircularProgressViewStyle())
+ }
+
+ // Start/Stop buttons.
+ HStack {
+ if viewModel.status == .stopped {
+ Button("Start") {
+ viewModel.startFilter()
+
+ }
+ }
+ if viewModel.status == .running {
+ Button("Stop") {
+ viewModel.stopFilter()
+ }
+ }
+ }
+ HStack {
+ TextField("Enter Url to test block", text: $newDomain)
+ Button("Test Url Blocking") {
+ viewModel.checkUrlRequest(url: newDomain)
+ }
+ }
+// Button {
+// viewModel.setBlockedUrls(urls: ProxyPreferences.getBlockedDomains())
+// } label: {
+// Text("Enable Url Blocking")
+// }
+ SafariExtensionView()
+ }
+ .padding()
+ .frame(minWidth: 150, minHeight: 150)
+ .onDisappear {
+// let urls = ProxyPreferences.getBlockedDomains()
+
+ }
+ }
+}
+
+private extension ContentView {
+ var statusView: some View {
+ HStack {
+ viewModel.status.color
+ .clipShape(Circle())
+ .frame(width: 20, height: 20)
+ Text("Status: \(viewModel.status.text)")
+ Button("Edit Blocklist") {
+ openWindow(id: "preferences")
+
+ }
+ }
+ }
+}
+
+#Preview {
+ ContentView()
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/DNSResolver.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/DNSResolver.swift
new file mode 100644
index 0000000..be301c4
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Main/DNSResolver.swift
@@ -0,0 +1,125 @@
+//
+// DNSResolver.swift
+// SelfControl
+//
+// Created by Satendra Singh on 21/10/25.
+//
+
+
+import Foundation
+import Network
+
+actor DNSResolverActor {
+ var resolvedIPs: Set = []
+ func resolve(hostURL: [String]) async -> Set {
+ var intendedHosts: Set = []
+ for url in hostURL {
+ print("Resolving: \(url)")
+ if let host = DNSResolver.getHost(from: url) {
+ intendedHosts.insert(host)
+ }
+ }
+
+ resolvedIPs = []
+ for host in intendedHosts {
+ let ips = await DNSResolver.resolve(hostname: host)
+ print("host: \(host), ips: \(ips)")
+ resolvedIPs.formUnion(ips)
+ }
+ return resolvedIPs
+ }
+}
+
+/// A modern async DNS resolver using Network.framework
+final class DNSResolver {
+ static func getHost(from input: String) -> String? {
+ var string = input
+ if !string.contains("://") {
+ string = "https://" + string
+ }
+ return URL(string: string)?.host
+ }
+
+ static func resolve(hostURL: String, timeout: TimeInterval = 5.0) async -> [String] {
+ guard let host = getHost(from: hostURL) else { return [] }
+ return await resolve(hostname: host, timeout: timeout)
+ }
+ /// Resolves IPv4/IPv6 addresses for a given domain asynchronously.
+ /// - Parameters:
+ /// - hostname: The domain name (e.g. "facebook.com").
+ /// - timeout: Optional timeout in seconds (default 5s).
+ /// - Returns: Array of resolved IP addresses as strings.
+ static func resolve(hostname: String, timeout: TimeInterval = 5.0) async -> [String] {
+ await withCheckedContinuation { continuation in
+ let params = NWParameters.tcp
+ params.allowLocalEndpointReuse = true
+
+ // We use the resolver service to resolve DNS asynchronously
+ let endpoint = NWEndpoint.hostPort(host: .name(hostname, nil), port: 80)
+
+ let resolver = NWConnection(to: endpoint, using: params)
+
+ var didResume = false
+
+ @Sendable func finish(_ ips: [String]) {
+ guard !didResume else { return }
+ didResume = true
+ continuation.resume(returning: ips)
+ resolver.cancel()
+ }
+
+ resolver.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ // Once ready, we can extract resolved IPs from the endpoint
+ if let remote = resolver.currentPath?.remoteEndpoint,
+ case let .hostPort(host, _) = remote {
+ switch host {
+ case .ipv4(let addr):
+ finish([ipv4ToString(addr)])
+ case .ipv6(let addr):
+ finish([ipv6ToString(addr)])
+ default:
+ finish([])
+ }
+ } else {
+ finish([])
+ }
+
+ case .failed(_):
+ finish([])
+ default:
+ break
+ }
+ }
+
+ // Start resolution
+ resolver.start(queue: .global(qos: .userInitiated))
+
+ // Timeout handler
+ DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { [weak resolver] in
+ // If still not finished, cancel and return empty
+ resolver?.cancel()
+ finish([])
+ }
+ }
+ }
+
+ /// Converts IPv4 address to string.
+ nonisolated private static func ipv4ToString(_ addr: IPv4Address) -> String {
+ let bytes = [UInt8](addr.rawValue)
+ guard bytes.count == 4 else { return "" }
+ return bytes.map(String.init).joined(separator: ".")
+ }
+
+ /// Converts IPv6 address to string.
+ nonisolated private static func ipv6ToString(_ addr: IPv6Address) -> String {
+ let bytes = [UInt8](addr.rawValue)
+ guard bytes.count == 16 else { return "" }
+ let segments = stride(from: 0, to: bytes.count, by: 2).map {
+ (UInt16(bytes[$0]) << 8) | UInt16(bytes[$0 + 1])
+ }
+ // Basic hex formatting; does not perform zero-compression (::)
+ return segments.map { String(format: "%x", $0) }.joined(separator: ":")
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift
new file mode 100644
index 0000000..a2d8051
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift
@@ -0,0 +1,336 @@
+//
+// FilterViewModel.swift
+// SelfControl
+//
+// Created by Egzon Arifi on 02/04/2025.
+//
+
+import SwiftUI
+import NetworkExtension
+import SystemExtensions
+import os.log
+import Cocoa
+
+final class FilterViewModel: NSObject, ObservableObject, OSSystemExtensionRequestDelegate, AppCommunication {
+ @Published var status: Status = .stopped
+ @State private var domains = ProxyPreferences.getBlockedDomains()
+ private let listner = PlistListner()
+ @Published var delay: Double = 0.0
+ var blockedIPAddressed: [String] = []
+
+ // Date formatter used to log entries
+ lazy var dateFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
+ return formatter
+ }()
+
+ // Observer for filter configuration changes
+ var observer: Any?
+ var extensionIdentifier: String?
+
+ // Load the system extension bundle from the appβs Contents/Library/SystemExtensions folder.
+ lazy var extensionBundle: Bundle = {
+ let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Library/SystemExtensions", relativeTo: Bundle.main.bundleURL)
+ let extensionURLs: [URL]
+ do {
+ extensionURLs = try FileManager.default.contentsOfDirectory(at: extensionsDirectoryURL,
+ includingPropertiesForKeys: nil,
+ options: .skipsHiddenFiles)
+ } catch let error {
+ fatalError("Failed to get the contents of \(extensionsDirectoryURL.absoluteString): \(error.localizedDescription)")
+ }
+ guard let extensionURL = extensionURLs.first else {
+ fatalError("Failed to find any system extensions")
+ }
+ guard let extensionBundle = Bundle(url: extensionURL) else {
+ fatalError("Failed to create a bundle with URL \(extensionURL.absoluteString)")
+ }
+ return extensionBundle
+ }()
+
+ override init() {
+ super.init()
+ onInit()
+ self.extensionIdentifier = extensionBundle.bundleIdentifier
+ self.listner.blockeddomainFetcher = {
+ return ProxyPreferences.getBlockedDomains()
+ }
+ self.listner.startListening()
+ }
+
+ deinit {
+ if let observer = observer {
+ NotificationCenter.default.removeObserver(observer, name: .NEFilterConfigurationDidChange, object: NEFilterManager.shared())
+ }
+ }
+
+ func onInit() {
+ // On initialization load the filter configuration and register for changes.
+ loadFilterConfiguration { success in
+ guard success else {
+ self.status = .stopped
+ return
+ }
+ self.updateStatus()
+ self.observer = NotificationCenter.default.addObserver(forName: .NEFilterConfigurationDidChange,
+ object: NEFilterManager.shared(),
+ queue: .main) { [weak self] _ in
+ self?.updateStatus()
+ }
+ }
+ }
+
+ // MARK: - UI and Filter Management
+
+ func setBlockedUrls(urls: [String]) {
+ IPCConnection.shared.enableURLBlocking(urls)
+ Task {
+ let ips: Set = await DNSResolverActor().resolve(hostURL: urls)
+ print("Resolved app:\(ips)")
+ setIPAddressesToBlock(addresses: Array(ips))
+ }
+ }
+
+ func setIPAddressesToBlock(addresses: [String]) {
+ IPCConnection.shared.enableIPAddressesBlocking(addresses)
+ }
+
+ private func refreshBlockedIPs() {
+ self.listner.blockeddomainFetcher = {
+ return ProxyPreferences.getBlockedDomains()
+ }
+ }
+
+ func updateStatus() {
+ if NEFilterManager.shared().isEnabled {
+ registerWithProvider()
+ } else {
+ status = .stopped
+ }
+ }
+
+ func logFlow(_ flowInfo: [String: String], at date: Date, userAllowed: Bool) {
+ guard let localPort = flowInfo[FlowInfoKey.localPort.rawValue],
+ let remoteAddress = flowInfo[FlowInfoKey.remoteAddress.rawValue] else {
+ return
+ }
+ let dateString = dateFormatter.string(from: date)
+ let message = "\(dateString) \(userAllowed ? "ALLOW" : "DENY") \(localPort) <-- \(remoteAddress)\n"
+ os_log("[SC] π] %@", message)
+ }
+
+ func loadFilterConfiguration(completionHandler: @escaping (Bool) -> Void) {
+ NEFilterManager.shared().loadFromPreferences { loadError in
+ DispatchQueue.main.async {
+ var success = true
+ if let error = loadError {
+ os_log("[SC] π] Failed to load the filter configuration: %@", error.localizedDescription)
+ success = false
+ }
+ completionHandler(success)
+ }
+ }
+ }
+
+ func enableFilterConfiguration() {
+ let filterManager = NEFilterManager.shared()
+ guard !filterManager.isEnabled else {
+ registerWithProvider()
+ return
+ }
+ loadFilterConfiguration { success in
+ guard success else {
+ self.status = .stopped
+ return
+ }
+ if filterManager.providerConfiguration == nil {
+ let providerConfiguration = NEFilterProviderConfiguration()
+ providerConfiguration.filterSockets = true
+ providerConfiguration.filterPackets = false
+ filterManager.providerConfiguration = providerConfiguration
+ if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String {
+ filterManager.localizedDescription = appName
+ }
+ }
+ filterManager.isEnabled = true
+ filterManager.saveToPreferences { saveError in
+ DispatchQueue.main.async {
+ if let error = saveError {
+ os_log("[SC] π] Failed to save the filter configuration: %@", error.localizedDescription)
+ self.status = .stopped
+ return
+ } else {
+// self.enableDNSProxy()
+ }
+ self.registerWithProvider()
+ }
+ }
+ }
+ }
+
+ func enableDNSProxy() {
+ let manager = NEDNSProxyManager.shared()
+
+ manager.loadFromPreferences { error in
+ guard error == nil else { return }
+
+ let proto = NEDNSProxyProviderProtocol()
+ proto.providerBundleIdentifier = "com.application.SelfControl.corebits.network"
+ proto.serverAddress = "127.0.0.1" // placeholder
+// proto.filterSockets = true
+ manager.localizedDescription = "DNS Logger"
+ manager.providerProtocol = proto
+ manager.isEnabled = true
+
+ manager.saveToPreferences { saveError in
+ if let saveError = saveError {
+ print("Failed to save: \(saveError)")
+ } else {
+ print("DNS proxy saved.")
+ }
+ }
+ }
+ }
+
+ func registerWithProvider() {
+ // Assuming an IPCConnection singleton similar to the AppKit sample
+ IPCConnection.shared.register(withExtension: extensionBundle, delegate: self) { success in
+ DispatchQueue.main.async {
+ self.status = success ? .running : .stopped
+ self.setBlockedUrls(urls: ProxyPreferences.getBlockedDomains())
+ }
+// setBlockedURLs([])
+ }
+ }
+
+ func activateExtension() {
+ // Start by activating the system extension.
+ guard let extensionIdentifier = extensionIdentifier else {
+ self.status = .stopped
+ return
+ }
+// let request = OSSystemExtensionRequest.propertiesRequest(forExtensionWithIdentifier: extensionIdentifier, queue: .main)
+// request.delegate = self
+// OSSystemExtensionManager.shared.submitRequest(request)
+ let activationRequest = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: extensionIdentifier, queue: .main)
+ activationRequest.delegate = self
+ OSSystemExtensionManager.shared.submitRequest(activationRequest)
+ }
+ // MARK: - UI Event Handlers.
+
+ func startFilter() {
+ status = .indeterminate
+ guard !NEFilterManager.shared().isEnabled else {
+ registerWithProvider()
+ return
+ }
+// guard let extensionIdentifier = extensionBundle.bundleIdentifier else {
+// status = .stopped
+// return
+// }
+ activateExtension()
+ }
+
+ func checkUrlRequest(url: String) {
+ URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in
+ print("Response: \(String(describing: response))")
+ print("Data: \(String(describing: data))")
+ print("Error: \(String(describing: error))")
+ }.resume()
+ }
+
+ func stopFilter() {
+ let filterManager = NEFilterManager.shared()
+ status = .indeterminate
+ guard filterManager.isEnabled else {
+ status = .stopped
+ return
+ }
+ loadFilterConfiguration { success in
+ guard success else {
+ self.status = .running
+ return
+ }
+ // Disable the content filter configuration.
+ filterManager.isEnabled = false
+ filterManager.saveToPreferences { saveError in
+ DispatchQueue.main.async {
+ if let error = saveError {
+ os_log("[SC] π] Failed to disable the filter configuration: %@", error.localizedDescription)
+ self.status = .running
+ return
+ }
+ self.status = .stopped
+ }
+ }
+ }
+ }
+ // MARK: - OSSystemExtensionRequestDelegate Methods
+
+ func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {
+ guard result == .completed else {
+ os_log("[SC] π] Unexpected result %d for system extension request", result.rawValue)
+ status = .stopped
+ return
+ }
+ enableFilterConfiguration()
+ }
+
+ func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
+ os_log("[SC] π] System extension request failed: %@", error.localizedDescription)
+ status = .stopped
+ }
+
+ func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
+ os_log("[SC] π] Extension %@ requires user approval", request.identifier)
+ }
+
+ func request(_ request: OSSystemExtensionRequest,
+ actionForReplacingExtension existing: OSSystemExtensionProperties,
+ withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
+ os_log("[SC] π] Replacing extension %@ version %@ with version %@", request.identifier, existing.bundleShortVersion, ext.bundleShortVersion)
+ return .replace
+ }
+
+ func request(_ request: OSSystemExtensionRequest, foundProperties properties: [OSSystemExtensionProperties]) {
+ os_log("[SC] π] foundProperties extension %@", properties)
+ }
+
+ // MARK: - App Communication (Prompting the User)
+
+ @objc func promptUser(aboutFlow flowInfo: [String: String], responseHandler: @escaping (Bool) -> Void) {
+ guard let localPort = flowInfo[FlowInfoKey.localPort.rawValue],
+ let remoteAddress = flowInfo[FlowInfoKey.remoteAddress.rawValue] else {
+ os_log("[SC] π] Got a promptUser call without valid flow info: %@", flowInfo)
+ responseHandler(true)
+ return
+ }
+ let connectionDate = Date()
+ DispatchQueue.main.async {
+ // For SwiftUI on macOS, use NSAlert via the shared NSApplication window.
+ if let window = NSApplication.shared.windows.first {
+ let alert = NSAlert()
+ alert.alertStyle = .informational
+ alert.messageText = "New incoming connection"
+ alert.informativeText = "A new connection on port \(localPort) has been received from \(remoteAddress)."
+ alert.addButton(withTitle: "Allow")
+ alert.addButton(withTitle: "Deny")
+ alert.beginSheetModal(for: window) { response in
+ let userAllowed = (response == .alertFirstButtonReturn)
+ self.logFlow(flowInfo, at: connectionDate, userAllowed: userAllowed)
+ responseHandler(userAllowed)
+ }
+ } else {
+ // Fallback if no window is available.
+ self.logFlow(flowInfo, at: connectionDate, userAllowed: true)
+ responseHandler(true)
+ }
+ }
+ }
+
+ func didSetUrls() {
+ print("didSetUrls+++++")
+ }
+}
+
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/SCDurationSlider.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/SCDurationSlider.swift
new file mode 100644
index 0000000..4b2487e
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Main/SCDurationSlider.swift
@@ -0,0 +1,88 @@
+//
+// SCDurationSlider.swift
+// SelfControl
+//
+// Created by Satendra Singh on 31/08/25.
+//
+
+import Foundation
+
+
+//import Foundation
+//import AppKit
+
+//class SCTimeIntervalFormatter {
+//
+// func string(for obj: Any?) -> String {
+// guard let number = obj as? NSNumber else {
+// return ""
+// }
+// return formatSeconds(number.doubleValue)
+// }
+//
+// func formatSeconds(_ seconds: TimeInterval) -> String {
+// let useModernBehavior = NSAppKitVersion.current >= NSAppKitVersion.macOS10_8
+// if useModernBehavior {
+// return formatSecondsUsingModernBehavior(seconds)
+// } else {
+// return formatSecondsUsingLegacyBehavior(seconds)
+// }
+// }
+//
+// private func formatSecondsUsingModernBehavior(_ seconds: TimeInterval) -> String {
+// struct FormatterHolder {
+// static let formatter: TTTTimeIntervalFormatter = {
+// let formatter = NSDateFormatter()
+// formatter.pastDeicticExpression = ""
+// formatter.presentDeicticExpression = ""
+// formatter.futureDeicticExpression = ""
+// formatter.significantUnits = [.year, .month, .day, .hour, .minute]
+// formatter.numberOfSignificantUnits = 0
+// formatter.leastSignificantUnit = .minute
+// return formatter
+// }()
+// }
+//
+// var formatted = FormatterHolder.formatter.string(forTimeInterval: seconds) ?? ""
+// if formatted.isEmpty {
+// formatted = stringIndicatingZeroMinutes()
+// }
+//
+// return formatted
+// }
+//
+// private func formatSecondsUsingLegacyBehavior(_ seconds: TimeInterval) -> String {
+// let numMinutes = Int(seconds / 60)
+// let formatDays = numMinutes / 1440
+// let formatHours = (numMinutes % 1440) / 60
+// let formatMinutes = numMinutes % 60
+//
+// var timeString = ""
+//
+// if numMinutes > 0 {
+// if formatDays > 0 {
+// timeString = "\(formatDays) " + (formatDays == 1 ? NSLocalizedString("day", comment: "Single day time string") : NSLocalizedString("days", comment: "Plural days time string"))
+// }
+// if formatHours > 0 {
+// let hoursPart = "\(formatHours) " + (formatHours == 1 ? NSLocalizedString("hour", comment: "Single hour time string") : NSLocalizedString("hours", comment: "Plural hours time string"))
+// timeString += (formatDays > 0 ? ", " : "") + hoursPart
+// }
+// if formatMinutes > 0 {
+// let minutesPart = "\(formatMinutes) " + (formatMinutes == 1 ? NSLocalizedString("minute", comment: "Single minute time string") : NSLocalizedString("minutes", comment: "Plural minutes time string"))
+// timeString += ((formatHours > 0 || formatDays > 0) ? ", " : "") + minutesPart
+// }
+// } else {
+// timeString = stringIndicatingZeroMinutes()
+// }
+//
+// return timeString
+// }
+//
+// private func stringIndicatingZeroMinutes() -> String {
+// return String(
+// format: "0 %@ (%@)",
+// NSLocalizedString("minutes", comment: "Plural minutes time string"),
+// NSLocalizedString("disabled", comment: "Shows that SelfControl is disabled")
+// )
+// }
+//}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/Status.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/Status.swift
new file mode 100644
index 0000000..cb8c962
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Main/Status.swift
@@ -0,0 +1,39 @@
+//
+// Status.swift
+// SelfControl
+//
+// Created by Egzon Arifi on 02/04/2025.
+//
+
+import Foundation
+import SwiftUI
+
+enum Status {
+ case stopped
+ case indeterminate
+ case running
+}
+
+extension Status {
+ var text: String {
+ switch self {
+ case .stopped:
+ return "Stopped"
+ case .indeterminate:
+ return "Indeterminate"
+ case .running:
+ return "Running"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .stopped:
+ return .red
+ case .indeterminate:
+ return .yellow
+ case .running:
+ return .green
+ }
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift b/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift
new file mode 100644
index 0000000..a57e5ed
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift
@@ -0,0 +1,68 @@
+//
+// PreferencesView.swift
+// SelfControl
+//
+// Created by Satendra Singh on 12/07/25.
+//
+
+import SwiftUI
+
+struct PreferencesView: View {
+ @State private var domains = ProxyPreferences.getBlockedDomains()
+ @State private var newDomain = ""
+ @EnvironmentObject var viewModel: FilterViewModel
+
+ var body: some View {
+ VStack {
+ List {
+ ForEach(domains, id: \.self) { domain in
+ HStack {
+ Text(domain)
+ Spacer()
+ Button(action: {
+ deleteItem(domain: domain)
+ }) {
+ Image(systemName: "trash")
+ .foregroundColor(.red)
+ }
+ .buttonStyle(BorderlessButtonStyle()) // ensures button works inside List
+ }
+ }
+ .onDelete(perform: delete) // still supports swipe-to-delete
+ }
+
+ HStack {
+ TextField("Add Domain", text: $newDomain)
+ Button("Add") {
+ addDomain()
+ }
+ }
+
+ Button("Save Preferences") {
+ ProxyPreferences.setBlockedDomains(domains)
+ viewModel.setBlockedUrls(urls: domains)
+ }
+ }
+ .padding()
+ .frame(width: 400, height: 300)
+ }
+
+ func addDomain() {
+ guard !newDomain.isEmpty else { return }
+// guard let domainValue = newDomain.domainString else { return }
+ domains.append(newDomain)
+ newDomain = ""
+ ProxyPreferences.setBlockedDomains(domains)
+ }
+
+ func delete(at offsets: IndexSet) {
+ domains.remove(atOffsets: offsets)
+ ProxyPreferences.setBlockedDomains(domains)
+ }
+
+ func deleteItem(domain: String) {
+ if let index = domains.firstIndex(of: domain) {
+ domains.remove(at: index)
+ }
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Preferences/ProxyPreferences.swift b/Self-Control-Extension/SelfControl/SelfControl/Preferences/ProxyPreferences.swift
new file mode 100644
index 0000000..4ba2f0b
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Preferences/ProxyPreferences.swift
@@ -0,0 +1,27 @@
+//
+// ProxyPreferences.swift
+// SelfControl
+//
+// Created by Satendra Singh on 12/07/25.
+//
+
+
+import Foundation
+
+struct ProxyPreferences {
+// static let appGroup = "X6FQ433AWK.com.application.SelfControl.Extension"
+ static let blockedDomainsKey = "BlockedDomains"
+
+ static func getBlockedDomains() -> [String] {
+ let defaults = UserDefaults.standard
+// let defaults = UserDefaults(suiteName: appGroup)
+
+ return defaults.stringArray(forKey: blockedDomainsKey) ?? []
+ }
+
+ static func setBlockedDomains(_ domains: [String]) {
+// let defaults = UserDefaults(suiteName: appGroup)
+ let defaults = UserDefaults.standard
+ defaults.set(domains, forKey: blockedDomainsKey)
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/Preview Content/Preview Assets.xcassets/Contents.json b/Self-Control-Extension/SelfControl/SelfControl/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/README.md b/Self-Control-Extension/SelfControl/SelfControl/README.md
new file mode 100644
index 0000000..dae091d
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/README.md
@@ -0,0 +1,9 @@
+# SelfControlApp
+
+This directory contains the Swift-based macOS app for SelfControl.
+
+## Structure
+
+- `UI/` - User interface code
+- `ViewControllers/` - View controllers
+- `Resources/` - App-specific resources
\ No newline at end of file
diff --git a/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListManager.swift b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListManager.swift
new file mode 100644
index 0000000..b12b12a
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListManager.swift
@@ -0,0 +1,96 @@
+import Foundation
+import SafariServices
+
+struct BlockRule: Codable {
+ struct Trigger: Codable {
+ let urlFilter: String
+
+ enum CodingKeys: String, CodingKey {
+ case urlFilter = "url-filter"
+ }
+ }
+
+ struct Action: Codable {
+ let type: String
+ }
+
+ let trigger: Trigger
+ let action: Action
+}
+
+
+enum BlockListManager {
+
+ static func updateSafariBlockList(blockedPaths: [String], appGroup: String, extensionIdentifier: String) {
+ var rules: [BlockRule] = []
+
+ for path in blockedPaths {
+// let components = path.split(separator: "/", maxSplits: 1, omittingEmptySubsequences: false)
+// let domain = String(components[0])
+// let pathPart = components.count == 2 ? String(components[1]) : ""
+//
+// let escapedDomain = NSRegularExpression.escapedPattern(for: domain)
+// var pattern = "^https?://(www\\.)?" + escapedDomain
+// if !pathPart.isEmpty {
+// let escapedPath = NSRegularExpression.escapedPattern(for: pathPart)
+// pattern += "/" + escapedPath
+// }
+// pattern += ".*"
+
+// rules.append(BlockRule(
+// trigger: .init(urlFilter: SimpleRegexConverter.regexFromURL(path) ?? path),
+// action: .init(type: "block")
+// ))
+ rules.append(BlockRule(
+ trigger: .init(urlFilter: path),
+ action: .init(type: "block")
+ ))
+ }
+
+ do {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.prettyPrinted]
+ let data = try encoder.encode(rules)
+
+ let fileManager = FileManager.default
+ guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
+ print("β App group container not found.")
+ return
+ }
+
+ // Ensure folder exists before writing
+ try fileManager.createDirectory(at: containerURL, withIntermediateDirectories: true, attributes: nil)
+
+ let fileURL = containerURL.appendingPathComponent("blockerList.json")
+ try data.write(to: fileURL, options: .atomic)
+
+ print("β
Wrote blockerList.json to: \(fileURL.path)")
+
+ // Query current state for diagnostics; proceed to reload regardless.
+ SFContentBlockerManager.getStateOfContentBlocker(withIdentifier: extensionIdentifier) { state, error in
+ if let error = error as NSError? {
+ print("β οΈ State check error for \(extensionIdentifier): \(error.domain) code \(error.code) β \(error.localizedDescription)")
+ print("βΉοΈ Tips: Ensure the content blocker extension bundle identifier matches exactly, the extension is installed and enabled in Safari, and the app/extension share the same App Group: \(appGroup)")
+ } else {
+ print("π Current state of \(extensionIdentifier):", state?.description ?? "Unknown")
+ if state?.isEnabled == false {
+ print("β οΈ Extension not enabled in Safari. Please enable it in Settings β Safari β Extensions.")
+ }
+ }
+
+ // Attempt reload regardless; this often yields a clearer error if the identifier is wrong.
+ SFContentBlockerManager.reloadContentBlocker(withIdentifier: extensionIdentifier) { error in
+ if let error = error as NSError? {
+ print("β Reload error for \(extensionIdentifier): \(error.domain) code \(error.code) β \(error.localizedDescription)")
+ print("βΉοΈ If this persists: verify the extension targetβs bundle ID, that the extension is enabled, and that it has access to the shared container.")
+ } else {
+ print("β
Safari Content Blocker reloaded successfully.")
+ }
+ }
+ }
+
+ } catch {
+ print("β Error writing blocker file:", error)
+ }
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListViewModel.swift b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListViewModel.swift
new file mode 100644
index 0000000..b8b2952
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListViewModel.swift
@@ -0,0 +1,37 @@
+import Foundation
+import Combine
+import SafariServices
+
+/// ViewModel to manage blocked paths and write the blockerList.json into the App Group container.
+class BlockListViewModel: ObservableObject {
+ @Published var blockedPaths: [String] = [
+ "facebook.com/friends",
+ "youtube.com/shorts/",
+ "example.com/private/"
+ ]
+ private let appGroup = "group.com.application.SelfControl.corebits"
+ private let extensionIdentifier = "com.application.SelfControl.corebits.SelfControl-Safari-Extension"
+
+ func addSample() {
+ blockedPaths.append("example.com/private/")
+ }
+
+ func remove(at index: Int) {
+ guard index >= 0 && index < blockedPaths.count else { return }
+ blockedPaths.remove(at: index)
+ }
+
+ func resetToDefaults() {
+ blockedPaths = [
+ "facebook.com/friends",
+ "youtube.com/shorts/",
+ "example.com/private/"
+ ]
+ }
+
+ func updateBlocker() {
+ let urls = ProxyPreferences.getBlockedDomains()
+ print("URLS: \(urls)")
+ BlockListManager.updateSafariBlockList(blockedPaths: urls, appGroup: appGroup, extensionIdentifier: extensionIdentifier)
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SafariExtensionView.swift b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SafariExtensionView.swift
new file mode 100644
index 0000000..4708094
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SafariExtensionView.swift
@@ -0,0 +1,57 @@
+//
+// SafariExtensionView.swift
+// SelfControl
+//
+// Created by Satendra Singh on 05/10/25.
+//
+
+import SwiftUI
+
+struct SafariExtensionView: View {
+ @StateObject private var vm = BlockListViewModel()
+
+ var body: some View {
+ VStack {
+// HStack {
+// Text("Blocked paths (domain/path)").font(.headline)
+// Spacer()
+// Button(action: vm.addSample) {
+// Text("Add sample")
+// }
+// }.padding(.horizontal)
+//
+// List {
+// ForEach(vm.blockedPaths.indices, id: \ .self) { idx in
+// HStack {
+// TextField("domain/path", text: $vm.blockedPaths[idx])
+// Button(action: { vm.remove(at: idx) }) {
+// Image(systemName: "minus.circle")
+// }.buttonStyle(BorderlessButtonStyle())
+// }
+// }
+// }.frame(minHeight: 200)
+
+ HStack {
+ Button(action: vm.updateBlocker) {
+ Text("Update Blocker")
+ }
+ Spacer()
+ Button(action: vm.resetToDefaults) {
+ Text("Reset Defaults")
+ }
+ }.padding()
+ }
+ .onAppear {
+ vm.updateBlocker()
+ }
+ .padding()
+// .frame(minWidth: 600, minHeight: 320)
+ }
+ //1550, 10300, soles, 250,
+ //50, sanitry, 1, 4, 2 ,6, 5
+ //solems, 45,
+}
+
+#Preview {
+ SafariExtensionView()
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SimpleRegexConverter.swift b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SimpleRegexConverter.swift
new file mode 100644
index 0000000..24ffd6c
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SimpleRegexConverter.swift
@@ -0,0 +1,103 @@
+//
+// SimpleRegexConverter.swift
+// SelfControl
+//
+// Created by Satendra Singh on 09/10/25.
+//
+
+import Foundation
+
+final class SimpleRegexConverter {
+ /// Converts a full URL string into a regex domain pattern suitable for Safari content blocker rules.
+ /// Example: "https://www.facebook.com/friends" β "https?://(www\\.)?facebook\\.com/.*"
+ static func regexPattern(from urlString: String) -> String? {
+ guard let url = buildValidURL(from: urlString),
+ let host = url.host else {
+ return nil
+ }
+
+ // Escape regex special characters in domain
+ let escapedHost = NSRegularExpression.escapedPattern(for: host)
+
+ // Detect common "www" prefix and make it optional
+ let pattern: String
+ if host.hasPrefix("www.") {
+ let domainWithoutWWW = String(host.dropFirst(4))
+ let escapedDomain = NSRegularExpression.escapedPattern(for: domainWithoutWWW)
+ pattern = "https?://(www\\.)?\(escapedDomain)/.*"
+ } else {
+ pattern = "https?://(www\\.)?\(escapedHost)/.*"
+ }
+
+ return pattern
+ }
+
+ static func buildValidURL(from path: String) -> URL? {
+ var trimmedPath = path.trimmingCharacters(in: .whitespacesAndNewlines)
+
+ // If it's already a valid URL with scheme
+ if let url = URL(string: trimmedPath), url.scheme != nil {
+ return url
+ }
+
+ // Add missing scheme (default to https)
+ if !trimmedPath.lowercased().hasPrefix("http://") && !trimmedPath.lowercased().hasPrefix("https://") {
+ trimmedPath = "https://" + trimmedPath
+ }
+
+ // Try building again
+ guard var components = URLComponents(string: trimmedPath) else {
+ return nil
+ }
+
+ // Fix missing host (e.g., if user entered "example.com/test")
+ if components.host == nil {
+ let parts = trimmedPath
+ .replacingOccurrences(of: "https://", with: "")
+ .replacingOccurrences(of: "http://", with: "")
+ .split(separator: "/", maxSplits: 1)
+
+ if let hostPart = parts.first {
+ components.host = String(hostPart)
+ components.path = parts.count > 1 ? "/" + parts[1] : ""
+ }
+ }
+
+ // Return final URL
+ return components.url
+ }
+
+
+ static func regexFromURL(_ input: String) -> String? {
+ var urlString = input.trimmingCharacters(in: .whitespacesAndNewlines)
+
+ // Add a scheme if missing (needed for URL parsing)
+ if !urlString.lowercased().hasPrefix("http://") &&
+ !urlString.lowercased().hasPrefix("https://") {
+ urlString = "https://" + urlString
+ }
+
+ guard let url = URL(string: urlString), let host = url.host else {
+ return nil
+ }
+
+ // Escape host and path for regex
+ let escapedHost = NSRegularExpression.escapedPattern(for: host)
+ let escapedPath = NSRegularExpression.escapedPattern(for: url.path)
+
+ // Build regex:
+ // ^[^:]+://+([^.]+\.)*facebook\.com(/friends|[/:]|$)
+ // - Matches any scheme (http/https)
+ // - Allows any subdomain levels
+ // - Matches the path if given
+ var regex = #"^[^:]+://+([^.]+\.)*\#(escapedHost)"#
+
+ if !escapedPath.isEmpty && escapedPath != "/" {
+ regex += escapedPath
+ }
+
+ regex += #"([/:]|$)"#
+
+ return regex
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements b/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements
new file mode 100644
index 0000000..9cc108e
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements
@@ -0,0 +1,17 @@
+
+
+
+
+ com.apple.developer.networking.networkextension
+
+ content-filter-provider-systemextension
+
+ com.apple.developer.system-extension.install
+
+ com.apple.security.application-groups
+
+ group.com.application.SelfControl.corebits
+ $(TeamIdentifierPrefix)com.application.SelfControl.corebits
+
+
+
diff --git a/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift b/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift
new file mode 100644
index 0000000..e7fa36f
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift
@@ -0,0 +1,99 @@
+//
+// SelfControlApp.swift
+// SelfControl
+//
+// Created by Egzon Arifi on 02/04/2025.
+//
+
+import SwiftUI
+
+@main
+struct SelfControlApp: App {
+ @StateObject var viewModel = FilterViewModel()
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ .environmentObject(viewModel) // Inject the object into the environment
+
+ Button("Test", action: {
+ let iPAddress = resolveIPToHostname(ipAddress: "8.8.8.8")
+ print("IP: \(iPAddress ?? "Unknown")")
+ // Example:
+ if let host = reverseDNS(ipAddress: "8.8.8.8") {
+ print("Hostname: \(host)")
+ } else {
+ print("Could not resolve.")
+ }
+ // Example
+ if let domain = reverseDNSUsingGetNameInfo(ipAddress: "163.70.145.35") {
+ print("Domain: \(domain)")
+ } else {
+ print("Reverse DNS lookup failed.")
+ }
+
+ })
+
+ }
+ Window("Preferences View", id: "preferences") {
+ PreferencesView() // Your view to be presented in the new window
+ .environmentObject(viewModel) // Inject the object into the environment
+ }
+ .windowStyle(.automatic)
+ }
+
+ func resolveIPToHostname(ipAddress: String) -> String? {
+ let hostRef = CFHostCreateWithName(nil, ipAddress as CFString).takeRetainedValue()
+
+ var resolved: DarwinBoolean = false
+ if CFHostStartInfoResolution(hostRef, .names, nil) {
+ if let names = CFHostGetNames(hostRef, &resolved)?.takeUnretainedValue() as NSArray?,
+ let hostname = names.firstObject as? String {
+ return hostname
+ }
+ }
+ return nil
+ }
+
+ func reverseDNS(ipAddress: String) -> String? {
+ let hostRef = CFHostCreateWithAddress(nil, ipAddressToData(ipAddress) as CFData).takeRetainedValue()
+ var resolved: DarwinBoolean = false
+ if CFHostStartInfoResolution(hostRef, .names, nil),
+ let names = CFHostGetNames(hostRef, &resolved)?.takeUnretainedValue() as NSArray?,
+ let hostname = names.firstObject as? String {
+ return hostname
+ }
+ return nil
+ }
+
+ private func ipAddressToData(_ ipAddress: String) -> Data {
+ var addr = in_addr()
+ inet_pton(AF_INET, ipAddress, &addr)
+ return Data(bytes: &addr, count: MemoryLayout.size)
+ }
+
+ func reverseDNSUsingGetNameInfo(ipAddress: String) -> String? {
+ var addr = sockaddr_in()
+ addr.sin_len = UInt8(MemoryLayout.size)
+ addr.sin_family = sa_family_t(AF_INET)
+ inet_pton(AF_INET, ipAddress, &addr.sin_addr)
+
+ var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
+
+ // Precompute length so we don't read from `addr` inside the closure.
+ let addrLen = socklen_t(addr.sin_len)
+
+ let result: Int32 = withUnsafePointer(to: &addr) { ptr in
+ ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { saPtr in
+ getnameinfo(saPtr,
+ addrLen,
+ &hostname, socklen_t(hostname.count),
+ nil, 0,
+ NI_NAMEREQD)
+ }
+ }
+
+ guard result == 0 else { return nil }
+ return String(cString: hostname)
+ }
+
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/BlockOrAllowList.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/BlockOrAllowList.swift
new file mode 100644
index 0000000..5a1b065
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/BlockOrAllowList.swift
@@ -0,0 +1,147 @@
+//
+// BlockOrAllowList.swift
+// SelfControl
+//
+// Created by Satendra Singh on 24/09/25.
+//
+
+
+import Foundation
+import os.log
+import NetworkExtension
+
+class BlockOrAllowList: NSObject {
+
+ // MARK: - Properties
+
+ var items: Set
+// var lastModified: Date?
+
+ // Serial queue for thread safety (replace @synchronized)
+// private let queue = DispatchQueue(label: "BlockOrAllowList.serial")
+
+ // MARK: - Initializer
+
+ init(items: [String]) {
+ self.items = Set(items)
+ super.init()
+ }
+
+ // MARK: - Methods
+
+// var isRemote: Bool {
+// return path.hasPrefix("http://") || path.hasPrefix("https://")
+// }
+
+// func load(_ path: String) {
+// queue.sync {
+// self.path = path
+// self.items.removeAll()
+//
+// guard !path.isEmpty else {
+// os_log("no list specified...", log: .default, type: .debug)
+// return
+// }
+//
+// var listString: String?
+// var error: Error?
+//
+// if isRemote {
+// os_log("(re)loading (remote) list", log: .default, type: .debug)
+// if let url = URL(string: path) {
+// do {
+// listString = try String(contentsOf: url, encoding: .utf8)
+// } catch let e {
+// error = e
+// }
+// }
+// if let error = error {
+// os_log("ERROR: failed to (re)load (remote) list, %{public}@ (error: %{public}@)", log: .default, type: .error, path, String(describing: error))
+// return
+// }
+// // (Re)load remote URL once a day
+// DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 24*60*60) {
+// self.load(self.path)
+// }
+// } else {
+// os_log("(re)loading (local) list, %{public}@", log: .default, type: .debug, path)
+// do {
+// listString = try String(contentsOfFile: path, encoding: .utf8)
+// } catch let e {
+// error = e
+// }
+// if let error = error {
+// os_log("ERROR: failed to (re)load (local) list, %{public}@ (error: %{public}@)", log: .default, type: .error, path, String(describing: error))
+// return
+// }
+// if let attrs = try? FileManager.default.attributesOfItem(atPath: path),
+// let fileModified = attrs[.modificationDate] as? Date {
+// self.lastModified = fileModified
+// }
+// }
+//
+// if let listString = listString {
+// let lines = listString.components(separatedBy: .newlines)
+// let filtered = lines.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
+// .filter { !$0.isEmpty && !$0.hasPrefix("#") }
+// self.items = Set(filtered)
+// os_log("SC] π(re)loaded %lu list items", log: .default, type: .debug, self.items.count)
+// }
+// }
+// }
+
+ /// Check if flow matches item on block or allow list
+ func isMatch(_ flow: NEFilterSocketFlow) -> Bool {
+ // Only access properties and perform mutation in the queue (thread safety)
+// return queue.sync {
+
+ var endpointNames = Set()
+
+ // Get remote endpoint host
+ if let url = flow.url, let urlString = url.absoluteString.lowercased() as String? {
+ endpointNames.insert(urlString)
+ }
+ if let url = flow.url, let host = url.host?.lowercased() {
+ endpointNames.insert(host)
+ }
+ if let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint {
+ endpointNames.insert(remoteEndpoint.hostname.lowercased())
+ }
+
+ // macOS 11+ specific property
+ if #available(macOS 11, *) {
+ if let remoteHostname = flow.remoteHostname?.lowercased() {
+ endpointNames.insert(remoteHostname)
+ if remoteHostname.hasPrefix("www.") {
+ let noWWW = String(remoteHostname.dropFirst(4))
+ endpointNames.insert(noWWW)
+ }
+ }
+ }
+ os_log("[SC] π] BlockList endpoint names : %{public}@", log: OSLog.default, type: .info, endpointNames.debugDescription)
+
+ // Find matches
+ let matches = items.intersection(endpointNames)
+ if !matches.isEmpty {
+ os_log("SC] π endpoint names %{public}@ matched the following list items %{public}@", log: .default, type: .debug, String(describing: endpointNames), String(describing: matches))
+ return true
+ }
+
+ return false
+// }
+ }
+}
+
+// MARK: - NEFilterSocketFlow Extensions
+
+//extension NEFilterSocketFlow {
+// /// You must implement these extensions if your Objective-C code provides them!
+// @objc public override var url: URL? {
+// // Implement according to your codebase (category/extension in Objective-C)
+// return nil // Placeholder
+// }
+// @objc var remoteHostname: String? {
+// // Implement according to your codebase (category/extension in Objective-C)
+// return nil // Placeholder
+// }
+//}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Constants.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Constants.swift
new file mode 100644
index 0000000..0db08d6
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Constants.swift
@@ -0,0 +1,35 @@
+//
+// Constants.swift
+// SelfControl
+//
+// Created by Satendra Singh on 11/10/25.
+//
+
+enum Constants {
+ /// File name for the JSON file with Safari rules.
+ static let KEY_UUID = "uuid"
+ static let KEY_PROCESS_ID = "pid"
+ static let KEY_PROCESS_ARGS = "args"
+ static let KEY_PROCESS_NAME = "name"
+ static let KEY_PROCESS_PATH = "path"
+ static let KEY_INDEX = "index"
+ static let KEY_PATH = "paths"
+ static let KEY_CS_SIGNER = "signatureSigner"
+ static let KEY_CS_ID = "signatureIdentifier"
+ static let KEY_CS_INFO = "signingInfo"
+ static let KEY_CS_AUTHS = "signatureAuthorities"
+}
+
+enum AllowedProcess: String {
+ case safari = "com.apple.Safari"
+ case chrome = "com.google.Chrome"
+ static var allCases: [AllowedProcess] = [.chrome, .safari]
+ static func isAllowed(_ bundleID: String) -> Bool {
+ allCases.contains(where: { $0.rawValue == bundleID })
+ }
+
+ static func isAllowedProcess(_ process: Process) -> Bool {
+ let ancestors: [String] = process.ancestors?.value(forKey: "name") as? [String] ?? []
+ return allCases.contains(where: { $0.rawValue == process.bundleID }) || ancestors.contains(where: { $0.lowercased().contains("chrome") || $0.lowercased().contains("safari") })
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/DNSProxyProvider.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/DNSProxyProvider.swift
new file mode 100644
index 0000000..15a0ab3
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/DNSProxyProvider.swift
@@ -0,0 +1,108 @@
+//
+// DNSProxyProvider.swift
+// SelfControl
+//
+// Created by Satendra Singh on 02/08/25.
+//
+
+
+import NetworkExtension
+import os.log
+
+class DNSProxyProvider: NEDNSProxyProvider {
+
+ // Cache for queried domains (thread-safe)
+ var observedDomains = Set()
+ let queue = DispatchQueue(label: "dns.sync.queue")
+
+ override func startProxy(options: [String : Any]? = nil, completionHandler: @escaping (Error?) -> Void) {
+ NSLog("DNS Proxy started")
+ os_log("SC] π DNS Proxy started")
+ completionHandler(nil)
+ }
+
+ override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
+ if let tcpFlow = flow as? NEAppProxyTCPFlow {
+ os_log("SC] π DNS Proxy NEAppProxyTCPFlow")
+ readFromTCP(flow: tcpFlow)
+ return true
+ }
+
+ guard let udpFlow = flow as? NEAppProxyUDPFlow else {
+ return true
+ }
+ os_log("SC] π DNS Proxy NEAppProxyUDPFlow")
+ udpFlow.readDatagrams { datagrams, remoteEndpoints, error in
+ guard let datagrams = datagrams else { return }
+
+ for data in datagrams {
+ if let message = DNSParser.parseMessage(data) {
+ for question in message.questions {
+ os_log("SC] π DNS Query for domain: %{public}@", question.name)
+ NSLog("π DNS Query for domain: \(question.name)")
+ // Save domain name, blocklist, etc.
+ }
+ }
+ }
+
+ // Respond or forward if you're proxying
+// udpFlow.closeReadWithError(nil)
+// udpFlow.closeWriteWithError(nil)
+ }
+
+ return true
+ }
+
+ override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
+ NSLog("DNS Proxy stopped")
+ os_log("SC] π DNS Proxy stopped")
+ completionHandler()
+ }
+
+ func readFromTCP(flow: NEAppProxyTCPFlow) {
+ flow.readData { [weak self] data, error in
+ guard let data = data, !data.isEmpty else {
+ os_log("SC] π No data or error: : %{public}@", error?.localizedDescription ?? "Empty")
+ flow.closeReadWithError(nil)
+ flow.closeWriteWithError(nil)
+ return
+ }
+
+ var offset = 0 58540
+
+ while offset + 2 <= data.count {
+ // DNS over TCP starts with 2-byte length prefix
+ let length = Int(data.uint16(at: offset))
+ offset += 2
+
+ guard offset + length <= data.count else {
+ NSLog("Incomplete DNS message")
+ os_log("SC] π Incomplete DNS message")
+ break
+ }
+
+ let dnsData = data.subdata(in: offset..<(offset + length))
+ if let message = DNSParser.parseMessage(dnsData) {
+ for q in message.questions {
+ NSLog("π DNS (TCP) Query: \(q.name)")
+ os_log("SC] π π (TCP) Query: %{public}@", q.name)
+ }
+ }
+
+ offset += length
+ }
+
+ // Optionally respond or forward, or keep reading
+ flow.closeReadWithError(nil)
+ flow.closeWriteWithError(nil)
+ }
+ }
+
+}
+
+extension Data {
+ public func uint16(at offset: Int) -> UInt16 {
+ guard offset + 1 < self.count else { return 0 }
+ return UInt16(self[offset]) << 8 | UInt16(self[offset + 1])
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/DNSQuestion.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/DNSQuestion.swift
new file mode 100644
index 0000000..8f00f1d
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/DNSQuestion.swift
@@ -0,0 +1,72 @@
+//
+// DNSQuestion.swift
+// SelfControl
+//
+// Created by Satendra Singh on 02/08/25.
+//
+
+import Foundation
+
+struct DNSQuestion {
+ let name: String
+ let type: UInt16
+ let qclass: UInt16
+}
+
+struct DNSMessage {
+ let id: UInt16
+ let isQuery: Bool
+ let opcode: UInt8
+ let questions: [DNSQuestion]
+}
+
+class DNSParser {
+ static func parseMessage(_ data: Data) -> DNSMessage? {
+ guard data.count >= 12 else { return nil } // DNS header size
+
+ let id = data.uint16(at: 0)
+ let flags = data.uint16(at: 2)
+
+ let isQuery = (flags & 0x8000) == 0
+ let opcode = UInt8((flags & 0x7800) >> 11)
+ let qdcount = data.uint16(at: 4)
+
+ var offset = 12
+
+ var questions: [DNSQuestion] = []
+
+ for _ in 0..= offset + 4 else { return nil }
+ let type = data.uint16(at: offset)
+ let qclass = data.uint16(at: offset + 2)
+ offset += 4
+ questions.append(DNSQuestion(name: name, type: type, qclass: qclass))
+ }
+
+ return DNSMessage(id: id, isQuery: isQuery, opcode: opcode, questions: questions)
+ }
+
+ private static func parseDomainName(_ data: Data, offset: Int) -> (String, Int)? {
+ var labels: [String] = []
+ var index = offset
+ while index < data.count {
+ let length = Int(data[index])
+ if length == 0 {
+ index += 1
+ break
+ }
+ guard index + length < data.count else { return nil }
+ let labelData = data.subdata(in: index+1..<(index+1+length))
+ if let label = String(data: labelData, encoding: .utf8) {
+ labels.append(label)
+ } else {
+ return nil
+ }
+ index += length + 1
+ }
+ return (labels.joined(separator: "."), index)
+ }
+}
+
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/ExtensionsPreferencesManager.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/ExtensionsPreferencesManager.swift
new file mode 100644
index 0000000..9958009
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/ExtensionsPreferencesManager.swift
@@ -0,0 +1,24 @@
+//
+// ExtensionsPreferencesManager.swift
+// SelfControl
+//
+// Created by Satendra Singh on 12/07/25.
+//
+
+import Foundation
+import os.log
+
+final class ExtensionsPreferencesManager {
+ let appGroup = "X6FQ433AWK.com.application.SelfControl.Extension"
+ var blockedDomains: [String] = []
+ lazy var defaults = UserDefaults(suiteName: appGroup)
+
+ init(blockedDomains: [String]) {
+ self.blockedDomains = blockedDomains
+ }
+
+ init () {
+ blockedDomains = defaults?.stringArray(forKey: "BlockedDomains") ?? []
+ os_log("SC] π DNS Blocked Domains Loaded: %{public}@", blockedDomains)
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift
new file mode 100644
index 0000000..3cd3f4c
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift
@@ -0,0 +1,531 @@
+import NetworkExtension
+import os.log
+import dnssd
+
+/// FilterDataProvider is a NEFilterDataProvider subclass that intercepts flows and applies a test rule.
+class FilterDataProvider: NEFilterDataProvider {
+ // MARK: - Properties
+// private let listner = PlistListner()
+ /// A dictionary for storing flows related to the same process.
+ private var relatedFlows: [String: [NEFilterSocketFlow]] = [:]
+
+ // MARK: - Initialization
+
+ override init() {
+ os_log("[SC] π] FilterDataProvider: init")
+ super.init()
+// listner.startListening()
+ }
+
+ // MARK: - Filter Lifecycle
+ static let localPort = "8888"
+
+ override func startFilter(completionHandler: @escaping (Error?) -> Void) {
+ os_log("[SC] π] FilterDataProvider: Starting filter", log: OSLog.default, type: .info)
+ os_log("[SC] π] captrued Domains: %{public}@", log: OSLog.default, type: .error, ExtensionsPreferencesManager().blockedDomains)
+// os_log("[SC] π] captrued blockedUrls: %{public}@", log: OSLog.default, type: .error, IPCConnection.shared.blockedUrls)
+
+ // Filter incoming TCP connections on port 8888
+ let blockedHosts = IPCConnection.shared.blockedUrls
+
+// let filterRules = blockedHosts.map { address -> NEFilterRule in
+// // let localNetwork = NWHostEndpoint(hostname: address as! String, port: FilterDataProvider.localPort)
+// let inboundNetworkRule = NENetworkRule(remoteNetwork: address,
+// remotePrefix: 0,
+// localNetwork: nil,
+// localPrefix: 0,
+// protocol: .any,
+// direction: .outbound)
+// return NEFilterRule(networkRule: inboundNetworkRule, action: .filterData)
+// }
+ // Filter incoming TCP connections on port 8888
+// let filterRules = ["0.0.0.0", "::"].map { address -> NEFilterRule in
+// let filterRules = ["*"].map { address -> NEFilterRule in
+// let localNetwork = NWHostEndpoint(hostname: address, port: "*")
+// let inboundNetworkRule = NENetworkRule(remoteNetwork: nil,
+// remotePrefix: 0,
+// localNetwork: localNetwork,
+// localPrefix: 0,
+// protocol: .any,
+// direction: .outbound)
+// return NEFilterRule(networkRule: inboundNetworkRule, action: .filterData)
+// }
+//
+ let filterRules = blockedHosts.map { address -> NEFilterRule in
+ let localNetwork = NWHostEndpoint(hostname: address, port: "*")
+ let inboundNetworkRule = NENetworkRule(remoteNetwork: nil,
+ remotePrefix: 0,
+ localNetwork: localNetwork,
+ localPrefix: 0,
+ protocol: .any,
+ direction: .outbound)
+ return NEFilterRule(networkRule: inboundNetworkRule, action: .filterData)
+ }
+
+
+ // Create a rule matching all outbound traffic.
+// let networkRule = NENetworkRule(remoteNetwork: nil,
+// remotePrefix: 0,
+// localNetwork: nil,
+// localPrefix: 0,
+// protocol: .any,
+// direction: .outbound)
+// let filterRule = NEFilterRule(networkRule: networkRule, action: .filterData)
+// let filterSettings = NEFilterSettings(rules: [filterRule], defaultAction: .allow)
+ let filterSettings = NEFilterSettings(rules: filterRules, defaultAction: .allow)
+
+ apply(filterSettings) { error in
+ if let error = error {
+ os_log("[SC] π] Error applying filter settings: %@", log: OSLog.default, type: .error, error.localizedDescription)
+ }
+ completionHandler(error)
+ }
+ }
+
+ override func stopFilter(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
+ os_log("[SC] π] FilterDataProvider: Stopping filter with reason %d", log: OSLog.default, type: .info, reason.rawValue)
+ completionHandler()
+ }
+
+ // MARK: - Flow Handlings
+
+
+ func reverseDNSLookup(ip: String) -> String? {
+ var hints = addrinfo(ai_flags: AI_NUMERICHOST, ai_family: AF_UNSPEC,
+ ai_socktype: SOCK_STREAM, ai_protocol: IPPROTO_TCP,
+ ai_addrlen: 0, ai_canonname: nil,
+ ai_addr: nil, ai_next: nil)
+ var res: UnsafeMutablePointer?
+
+ if getaddrinfo(ip, nil, &hints, &res) == 0, let addr = res?.pointee.ai_addr {
+ var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
+ if getnameinfo(addr, socklen_t(addr.pointee.sa_len),
+ &hostBuffer, socklen_t(hostBuffer.count),
+ nil, 0, NI_NAMEREQD) == 0 {
+ return String(cString: hostBuffer)
+ }
+ }
+ return nil
+ }
+
+ func extractHost(from request: String) -> String? {
+ for line in request.split(separator: "\r\n") {
+ if line.lowercased().hasPrefix("host:") {
+ return line.replacingOccurrences(of: "Host:", with: "", options: .caseInsensitive)
+ .trimmingCharacters(in: .whitespaces)
+ }
+ }
+ return nil
+ }
+
+ func extractPath(from request: String) -> String? {
+ guard let firstLine = request.split(separator: "\r\n").first else { return nil }
+ let comps = firstLine.split(separator: " ")
+ return comps.count > 1 ? String(comps[1]) : nil
+ }
+
+ func extractSNI(fromTLSData data: Data) -> String? {
+ // TLS record starts with 0x16 (Handshake), version bytes, then length
+ guard data.count > 5, data[0] == 0x16 else { return nil }
+
+ var offset = 5 // Skip record header
+
+ // Verify handshake type = ClientHello (0x01)
+ guard data.count > offset, data[offset] == 0x01 else { return nil }
+ offset += 4 // Skip type + length (3 bytes)
+
+ // Skip protocol version (2), random (32), session id length + session id
+ guard data.count > offset + 34 else { return nil }
+ offset += 34
+ guard data.count > offset else { return nil }
+
+ // Skip session ID
+ if offset < data.count {
+ let sessionIDLength = Int(data[offset])
+ offset += 1 + sessionIDLength
+ }
+
+ // Skip cipher suites
+ guard offset + 2 <= data.count else { return nil }
+ let cipherSuiteLength = Int(data[offset]) << 8 | Int(data[offset + 1])
+ offset += 2 + cipherSuiteLength
+
+ // Skip compression methods
+ guard offset < data.count else { return nil }
+ let compressionLength = Int(data[offset])
+ offset += 1 + compressionLength
+
+ // Skip extensions length
+ guard offset + 2 <= data.count else { return nil }
+ let extensionsLength = Int(data[offset]) << 8 | Int(data[offset + 1])
+ offset += 2
+ guard offset + extensionsLength <= data.count else { return nil }
+
+ var extOffset = offset
+ while extOffset + 4 <= offset + extensionsLength {
+ let extType = Int(data[extOffset]) << 8 | Int(data[extOffset + 1])
+ let extLen = Int(data[extOffset + 2]) << 8 | Int(data[extOffset + 3])
+ extOffset += 4
+
+ // Extension type 0 = Server Name
+ if extType == 0 {
+ // Parse Server Name extension
+ var nameOffset = extOffset + 2 // skip list length
+ while nameOffset + 3 < extOffset + extLen {
+ let nameType = data[nameOffset]
+ let nameLen = Int(data[nameOffset + 1]) << 8 | Int(data[nameOffset + 2])
+ nameOffset += 3
+ if nameType == 0, nameOffset + nameLen <= data.count {
+ let nameData = data.subdata(in: nameOffset ..< nameOffset + nameLen)
+ return String(data: nameData, encoding: .utf8)
+ }
+ nameOffset += nameLen
+ }
+ }
+
+ extOffset += extLen
+ }
+
+ return nil
+ }
+
+ override func handleOutboundData(
+ from flow: NEFilterFlow,
+ readBytesStartOffset offset: Int,
+ readBytes data: Data
+ ) -> NEFilterDataVerdict {
+
+
+ let process = processFromflow(flow: flow)
+ // os_log(" FilterDataProvider: appIdAndName %{public}@, %{public}@", log: OSLog.default, type: .debug, process?.name ?? "", process?.path ?? "")
+ os_log(" FilterDataProvider: app %{public}@", log: OSLog.default, type: .debug, process?.description ?? "")
+
+ if let process = process, AllowedProcess.isAllowedProcess(process) {
+ os_log("[SC] π] FilterDataProvider: allowing as it is in allowed list.")
+ return .allow()
+ }
+ guard let socketFlow = flow as? NEFilterSocketFlow else {
+ os_log("[SC] π] Not a socket flow. Allowing.", log: OSLog.default, type: .info)
+ return .allow()
+ }
+ if socketFlow.direction != .outbound {
+ os_log("[SC] π] Not a inbound socket flow. Allowing.", log: OSLog.default, type: .info)
+ return .allow()
+ }
+
+// guard let socketFlow = flow as? NEFilterSocketFlow else {
+// return .allow()
+// }
+ let requestString = String(data: data, encoding: .utf8)
+ if let requestString {
+ os_log("[SC] π] data HTTP Request: %{public}@", requestString)
+ }
+
+ // Try HTTP detection
+ if let requestString = requestString,
+ requestString.hasPrefix("GET ") || requestString.hasPrefix("POST ") {
+
+ if let host = extractHost(from: requestString),
+ let path = extractPath(from: requestString) {
+ let urlString = "http://\(host)\(path)"
+ os_log("[SC] π] data HTTP Request: %{public}@", urlString)
+ }
+
+ } else if let sni = extractSNI(fromTLSData: data) {
+ os_log("[SC] π] data TLS SNI Host: %{public}@", sni)
+ guard let hostDomain = TLDURLToDomain.getURLDomain(from: sni) else {
+ return .allow()
+ }
+ os_log("[SC] π] data hostDomain: %{public}@", hostDomain)
+
+ for url in IPCConnection.shared.blockedUrls {
+ if url.contains(hostDomain) {
+ os_log("[SC] π] data Blocking flow Host match SNI:%{public}@, host:%{public}@", sni, hostDomain)
+ return .drop()
+ }
+ }
+ }
+
+ return .allow()
+ }
+
+ // Called for each new flow.
+ override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict {
+ // Provide peek sizes required by this overload
+ return .filterDataVerdict(withFilterInbound: false,
+ peekInboundBytes: 0,
+ filterOutbound: true,
+ peekOutboundBytes: Int.max)
+
+ os_log("[SC] π] FilterDataProvider: handleNewFlow invoked", log: OSLog.default, type: .debug)
+// if let appID = flow.sourceAppIdentifier {
+// print("App making the request: \(appID)")
+// }
+// let appIdAndName = SourceAppAuditTokenBuilder.getAppInfo(from: flow)
+// os_log("[SC] π] FilterDataProvider: appIdAndName %@, %@", log: OSLog.default, type: .debug, appIdAndName.appName ?? "", appIdAndName.bundleID ?? "")
+ guard IPCConnection.shared.blockedUrls.count > 0 else { //No signinficant urls to block
+ return .allow()
+ }
+
+ let process = processFromflow(flow: flow)
+// os_log(" FilterDataProvider: appIdAndName %{public}@, %{public}@", log: OSLog.default, type: .debug, process?.name ?? "", process?.path ?? "")
+ os_log(" FilterDataProvider: app %{public}@", log: OSLog.default, type: .debug, process?.description ?? "")
+
+ if let process = process, AllowedProcess.isAllowedProcess(process) {
+ os_log("[SC] π] FilterDataProvider: allowing as it is in allowed list.")
+ return .allow()
+ }
+ guard let socketFlow = flow as? NEFilterSocketFlow else {
+ os_log("[SC] π] Not a socket flow. Allowing.", log: OSLog.default, type: .info)
+ return .allow()
+ }
+ if socketFlow.direction != .outbound {
+ os_log("[SC] π] Not a inbound socket flow. Allowing.", log: OSLog.default, type: .info)
+ return .allow()
+ }
+ var remoteHost = ""
+ if let flowURL = flow.url {
+ remoteHost = flowURL.absoluteString
+ os_log("[SC] π] flow.url: \(remoteHost)")
+ } else {
+ if let remoteEndpoint = socketFlow.remoteEndpoint as? NWHostEndpoint {
+ remoteHost = remoteEndpoint.hostname
+ let port = remoteEndpoint.port
+ os_log("[SC] π] NWEndpoint to host: %{public}@, URL: %{public}@, url: %{public}@", log: OSLog.default, type: .debug, remoteHost, port, flow.url?.host() ?? "")
+ }
+ }
+
+
+ //todo: check host name, work on reverse rule if domain match and if path is nil or blocked, drop connection.
+//
+// if IPCConnection.shared.blockedList.isMatch(flow as! NEFilterSocketFlow) {
+// os_log("[SC] π] Dropped", log: OSLog.default, type: .info)
+// return .drop()
+// }
+// os_log("[SC] π] Allowed", log: OSLog.default, type: .info)
+//
+// return .allow()
+
+ os_log("[SC] π] Flow from remote endpoint: %{public}@, URL: %{public}@", log: OSLog.default, type: .debug, socketFlow.remoteEndpoint.debugDescription, flow.url?.description ?? "nil")
+
+ // Extract remote endpoint (if available).
+// guard let remoteEndpoint = socketFlow.remoteEndpoint as? NWHostEndpoint else {
+// os_log("[SC] π] No valid remote endpoint. Allowing flow.", log: OSLog.default, type: .error)
+// return .allow()
+// }
+
+ if remoteHost.isEmpty { //Unable to get host
+ return .allow()
+ }
+ if remoteHost.isValidIpAddress {
+ if IPCConnection.shared.blockedIPAddresses.contains(remoteHost) {
+ os_log("[SC] π] Blocking flow IP match: %{public}@", remoteHost)
+ return .drop()
+ }
+ if let host = ReverseDomainMapper.reverseDNSUsingGetNameInfo(ipAddress: remoteHost) {
+ os_log("[SC] π] Converted IP: %{public}@ to: %{public}@", remoteHost, host)
+ remoteHost = host
+ } else { //unable to convert, simple ignore
+ return .allow()
+ }
+ } else {
+ return .allow()
+ }
+ guard let hostDomain = TLDURLToDomain.getURLDomain(from: remoteHost) else {
+ return .allow()
+ }
+ for url in IPCConnection.shared.blockedUrls {
+ if url.contains(hostDomain) {
+ os_log("[SC] π] Blocking flow Host match %{public}@, %{public}@", remoteHost, hostDomain)
+ return .drop()
+ }
+ }
+// if var urlString = flow.url?.absoluteString {
+// os_log("[SC] π] Checking URL path: %{public}@", urlString)
+//// let blockedHosts = ["google.com/mail", "google.com/news", "facebook.com"]
+// let blockedHosts = IPCConnection.shared.blockedUrls
+// os_log("[SC] π] BlockedList: %{public}@ checking:%{public}@",blockedHosts, urlString)
+// if urlString.hasPrefix("www.") {
+// let noWWW = String(urlString.dropFirst(4))
+// urlString = noWWW
+// }
+//
+// for host in blockedHosts {
+// if urlString.contains(host) {
+// os_log("[SC] π] Blocking flow to handleNewFlow %{public}@", urlString)
+// return .drop()
+//// return .allow()
+// }
+//// flow.url?.host() == url
+// }
+// }
+ // if blockedHosts.contains(flow.url?.path() ?? "") {
+ // os_log("Blocking flow to %@", remoteEndpoint.hostname)
+ // return .drop()
+ // }
+ // Only process outbound traffic.
+// if socketFlow.direction != .outbound {
+// os_log("[SC] π] Non-outbound traffic. Allowing.", log: OSLog.default, type: .info)
+// return .allow()
+// }
+
+ return .allow()
+
+ // Process the flow and decide a verdict.
+ let verdict = processEvent(for: socketFlow)
+ os_log("[SC] π] Verdict for flow: %{public}@", log: OSLog.default, type: .debug, verdict.debugDescription)
+ return verdict
+ }
+
+// override func handleInboundDataComplete(for flow: NEFilterFlow) -> NEFilterDataVerdict {
+// if let socketFlow = flow as? NEFilterSocketFlow {
+// if let data = socketFlow.readData {
+// if let requestString = String(data: data, encoding: .utf8) {
+// if requestString.hasPrefix("GET") || requestString.hasPrefix("POST") {
+// print("Request: \(requestString)")
+// // Parse the path here
+// }
+// }
+// }
+// }
+// return .allow()
+// }
+//
+// override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict {
+// return .filterDataVerdict(withFilterInbound: true,
+// peekInboundBytes: 1024,
+// filterOutbound: true,
+// peekOutboundBytes: 1024)
+// }
+
+
+ override func handleInboundData(from flow: NEFilterFlow, readBytesStartOffset offset: Int, readBytes: Data) -> NEFilterDataVerdict {
+ if let requestString = String(data: readBytes, encoding: .utf8) {
+ print("Outbound data: \(requestString)")
+ os_log("[SC] π] handleInboundData: %{public}@", requestString)
+
+ if requestString.contains("facebook.com/friends") {
+ return .drop()
+ }
+ }
+ return .allow()
+ }
+
+ override func displayMessage(_ message: String, completionHandler: @escaping (Bool) -> Void) {
+ return completionHandler(true)
+ }
+
+// override func handleOutboundData(from flow: NEFilterFlow, readBytesStartOffset offset: Int, readBytes: Data) -> NEFilterDataVerdict {
+// if let urlString = flow.url?.absoluteString {
+// os_log("[SC] π] handleOutboundData URL: %{public}@", urlString)
+// if urlString.contains("facebook.com/friends") {
+// return .drop()
+// }
+// }
+//
+// if let requestString = String(data: readBytes, encoding: .utf8) {
+// print("Outbound data: \(requestString)")
+// os_log("[SC] π] handleOutboundData: %{public}@", requestString)
+//
+// if requestString.contains("facebook.com/friends") {
+// return .drop()
+// }
+// }
+// return .allow()
+// }
+
+// override func handleOutboundData(from flow: NEFilterFlow,
+// readBytesStartOffset offset: Int,
+// readBytes: Data,
+// completionHandler: @escaping (NEFilterDataVerdict) -> Void) {
+//
+// if let requestString = String(data: readBytes, encoding: .utf8) {
+// print("Outbound data: \(requestString)")
+//
+// if requestString.contains("facebook.com") {
+// completionHandler(.drop())
+// return
+// }
+// }
+//
+// // If undecided, ask for more data
+// completionHandler(.allow())
+// }
+
+ /// Processes the flow and returns a verdict.
+ /// This is a simplified test rule that blocks flows destined for "example.com".
+ private func processEvent(for flow: NEFilterSocketFlow) -> NEFilterNewFlowVerdict {
+ guard let endpoint = flow.remoteEndpoint as? NWHostEndpoint else {
+ return .allow()
+ }
+ os_log("[SC] π] processEvent endpoint.hostname: %{public}@ ", endpoint.hostname)
+ os_log("[SC] π] processEvent remoteHostname: %{public}@ ", flow.remoteHostname ?? "NOTHING")
+ guard let host = flow.remoteHostname?.lowercased().domainString else {
+ os_log("[SC] π] processEvent No Host")
+ return .allow()
+ }
+// os_log("[SC] π] This is localFlowEndpoint: %{public}@ ", flow.localFlowEndpoint?.debugDescription ?? "NOTHING")
+ let blockedHosts = IPCConnection.shared.blockedUrls
+
+// if host == "google.com" || host == "8.8.8.8" {
+ if blockedHosts.contains(host) {
+ os_log("[SC] π] processEvent: Blocking flow to processEvent %{public}@ ", host)
+ return .drop()
+// return .allow()
+ }
+ // Optionally log other flows for debugging
+ os_log("[SC] π] processEvent: Allowing flow to %{public}@", log: OSLog.default, type: .info, host)
+ return .allow()
+ }
+
+
+ // MARK: - (Optional) Handling Related Flows & Alerts
+
+ /// Adds a flow to a list of related flows for a given key.
+ private func addRelatedFlow(forKey key: String, flow: NEFilterSocketFlow) {
+ os_log("[SC] π] Adding related flow for key: %@", log: OSLog.default, type: .debug, key)
+ if relatedFlows[key] == nil {
+ relatedFlows[key] = []
+ }
+ relatedFlows[key]?.append(flow)
+ }
+
+ /// Processes related flows once a decision is made for a given key.
+ private func processRelatedFlows(forKey key: String) {
+ guard let flows = relatedFlows[key] else {
+ os_log("[SC] π] No related flows for key: %@", log: OSLog.default, type: .debug, key)
+ return
+ }
+ for flow in flows {
+ let verdict = processEvent(for: flow)
+ resumeFlow(flow, with: verdict)
+ }
+ relatedFlows[key] = nil
+ }
+
+ /// A stub method for resuming a flow with a verdict.
+ private func resumeFlow(_ flow: NEFilterSocketFlow, with verdict: NEFilterNewFlowVerdict) {
+ // In a complete implementation, this would resume the paused flow with the provided verdict.
+ os_log("[SC] π] Resuming flow %@ with verdict %@", log: OSLog.default, type: .info, flow.debugDescription, verdict.debugDescription)
+ }
+
+ /// A stub method to simulate alerting the user.
+ /// In a complete implementation, this might trigger an IPC to your app for user intervention.
+ private func alertUser(for flow: NEFilterSocketFlow) {
+ os_log("[SC] π] Alert: User decision needed for flow %@", log: OSLog.default, type: .info, flow.debugDescription)
+ }
+
+ func processFromflow(flow: NEFilterFlow) -> Process? {
+ guard let auditTokenData = flow.sourceAppAuditToken else {
+ return nil
+ }
+
+ // Convert NSData β audit_token_t
+ var auditToken = auditTokenData.withUnsafeBytes { ptr -> audit_token_t in
+ return ptr.load(as: audit_token_t.self)
+ }
+ return Process.init(&auditToken)
+ }
+ }
+//https://developer.chrome.com/docs/extensions/how-to/distribute/install-extensions
+
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/HostToIpMapping.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/HostToIpMapping.swift
new file mode 100644
index 0000000..8857b4f
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/HostToIpMapping.swift
@@ -0,0 +1,82 @@
+//
+// HostToIpMapping.swift
+// SelfControl
+//
+// Created by Satendra Singh on 20/10/25.
+//
+
+import Foundation
+
+class HostToIpMapping {
+ static func string(for addressData: Data) throws -> String {
+ var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
+
+ let result = addressData.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> Int32 in
+ guard let addrPtr = ptr.baseAddress else {
+ return EINVAL
+ }
+ return getnameinfo(
+ addrPtr.assumingMemoryBound(to: sockaddr.self),
+ socklen_t(addressData.count),
+ &hostBuffer,
+ socklen_t(hostBuffer.count),
+ nil,
+ 0,
+ NI_NUMERICHOST
+ )
+ }
+
+ if result != 0 {
+ let message = String(cString: gai_strerror(result))
+ throw NSError(domain: "HostToIpMappingError", code: Int(result), userInfo: [
+ NSLocalizedDescriptionKey: message
+ ])
+ }
+
+ return String(cString: hostBuffer)
+ }
+
+ static func ipAddresses(for domainName: String) -> [String] {
+ let startTime = Date()
+
+ // β
Correct initialization
+ let cfHostOpt: CFHost? = CFHostCreateWithName(kCFAllocatorDefault, domainName as CFString).takeRetainedValue()
+ guard let cfHost = cfHostOpt else {
+ print("HostToIpMapping: Failed to create CFHost for \(domainName)")
+ return []
+ }
+
+ var streamError = CFStreamError()
+ let success = CFHostStartInfoResolution(cfHost, .addresses, &streamError)
+
+ if !success || streamError.error != 0 {
+ print("HostToIpMapping: Warning: failed to resolve addresses for \(domainName) with stream error \(streamError.error)")
+ return []
+ }
+
+ // β
Use takeUnretainedValue safely
+ guard let addressArray = CFHostGetAddressing(cfHost, nil)?
+ .takeUnretainedValue() as? [Data] else {
+ print("HostToIpMapping: Warning: failed to resolve addresses for \(domainName)")
+ return []
+ }
+
+ var stringAddresses: [String] = []
+
+ for addrData in addressArray {
+ do {
+ let ip = try string(for: addrData)
+ stringAddresses.append(ip)
+ } catch {
+ print("HostToIpMapping: Warning: Failed to parse IP struct for \(domainName) with error: \(error.localizedDescription)")
+ }
+ }
+
+ let elapsed = Date().timeIntervalSince(startTime)
+ if elapsed > 2.5 {
+ print("HostToIpMapping: Warning: took \(elapsed) seconds to resolve \(domainName)")
+ }
+
+ return stringAddresses
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift
new file mode 100644
index 0000000..286fbf6
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift
@@ -0,0 +1,219 @@
+/*
+ See the LICENSE.txt file for this sampleβs licensing information.
+
+ Abstract:
+ This file contains the implementation of the app <-> provider IPC connection
+ */
+
+import Foundation
+import os.log
+import Network
+
+/// App --> Provider IPC
+@objc protocol ProviderCommunication {
+ func register(_ completionHandler: @escaping (Bool) -> Void)
+ func setBlockedURLs(_ urls: [String])
+ func setBlockedIPAddresses(_ ips: [String])
+}
+
+/// Provider --> App IPC
+@objc protocol AppCommunication {
+
+ func promptUser(aboutFlow flowInfo: [String: String], responseHandler: @escaping (Bool) -> Void)
+ func didSetUrls()
+}
+
+enum FlowInfoKey: String {
+ case localPort
+ case remoteAddress
+}
+
+/// The IPCConnection class is used by both the app and the system extension to communicate with each other
+class IPCConnection: NSObject {
+
+ // MARK: Properties
+
+ var listener: NSXPCListener?
+ var currentConnection: NSXPCConnection?
+ weak var delegate: AppCommunication?
+ static let shared = IPCConnection()
+// var blockedUrls: [String] = ProxyPreferences.getBlockedDomains()
+ var blockedUrls: [String] = [String]()
+ var blockedList = BlockOrAllowList(items: [])
+ var blockedIPAddresses: Set = []
+
+ // MARK: Methods
+
+ /**
+ The NetworkExtension framework registers a Mach service with the name in the system extension's NEMachServiceName Info.plist key.
+ The Mach service name must be prefixed with one of the app groups in the system extension's com.apple.security.application-groups entitlement.
+ Any process in the same app group can use the Mach service to communicate with the system extension.
+ */
+ private func extensionMachServiceName(from bundle: Bundle) -> String {
+
+ guard let networkExtensionKeys = bundle.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any],
+ let machServiceName = networkExtensionKeys["NEMachServiceName"] as? String else {
+ fatalError("Mach service name is missing from the Info.plist")
+ }
+
+ return machServiceName
+ }
+
+ func startListener() {
+
+ let machServiceName = extensionMachServiceName(from: Bundle.main)
+ os_log("[SC] π] Starting XPC listener for mach service %@", machServiceName)
+
+ let newListener = NSXPCListener(machServiceName: machServiceName)
+ newListener.delegate = self
+ newListener.resume()
+ listener = newListener
+ }
+
+ /// This method is called by the app to register with the provider running in the system extension.
+ func register(withExtension bundle: Bundle, delegate: AppCommunication, completionHandler: @escaping (Bool) -> Void) {
+
+ self.delegate = delegate
+
+ guard currentConnection == nil else {
+ os_log("[SC] π] Already registered with the provider")
+ completionHandler(true)
+ return
+ }
+
+ let machServiceName = extensionMachServiceName(from: bundle)
+ let newConnection = NSXPCConnection(machServiceName: machServiceName, options: [])
+
+ // The exported object is the delegate.
+ newConnection.exportedInterface = NSXPCInterface(with: AppCommunication.self)
+ newConnection.exportedObject = delegate
+
+ // The remote object is the provider's IPCConnection instance.
+ newConnection.remoteObjectInterface = NSXPCInterface(with: ProviderCommunication.self)
+
+ currentConnection = newConnection
+ newConnection.resume()
+
+ guard let providerProxy = newConnection.remoteObjectProxyWithErrorHandler({ registerError in
+ os_log("[SC] π] Failed to register with the provider: %@", registerError.localizedDescription)
+ self.currentConnection?.invalidate()
+ self.currentConnection = nil
+ completionHandler(false)
+ }) as? ProviderCommunication else {
+ fatalError("Failed to create a remote object proxy for the provider")
+ }
+ providerProxy.register(completionHandler)
+// providerProxy.setBlockedURLs(blockedUrls)
+ }
+
+ /**
+ This method is called by the provider to cause the app (if it is registered) to display a prompt to the user asking
+ for a decision about a connection.
+ */
+ func promptUser(aboutFlow flowInfo: [String: String], responseHandler:@escaping (Bool) -> Void) -> Bool {
+
+ guard let connection = currentConnection else {
+ os_log("[SC] π] Cannot prompt user because the app isn't registered")
+ return false
+ }
+
+ guard let appProxy = connection.remoteObjectProxyWithErrorHandler({ promptError in
+ os_log("[SC] π] Failed to prompt the user: %@", promptError.localizedDescription)
+ self.currentConnection = nil
+ responseHandler(true)
+ }) as? AppCommunication else {
+ fatalError("Failed to create a remote object proxy for the app")
+ }
+
+ appProxy.promptUser(aboutFlow: flowInfo, responseHandler: responseHandler)
+
+ return true
+ }
+}
+
+extension IPCConnection: NSXPCListenerDelegate {
+
+ // MARK: NSXPCListenerDelegate
+
+ func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
+
+ // The exported object is this IPCConnection instance.
+ newConnection.exportedInterface = NSXPCInterface(with: ProviderCommunication.self)
+ newConnection.exportedObject = self
+
+ // The remote object is the delegate of the app's IPCConnection instance.
+ newConnection.remoteObjectInterface = NSXPCInterface(with: AppCommunication.self)
+
+ newConnection.invalidationHandler = {
+ self.currentConnection = nil
+ }
+
+ newConnection.interruptionHandler = {
+ self.currentConnection = nil
+ }
+
+ currentConnection = newConnection
+ newConnection.resume()
+
+ return true
+ }
+
+ func enableURLBlocking(_ urls: [String]) {
+ os_log("[SC] π] Enabling URL blocking")
+ guard let providerProxy = currentConnection?.remoteObjectProxyWithErrorHandler({ registerError in
+ os_log("[SC] π] Failed to register with the provider: %@", registerError.localizedDescription)
+ }) as? ProviderCommunication else {
+ fatalError("Failed to create a remote object proxy for the provider")
+ }
+ providerProxy.setBlockedURLs(urls)
+ }
+
+ func enableIPAddressesBlocking(_ urls: [String]) {
+ os_log("[SC] π] Enabling URL blocking")
+ guard let providerProxy = currentConnection?.remoteObjectProxyWithErrorHandler({ registerError in
+ os_log("[SC] π] Failed to register with the provider: %@", registerError.localizedDescription)
+ }) as? ProviderCommunication else {
+ fatalError("Failed to create a remote object proxy for the provider")
+ }
+ providerProxy.setBlockedIPAddresses(urls)
+ }
+}
+
+
+extension IPCConnection: ProviderCommunication {
+
+ func setBlockedIPAddresses(_ ips: [String]) {
+ blockedIPAddresses = Set(ips)
+ os_log("[SC] π] setBlockedIPAddresses: %{public}@", blockedIPAddresses)
+ }
+
+ func setBlockedURLs(_ urls: [String]) {
+ os_log("[SC] π] Blocking: %{public}@",urls)
+ blockedUrls = urls
+// delegate?.didSetUrls()
+ blockedList = BlockOrAllowList(items: blockedUrls)
+
+ guard let connection = currentConnection else {
+ print("[SC] π] Cannot update blocked urls, app isn't registered")
+ return
+ }
+
+ guard let appProxy = connection.remoteObjectProxyWithErrorHandler({ promptError in
+ os_log("[SC] π] Failed to prompt the user: %@", promptError.localizedDescription)
+// self.currentConnection = nil
+// responseHandler(true)
+ }) as? AppCommunication else {
+ fatalError("Failed to create a remote object proxy for the app")
+ }
+ appProxy.didSetUrls()
+ }
+
+
+ // MARK: ProviderCommunication
+
+ func register(_ completionHandler: @escaping (Bool) -> Void) {
+
+ os_log("[SC] π] App registered")
+ completionHandler(true)
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Info.plist b/Self-Control-Extension/SelfControl/SelfControlExtension/Info.plist
new file mode 100644
index 0000000..cf77fee
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Info.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ NetworkExtension
+
+ NEMachServiceName
+ $(TeamIdentifierPrefix)com.application.SelfControl.corebits.network
+ NEProviderClasses
+
+ com.apple.networkextension.filter-data
+ $(PRODUCT_MODULE_NAME).FilterDataProvider
+
+
+
+
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/PlistListner.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/PlistListner.swift
new file mode 100644
index 0000000..d31f946
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/PlistListner.swift
@@ -0,0 +1,58 @@
+//
+// PlistListner.swift
+// SelfControlExtension
+//
+// Created by Satendra Singh on 16/08/25.
+//
+
+import Foundation
+import Network
+import os.log
+
+final class PlistListner: NSObject, ObservableObject {
+
+ var listener: NWListener? = try! NWListener(using: .tcp, on: 8080)
+ var blockeddomainFetcher: (() -> [String])?
+
+ func startListening() {
+ os_log("[SC] π] NW startListening")
+// let objects: [[String: Any]] = [
+// ["id": 1, "name": "Alice", "status": "running"],
+// ["id": 2, "name": "Bob", "status": "stopped"],
+// ["id": 3, "name": "Charlie", "status": "idle"]
+// ]
+ listener = try! NWListener(using: .tcp, on: 8080)
+ listener?.newConnectionHandler = { conn in
+ let blockedDomainList: [String] = self.blockeddomainFetcher?() ?? []
+ let blockedUrls = ["blocked": blockedDomainList]
+ let jsonData = try! JSONSerialization.data(withJSONObject: blockedUrls, options: [])
+ let jsonString = String(data: jsonData, encoding: .utf8)!
+
+ os_log("[SC] π] NW newConnectionHandler")
+ conn.start(queue: DispatchQueue.global(qos: .userInitiated))
+// conn.receiveMessage { data, _, _, _ in
+ os_log("[SC] π] NW conn.receiveMessage")
+ let response = """
+ HTTP/1.1 200 OK\r
+ Content-Type: application/json\r
+ Access-Control-Allow-Origin: *\r
+ \r
+ \(jsonString)
+ """
+ conn.send(content: response.data(using: .utf8), contentContext: .finalMessage , completion: .contentProcessed { error in
+ os_log("[SC] π] NW Sent response:\(error)")
+// conn.cancel()
+
+ })
+// }
+
+ conn.stateUpdateHandler = { state in
+ if state == .ready {
+ os_log("[SC] π] NW stateUpdateHandler ready")
+ }
+ }
+ }
+ listener?.start(queue: DispatchQueue.global(qos: .userInitiated))
+// RunLoop.main.run()
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Binary.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Binary.swift
new file mode 100644
index 0000000..3397339
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Binary.swift
@@ -0,0 +1,175 @@
+//
+// Binary.swift
+// SelfControl
+//
+// Created by Satendra Singh on 10/10/25.
+//
+
+import Foundation
+import AppKit
+import OSLog
+import Security
+import CoreServices // for MDItem
+
+@objcMembers
+class Binary: NSObject {
+
+ // MARK: - Properties (mirroring Binary.h)
+
+ dynamic var path: String
+ dynamic var name: String = ""
+ dynamic var icon: NSImage = NSImage()
+ dynamic var attributes: NSDictionary?
+ dynamic var metadata: NSDictionary?
+ dynamic var bundle: Bundle?
+ dynamic var csInfo: NSMutableDictionary = NSMutableDictionary()
+ dynamic var sha256: NSMutableString = NSMutableString()
+
+ // MARK: - Init
+
+ init(_ path: String) {
+ self.path = (path as NSString).resolvingSymlinksInPath
+ super.init()
+
+ // Try load app bundle (nil for non-apps)
+ self.getBundle()
+
+ // Get name
+ self.getName()
+
+ // File attributes
+ self.getAttributes()
+
+ // Spotlight metadata
+ self.getMetadata()
+ }
+
+ // MARK: - Bundle
+
+ // Try load app bundle; nil for non-apps
+ func getBundle() {
+ // First try direct path
+ if let b = Bundle(path: self.path) {
+ self.bundle = b
+ return
+ }
+ // Else find dynamically
+ self.bundle = Utilities().findAppBundle(self.path)
+ }
+
+ // MARK: - Name
+
+ // Figure out binary's name via bundle CFBundleName or lastPathComponent
+ func getName() {
+ if let bundle = self.bundle,
+ let bundleName = bundle.infoDictionary?["CFBundleName"] as? String {
+ self.name = bundleName
+ } else {
+ self.name = (self.path as NSString).lastPathComponent
+ }
+ }
+
+ // MARK: - Attributes
+
+ func getAttributes() {
+ self.attributes = try? FileManager.default.attributesOfItem(atPath: self.path) as NSDictionary
+ }
+
+ // MARK: - Spotlight metadata
+
+ func getMetadata() {
+ // MDItemCreate wants CFString path
+ let cfPath = self.path as CFString
+ guard let mdItem = MDItemCreate(kCFAllocatorDefault, cfPath) else {
+ return
+ }
+ defer { }
+
+ guard let attributeNames = MDItemCopyAttributeNames(mdItem) else {
+ return
+ }
+ defer { }
+
+ if let attrs = MDItemCopyAttributes(mdItem, attributeNames) {
+ self.metadata = attrs as NSDictionary
+ }
+ }
+
+ // MARK: - Icon
+
+ // Get an icon for the process: appβs icon or system one
+ func getIcon() {
+ // Skip short/non-absolute paths if no bundle (system logs errors otherwise)
+ if !self.path.hasPrefix("/"), self.bundle == nil {
+ return
+ }
+
+ // For apps, try CFBundleIconFile
+ if let bundle = self.bundle {
+ if let iconFile = bundle.infoDictionary?["CFBundleIconFile"] as? String {
+ let iconExt = (iconFile as NSString).pathExtension.isEmpty ? "icns" : (iconFile as NSString).pathExtension
+ let iconBase = (iconFile as NSString).deletingPathExtension
+ if let iconPath = bundle.path(forResource: iconBase, ofType: iconExt) {
+ if let img = NSImage(contentsOfFile: iconPath) {
+ self.icon = img
+ }
+ }
+ }
+ }
+
+ // Fallback to workspace icon
+ if self.bundle == nil || self.icon.size == .zero {
+ self.icon = NSWorkspace.shared.icon(forFile: self.path)
+ }
+
+ // Standard size 128x128
+ self.icon.size = NSSize(width: 128, height: 128)
+ }
+
+ // MARK: - Code signing (static)
+
+ // You likely have this helper already, but defining its expected signature for clarity:
+ func extractSigningInfo(_ code: SecStaticCode?, _ path: String, _ flags: SecCSFlags) -> [String: Any]? {
+ var staticCode: SecStaticCode?
+ let url = URL(fileURLWithPath: path) as CFURL
+ let status = SecStaticCodeCreateWithPath(url, SecCSFlags(), &staticCode)
+ guard status == errSecSuccess, let codeRef = staticCode else {
+ return nil
+ }
+
+ var signingInfo: CFDictionary?
+ let infoStatus = SecCodeCopySigningInformation(codeRef, flags, &signingInfo)
+ guard infoStatus == errSecSuccess, let info = signingInfo as? [String: Any] else {
+ return nil
+ }
+ return info
+ }
+
+ // Your corrected function:
+ func generateSigningInfo(_ flags: SecCSFlags) {
+ // These constants are C macros in Security framework headers, not imported automatically:
+ let kSecCodeInfoStatus = "Status" // corresponds to kSecCodeInfoStatus in Security framework
+ let KEY_CS_STATUS = kSecCodeInfoStatus // for compatibility with your existing code
+
+ // Instead of 'nil', explicitly pass an optional SecStaticCode? = nil
+ if let extracted = extractSigningInfo(nil as SecStaticCode?, self.path, flags),
+ let statusNum = extracted[KEY_CS_STATUS] as? NSNumber,
+ statusNum.intValue == Int(noErr) {
+ // Bridge [String: Any] to NSMutableDictionary
+ self.csInfo = NSMutableDictionary(dictionary: extracted)
+ } else {
+ // logging commented out in original code
+ }
+ }
+
+ // MARK: - Description
+
+ override var description: String {
+ return String(format: "name: %@\npath: %@\nattributes: %@\nsigning info: %@\nmetadata: %@",
+ self.name,
+ self.path,
+ self.attributes ?? [:],
+ self.csInfo,
+ self.metadata ?? [:])
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Process.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Process.swift
new file mode 100644
index 0000000..823bb5b
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Process.swift
@@ -0,0 +1,330 @@
+//
+// Process.swift
+// SelfControl
+//
+// Created by Satendra Singh on 10/10/25.
+//
+
+import Foundation
+import OSLog
+import Security
+import Darwin
+import NetworkExtension
+
+@objcMembers
+class Process: NSObject {
+
+ // MARK: - Properties (mirroring Process.h)
+
+ dynamic var pid: pid_t = -1
+ dynamic var uid: uid_t = UInt32(bitPattern: -1)
+ dynamic var type: UInt16 = 0
+ dynamic var exit: UInt32 = UInt32(bitPattern: -1)
+ dynamic var deleted: Bool = false
+
+ dynamic var name: String?
+ dynamic var path: String?
+ dynamic var arguments: NSMutableArray? = NSMutableArray()
+ dynamic var ancestors: NSMutableArray? = NSMutableArray()
+ dynamic var csInfo: NSMutableDictionary?
+ dynamic var key: String = ""
+ dynamic var bundleID: String = ""
+ dynamic var binary: Binary = Binary.init("")
+ dynamic var timestamp: Date = Date()
+
+ // MARK: - Init
+
+ override init() {
+ super.init()
+ self.arguments = NSMutableArray()
+ self.ancestors = NSMutableArray()
+ self.timestamp = Date()
+ self.pid = -1
+ self.uid = UInt32(bitPattern: -1)
+ self.exit = UInt32(bitPattern: -1)
+ }
+
+ // Init with audit token pointer
+ // Matches -(id)init:(audit_token_t*)token
+ convenience init?(_ token: UnsafePointer) {
+ self.init()
+
+ // Save pid
+ let tokenVal = token.pointee
+ let procPID = audit_token_to_pid(tokenVal)
+ self.pid = procPID
+ if self.pid == 0 {
+// os_log_error(logHandle, "ERROR: 'audit_token_to_pid' returned NULL")
+ return nil
+ }
+
+ // Get path via SecCode (also sets deleted flag)
+ self.getPath(token)
+ if (self.path ?? "").isEmpty {
+// os_log_error(logHandle, "ERROR: failed to find path for process %d", self.pid)
+ return nil
+ }
+
+ // Set name
+ if self.deleted != true {
+ self.name = Utilities().getProcessName(0, self.path!)
+ } else {
+ self.name = Utilities().getProcessName(self.pid, self.path!)
+ }
+
+ // Get user
+ self.uid = audit_token_to_euid(tokenVal)
+ self.bundleID = Utilities().getBundleID(self.path ?? "") ?? ""
+ // Generate (dynamic) code information
+ self.generateSigningInfo(token)
+
+ // Generate key
+ self.key = self.generateKey()
+
+ // Init binary
+ self.binary = Binary(self.path ?? "")
+
+ // PID specific logic: args and ancestors
+ self.getArgs()
+ self.ancestors = Utilities().generateProcessHierarchy(self.pid)
+
+ // Verify pid-version (avoid pid reuse)
+ if let currentTokenNSData = Utilities().tokenForPid(self.pid),
+ currentTokenNSData.length == MemoryLayout.size {
+ let currentTokenData = Data(referencing: currentTokenNSData)
+ currentTokenData.withUnsafeBytes { (rawPtr: UnsafeRawBufferPointer) in
+ if let curPtr = rawPtr.baseAddress?.assumingMemoryBound(to: audit_token_t.self) {
+ let origVersion = audit_token_to_pidversion(tokenVal)
+ let curVersion = audit_token_to_pidversion(curPtr.pointee)
+ if origVersion != curVersion {
+// os_log_error(logHandle, "ERROR: audit token mismatch ...pid re-used?")
+ self.arguments = nil
+ self.ancestors = nil
+ }
+ }
+ }
+ }
+
+ // Process alive check
+ if Utilities().isAlive(self.pid) != true {
+// os_log_error(logHandle, "ERROR: process (%d)%{public}@ has already exited", self.pid, self.path ?? "")
+ return nil
+ }
+ }
+
+ // MARK: - Key generation
+
+ // Matches -(NSString*)generateKey
+ func generateKey() -> String {
+ var id: String = ""
+ var signer: Int = 0
+ enum Signer: Int {
+ case None = 0
+ case Apple
+ case AppStore
+ case DevID
+ case AdHoc
+ }
+
+ if let cs = self.csInfo {
+ if let v = cs[Constants.KEY_CS_SIGNER] as? NSNumber {
+ signer = v.intValue
+ }
+
+ // Apple/App Store: just use cs id
+ if signer == Signer.Apple.rawValue || signer == Signer.AppStore.rawValue {
+ if let csid = cs[Constants.KEY_CS_ID] as? String, !csid.isEmpty {
+ id = csid
+ }
+ }
+ // Dev ID: use cs id + leaf signer
+ else if signer == Int(Signer.DevID.rawValue) {
+ if let csid = cs[Constants.KEY_CS_ID] as? String, !csid.isEmpty,
+ let auths = cs[Constants.KEY_CS_AUTHS] as? [Any], let first = auths.first as? String, !first.isEmpty {
+ id = "\(csid):\(first)"
+ }
+ }
+ }
+
+ if id.isEmpty {
+ id = self.path ?? ""
+ }
+
+// os_log_debug(logHandle, "generated process key: %{public}@", id)
+ return id
+ }
+
+ // MARK: - Path
+
+ // Matches -(void)getPath:(audit_token_t*)token
+ func getPath(_ token: UnsafePointer) {
+ var status: OSStatus = errSecParam
+ var code: SecCode?
+ var staticCode: SecStaticCode?
+ var pathURL: CFURL?
+
+ // SecCodeCopyGuestWithAttributes using audit token
+ let attrs: [String: Any] = [kSecGuestAttributeAudit as String: Data(bytes: token, count: MemoryLayout.size)]
+ status = SecCodeCopyGuestWithAttributes(nil, attrs as CFDictionary, SecCSFlags(), &code)
+ if status == errSecSuccess, let code = code {
+ // Convert dynamic code (SecCode) to static code (SecStaticCode)
+ status = SecCodeCopyStaticCode(code, SecCSFlags(), &staticCode)
+ if status == errSecSuccess, let staticCode = staticCode {
+ status = SecCodeCopyPath(staticCode, SecCSFlags(), &pathURL)
+ if status == errSecSuccess, let url = pathURL as NSURL? {
+ self.path = (url as URL).path
+ } else {
+// os_log_error(logHandle, "ERROR: 'SecCodeCopyPath' failed with': %#x", status)
+ }
+ } else {
+// os_log_error(logHandle, "ERROR: 'SecCodeCopyStaticCode' failed with': %#x", status)
+ }
+ } else {
+// os_log_error(logHandle, "ERROR: 'SecCodeCopyGuestWithAttributes' failed with': %#x", status)
+ }
+
+ // Deleted binary?
+ if status == OSStatus(kPOSIXErrorENOENT) {
+// os_log_debug(logHandle, "process %d's binary appears to be deleted", self.pid)
+ self.deleted = true
+ }
+
+ // Fallback
+ if pathURL == nil {
+ self.path = Utilities().getProcessPath(self.pid)
+ }
+
+ if let p = self.path {
+ self.path = (p as NSString).resolvingSymlinksInPath
+ }
+
+ // No CFRelease in Swift ARC; CoreFoundation objects are memory-managed automatically.
+ }
+
+ // MARK: - Args
+
+ // Matches -(void)getArgs
+ func getArgs() {
+ var mib: [Int32] = [CTL_KERN, KERN_ARGMAX, 0]
+ var systemMaxArgs: Int32 = 0
+ var size = MemoryLayout.size(ofValue: systemMaxArgs)
+
+ // Get system arg max
+ if sysctl(&mib, 2, &systemMaxArgs, &size, nil, 0) == -1 {
+ return
+ }
+
+ guard systemMaxArgs > 0 else { return }
+ let bufSize = Int(systemMaxArgs)
+ let processArgs = UnsafeMutablePointer.allocate(capacity: bufSize)
+ defer { processArgs.deallocate() }
+
+ mib = [CTL_KERN, KERN_PROCARGS2, Int32(self.pid)]
+ size = bufSize
+
+ if sysctl(&mib, 3, processArgs, &size, nil, 0) == -1 {
+ return
+ }
+
+ if size <= MemoryLayout.size {
+ return
+ }
+
+ // numberOfArgs at start
+ var numberOfArgs: Int32 = 0
+ memcpy(&numberOfArgs, processArgs, MemoryLayout.size)
+
+ // parser after numberOfArgs
+ var parser = processArgs.advanced(by: MemoryLayout.size)
+ let endPtr = processArgs.advanced(by: size)
+
+ // Skip executable path (NULL-terminated)
+ while parser < endPtr {
+ if parser.pointee == 0 { break }
+ parser = parser.advanced(by: 1)
+ }
+ if parser == endPtr { return }
+
+ // Skip trailing NULLs to argv[0]
+ while parser < endPtr {
+ if parser.pointee != 0 { break }
+ parser = parser.advanced(by: 1)
+ }
+ if parser == endPtr { return }
+
+ // Now parse args until count reached
+ var argStart = parser
+ let argsArray = NSMutableArray()
+ while parser < endPtr {
+ if parser.pointee == 0 {
+ if let arg = String(validatingUTF8: UnsafePointer(argStart)) {
+ argsArray.add(arg)
+ }
+ parser = parser.advanced(by: 1)
+ argStart = parser
+ if argsArray.count == Int(numberOfArgs) {
+ break
+ }
+ continue
+ }
+ parser = parser.advanced(by: 1)
+ }
+
+ self.arguments = argsArray
+ }
+
+ // MARK: - Code signing
+
+ // Local key mapping for Security's kSecCodeInfoStatus
+ private let KEY_CS_STATUS = "Status"
+
+ // Helper to extract signing info from an audit token (dynamic code)
+ private func extractSigningInfo(_ token: UnsafeMutablePointer,
+ _ requirement: SecRequirement?,
+ _ flags: SecCSFlags) -> NSMutableDictionary? {
+ // Build attributes with audit token
+ let attrs: [String: Any] = [kSecGuestAttributeAudit as String:
+ Data(bytes: token, count: MemoryLayout.size)]
+ var code: SecCode?
+ var status = SecCodeCopyGuestWithAttributes(nil, attrs as CFDictionary, flags, &code)
+ guard status == errSecSuccess, let codeRef = code else {
+ return nil
+ }
+
+ // Convert SecCode (dynamic) to SecStaticCode before querying signing info
+ var staticCode: SecStaticCode?
+ status = SecCodeCopyStaticCode(codeRef, flags, &staticCode)
+ guard status == errSecSuccess, let staticCodeRef = staticCode else {
+ return nil
+ }
+
+ var info: CFDictionary?
+ status = SecCodeCopySigningInformation(staticCodeRef, flags, &info)
+ guard status == errSecSuccess, let dict = info as? [String: Any] else {
+ return nil
+ }
+ return NSMutableDictionary(dictionary: dict)
+ }
+
+ // Matches -(void)generateSigningInfo:(audit_token_t*)token
+ func generateSigningInfo(_ token: UnsafePointer) {
+ // Pass typed nil for requirement to avoid "nil requires a contextual type"
+ let requirement: SecRequirement? = nil
+ if let extracted = extractSigningInfo(UnsafeMutablePointer(mutating: token), requirement, SecCSFlags()) {
+ if let statusNum = extracted[KEY_CS_STATUS] as? NSNumber, statusNum.intValue == Int(noErr) {
+ self.csInfo = extracted
+ } else {
+// os_log_error(logHandle, "ERROR: invalid code signing information for %{public}@: %{public}@", self.path ?? "", extracted)
+ }
+ } else {
+// os_log_error(logHandle, "ERROR: failed to extract code signing information for %{public}@", self.path ?? "")
+ }
+ }
+
+ // MARK: - Description
+
+ override var description: String {
+ return String(format: "pid: %d\npath: %@\nuser: %d\nargs: %@\nancestors: %@\n signing info: %@\n binary:\n%@", self.pid, self.path ?? "nil", self.uid, self.arguments ?? [], self.ancestors ?? [], self.csInfo ?? [:], self.binary.description)
+ }
+}
+
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Process/SourceAppAuditTokenBuilder.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/SourceAppAuditTokenBuilder.swift
new file mode 100644
index 0000000..68b9641
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/SourceAppAuditTokenBuilder.swift
@@ -0,0 +1,61 @@
+//
+// SourceAppAuditTokenBuilder.swift
+// SelfControlExtension
+//
+// Created by Satendra Singh on 10/10/25.
+//
+
+import NetworkExtension
+import Security
+import Foundation
+import AppKit
+
+struct SourceAppAuditTokenBuilder {
+
+ static func getAppInfo(from flow: NEFilterFlow) -> (bundleID: String?, appName: String?) {
+ guard let auditTokenData = flow.sourceAppAuditToken else {
+ return (nil, nil)
+ }
+
+ // Convert NSData β audit_token_t
+ let auditToken = auditTokenData.withUnsafeBytes { ptr -> audit_token_t in
+ return ptr.load(as: audit_token_t.self)
+ }
+
+ // MARK: 1. Extract PID from audit token
+ let pid = audit_token_to_pid(auditToken)
+
+ // MARK: 2. Use SecTask to get bundle identifier
+ var bundleID: String? = nil
+ if let secTask = SecTaskCreateWithAuditToken(nil, auditToken) {
+ // macOS doesnβt define kSecEntitlementApplicationIdentifier constant,
+ // so use the raw entitlement string instead:
+ let entitlementKey = "application-identifier" as CFString
+ bundleID = SecTaskCopyValueForEntitlement(secTask, entitlementKey, nil) as? String
+ }
+
+ // MARK: 3. Try to get app name from bundle ID
+ var appName: String? = nil
+ if let bundleID = bundleID,
+ let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) {
+ appName = appURL.deletingPathExtension().lastPathComponent
+ }
+
+ // MARK: 4. Fallback: get executable name via proc_pidpath()
+ if appName == nil {
+ var pathBuffer = [CChar](repeating: 0, count: Int(PATH_MAX))
+ let result = proc_pidpath(pid, &pathBuffer, UInt32(pathBuffer.count))
+ if result > 0 {
+ let path = String(cString: pathBuffer)
+ appName = URL(fileURLWithPath: path).lastPathComponent
+ }
+ }
+
+ return (bundleID, appName)
+ }
+
+ // Helper: extract PID from audit_token_t
+ private static func audit_token_to_pid(_ token: audit_token_t) -> pid_t {
+ return pid_t(token.val.0)
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Utilities.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Utilities.swift
new file mode 100644
index 0000000..0ee8216
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Utilities.swift
@@ -0,0 +1,714 @@
+//
+// File.swift
+// SelfControl
+//
+// Created by Satendra Singh on 10/10/25.
+//
+
+
+// Swift port of utilities.m
+//
+// Note: This file relies on C system APIs. Ensure your bridging header
+// imports the needed headers: libproc.h, sys/sysctl.h, CommonCrypto/CommonCrypto.h,
+// mach/mach.h, Security/Security.h, Carbon/Carbon.h, CoreServices/CoreServices.h, CFNetwork/CFHost.h
+//
+
+import AppKit
+import Foundation
+import OSLog
+import SystemConfiguration
+import Darwin
+import CoreServices // for MDItem
+import Security
+import os.log
+import os
+
+// Define C macro from libproc.h (not imported automatically into Swift)
+private let PROC_PIDPATHINFO_MAXSIZE: Int = 4096
+
+// MARK: - Globals
+
+final class Utilities {
+ // Provided elsewhere in project
+
+ let logger = Logger(subsystem: "com.example.myapp", category: "network")
+
+ // MARK: - Version / Bundle
+
+ @objc
+ public func getAppVersion() -> String? {
+ return Bundle.main.infoDictionary?["CFBundleVersion"] as? String
+ }
+
+ @objc
+ public func getBundleExecutable(_ appPath: String) -> String? {
+ guard let bundle = Bundle(path: appPath) else {
+ logger.error("ERROR: failed to load app bundle for %{public}\(appPath)")
+ return nil
+ }
+ return (bundle.executablePath as NSString?)?.resolvingSymlinksInPath
+ }
+
+ // MARK: - Parent process (Carbon/deprecated)
+
+ @objc
+ public func getRealParent(_ pid: pid_t) -> NSDictionary? {
+ let PROC_PIDPATHINFO_MAXSIZE = 4096
+
+ // Step 1: Get parent PID
+ var info = kinfo_proc()
+ var size = MemoryLayout.stride
+ var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]
+
+ guard sysctl(&mib, u_int(mib.count), &info, &size, nil, 0) == 0 else {
+ return nil
+ }
+
+ let ppid = info.kp_eproc.e_ppid
+ if ppid == 0 { return nil }
+
+ // Step 2: Get parent process path
+ var pathBuffer = [CChar](repeating: 0, count: Int(PROC_PIDPATHINFO_MAXSIZE))
+ let result = proc_pidpath(ppid, &pathBuffer, UInt32(pathBuffer.count))
+ let path = (result > 0) ? String(cString: pathBuffer) : nil
+
+ // Step 3: Try to get bundle info via NSRunningApplication
+ var appName: String? = nil
+ var bundleID: String? = nil
+
+ if let app = NSRunningApplication(processIdentifier: ppid) {
+ appName = app.localizedName
+ bundleID = app.bundleIdentifier
+ }
+
+ // Step 4: Construct NSDictionary for compatibility
+ let dict: NSDictionary = [
+ "ParentPID": NSNumber(value: ppid),
+ "ParentPath": path ?? "",
+ "ParentAppName": appName ?? "",
+ "ParentBundleID": bundleID ?? ""
+ ]
+
+ return dict
+ }
+
+
+ // MARK: - Process hierarchy
+
+ @objc
+ public func generateProcessHierarchy(_ child: pid_t) -> NSMutableArray {
+ let ancestors = NSMutableArray()
+ typealias RPIDFunc = @convention(c) (pid_t) -> pid_t
+ let rpidSym = dlsym(dlopen(nil, RTLD_NOW), "responsibility_get_pid_responsible_for_pid")
+ let getRPID = rpidSym.map { unsafeBitCast($0, to: RPIDFunc.self) }
+
+ var currentPID = child
+
+ while true {
+ let currentPath = getProcessPath(currentPID) ?? NSLocalizedString("unknown", comment: "unknown")
+ let currentName = getProcessName(0, currentPath) ?? NSLocalizedString("unknown", comment: "unknown")
+ ancestors.insert([
+ Constants.KEY_PROCESS_ID: NSNumber(value: currentPID),
+ Constants.KEY_PROCESS_PATH: currentPath,
+ Constants.KEY_PROCESS_NAME: currentName
+ ], at: 0)
+
+ var parentPID: pid_t = 0
+
+ if getuid() != 0 {
+ if let parent = getRealParent(currentPID), let p = parent["pid"] as? NSNumber {
+ parentPID = pid_t(truncating: p)
+ }
+ }
+
+ if parentPID == 0, let getRPID = getRPID {
+ parentPID = getRPID(currentPID)
+ }
+
+ if parentPID <= 0 || parentPID == currentPID {
+ parentPID = getParent(Int32(currentPID))
+ }
+
+ if parentPID <= 0 || parentPID == currentPID {
+ break
+ }
+
+ currentPID = parentPID
+ }
+
+ // add KEY_INDEX for UI
+ for i in 0.. String? {
+ var uid: uid_t = 0
+ var gid: gid_t = 0
+
+ // explicitly type `nil` as `SCDynamicStore?`
+ if let cfUser = SCDynamicStoreCopyConsoleUser(nil as SCDynamicStore?, &uid, &gid) {
+ return cfUser as String
+ }
+ return nil
+ }
+
+ // MARK: - Process name
+
+ @objc
+ public func getProcessName(_ pid: pid_t, _ path: String) -> String? {
+
+ let PROC_PIDPATHINFO_MAXSIZE = 4096 // actual constant value from libproc.h
+
+ if pid != 0 {
+ var nameBuf = [CChar](repeating: 0, count: Int(PROC_PIDPATHINFO_MAXSIZE))
+ let status = proc_name(pid, &nameBuf, UInt32(nameBuf.count))
+ if status >= 0 {
+ return String(cString: nameBuf)
+ }
+ }
+
+ if let bundle = findAppBundle(path),
+ let name = bundle.infoDictionary?["CFBundleName"] as? String {
+ return name
+ }
+
+ return (path as NSString).lastPathComponent
+ }
+
+ @objc
+ public func getBundleID(_ path: String) -> String? {
+ if let bundle = findAppBundle(path),
+ let name = bundle.infoDictionary?["CFBundleIdentifier"] as? String {
+ return name
+ }
+ return nil
+ }
+
+ // MARK: - Find app bundle
+
+ @objc
+ public func findAppBundle(_ path: String) -> Bundle? {
+ let standardized = ((path as NSString).standardizingPath as NSString).resolvingSymlinksInPath
+ var appPath: NSString = standardized as NSString
+
+ while true {
+ if let bundle = Bundle(path: appPath as String) {
+ if bundle.bundlePath == standardized { return bundle }
+ if bundle.executablePath == standardized { return bundle }
+ }
+
+ let next = appPath.deletingLastPathComponent as NSString
+ if next.length == 0 || next as String == "/" { break }
+ appPath = next
+ }
+
+ return nil
+ }
+
+ // MARK: - Process path
+
+ @objc
+ public func getProcessPath(_ pid: pid_t) -> String? {
+ var buf = [CChar](repeating: 0, count: Int(PROC_PIDPATHINFO_MAXSIZE))
+ let status = proc_pidpath(pid, &buf, UInt32(buf.count))
+ if status > 0 {
+ return String(cString: buf)
+ } else {
+// os_log_error(logHandle, "ERROR: for process %d, 'proc_pidpath' failed with %d (errno: %d)", pid, status, errno)
+
+ var mib = [CTL_KERN, KERN_ARGMAX, 0]
+ var argMax: Int = 0
+ var size = MemoryLayout.size(ofValue: argMax)
+ if sysctl(&mib, 2, &argMax, &size, nil, 0) == -1 { return nil }
+
+ let argBuf = UnsafeMutablePointer.allocate(capacity: argMax)
+ defer { argBuf.deallocate() }
+
+ mib = [CTL_KERN, KERN_PROCARGS2, Int32(pid)]
+ size = argMax
+ if sysctl(&mib, 3, argBuf, &size, nil, 0) == -1 { return nil }
+ if size <= MemoryLayout.size { return nil }
+
+ // argv0 path follows int argc
+ let pathPtr = argBuf.advanced(by: MemoryLayout.size)
+ let p = String(cString: pathPtr)
+
+ if p.hasPrefix("./") {
+ let trimmed = String(p.dropFirst(2))
+ if let cwd = getProcessCWD(pid) {
+ return (cwd as NSString).appendingPathComponent(trimmed)
+ }
+ }
+
+ return p
+ }
+ }
+
+ // MARK: - CWD
+
+ @objc
+ public func getProcessCWD(_ pid: pid_t) -> String? {
+ var vpi = proc_vnodepathinfo()
+ let status = withUnsafeMutablePointer(to: &vpi) { ptr -> Int32 in
+ return proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, ptr, Int32(MemoryLayout.size))
+ }
+ if status > 0 {
+ return withUnsafePointer(to: vpi.pvi_cdir.vip_path) { ptr in
+ return String(cString: UnsafeRawPointer(ptr).assumingMemoryBound(to: CChar.self))
+ }
+ }
+ return nil
+ }
+
+ // MARK: - PIDs for path/user
+
+ @objc
+ public func getProcessIDs(_ processPath: String, _ userID: Int32) -> NSMutableArray {
+ let result = NSMutableArray()
+
+ let count = proc_listallpids(nil, 0)
+ guard count > 0 else { return result }
+
+ let pids = UnsafeMutablePointer.allocate(capacity: Int(count))
+ defer { pids.deallocate() }
+
+ let status = proc_listallpids(pids, count * Int32(MemoryLayout.size))
+ guard status >= 0 else { return result }
+
+ var mib = [CTL_KERN, KERN_PROC, KERN_PROC_PID, 0]
+ var kproc = kinfo_proc()
+ let kprocSize = MemoryLayout.size
+
+ for i in 0.. NSImage? {
+ // invalid path -> generic application icon
+ guard FileManager.default.fileExists(atPath: path) else {
+ let icon = NSWorkspace.shared.icon(forFileType: NSFileTypeForHFSTypeCode(OSType(kGenericApplicationIcon)))
+ icon.size = NSSize(width: 128, height: 128)
+ return icon
+ }
+
+ if let bundle = findAppBundle(path) {
+ if let icon = NSWorkspace.shared.icon(forFile: bundle.bundlePath) as NSImage? {
+ return icon
+ }
+
+ if let iconFile = bundle.infoDictionary?["CFBundleIconFile"] as? NSString {
+ var ext = iconFile.pathExtension
+ if ext.isEmpty { ext = "icns" }
+ if let iconPath = bundle.path(forResource: iconFile.deletingPathExtension, ofType: ext) {
+ return NSImage(contentsOfFile: iconPath)
+ }
+ }
+ }
+
+ var icon = NSWorkspace.shared.icon(forFile: path)
+ // replace generic document icon with application icon
+ let documentIcon = NSWorkspace.shared.icon(forFileType: NSFileTypeForHFSTypeCode(OSType(kGenericDocumentIcon)))
+ if icon == documentIcon {
+ icon = NSWorkspace.shared.icon(forFileType: NSFileTypeForHFSTypeCode(OSType(kGenericApplicationIcon)))
+ }
+ icon.size = NSSize(width: 128, height: 128)
+ return icon
+ }
+
+ // MARK: - Make modal
+
+ @objc
+ public func makeModal(_ controller: NSWindowController) {
+ var window: NSWindow?
+
+ for _ in 0..<20 {
+ DispatchQueue.main.sync {
+ window = controller.window
+ }
+ if window == nil {
+ Thread.sleep(forTimeInterval: 0.05)
+ continue
+ }
+ DispatchQueue.main.sync {
+ NSApplication.shared.runModal(for: controller.window!)
+ }
+ break
+ }
+ }
+
+ // MARK: - Find processes by name
+
+ @objc
+ public func findProcesses(_ processName: String) -> NSMutableArray {
+ let processes = NSMutableArray()
+ let count = proc_listpids(UInt32(PROC_ALL_PIDS), 0, nil, 0)
+ guard count > 0 else { return processes }
+
+ let pids = UnsafeMutablePointer.allocate(capacity: Int(count))
+ defer { pids.deallocate() }
+
+ let status = proc_listpids(UInt32(PROC_ALL_PIDS), 0, pids, count * Int32(MemoryLayout.size))
+ guard status >= 0 else { return processes }
+
+ for i in 0.. Date? {
+// os_log_debug(logHandle, "extracting 'kMDItemDateAdded' for %{public}@", file)
+//
+// let url: URL
+// if let bundle = findAppBundle(file) {
+// url = bundle.bundleURL
+// } else {
+// url = URL(fileURLWithPath: file)
+// }
+//
+// guard let item = MDItemCreateWithURL(nil, url as CFURL) else { return nil }
+// defer { CFRelease(item) }
+//
+// if let date = MDItemCopyAttribute(item, kMDItemDateAdded)?.takeRetainedValue() as? Date {
+// os_log_debug(logHandle, "extacted date, %{public}@, for %{public}@", String(describing: date), file)
+// return date
+// } else {
+// os_log_debug(logHandle, "'kMDItemDateAdded' is nil ...falling back to 'kMDItemFSCreationDate'")
+// if let date = MDItemCopyAttribute(item, kMDItemFSCreationDate)?.takeRetainedValue() as? Date {
+// os_log_debug(logHandle, "extacted date, %{public}@, for %{public}@", String(describing: date), file)
+// return date
+// }
+// }
+// return nil
+// }
+
+ // MARK: - Parent PID
+
+ @objc
+ public func getParent(_ pid: Int32) -> pid_t {
+ var kproc = kinfo_proc()
+ var size = MemoryLayout.size
+ var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid] // π consistent Int32s
+ let res = sysctl(&mib, u_int(mib.count), &kproc, &size, nil, 0)
+ if res == 0 && size != 0 {
+ let ppid = kproc.kp_eproc.e_ppid
+// os_log_debug(logHandle, "extracted parent ID %d for process: %d", ppid, pid)
+ return ppid
+ }
+ return -1
+ }
+
+ // MARK: - Dark mode
+
+ @objc
+ public func isDarkMode() -> Bool {
+ return UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
+ }
+
+ // MARK: - String default
+
+ @objc
+ public func valueForStringItem(_ item: String?) -> String {
+ return item ?? NSLocalizedString("unknown", comment: "unknown")
+ }
+
+ // MARK: - Alerts
+
+ @objc
+ public func showAlert(_ style: NSAlert.Style, _ messageText: String, _ informativeText: String?, _ buttons: [String]) -> NSApplication.ModalResponse {
+ let alert = NSAlert()
+ alert.alertStyle = style
+ alert.messageText = messageText
+ if let info = informativeText {
+ alert.informativeText = info
+ }
+ for title in buttons {
+ alert.addButton(withTitle: title)
+ }
+ if let first = alert.buttons.first {
+ first.keyEquivalent = "\r"
+ }
+
+ NSApp.setActivationPolicy(.regular)
+ if #available(macOS 14.0, *) {
+ NSApp.activate()
+ } else {
+ NSApp.activate(ignoringOtherApps: true)
+ }
+ NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
+
+ alert.window.makeKeyAndOrderFront(nil)
+ alert.window.center()
+
+ let response = alert.runModal()
+// (NSApp.delegate as? AppDelegate)?.setActivationPolicy()
+ return response
+ }
+
+ // MARK: - Audit token for pid
+
+ // Swift doesn't expose this C macro β we define it manually.
+ let TASK_AUDIT_TOKEN_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size)
+
+ @objc
+ public func tokenForPid(_ pid: pid_t) -> NSData? {
+ var task: mach_port_t = 0
+ var token = audit_token_t()
+ var size = TASK_AUDIT_TOKEN_COUNT
+
+// os_log_debug(logHandle, "retrieving audit token for %d", pid)
+
+ var kr = task_name_for_pid(mach_task_self_, pid, &task)
+ guard kr == KERN_SUCCESS else {
+// os_log_error(logHandle, "ERROR: 'task_name_for_pid' failed with %x", kr)
+ return nil
+ }
+ defer { mach_port_deallocate(mach_task_self_, task) }
+
+ kr = withUnsafeMutablePointer(to: &token) { ptr in
+ ptr.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { intPtr in
+ return task_info(task, task_flavor_t(TASK_AUDIT_TOKEN), intPtr, &size)
+ }
+ }
+
+ guard kr == KERN_SUCCESS else {
+// os_log_error(logHandle, "ERROR: 'task_info' failed with %x", kr)
+ return nil
+ }
+
+// os_log_debug(logHandle, "retrieved audit token")
+ return NSData(bytes: &token, length: MemoryLayout.size)
+ }
+
+ // MARK: - Reverse DNS resolve
+
+ @objc
+ public func resolveAddress(_ ipAddr: String) -> NSArray? {
+// os_log_debug(logHandle, "(attempting to) reverse resolve %{public}@", ipAddr)
+
+ var hints = addrinfo(ai_flags: AI_NUMERICHOST, ai_family: PF_UNSPEC, ai_socktype: SOCK_STREAM, ai_protocol: 0, ai_addrlen: 0, ai_canonname: nil, ai_addr: nil, ai_next: nil)
+ var res: UnsafeMutablePointer?
+ guard getaddrinfo(ipAddr, nil, &hints, &res) == 0, let result = res else {
+ return nil
+ }
+ defer { freeaddrinfo(result) }
+
+ guard let addrData = CFDataCreate(nil, UnsafePointer(OpaquePointer(result.pointee.ai_addr)), CFIndex(result.pointee.ai_addrlen)) else {
+ return nil
+ }
+ defer { /*addrData*/ }
+
+ let host = CFHostCreateWithAddress(kCFAllocatorDefault, addrData).takeRetainedValue()
+ var streamErr = CFStreamError()
+ guard CFHostStartInfoResolution(host, .names, &streamErr) else {
+ return nil
+ }
+ guard let names = CFHostGetNames(host, nil)?.takeUnretainedValue() as NSArray? else {
+ return nil
+ }
+ return names
+ }
+
+ // MARK: - Process alive
+
+ @objc
+ public func isAlive(_ processID: pid_t) -> Bool {
+ errno = 0
+ _ = kill(processID, 0)
+ if errno == ESRCH { return false }
+
+ var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, Int32(processID)]
+ var info = kinfo_proc()
+ var size = MemoryLayout.size
+ if sysctl(&mib, 4, &info, &size, nil, 0) == 0 {
+ // SZOMB check
+ if (UInt8(info.kp_proc.p_stat) & UInt8(SZOMB)) == UInt8(SZOMB) {
+ return false
+ }
+ }
+ return true
+ }
+
+ // MARK: - Simulator app?
+
+ @objc
+ public func isSimulatorApp(_ path: String) -> Bool {
+// os_log_debug(logHandle, "checking if %{public}@ is a simulator application", path)
+ guard let bundle = findAppBundle(path) else { return false }
+ guard let platforms = bundle.infoDictionary?["CFBundleSupportedPlatforms"] as? [String], !platforms.isEmpty else {
+ return false
+ }
+// os_log_debug(logHandle, "supported platforms: %{public}@", String(describing: platforms))
+ let set = Set(platforms)
+ return set.isSubset(of: Set(["iPhoneSimulator", "AppleTVSimulator"]))
+ }
+
+ // MARK: - Launched by user?
+
+ @objc
+ public func launchedByUser() -> Bool {
+ guard let parent = getRealParent(getpid()) else { return false }
+ let bid = parent["CFBundleIdentifier"] as? NSString
+ if bid == "com.apple.dock" || bid == "com.apple.finder" || bid == "com.apple.Terminal" {
+ return true
+ }
+ return false
+ }
+
+ // MARK: - Fade out
+
+ @objc
+ public func fadeOut(_ window: NSWindow, _ duration: Float) {
+ NSAnimationContext.runAnimationGroup { ctx in
+ ctx.duration = TimeInterval(duration)
+ window.animator().alphaValue = 0.0
+ } completionHandler: {
+ window.close()
+ }
+ }
+
+ // MARK: - Code-signing info match
+
+// @objc
+// public func matchesCSInfo(_ csInfo1: NSDictionary?, _ csInfo2: NSDictionary?) -> Bool {
+// var status1 = -1, status2 = -1
+// var signer1 = -1, signer2 = -1
+// var id1: String?, id2: String?
+// var auths1: [Any]?, auths2: [Any]?
+//
+// if let n = csInfo1?[KEY_CS_STATUS] as? NSNumber { status1 = n.intValue }
+// if let n = csInfo2?[KEY_CS_STATUS] as? NSNumber { status2 = n.intValue }
+// if status1 != status2 {
+// os_log_error(logHandle, "ERROR: code signing mismatch (signing status): %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2))
+// return false
+// }
+//
+// if let n = csInfo1?[KEY_CS_SIGNER] as? NSNumber { signer1 = n.intValue }
+// if let n = csInfo2?[KEY_CS_SIGNER] as? NSNumber { signer2 = n.intValue }
+// if signer1 != signer2 {
+// if (signer1 == Apple && signer2 == AppStore) || (signer1 == AppStore && signer2 == Apple) {
+// os_log_error(logHandle, "ignoring case where Apple App moved to/from Mac App Store: %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2))
+// } else {
+// os_log_error(logHandle, "ERROR: code signing mismatch (signer): %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2))
+// return false
+// }
+// }
+//
+// if let s = csInfo1?[KEY_CS_ID] as? String { id1 = s }
+// if let s = csInfo2?[KEY_CS_ID] as? String { id2 = s }
+// if (id1 != nil || id2 != nil), id1 != id2 {
+// os_log_error(logHandle, "ERROR: code signing mismatch (signing ID): %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2))
+// return false
+// }
+//
+// if let a = csInfo1?[KEY_CS_AUTHS] as? [Any] { auths1 = a }
+// if let a = csInfo2?[KEY_CS_AUTHS] as? [Any] { auths2 = a }
+// if (auths1 != nil || auths2 != nil) && !(auths1 as NSArray? ?? []).isEqual(to: auths2 ?? []) {
+// os_log_error(logHandle, "ERROR: code signing mismatch (signing auths): %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2))
+// return false
+// }
+//
+// return true
+// }
+
+ // MARK: - Escape JSON
+
+ @objc
+ public func toEscapedJSON(_ input: String) -> String? {
+ do {
+ let data = try JSONSerialization.data(withJSONObject: input, options: .fragmentsAllowed)
+ return String(data: data, encoding: .utf8)
+ } catch {
+// os_log_error(logHandle, "ERROR: failed to convert/escape %{public}@ to JSON (error: %{public}@)", input, error.localizedDescription)
+ return nil
+ }
+ }
+
+ // MARK: - Absolute date from HH:mm (next 24h)
+
+ @objc
+ public func absoluteDate(_ date: Date) -> Date {
+// os_log_debug(logHandle, "function '%{public}s' invoked with %{public}@", #function, String(describing: date))
+
+ let now = Date()
+ let cal = Calendar.current
+
+ let comps = cal.dateComponents([.hour, .minute], from: date)
+ var nowComps = cal.dateComponents([.year, .month, .day, .hour, .minute], from: now)
+ nowComps.hour = comps.hour
+ nowComps.minute = comps.minute
+
+ var absDate = cal.date(from: nowComps) ?? now
+ if absDate < now {
+ absDate = cal.date(byAdding: .day, value: 1, to: absDate) ?? absDate
+ }
+ return absDate
+ }
+
+ // MARK: - Internal volume?
+
+ @objc
+ public func isInternalProcess(_ path: String) -> Bool {
+ var isInternal: AnyObject?
+ do {
+ var url = URL(fileURLWithPath: path)
+ try (url as NSURL).getResourceValue(&isInternal, forKey: URLResourceKey.volumeIsInternalKey)
+ return (isInternal as? NSNumber)?.boolValue ?? false
+ } catch {
+// os_log_error(logHandle, "ERROR: 'getResourceValue'/'NSURLVolumeIsInternalKey' failed with %@", error.localizedDescription)
+ return false
+ }
+ }
+
+
+}
+
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/README.md b/Self-Control-Extension/SelfControl/SelfControlExtension/README.md
new file mode 100644
index 0000000..234977c
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/README.md
@@ -0,0 +1,8 @@
+# SelfControlExtension
+
+This directory contains the NetworkExtension implementation for SelfControl, responsible for URL and domain filtering.
+
+## Structure
+
+- `FilterProvider/` - NetworkExtension filter provider implementation
+- `Configuration/` - Extension configuration code
\ No newline at end of file
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/ReverseDomainMapper.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/ReverseDomainMapper.swift
new file mode 100644
index 0000000..0610f54
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/ReverseDomainMapper.swift
@@ -0,0 +1,57 @@
+//
+// ReverseDomainMapper.swift
+// SelfControlExtension
+//
+// Created by Satendra Singh on 14/10/25.
+//
+
+import Foundation
+
+final class ReverseDomainMapper {
+ static func reverseDNSUsingGetNameInfo(ipAddress: String) -> String? {
+ var addr = sockaddr_in()
+ addr.sin_len = UInt8(MemoryLayout.size)
+ addr.sin_family = sa_family_t(AF_INET)
+ inet_pton(AF_INET, ipAddress, &addr.sin_addr)
+
+ var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
+
+ // Precompute length so we don't read from `addr` inside the closure.
+ let addrLen = socklen_t(addr.sin_len)
+
+ let result: Int32 = withUnsafePointer(to: &addr) { ptr in
+ ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { saPtr in
+ getnameinfo(saPtr,
+ addrLen,
+ &hostname, socklen_t(hostname.count),
+ nil, 0,
+ NI_NAMEREQD)
+ }
+ }
+
+ guard result == 0 else { return nil }
+ return String(cString: hostname)
+ }
+}
+
+enum Regex {
+ static let ipAddress = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
+ static let hostname = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"
+}
+
+extension String {
+ var isValidIpAddress: Bool {
+ return self.matches(pattern: Regex.ipAddress)
+ }
+
+ var isValidHostname: Bool {
+ return self.matches(pattern: Regex.hostname)
+ }
+
+ private func matches(pattern: String) -> Bool {
+ return self.range(of: pattern,
+ options: .regularExpression,
+ range: nil,
+ locale: nil) != nil
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements b/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements
new file mode 100644
index 0000000..f6ded53
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.developer.networking.networkextension
+
+ content-filter-provider-systemextension
+
+ com.apple.security.application-groups
+
+ $(TeamIdentifierPrefix)com.application.SelfControl.corebits
+
+
+
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/String+URL.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/String+URL.swift
new file mode 100644
index 0000000..b2758df
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/String+URL.swift
@@ -0,0 +1,35 @@
+//
+// String+URL.swift
+// SelfControlExtension
+//
+// Created by Satendra Singh on 05/08/25.
+//
+
+import Foundation
+
+extension String {
+ var domainString: String? {
+ Self.extractDomain(from: self)
+ }
+
+ static func extractDomain(from urlString: String) -> String? {
+ var formattedURLString = urlString
+ if !formattedURLString.lowercased().hasPrefix("http://") &&
+ !formattedURLString.lowercased().hasPrefix("https://") {
+ formattedURLString = "https://" + formattedURLString
+ }
+
+ guard let url = URL(string: formattedURLString),
+ let host = url.host else {
+ return nil
+ }
+
+ // Optional: extract root domain (e.g., "facebook.com" from "sub.facebook.com")
+ let components = host.components(separatedBy: ".")
+ if components.count >= 2 {
+ return components.suffix(2).joined(separator: ".")
+ } else {
+ return host
+ }
+ }
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/TLDURLToDomain.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/TLDURLToDomain.swift
new file mode 100644
index 0000000..f652a95
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/TLDURLToDomain.swift
@@ -0,0 +1,49 @@
+//
+// TLDURLToDomain.swift
+// SelfControlExtension
+//
+// Created by Satendra Singh on 15/10/25.
+//
+
+import Foundation
+
+final class TLDURLToDomain {
+ func domain(for tldURL: URL) -> String? {
+ return tldURL.host
+ }
+
+ private func extractDomain(from url: URL) -> String? {
+ return url.host
+ }
+
+
+ func getDomain(from urlString: String) -> String? {
+ guard let url = URL(string: urlString) else {
+ print("Invalid URL string.")
+ return nil
+ }
+
+ // Using URL.host
+ if let host = url.host {
+ return host
+ }
+ return nil
+ }
+
+ static func getURLDomain(from input: String) -> String? {
+ // Normalize input to ensure it has a scheme
+ let formatted = input.contains("://") ? input : "https://\(input)"
+ guard let host = URL(string: formatted)?.host else { return nil }
+
+ let parts = host.split(separator: ".")
+ guard parts.count >= 2 else { return host }
+
+ // Return last two parts (e.g., facebook.com, google.co.uk)
+ if parts.count >= 3 && parts[parts.count - 2] == "co" {
+ return parts.suffix(3).joined(separator: ".") // handles e.g. google.co.uk
+ } else {
+ return parts.suffix(2).joined(separator: ".")
+ }
+ }
+
+}
diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/main.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/main.swift
new file mode 100644
index 0000000..ffadae7
--- /dev/null
+++ b/Self-Control-Extension/SelfControl/SelfControlExtension/main.swift
@@ -0,0 +1,18 @@
+//
+// main.swift
+// SelfControlExtension
+//
+// Created by Egzon Arifi on 02/04/2025.
+//
+
+import Foundation
+import NetworkExtension
+import os.log
+
+autoreleasepool {
+ os_log("[SC] π] first light")
+ NEProvider.startSystemExtensionMode()
+ IPCConnection.shared.startListener()
+}
+
+dispatchMain()
diff --git a/Self-Control-Extension/dynamic-url-blocker/background.js b/Self-Control-Extension/dynamic-url-blocker/background.js
new file mode 100644
index 0000000..3625f6a
--- /dev/null
+++ b/Self-Control-Extension/dynamic-url-blocker/background.js
@@ -0,0 +1,199 @@
+
+// Dynamic URL Blocker β background service worker (Manifest V3)
+
+// ---- Helpers ----
+function normalizeDomain(input) {
+ // strip protocol, path, query, hash
+ let domain = (input || "").trim().toLowerCase();
+ domain = domain.replace(/^https?:\/\//, "");
+ domain = domain.replace(/^www\./, "");
+ domain = domain.split("/")[0].split("?")[0].split("#")[0];
+ // allow punycode etc. Keep only allowed domain chars, dots, and dashes
+ domain = domain.replace(/[^a-z0-9\.\-]/g, "");
+ return domain;
+}
+
+function escapeRegex(s) {
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+// Build a regex that matches the domain and any subdomain, main-frame only.
+function regexForDomain(domain) {
+ const d = escapeRegex(domain);
+ // ^https?://([sub.]* )?domain(/|:|$)
+ return `^https?:\\/\\/([a-z0-9-]+\\.)*${d}(\\/|:|$)`;
+}
+
+// Simple deterministic hash for rule IDs (32-bit positive)
+function hash32(str) {
+ let h = 5381;
+ for (let i = 0; i < str.length; i++) {
+ h = ((h << 5) + h) ^ str.charCodeAt(i); // djb2 xor
+ h = h | 0; // force 32-bit
+ }
+ // make positive and avoid 0 & small ids
+ h = Math.abs(h);
+ return (h % 2000000000) + 1000; // keep under Chrome's int32 max
+}
+
+function ruleForDomain(domain) {
+ return {
+ id: hash32(domain),
+ priority: 1,
+ action: { type: "block" },
+ condition: {
+ regexFilter: regexForDomain(domain),
+ resourceTypes: ["main_frame"]
+ }
+ };
+}
+
+async function fetchData() {
+ // try {
+ // const response = await fetch("http://127.0.0.1:8080/");
+ // const data = await response.json();
+ // console.log("Swift app data:", data);
+ // } catch (err) {
+ // console.error("Error fetching from Swift app:", err);
+ // }
+ try {
+ const response = await fetch("http://127.0.0.1:8080/");
+ const data = await response.json();
+ console.log("Array from Swift:", data);
+
+ // // iterate
+ // data.forEach(item => {
+ // console.log(`${item. } is ${item.status}`);
+ // });
+ // Parse into JS object
+// const data = JSON.parse(jsonString);
+
+// Access blocked domain list
+const blockedDomains = data.blocked;
+
+console.log(blockedDomains);
+// π ["domain1.com", "domain2.com", "domain3.com"]
+
+// Example: loop over them
+blockedDomains.forEach(domain => {
+ console.log("Blocked:", domain);
+});
+ return blockedDomains;
+ } catch (err) {
+ console.error("Fetch failed:", err);
+ return [];
+ }
+}
+
+
+async function getDynamicRuleIds() {
+ const rules = await chrome.declarativeNetRequest.getDynamicRules();
+ return rules.map(r => r.id);
+}
+
+async function applyRulesFrom(blockedDomains) {
+ const desiredRules = blockedDomains.map(ruleForDomain);
+ const desiredIds = new Set(desiredRules.map(r => r.id));
+ const currentIds = new Set(await getDynamicRuleIds());
+
+ // Remove any rules that shouldn't exist
+ const removeRuleIds = [...currentIds].filter(id => !desiredIds.has(id));
+
+ // Add rules that don't yet exist
+ const addRules = desiredRules.filter(r => !currentIds.has(r.id));
+
+ await chrome.declarativeNetRequest.updateDynamicRules({
+ removeRuleIds,
+ addRules
+ });
+}
+
+async function loadBlockedDomains() {
+ const domains = await fetchData();
+ return domains;
+ // return new Promise(resolve => {
+ // blockedDomains
+ // chrome.storage.local.get({ blockedSites: [] }, (data) => {
+ // resolve(data.blockedSites || []);
+ // });
+ // });
+}
+
+async function saveBlockedDomains(domains) {
+ return new Promise(resolve => {
+ chrome.storage.local.set({ blockedSites: domains }, () => resolve());
+ });
+}
+
+// ---- Lifecycle ----
+chrome.runtime.onInstalled.addListener(async () => {
+ const domains = await loadBlockedDomains();
+ await applyRulesFrom(domains);
+ console.log("Dynamic URL Blocker installed. Domains:", domains);
+ // fetchData();
+});
+
+chrome.tabs.onCreated.addListener((tab) => {
+ (async () => {
+ console.log("New tab:", tab.id);
+ const domains = await loadBlockedDomains();
+ await applyRulesFrom(domains);
+ // Example: get tab info with async API
+ let tabInfo = await chrome.tabs.get(tab.id);
+ console.log("Fetched tab info:", tabInfo);
+ })();
+});
+
+chrome.windows.onCreated.addListener((window) => {
+ (async () => {
+ const domains = await loadBlockedDomains();
+ await applyRulesFrom(domains);
+ // Example: get tab info with async API
+ let windowInfo = await chrome.tabs.get(window.id);
+ console.log("New Window created:", windowInfo);
+ })();
+});
+
+// ---- Messaging API for popup ----
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ (async () => {
+ if (message?.action === "list") {
+ const domains = await loadBlockedDomains();
+ sendResponse({ success: true, domains });
+ return;
+ }
+
+ if (message?.action === "add") {
+ let domain = normalizeDomain(message.site || "");
+ if (!domain) {
+ sendResponse({ success: false, error: "Please enter a valid domain (e.g., example.com)." });
+ return;
+ }
+ let domains = await loadBlockedDomains();
+ if (!domains.includes(domain)) {
+ domains.push(domain);
+ await saveBlockedDomains(domains);
+ await applyRulesFrom(domains);
+ sendResponse({ success: true, domains });
+ } else {
+ sendResponse({ success: false, error: "Already blocked." });
+ }
+ return;
+ }
+
+ if (message?.action === "remove") {
+ const domain = normalizeDomain(message.site || "");
+ let domains = await loadBlockedDomains();
+ domains = domains.filter(d => d !== domain);
+ await saveBlockedDomains(domains);
+ await applyRulesFrom(domains);
+ sendResponse({ success: true, domains });
+ return;
+ }
+
+ sendResponse({ success: false, error: "Unknown action." });
+ })();
+
+ // Indicate we'll respond asynchronously
+ return true;
+});
diff --git a/Self-Control-Extension/dynamic-url-blocker/manifest.json b/Self-Control-Extension/dynamic-url-blocker/manifest.json
new file mode 100644
index 0000000..e64a8f3
--- /dev/null
+++ b/Self-Control-Extension/dynamic-url-blocker/manifest.json
@@ -0,0 +1,23 @@
+{
+ "manifest_version": 3,
+ "name": "Dynamic URL Blocker",
+ "version": "1.0.0",
+ "description": "Block websites by typing them into a popup. Uses dynamic Declarative Net Request rules.",
+ "permissions": [
+ "declarativeNetRequest",
+ "storage",
+ "scripting"
+ ],
+ "host_permissions": [
+ "http://localhost/*",
+ "http://127.0.0.1/*"
+ ],
+ "background": {
+ "service_worker": "background.js",
+ "type": "module"
+ },
+ "action": {
+ "default_popup": "popup.html",
+ "default_title": "Dynamic URL Blocker"
+ }
+}
\ No newline at end of file
diff --git a/Self-Control-Extension/dynamic-url-blocker/popup.css b/Self-Control-Extension/dynamic-url-blocker/popup.css
new file mode 100644
index 0000000..d856722
--- /dev/null
+++ b/Self-Control-Extension/dynamic-url-blocker/popup.css
@@ -0,0 +1,52 @@
+
+body {
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
+ padding: 12px;
+ width: 320px;
+}
+
+h2 { margin: 0 0 8px; font-size: 16px; }
+h3 { margin: 12px 0 6px; font-size: 14px; }
+
+.row { display: flex; gap: 8px; }
+
+input {
+ flex: 1;
+ padding: 8px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+}
+
+button {
+ padding: 8px 12px;
+ border: none;
+ border-radius: 8px;
+ background: #111827;
+ color: white;
+ cursor: pointer;
+}
+
+button:hover { opacity: 0.9; }
+
+ul { list-style: none; padding: 0; margin: 0; }
+
+li {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 0;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+li .domain { font-size: 13px; }
+
+li button.remove {
+ background: transparent;
+ color: #ef4444;
+ border: 1px solid #ef4444;
+ border-radius: 6px;
+ padding: 2px 8px;
+}
+
+.hint { color: #666; font-size: 12px; }
+.error { color: #b91c1c; font-size: 12px; margin-top: 6px; }
diff --git a/Self-Control-Extension/dynamic-url-blocker/popup.html b/Self-Control-Extension/dynamic-url-blocker/popup.html
new file mode 100644
index 0000000..abb3a07
--- /dev/null
+++ b/Self-Control-Extension/dynamic-url-blocker/popup.html
@@ -0,0 +1,24 @@
+\
+
+
+
+
+ Dynamic URL Blocker
+
+
+
+
+
+ Blocks the domain and all its subdomains.
+
+ Blocked Domains
+
+
+
+
+
diff --git a/Self-Control-Extension/dynamic-url-blocker/popup.js b/Self-Control-Extension/dynamic-url-blocker/popup.js
new file mode 100644
index 0000000..89c26a8
--- /dev/null
+++ b/Self-Control-Extension/dynamic-url-blocker/popup.js
@@ -0,0 +1,73 @@
+
+const siteInput = document.getElementById("siteInput");
+const addBtn = document.getElementById("addBtn");
+const siteList = document.getElementById("siteList");
+
+function normalizeDomain(input) {
+ let domain = (input || "").trim().toLowerCase();
+ domain = domain.replace(/^https?:\/\//, "");
+ domain = domain.replace(/^www\./, "");
+ domain = domain.split("/")[0].split("?")[0].split("#")[0];
+ domain = domain.replace(/[^a-z0-9\.\-]/g, "");
+ return domain;
+}
+
+function render(list) {
+ siteList.innerHTML = "";
+ if (!list || list.length === 0) {
+ const li = document.createElement("li");
+ li.innerHTML = 'No blocked domains yet.';
+ siteList.appendChild(li);
+ return;
+ }
+
+ list.forEach(site => {
+ const li = document.createElement("li");
+
+ const span = document.createElement("span");
+ span.className = "domain";
+ span.textContent = site;
+
+ // const removeBtn = document.createElement("button");
+ // removeBtn.className = "remove";
+ // removeBtn.textContent = "Unblock";
+ // removeBtn.onclick = () => {
+ // chrome.runtime.sendMessage({ action: "remove", site }, (resp) => {
+ // render(resp.domains || []);
+ // });
+ // };
+
+ li.appendChild(span);
+ // li.appendChild(removeBtn);
+ siteList.appendChild(li);
+ });
+}
+
+function load() {
+ chrome.runtime.sendMessage({ action: "list" }, (resp) => {
+ render(resp.domains || []);
+ });
+}
+
+// addBtn.onclick = () => {
+// const raw = siteInput.value;
+// const site = normalizeDomain(raw);
+// if (!site) {
+// alert("Please enter a valid domain, e.g. example.com");
+// return;
+// }
+// chrome.runtime.sendMessage({ action: "add", site }, (resp) => {
+// if (!resp?.success) {
+// alert(resp?.error || "Failed to add.");
+// return;
+// }
+// siteInput.value = "";
+// render(resp.domains || []);
+// });
+// };
+
+// siteInput.addEventListener("keydown", (e) => {
+// if (e.key === "Enter") addBtn.click();
+// });
+
+document.addEventListener("DOMContentLoaded", load);