- 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()
This commit is contained in:
David Anderson 2024-08-05 16:33:29 -07:00
parent b3e64f761e
commit 81f9ab467a
2 changed files with 140 additions and 26 deletions

59
html/ops/botnet.php Normal file
View File

@ -0,0 +1,59 @@
<?php
// identify possible botnet accounts:
// those with > 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();
?>

View File

@ -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) {