Skip to content

FalkinTech/URLFilterExample

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimpleURLFilter

SimpleURLFilter makes use of the NEURLFilterManager to demonstrate managing URL filtering configurations.

Overview

URL filtering is achieved as a two step process in which request URLs are rapidly checked against local bloom filter data, and in the case of a potential match, a configured Private Information Retrieval (PIR) server is subsequently queried. PIR server data is cached locally, and is refreshed on a regular schedule. This results in a very fast filtering implementation, preserving user privacy, which is robust and continually refreshed with current filtering criteria. This mechanism is very similar to the Live Caller ID Lookup capabilities, so many of the concepts and technology overlap.

The bloom filter pre-filter is provided to the system through the use of an app extension protocol defined by NEURLFilterControlProvider.

NEURLFilterManager provides the means to manage the URL filtering configuration, including the PIR server URL, cache management, and refresh timing. Filtering status and configuration updates are available through async sequences.

This SimpleURLFilter sample application utilizes the interface presented by NEURLFilterManager to configure and manage URL filtering capabilities, and includes an implementation of the NEURLFilterControlProvider protocol in an embedded application extension. The extension implementation delivers bundled bloom filter data to the system.

Configure the sample code project

Open the sample code project in Xcode. Before building it, do the following:

  1. Set the developer team for all targets to your team so Xcode automatically manages the provisioning profile. For more information, see Assign a project to a team.

  2. Optionally build and run the PIR server sample (or have your own server ready for use).

PIR Server

A sample PIR Server service and configuration is included in the "PIR Server" directory. Please refer to the README.md.

Get started

To see the sample app in action, use Xcode to build and run the app on your iOS device.

Running the sample project on device will allow you to configure and apply a URL filter.

The main view of the application shows status information about the currently installed filter, if there is one, and transient status messages from the application as various actions are performed.

Two main butons are prominent. The "Enable"/"Disable" button is a quick way to enable or disable the current filter configuration and is equivalent to saving the entire configuration after changing the isEnabled flag on the manager. The "Configure" button presents an interface to view and change the filter properties, and then apply them.

Within the configure interface are fields to enable/disable the configuration, specify a URL for the PIR server, optionally specify a URL for the Privacy Pass server, the Authentication token used with the PIR server, a picker to choose the pre-filter fetch frequency interval, and a toggle treat failures in the system as allowing or disallowing the requested URL by default.

The Privacy Pass server URL is optional in this context and if not specified, the system will assume the configured PIR server URL handles this responsibility as well (which is true of the sample server).

For example, if you decide to use the sample PIR server and have it running, you would:

  • Toggle the "Enabled" switch to the on state.
  • Set the "PIR Server URL" to http://localhost:8080
  • Leave the "PIR Privacy Pass Issuer URL" empty.
  • Populate the "Authentication Token" with AAAA (as indicated in the service-config.json file).
  • Leave the "Pre-filter Fetch Frequency" at the default setting of "45 minutes"
  • Toggle the "Fail Closed" switch to the on state.

Then use the "Apply" button to save these configuration changes and start the filter.

At this point you should be prompted to allow the filter, and if you choose to proceed you will enter the device password, and be taken into Settings to verify. You can then return to the application to check on the filter status and perform other actions.

API Notes

The main interface to the NEURLFilterManager for this sample is the ConfigurationModel class in the ConfigurationModel.swift file. This is @Observable so changes here can allow the user interface to react.

The NEURLFilterManager is addressed through a Singleton presented via the static shared property, and maintains state internally for the filter configuration.

The configuration state of the NEURLFilterManager must be loaded from the system by calling loadFromPreferences() and the system needs to be informed of any changes made to the configuration state by calling saveToPreferences(). Additionally, the configuration can be removed from the system by calling the removeFromPreferences() function.

The setConfiguration(pirServerURL:pirPrivacyPassIssuerURL:pirAuthenticationToken:controlProviderBundleIdentifier:) function is used to set the required configuration properties, but you likely will also want to set values for the prefilterFetchInterval, shouldFailClosed, and isEnabled properties. Once configured as desired, be sure to call saveToPreferences() to inform the system of the changes.

Once an enabled configuration is set and saved the system will attempt to place it in a running state. You can directly query the filter state through the NEURLFilterManager status property, or use the handleStatusChange() async sequence API to be informed of status updates. Should the state be in an unexpected error state you can query the lastDisconnectError property for the most recent error, keeping in mind this error represents the most recently available error and may be a remnant from a previous issue, if nothing has caused a more recent error to overwrite it.

Development Guide

Critical Bug Fixes

This implementation includes fixes for critical bugs in the original sample code:

  1. murmurSeed Overflow: The original code read murmurSeed as Int, causing overflow for large unsigned values. Fixed by reading as UInt32 using NSNumber.uint32Value.

  2. Unsafe Optional Handling: The original code used optional chaining (bitVectorData?.write()), which fails silently if bitVectorData is nil. Fixed by using guard let for safe unwrapping.

These fixes are in SimpleURLFilterExtension/URLFilterControlProvider.swift and are essential for the extension to function correctly.

Bundle Identifier Configuration

The PIR server must be configured with your app's bundle identifier plus the .url.filtering suffix:

  1. Open PIR Server/service-config.json
  2. Update the name field in the usecases array:
    "name": "com.your.bundleid.url.filtering"
  3. Rebuild the PIR server (see below)

Generating a New Bloom Filter

The bloom filter must contain bare domains (without schemes). iOS normalizes URLs internally before checking the filter.

Prerequisites:

  • Install the swift-bloom tool: git clone https://github.com/adamleozar/swift-bloom && cd swift-bloom && swift build -c release

Steps:

  1. Create a text file with one bare domain per line:

    example.com
    www.example.com
    badsite.com
    
  2. Generate the bloom filter:

    .build/release/BloomFilterBuilder build \
      --input-path domains.txt \
      --false-positive-tolerance 0.01 \
      --output-path SimpleURLFilter/SimpleURLFilterExtension/bloom_filter_v3.plist
  3. Update URLFilterControlProvider.swift if you change the filename:

    let filterPlistFileName = "bloom_filter_v3"
  4. Rebuild the iOS app in Xcode

Important: The bloom filter and PIR database must contain the same domains. iOS extracts the domain from full URLs (e.g., https://example.comexample.com) before checking.

Rebuilding the PIR Server

After modifying PIR Server/data/input.txtpb, rebuild the Docker container:

cd "PIR Server"
docker compose down
docker compose build --no-cache
docker compose up -d

The --no-cache flag ensures the new database is processed.

Database Format:

rows: [{
    keyword: "example.com",
    value: "1"
}]

Use bare domains only (no https:// prefix).

Running URL Verdict Tests

The app includes a synthetic test to verify URL filtering behavior:

  1. Run the app on your iOS device
  2. Open the menu (wrench icon in top-right)
  3. Select "Test URL Verdicts"
  4. View results in the alert and in Console.app logs

Expected behavior:

  • First query to each unique domain: ~100-250ms (PIR query)
  • Subsequent queries: ~1-3ms (cached)
  • URLs matching bloom filter domains: DENY
  • URLs not in filter: ALLOW

Debugging:

  • Filter logs with SimpleURLFilter in Console.app
  • Check PIR server logs: docker compose logs -f pir-server

Troubleshooting

Error 14 (Extension Won't Start):

  • Ensure murmurSeed and bitVectorData fixes are in place
  • Clean build (Cmd+Shift+K in Xcode)
  • Delete app from device and reinstall
  • Clear Xcode Derived Data

URLs Not Blocking:

  • Verify bloom filter and PIR database contain the same domains
  • Use bare domains (not full URLs)
  • Check PIR server is running: docker ps
  • Check PIR server logs for "usecase not found" errors
  • Reset PIR cache in app (wrench menu → "Reset PIR Cache")
  • Toggle filter OFF → ON

PIR Server "Usecase not found":

  • Verify bundle ID in service-config.json matches your app + .url.filtering
  • Rebuild PIR server with --no-cache

State Corruption: Perform a "nuclear cleanup":

  1. Delete the configuration in the app
  2. Delete the app from device
  3. Restart the iPhone
  4. Clear Xcode Derived Data
  5. Rebuild and reinstall

URL Normalization

iOS performs automatic URL normalization for bloom filter checks:

  • Input: https://example.com/path
  • Normalized: example.com
  • Bloom filter lookup: example.com
  • PIR query: example.com

This means:

  • Bloom filter should contain: example.com
  • PIR database should contain: example.com
  • iOS handles the rest automatically

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published