Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Block Management/HostFileBlocker.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- (void)appendExistingBlockWithRuleForDomain:(NSString*)domainName;

- (BOOL)containsSelfControlBlock;
- (BOOL)containsExpectedRulesForBlocklist:(NSArray<NSString*>*)blocklist;

- (void)removeSelfControlBlock;

Expand Down
70 changes: 70 additions & 0 deletions Block Management/HostFileBlocker.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#import "HostFileBlocker.h"
#import "SCBlockEntry.h"
#import "NSString+IPAddress.h"

NSString* const kHostFileBlockerPath = @"/etc/hosts";
NSString* const kHostFileBlockerSelfControlHeader = @"# BEGIN SELFCONTROL BLOCK";
Expand Down Expand Up @@ -182,6 +184,74 @@ - (BOOL)containsSelfControlBlock {
return ret;
}

- (NSSet<NSString*>*)selfControlBlockRuleSet {
NSMutableSet<NSString*>* ruleSet = [NSMutableSet set];

NSRange startRange = [newFileContents rangeOfString: kHostFileBlockerSelfControlHeader];
NSRange endRange = [newFileContents rangeOfString: kHostFileBlockerSelfControlFooter];
if (startRange.location == NSNotFound || endRange.location == NSNotFound || endRange.location <= startRange.location) {
return ruleSet;
}

NSUInteger blockStart = startRange.location + startRange.length;
NSRange blockRange = NSMakeRange(blockStart, endRange.location - blockStart);
NSString* blockContents = [newFileContents substringWithRange: blockRange];
NSArray<NSString*>* lines = [blockContents componentsSeparatedByCharactersInSet: [NSCharacterSet newlineCharacterSet]];
NSCharacterSet* whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet];

for (NSString* line in lines) {
NSString* trimmedLine = [line stringByTrimmingCharactersInSet: whitespace];
if (trimmedLine.length == 0 || [trimmedLine hasPrefix: @"#"]) {
continue;
}

NSArray<NSString*>* parts = [trimmedLine componentsSeparatedByCharactersInSet: whitespace];
NSMutableArray<NSString*>* nonEmptyParts = [NSMutableArray array];
for (NSString* part in parts) {
if (part.length > 0) {
[nonEmptyParts addObject: part];
}
}
if (nonEmptyParts.count < 2) {
continue;
}

NSString* address = nonEmptyParts[0];
for (NSUInteger i = 1; i < nonEmptyParts.count; i++) {
[ruleSet addObject: [NSString stringWithFormat: @"%@ %@", address, nonEmptyParts[i]]];
}
}

return ruleSet;
}

- (BOOL)containsExpectedRulesForBlocklist:(NSArray<NSString*>*)blocklist {
[strLock lock];

BOOL ret = ([newFileContents rangeOfString: kHostFileBlockerSelfControlHeader].location != NSNotFound);
if (ret) {
NSSet<NSString*>* blockRuleSet = [self selfControlBlockRuleSet];

for (NSString* blocklistString in blocklist) {
SCBlockEntry* entry = [SCBlockEntry entryFromString: blocklistString];
if (entry == nil || entry.port || [entry.hostname rangeOfString: @"*"].location != NSNotFound || [entry.hostname isValidIPAddress]) {
continue;
}

NSString* ipv4Rule = [NSString stringWithFormat: @"0.0.0.0 %@", entry.hostname];
NSString* ipv6Rule = [NSString stringWithFormat: @":: %@", entry.hostname];
if (![blockRuleSet containsObject: ipv4Rule] || ![blockRuleSet containsObject: ipv6Rule]) {
NSLog(@"INFO: SelfControl hosts block is missing expected rule(s) for %@", entry.hostname);
ret = NO;
break;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

[strLock unlock];
return ret;
}

- (void)removeSelfControlBlock {
if(![self containsSelfControlBlock])
return;
Expand Down
8 changes: 8 additions & 0 deletions Block Management/HostFileBlockerSet.m
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ - (BOOL)containsSelfControlBlock {
return ret;
}

- (BOOL)containsExpectedRulesForBlocklist:(NSArray<NSString*>*)blocklist {
BOOL ret = YES;
for (HostFileBlocker* blocker in self.blockers) {
ret = ret && [blocker containsExpectedRulesForBlocklist: blocklist];
}
return ret;
}

- (void)removeSelfControlBlock {
for (HostFileBlocker* blocker in self.blockers) {
[blocker removeSelfControlBlock];
Expand Down
8 changes: 4 additions & 4 deletions Common/SCSettings.m
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,10 @@ - (void)writeSettingsWithCompletion:(nullable void(^)(NSError* _Nullable))comple
NSError* chmodErr;
BOOL chmodSuccessful = [[NSFileManager defaultManager]
setAttributes: @{
NSFileOwnerAccountID: [NSNumber numberWithUnsignedLong: 0],
NSFileGroupOwnerAccountID: [NSNumber numberWithUnsignedLong: 0],
NSFilePosixPermissions: [NSNumber numberWithShort: 0755]
}
NSFileOwnerAccountID: [NSNumber numberWithUnsignedLong: 0],
NSFileGroupOwnerAccountID: [NSNumber numberWithUnsignedLong: 0],
NSFilePosixPermissions: [NSNumber numberWithShort: 0644]
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ofItemAtPath: SCSettings.securedSettingsFilePath
error: &chmodErr];

Expand Down
3 changes: 2 additions & 1 deletion Daemon/SCDaemonBlockMethods.m
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,8 @@ + (void)checkBlockIntegrity {
SCSettings* settings = [SCSettings sharedSettings];
PacketFilter* pf = [[PacketFilter alloc] init];
HostFileBlockerSet* hostFileBlockerSet = [[HostFileBlockerSet alloc] init];
if(![pf containsSelfControlBlock] || (![settings boolForKey: @"ActiveBlockAsWhitelist"] && ![hostFileBlockerSet.defaultBlocker containsSelfControlBlock])) {
BOOL hostsBlockIsValid = [settings boolForKey: @"ActiveBlockAsWhitelist"] || [hostFileBlockerSet.defaultBlocker containsExpectedRulesForBlocklist: [settings valueForKey: @"ActiveBlocklist"]];
if(![pf containsSelfControlBlock] || !hostsBlockIsValid) {
NSLog(@"INFO: Block is missing in PF or hosts, re-adding...");
// The firewall is missing at least the block header. Let's clear everything
// before we re-add to make sure everything goes smoothly.
Expand Down
42 changes: 42 additions & 0 deletions SelfControlTests/SCUtilityTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import "SCSentry.h"
#import "SCErr.h"
#import "SCSettings.h"
#import "HostFileBlocker.h"

@interface SCUtilityTests : XCTestCase

Expand Down Expand Up @@ -164,6 +165,47 @@ - (void) testModernBlockDetection {
XCTAssert([SCBlockUtilities currentBlockIsExpired]);
}

- (NSURL*)temporaryHostsFileURLWithName:(NSString*)name contents:(NSString*)contents {
NSURL* url = [[NSURL fileURLWithPath: NSTemporaryDirectory()] URLByAppendingPathComponent: name];
[contents writeToURL: url atomically: YES encoding: NSUTF8StringEncoding error: nil];
return url;
}

- (void) testHostBlockIntegrityRequiresExpectedRules {
NSURL* hostsURL = [self temporaryHostsFileURLWithName: @"selfcontrol-empty-hosts-block-test"
contents: @"127.0.0.1 localhost\n\n# BEGIN SELFCONTROL BLOCK\n# END SELFCONTROL BLOCK\n"];
HostFileBlocker* blocker = [[HostFileBlocker alloc] initWithPath: hostsURL.path];

XCTAssert([blocker containsSelfControlBlock]);
XCTAssertFalse([blocker containsExpectedRulesForBlocklist: @[ @"youtube.com" ]]);

[[NSFileManager defaultManager] removeItemAtURL: hostsURL error: nil];
}

- (void) testHostBlockIntegrityAcceptsExpectedRules {
NSURL* hostsURL = [self temporaryHostsFileURLWithName: @"selfcontrol-valid-hosts-block-test"
contents: @"127.0.0.1 localhost\n"];
HostFileBlocker* blocker = [[HostFileBlocker alloc] initWithPath: hostsURL.path];
[blocker addSelfControlBlockHeader];
[blocker addRuleBlockingDomain: @"youtube.com"];
[blocker addSelfControlBlockFooter];

XCTAssert([blocker containsExpectedRulesForBlocklist: @[ @"youtube.com" ]]);
XCTAssertFalse([blocker containsExpectedRulesForBlocklist: @[ @"youtube.com", @"www.youtube.com" ]]);

[[NSFileManager defaultManager] removeItemAtURL: hostsURL error: nil];
}

- (void) testHostBlockIntegrityIgnoresRulesHostsCannotRepresent {
NSURL* hostsURL = [self temporaryHostsFileURLWithName: @"selfcontrol-hosts-unrepresentable-rules-test"
contents: @"127.0.0.1 localhost\n\n# BEGIN SELFCONTROL BLOCK\n# END SELFCONTROL BLOCK\n"];
HostFileBlocker* blocker = [[HostFileBlocker alloc] initWithPath: hostsURL.path];

XCTAssert([blocker containsExpectedRulesForBlocklist: @[ @"*.youtube.com", @"127.0.0.1", @"example.com:443" ]]);

[[NSFileManager defaultManager] removeItemAtURL: hostsURL error: nil];
}

- (void) testLegacyBlockDetection {
// test blockIsRunningInLegacyDictionary
// the block is "running" even if it's expired, since it hasn't been removed
Expand Down