Compare commits

...

4 Commits

Author SHA1 Message Date
Russell Hancox
6093118ba1 sync: Improve logging when connection restored, avoid retries. (#1408)
* sync: Improve logging when connection restored, avoid retries.

Don't retry requests if the error is that the machine is offline
2024-07-31 13:29:25 -04:00
Russell Hancox
6719d4c32a sync: Upgrade from SCNetworkReachability -> nw_path_monitor (#1406) 2024-07-31 10:53:16 -04:00
Russell Hancox
1ce4756771 santad: Synchronize access to metric callback array (#1405) 2024-07-29 12:09:03 -04:00
Russell Hancox
9a7dcefb92 sync: Fix serial_num field name (#1404)
Disable the preserve_proto_field_names option when marshalling JSON requests as this prevents the json_name attribute on fields from working properly. Add that attribute to all fields so that they marshal as expected. Stop setting the always_print_enums_as_ints field as the value we're setting to is the default anyway.

Also add a test that preflight request data looks as expected.
2024-07-29 12:08:21 -04:00
13 changed files with 169 additions and 156 deletions

View File

@@ -605,10 +605,15 @@ NSString *SNTMetricMakeStringFromMetricType(SNTMetricType metricType) {
/** Export current state of the SNTMetricSet as an NSDictionary. */
- (NSDictionary *)export {
NSDictionary *exported = nil;
NSDictionary *exported;
NSArray *callbacks;
@synchronized(self) {
callbacks = [_callbacks mutableCopy];
}
// Invoke callbacks to ensure metrics are up to date.
for (void (^cb)(void) in _callbacks) {
for (void (^cb)(void) in callbacks) {
cb();
}

View File

@@ -54,4 +54,19 @@
///
+ (NSString *)modelIdentifier;
///
/// @return The Santa product version, e.g. 2024.6
///
+ (NSString *)santaProductVersion;
///
/// @return The Santa build version, e.g. 655965194
///
+ (NSString *)santaBuildVersion;
///
/// @return The full Santa versoin, e.g. 2024.6.655965194
///
+ (NSString *)santaFullVersion;
@end

View File

@@ -74,6 +74,21 @@
return @(model);
}
+ (NSString *)santaProductVersion {
NSDictionary *info_dict = [[NSBundle mainBundle] infoDictionary];
return info_dict[@"CFBundleShortVersionString"];
}
+ (NSString *)santaBuildVersion {
NSDictionary *info_dict = [[NSBundle mainBundle] infoDictionary];
return [[info_dict[@"CFBundleVersion"] componentsSeparatedByString:@"."] lastObject];
}
+ (NSString *)santaFullVersion {
NSDictionary *info_dict = [[NSBundle mainBundle] infoDictionary];
return info_dict[@"CFBundleVersion"];
}
#pragma mark - Internal
+ (NSDictionary *)_systemVersionDictionary {

View File

@@ -823,6 +823,7 @@ objc_library(
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTSystemInfo",
"//Source/common:SNTXPCControlInterface",
"//Source/common:SystemResources",
],

View File

@@ -20,6 +20,7 @@
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTMetricSet.h"
#import "Source/common/SNTSystemInfo.h"
#import "Source/common/SystemResources.h"
#import "Source/santad/Santad.h"
#include "Source/santad/SantadDeps.h"
@@ -111,13 +112,10 @@ int main(int argc, char *argv[]) {
// Do not wait on child processes
signal(SIGCHLD, SIG_IGN);
NSDictionary *info_dict = [[NSBundle mainBundle] infoDictionary];
NSString *product_version = [SNTSystemInfo santaProductVersion];
NSString *build_version = [SNTSystemInfo santaBuildVersion];
NSProcessInfo *pi = [NSProcessInfo processInfo];
NSString *product_version = info_dict[@"CFBundleShortVersionString"];
NSString *build_version =
[[info_dict[@"CFBundleVersion"] componentsSeparatedByString:@"."] lastObject];
if ([pi.arguments containsObject:@"-v"]) {
printf("%s (build %s)\n", [product_version UTF8String], [build_version UTF8String]);
return 0;

View File

@@ -22,7 +22,7 @@ objc_library(
name = "FCM_lib",
srcs = ["SNTSyncFCM.m"],
hdrs = ["SNTSyncFCM.h"],
sdk_frameworks = ["SystemConfiguration"],
sdk_frameworks = ["Network"],
deps = [
"@MOLAuthenticatingURLSession",
],
@@ -55,6 +55,7 @@ objc_library(
],
hdrs = ["SNTSyncManager.h"],
sdk_dylibs = ["libz"],
sdk_frameworks = ["Network"],
deps = [
":FCM_lib",
":broadcaster_lib",

View File

@@ -14,7 +14,7 @@
#import "Source/santasyncservice/SNTSyncFCM.h"
#import <SystemConfiguration/SystemConfiguration.h>
#import <Network/Network.h>
#import <MOLAuthenticatingURLSession/MOLAuthenticatingURLSession.h>
@@ -80,7 +80,7 @@ static const uint32_t kDefaultConnectDelayMaxSeconds = 10;
@property(copy, nonatomic) SNTSyncFCMMessageHandler messageHandler;
/** Is used throughout the class to reconnect to FCM after a connection loss. */
@property SCNetworkReachabilityRef reachability;
@property nw_path_monitor_t pathMonitor;
/** FCM client identities. */
@property(nonatomic, readonly) NSString *project;
@@ -97,21 +97,6 @@ static const uint32_t kDefaultConnectDelayMaxSeconds = 10;
@end
#pragma mark SCNetworkReachabilityCallBack
/** Called when the network state changes. */
static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags,
void *info) {
dispatch_async(dispatch_get_main_queue(), ^{
if (flags & kSCNetworkReachabilityFlagsReachable) {
SNTSyncFCM *FCMClient = (__bridge SNTSyncFCM *)info;
SEL s = @selector(reachabilityRestored);
[NSObject cancelPreviousPerformRequestsWithTarget:FCMClient selector:s object:nil];
[FCMClient performSelector:s withObject:nil afterDelay:1];
}
});
}
@implementation SNTSyncFCM
#pragma mark init/dealloc methods
@@ -184,11 +169,6 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
messageHandler:messageHandler];
}
/** Before this object is released ensure reachability release. */
- (void)dealloc {
[self stopReachability];
}
#pragma mark property methods
- (BOOL)isConnected {
@@ -212,24 +192,27 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
/** Start listening for network state changes on a background thread. */
- (void)startReachability {
if (self.reachability) return;
if (self.pathMonitor) return;
self.pathMonitor = nw_path_monitor_create();
nw_path_monitor_set_queue(self.pathMonitor, dispatch_get_main_queue());
nw_path_monitor_set_update_handler(self.pathMonitor, ^(nw_path_t path) {
if (nw_path_get_status(path) == nw_path_status_satisfied) {
SEL s = @selector(reachabilityRestored);
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:s object:nil];
[self performSelector:s withObject:nil afterDelay:1];
}
});
nw_path_monitor_set_cancel_handler(self.pathMonitor, ^{
self.pathMonitor = nil;
});
nw_path_monitor_start(self.pathMonitor);
LOGD(@"Reachability started.");
self.reachability =
SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, _connectComponents.host.UTF8String);
SCNetworkReachabilityContext context = {.info = (__bridge void *)self};
if (SCNetworkReachabilitySetCallback(self.reachability, reachabilityHandler, &context)) {
SCNetworkReachabilitySetDispatchQueue(
self.reachability, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
}
}
/** Stop listening for network state changes. */
- (void)stopReachability {
if (self.reachability) {
SCNetworkReachabilitySetDispatchQueue(self.reachability, NULL);
if (self.reachability) CFRelease(self.reachability);
self.reachability = NULL;
}
if (!self.pathMonitor) return;
nw_path_monitor_cancel(self.pathMonitor);
}
#pragma mark message methods

View File

@@ -16,7 +16,7 @@
#import <MOLAuthenticatingURLSession/MOLAuthenticatingURLSession.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import <SystemConfiguration/SystemConfiguration.h>
#import <Network/Network.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
@@ -35,9 +35,7 @@
static const uint8_t kMaxEnqueuedSyncs = 2;
@interface SNTSyncManager () <SNTPushNotificationsDelegate> {
SCNetworkReachabilityRef _reachability;
}
@interface SNTSyncManager () <SNTPushNotificationsDelegate>
@property(nonatomic) dispatch_source_t fullSyncTimer;
@property(nonatomic) dispatch_source_t ruleSyncTimer;
@@ -48,6 +46,7 @@ static const uint8_t kMaxEnqueuedSyncs = 2;
@property(nonatomic) MOLXPCConnection *daemonConn;
@property(nonatomic) BOOL reachable;
@property nw_path_monitor_t pathMonitor;
@property SNTPushNotifications *pushNotifications;
@@ -58,22 +57,6 @@ static const uint8_t kMaxEnqueuedSyncs = 2;
@end
// Called when the network state changes
static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags,
void *info) {
// Put this check and set on the main thread to ensure serial access.
dispatch_async(dispatch_get_main_queue(), ^{
SNTSyncManager *commandSyncManager = (__bridge SNTSyncManager *)info;
// Only call the setter when there is a change. This will filter out the redundant calls to this
// callback whenever the network interface states change.
int reachable =
(flags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable;
if (commandSyncManager.reachable != reachable) {
commandSyncManager.reachable = reachable;
}
});
}
@implementation SNTSyncManager
#pragma mark init
@@ -102,11 +85,6 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
return self;
}
- (void)dealloc {
// Ensure reachability is always stopped
[self stopReachability];
}
#pragma mark SNTSyncServiceXPC methods
- (void)postEventsToSyncServer:(NSArray<SNTStoredEvent *> *)events fromBundle:(BOOL)isFromBundle {
@@ -279,8 +257,8 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
return [self eventUploadWithSyncState:syncState];
}
LOGE(@"Preflight failed, will try again once %@ is reachable",
[[SNTConfigurator configurator] syncBaseURL].absoluteString);
SLOGE(@"Preflight failed, will try again once %@ is reachable",
[[SNTConfigurator configurator] syncBaseURL].absoluteString);
[self startReachability];
return SNTSyncStatusTypePreflightFailed;
}
@@ -408,39 +386,36 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
- (void)setReachable:(BOOL)reachable {
_reachable = reachable;
if (reachable) {
LOGD(@"Internet connection has been restored, triggering a new sync.");
[self stopReachability];
[self sync];
}
}
// Start listening for network state changes on a background thread
// Start listening for network state changes.
- (void)startReachability {
dispatch_async(dispatch_get_main_queue(), ^{
if (self->_reachability) return;
const char *nodename = [[SNTConfigurator configurator] syncBaseURL].host.UTF8String;
self->_reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, nodename);
SCNetworkReachabilityContext context = {
.info = (__bridge_retained void *)self,
.release = (void (*)(const void *))CFBridgingRelease,
};
if (SCNetworkReachabilitySetCallback(self->_reachability, reachabilityHandler, &context)) {
SCNetworkReachabilitySetDispatchQueue(
self->_reachability, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
} else {
[self stopReachability];
if (self.pathMonitor) return;
self.pathMonitor = nw_path_monitor_create();
// Put the callback on the main thread to ensure serial access.
nw_path_monitor_set_queue(self.pathMonitor, dispatch_get_main_queue());
nw_path_monitor_set_update_handler(self.pathMonitor, ^(nw_path_t path) {
// Only call the setter when there is a change. This will filter out the redundant calls to
// this callback whenever the network interface states change.
int reachable = nw_path_get_status(path) == nw_path_status_satisfied;
if (self.reachable != reachable) {
self.reachable = reachable;
}
});
nw_path_monitor_set_cancel_handler(self.pathMonitor, ^{
self.pathMonitor = nil;
});
nw_path_monitor_start(self.pathMonitor);
}
// Stop listening for network state changes
- (void)stopReachability {
dispatch_async(dispatch_get_main_queue(), ^{
if (self->_reachability) {
SCNetworkReachabilitySetDispatchQueue(self->_reachability, NULL);
if (self->_reachability) CFRelease(self->_reachability);
self->_reachability = NULL;
}
});
if (!self.pathMonitor) return;
nw_path_monitor_cancel(self.pathMonitor);
}
@end

View File

@@ -87,8 +87,7 @@ The following table expands upon the above logic to list most of the permutation
req->set_os_version(NSStringToUTF8String([SNTSystemInfo osVersion]));
req->set_os_build(NSStringToUTF8String([SNTSystemInfo osBuild]));
req->set_model_identifier(NSStringToUTF8String([SNTSystemInfo modelIdentifier]));
req->set_santa_version(
NSStringToUTF8String([[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]));
req->set_santa_version(NSStringToUTF8String([SNTSystemInfo santaFullVersion]));
req->set_primary_user(NSStringToUTF8String(self.syncState.machineOwner));
if (self.syncState.pushNotificationsToken) {

View File

@@ -14,6 +14,7 @@
#import "Source/santasyncservice/SNTSyncStage.h"
#include <Foundation/Foundation.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTCommonEnums.h"
@@ -74,10 +75,7 @@ using santa::NSStringToUTF8String;
contentType:@"application/x-protobuf"];
}
google::protobuf::json::PrintOptions options{
.always_print_enums_as_ints = false,
.preserve_proto_field_names = true,
};
google::protobuf::json::PrintOptions options{};
std::string json;
absl::Status status = google::protobuf::json::MessageToJsonString(*message, &json, options);
@@ -179,6 +177,11 @@ using santa::NSStringToUTF8String;
data = [self performRequest:request timeout:timeout response:&response error:&requestError];
if (response.statusCode == 200) break;
// If the original request failed because of a "No network" error, break out of the loop,
// subsequent retries are pointless and the entire sync will be retried once a connection
// is established.
if (requestError.code == NSURLErrorNotConnectedToInternet) break;
// If the original request failed because of an auth error, attempt to get a new XSRF token and
// try again. Unfortunately some servers cause NSURLSession to return 'client cert required' or
// 'could not parse response' when a 403 occurs and SSL cert auth is enabled.

View File

@@ -22,6 +22,7 @@
#import "Source/common/SNTRule.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTSystemInfo.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santasyncservice/SNTSyncEventUpload.h"
#import "Source/santasyncservice/SNTSyncPostflight.h"
@@ -60,11 +61,19 @@
OCMStub([self.syncState.daemonConn remoteObjectProxy]).andReturn(self.daemonConnRop);
OCMStub([self.syncState.daemonConn synchronousRemoteObjectProxy]).andReturn(self.daemonConnRop);
id siMock = OCMClassMock([SNTSystemInfo class]);
OCMStub([siMock serialNumber]).andReturn(@"QYGF4QM373");
OCMStub([siMock longHostname]).andReturn(@"full-hostname.example.com");
OCMStub([siMock osVersion]).andReturn(@"14.5");
OCMStub([siMock osBuild]).andReturn(@"23F79");
OCMStub([siMock modelIdentifier]).andReturn(@"MacBookPro18,3");
OCMStub([siMock santaFullVersion]).andReturn(@"2024.6.655965194");
self.syncState.session = OCMClassMock([NSURLSession class]);
self.syncState.syncBaseURL = [NSURL URLWithString:@"https://myserver.local/"];
self.syncState.machineID = [[NSUUID UUID] UUIDString];
self.syncState.machineOwner = NSUserName();
self.syncState.machineID = @"50C7E1EB-2EF5-42D4-A084-A7966FC45A95";
self.syncState.machineOwner = @"username1";
}
#pragma mark Test Helpers
@@ -271,7 +280,15 @@
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
NSData *respData = [self dataFromFixture:@"sync_preflight_basic.json"];
[self stubRequestBody:respData response:nil error:nil validateBlock:nil];
[self stubRequestBody:respData
response:nil
error:nil
validateBlock:^BOOL(NSURLRequest *req) {
NSData *gotReqData = [req HTTPBody];
NSData *expectedReqData = [self dataFromFixture:@"sync_preflight_request.json"];
XCTAssertEqualObjects(gotReqData, expectedReqData);
return YES;
}];
XCTAssertTrue([sut sync]);
XCTAssertEqual(self.syncState.clientMode, SNTClientModeMonitor);

View File

@@ -35,27 +35,27 @@ enum ClientMode {
}
message PreflightRequest {
string serial_number = 1 [json_name="serial_num"];
string hostname = 2;
string os_version = 3;
string os_build = 4;
string model_identifier = 5;
string santa_version = 6;
string primary_user = 7;
string push_notification_token = 8;
string serial_number = 1 [json_name="serial_num"];
string hostname = 2 [json_name="hostname"];
string os_version = 3 [json_name="os_version"];
string os_build = 4 [json_name="os_build"];
string model_identifier = 5 [json_name="model_identifier"];
string santa_version = 6 [json_name="santa_version"];
string primary_user = 7 [json_name="primary_user"];
string push_notification_token = 8 [json_name="push_notification_token"];
ClientMode client_mode = 9;
bool request_clean_sync = 10;
ClientMode client_mode = 9 [json_name="client_mode"];
bool request_clean_sync = 10 [json_name="request_clean_sync"];
uint32 binary_rule_count = 11;
uint32 certificate_rule_count = 12;
uint32 compiler_rule_count = 13;
uint32 transitive_rule_count = 14;
uint32 teamid_rule_count = 15;
uint32 signingid_rule_count = 16;
uint32 cdhash_rule_count = 17;
uint32 binary_rule_count = 11 [json_name="binary_rule_count"];
uint32 certificate_rule_count = 12 [json_name="certificate_rule_count"];
uint32 compiler_rule_count = 13 [json_name="compiler_rule_count"];
uint32 transitive_rule_count = 14 [json_name="transitive_rule_count"];
uint32 teamid_rule_count = 15 [json_name="teamid_rule_count"];
uint32 signingid_rule_count = 16 [json_name="signingid_rule_count"];
uint32 cdhash_rule_count = 17 [json_name="cdhash_rule_count"];
// The UUID of the machine that is sending this preflight.
string machine_id = 18;
string machine_id = 18 [json_name="machine_id"];
}
enum SyncType {
@@ -193,53 +193,53 @@ message Certificate {
string cn = 2;
string org = 3;
string ou = 4;
uint32 valid_from = 5;
uint32 valid_until = 6;
uint32 valid_from = 5 [json_name="valid_from"];
uint32 valid_until = 6 [json_name="valid_until"];
}
message Event {
string file_sha256 = 1;
string file_path = 2;
string file_name = 3;
string executing_user = 4;
double execution_time = 5;
repeated string logged_in_users = 6;
repeated string current_sessions = 7;
Decision decision = 8;
string file_sha256 = 1 [json_name="file_sha256"];
string file_path = 2 [json_name="file_path"];
string file_name = 3 [json_name="file_name"];
string executing_user = 4 [json_name="executing_user"];
double execution_time = 5 [json_name="execution_time"];
repeated string logged_in_users = 6 [json_name="logged_in_users"];
repeated string current_sessions = 7 [json_name="current_sessions"];
Decision decision = 8 [json_name="decision"];
string file_bundle_id = 9;
string file_bundle_path = 10;
string file_bundle_executable_rel_path = 11;
string file_bundle_name = 12;
string file_bundle_version = 13;
string file_bundle_version_string = 14;
string file_bundle_hash = 15;
uint64 file_bundle_hash_millis = 16;
uint64 file_bundle_binary_count = 17;
string file_bundle_id = 9 [json_name="file_bundle_id"];
string file_bundle_path = 10 [json_name="file_bundle_path"];
string file_bundle_executable_rel_path = 11 [json_name="file_bundle_executable_rel_path"];
string file_bundle_name = 12 [json_name="file_bundle_name"];
string file_bundle_version = 13 [json_name="file_bundle_version"];
string file_bundle_version_string = 14 [json_name="file_bundle_version_string"];
string file_bundle_hash = 15 [json_name="file_bundle_hash"];
uint64 file_bundle_hash_millis = 16 [json_name="file_bundle_hash_millis"];
uint64 file_bundle_binary_count = 17 [json_name="file_bundle_binary_count"];
// pid_t is an int32
int32 pid = 18;
int32 ppid = 19;
string parent_name = 20;
int32 pid = 18 [json_name="pid"];
int32 ppid = 19 [json_name="ppid"];
string parent_name = 20 [json_name="parent_name"];
string team_id = 21;
string signing_id = 22;
string cdhash = 23;
string team_id = 21 [json_name="team_id"];
string signing_id = 22 [json_name="signing_id"];
string cdhash = 23 [json_name="cdhash"];
string quarantine_data_url = 24;
string quarantine_referer_url = 25;
string quarantine_data_url = 24 [json_name="quarantine_data_url"];
string quarantine_referer_url = 25 [json_name="quarantine_referer_url"];
// Seconds since UNIX epoch. This field would ideally be an int64 but the protobuf library
// encodes that as a string, unlike NSJSONSerialization
uint32 quarantine_timestamp = 26;
string quarantine_agent_bundle_id = 27;
uint32 quarantine_timestamp = 26 [json_name="quarantine_timestamp"];
string quarantine_agent_bundle_id = 27 [json_name="quarantine_agent_bundle_id"];
repeated Certificate signing_chain = 28;
repeated Certificate signing_chain = 28 [json_name="signing_chain"];
}
message EventUploadRequest {
repeated Event events = 1;
repeated Event events = 1 [json_name="events"];
// The UUID of the machine where the event(s) occurred
string machine_id = 2;
string machine_id = 2 [json_name="machine_id"];
}
message EventUploadResponse {
@@ -294,9 +294,9 @@ message Rule {
}
message RuleDownloadRequest {
string cursor = 1;
string cursor = 1 [json_name="cursor"];
// The UUID of the machine that is requesting the rules.
string machine_id = 2;
string machine_id = 2 [json_name="machine_id"];
}
message RuleDownloadResponse {
@@ -305,10 +305,10 @@ message RuleDownloadResponse {
}
message PostflightRequest {
uint64 rules_received = 1;
uint64 rules_processed = 2;
uint64 rules_received = 1 [json_name="rules_received"];
uint64 rules_processed = 2 [json_name="rules_processed"];
// The UUID of the machine that is sending this postflight.
string machine_id = 3;
string machine_id = 3 [json_name="machine_id"];
}
message PostflightResponse { }

View File

@@ -0,0 +1 @@
{"serial_num":"QYGF4QM373","hostname":"full-hostname.example.com","os_version":"14.5","os_build":"23F79","model_identifier":"MacBookPro18,3","santa_version":"2024.6.655965194","primary_user":"username1","client_mode":"MONITOR","machine_id":"50C7E1EB-2EF5-42D4-A084-A7966FC45A95"}