From 81f9ab467ab4ed6613eab25486c9a9599943405f Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 5 Aug 2024 16:33:29 -0700 Subject: [PATCH] - add ops script to check for users to large numbers of hosts. Could be evidence of botnets, like the recent one. - scheduler: update comments in authenticate_user() --- html/ops/botnet.php | 59 +++++++++++++++++++++ sched/handle_request.cpp | 107 +++++++++++++++++++++++++++++---------- 2 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 html/ops/botnet.php diff --git a/html/ops/botnet.php b/html/ops/botnet.php new file mode 100644 index 0000000000..31c22f44db --- /dev/null +++ b/html/ops/botnet.php @@ -0,0 +1,59 @@ + MAX_HOSTS hosts +// +// could: +// add other criteria like # of countries (based on IP addr) +// consider only hosts with recent last RPC time + +define ('MAX_HOSTS', 100); + +require_once('../inc/boinc_db.inc'); + +// when hosts are merged, userid is set to zero +// and the rpc_seqno field is used to link to the non-zombie host. +// Follow links and return the latter. +// +function follow_links($id) { + $orig_id = $id; + $host = BoincHost::lookup_id($id); + while ($host->userid == 0) { + $host = BoincHost::lookup_id($host->rpc_seqno); + if (!$host) { + echo "(broken link in chain from host $orig_id)\n"; + return null; + } + } + return $host; +} + +function main() { + $hosts = BoincHost::enum_fields('id, userid, last_ip_addr', ''); + $users = []; + foreach ($hosts as $host) { + if ($host->userid == 0) { + $host = follow_links($host->id); + if (!$host) continue; + } + if (array_key_exists($host->userid, $users)) { + $users[$host->userid]++; + } else { + $users[$host->userid] = 1; + } + } + + foreach ($users as $id => $nhosts) { + if ($nhosts < MAX_HOSTS) continue; + $user = BoincUser::lookup_id($id); + if ($user) { + echo "$user->name has $nhosts hosts\n"; + } else { + echo "unknown user $id has $nhosts hosts\n"; + } + } +} + +main(); + +?> diff --git a/sched/handle_request.cpp b/sched/handle_request.cpp index 350d4a272e..0b2039d206 100644 --- a/sched/handle_request.cpp +++ b/sched/handle_request.cpp @@ -231,20 +231,52 @@ static void mark_results_over(DB_HOST& host) { } // Based on the info in the request message, -// look up the host and its user, and make sure the authenticator matches. -// Some special cases: -// 1) If no host ID is supplied, or if RPC seqno mismatch, -// create a new host record -// 2) If the host record specified by g_request->hostid is a "zombie" -// (i.e. it was merged with another host via the web site) -// then follow links to find the proper host +// look up the appropriate user and host records; create host record if needed. +// Return error if couldn't authenticate user. +// +// The request message may contain any or all of: +// user info: +// authenticator (can be weak auth) +// cross_project_id +// host info: +// hostid (DB ID of host) +// rpc_seqno +// host_cpid +// host info like name/IP/processor/RAM +// +// general logic (see comments for details) +// if hostid given +// lookup host record +// lookup user based on host.userid +// if host.authenticator doesn't match request +// lookup user based on request auth +// check that user.id == host.userid +// else +// lookup user based on request auth +// look for a host return matching host_cpid, +// or host info; probably means detach/reattach. +// if none, create host record +// +// Special cases: +// - If no host ID is supplied, this is first RPC from host; +// create a new host record +// - if RPC seqno mismatch, user probably copied client_state.xml +// to a new host; create a new host record +// - If the host record specified by hostid is a "zombie" +// (i.e. it was merged with another host via the web) +// follow links to find the master host record +// We use the rpc_seqno field for these links. // // POSTCONDITION: // If this function returns zero, then: // - reply.host contains a valid host record (possibly new) -// - reply.user contains a valid user record +// - reply.user contains a valid user record, +// and host.userid == user.id // - if user belongs to a team, reply.team contains team record // +// Also sets reply->email_hash, +// and updates user CPID if needed to match request +// int authenticate_user() { int retval; char buf[1024]; @@ -256,6 +288,7 @@ int authenticate_user() { retval = host.lookup_id(g_request->hostid); while (!retval && host.userid==0) { // if host record is zombie, follow link to new host + // TODO: check for infinite loop // retval = host.lookup_id(host.rpc_seqno); if (!retval) { @@ -267,6 +300,9 @@ int authenticate_user() { } } if (retval) { + // bad host ID, or broken zombie chain. + // Make new host record. + // g_reply->insert_message("Can't find host record", "low"); log_messages.printf(MSG_NORMAL, "[HOST#%lu?] can't find host\n", @@ -278,8 +314,9 @@ int authenticate_user() { g_reply->host = host; - // look up user based on the ID in host record, - // and see if the authenticator matches (regular or weak) + // We have a host record based on ID. + // Look up user based on host.userid, + // and see if the authenticator matches request (regular or weak) // g_request->using_weak_auth = false; sprintf(buf, "where id=%lu", host.userid); @@ -324,8 +361,14 @@ int authenticate_user() { } } + // At this point we have a host record, + // and a user record that's authenticated with the request + g_reply->user = user; + // Check that the host and user records are consistent + // If not, make a new host record + if (host.userid != user.id) { // If the request's host ID isn't consistent with the authenticator, // create a new host record. @@ -334,10 +377,9 @@ int authenticate_user() { "[HOST#%lu] [USER#%lu] inconsistent host ID; creating new host\n", host.id, user.id ); - goto make_new_host; + goto didnt_find_host; } - // If the seqno from the host is less than what we expect, // the user must have copied the state file to a different host. // Make a new host record. @@ -348,7 +390,7 @@ int authenticate_user() { "[HOST#%lu] [USER#%lu] RPC seqno %d less than expected %d; creating new host\n", g_reply->host.id, user.id, g_request->rpc_seqno, g_reply->host.rpc_seqno ); - goto make_new_host; + goto didnt_find_host; } } else { @@ -359,6 +401,7 @@ lookup_user_and_make_new_host: // if authenticator contains _, it's a weak auth // if (strchr(g_request->authenticator, '_')) { + // weak auths start with 'userid_' int userid = atoi(g_request->authenticator); retval = user.lookup_id(userid); if (!retval) { @@ -388,12 +431,16 @@ lookup_user_and_make_new_host: ); return ERR_AUTHENTICATOR; } + + // at this point we have a user record, authenticated by the request + g_reply->user = user; // If host CPID is present, - // scan backwards through this user's hosts, - // looking for one with the same host CPID. - // If we find one, it means the user detached and reattached. + // find most recent host with the same host CPID + // belonging to this user. + // If we find one, and it has similar properties, + // it probably means the user detached and reattached. // Use the existing host record, // and mark in-progress results as over. // @@ -419,15 +466,23 @@ lookup_user_and_make_new_host: } } -make_new_host: - // One final attempt to locate an existing host record: +didnt_find_host: + // we didn't find a usable host record; either + // - host ID was given but didn't match user ID, + // or RPC seqno was too low. + // - host ID wasn't given + // + // Before we create a new host record, + // a final attempt to locate an existing host record: // scan backwards through this user's hosts, // looking for one with the same host name, // IP address, processor and amount of RAM. - // If found, use the existing host record, + // If found, it probably means the client detached and reattached. + // Use the existing host record, // and mark in-progress results as over. // - // NOTE: If the client was run with --allow_multiple_clients, skip this. + // NOTE: If the client was run with --allow_multiple_clients, + // skip this; each client instance needs its own host record. // if ((g_request->allow_multiple_clients != 1) && find_host_by_other(user, g_request->host, host) @@ -444,11 +499,8 @@ make_new_host: } goto got_host; } - // either of the above cases, - // or host ID didn't match user ID, - // or RPC seqno was too low. - // - // Create a new host. + + // Create a new host record. // g_reply->user is filled in and valid at this point // host = g_request->host; @@ -471,6 +523,8 @@ make_new_host: host.id = boinc_db.insert_id(); got_host: + // at this point we have a (possibly new) host record. + // g_reply->host = host; g_reply->hostid = g_reply->host.id; // this tells client to updates its host ID @@ -480,7 +534,8 @@ got_host: // This kludge forces this. } - // have user record in g_reply->user at this point + // at this point we have user record in g_reply->user, + // and host record in g_reply->host // if (g_reply->user.teamid) {