/* * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * Copyright 2014 Range Networks, Inc. * * This software is distributed under multiple licenses; * see the COPYING file in the main directory for licensing * information for this specific distribution. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ #define LOG_GROUP LogGroup::Control #include #include #include #include #include #include #include #include #include #include #include #include "TMSITable.h" #include "ControlTransfer.h" #include "L3MobilityManagement.h" using namespace std; // (pat) For l3rewrite the TMSI will be assigned by the VLR, which will be either a new SubscriberRegistry program or Yate. // As a result, everything here has to change. // (pat) The access to any database needs to be message oriented if there is any chance of stalling, which could goof up the invoking state machine. Control::TMSITable gTMSITable; namespace Control { static Mutex sTmsiMutex; // We have to serialize tmsi creation. static unsigned sHighestFakeTmsi; // The highest fake tmsi value in the table to create an entry with no tmsi. #define MAX_VALID_TMSI 0x3fffffff // 30 bits. We dont make TMSIs this high, but the invalid TMSIs start one higher than this. // Note: we are using the TMSI as the primary key for efficiency, which means we must have a unique tmsi value in the // TMSITable for every entry whether it is assigned to the MS or not. // We keep valid TMSIs that we are not sure the MS received, and also TMSIs that may have authorized in the past and subsequently // become unauthorized. // For unauthorized phones, we used to go ahead and create valid TMSIs anyway, but I dont want to do that now that TMSIs are // genuine random numbers and somewhat expensive to allocate, plus there could be many of them and I dont want them // using up TMSI address space, because eventually we may receive TMSIs from neighbors as well. // Another alternative was to have a separate TMSI->IMSI mapping table, but I dont think that is any better. // If you do not specify a primary key, sqlite assigns a new key one higher than the highest existing in the table. // So I will do is let it go ahead and assign TMSIs 1,2,3,4 for the case where the TMSI is not used, // Fine, so we will use values up to 64K as valid TMSIs, and values above that for invalid TMSIs. // This seems to do a good job and sqlite does not seem to increase the database size much when you // assign primary keys that are not tightly clustered. // The TMSI_ASSIGNED field indicates if the MS has confirmed receipt of the TMSI. // Eventually we may receive TMSIs from neighbors as well. namespace TmsiTableDefinition { static const char *tmsiTableVersion = "7"; // Change this version whenever you change anything in the TMSI table. // The fields from the tmsi table that are printed by the CLI 'tmsis' command. // TMSITabDump assumes the names of some of these header fields to decide the appropriate formatting. static string shortHeader = "IMSI TMSI IMEI AUTH CREATED ACCESSED TMSI_ASSIGNED"; static string longHeader = shortHeader + " PTMSI_ASSIGNED AUTH_EXPIRY REJECT_CODE ASSOCIATED_URI ASSERTED_IDENTITY WELCOME_SENT"; //"IMSI TMSI IMEI AUTH CREATED ACCESSED TMSI_ASSIGNED PTMSI_ASSIGNED AUTH_EXPIRY REJECT_CODE ASSOCIATED_URI ASSERTED_IDENTITY WELCOME_SENT"; static string longerHeader = longHeader + " A5_SUPPORT POWER_CLASS RRLP_STATUS OLD_TMSI OLD_MCC OLD_MNC OLD_LAC"; //"IMSI TMSI IMEI AUTH CREATED ACCESSED TMSI_ASSIGNED PTMSI_ASSIGNED AUTH_EXPIRY REJECT_CODE ASSOCIATED_URI ASSERTED_IDENTITY WELCOME_SENT A5_SUPPORT POWER_CLASS RRLP_STATUS OLD_TMSI OLD_MCC OLD_MNC OLD_LAC"; static const char* create = "CREATE TABLE IF NOT EXISTS TMSI_TABLE (" // Valid TMSIs use negative values. Small positive auto-incremented numbers are dummy values // automatically assigned by sqlite used for entries where the TMSI is unspecified. "TMSI INTEGER PRIMARY KEY, " // AUTOINCREMENT This will also be used as the P-TMSI // IMSI is just the digits without "IMSI" in front. // (pat) the UNIQUE tells sqlite to create an index on this field to optimize queries. "IMSI TEXT UNIQUE NOT NULL, " // IMSI "IMEI TEXT, " // IMEI "CREATED INTEGER NOT NULL, " // Unix time (seconds) of record creation "ACCESSED INTEGER NOT NULL, " // Unix time (seconds) of last encounter // "APP_FLAGS INTEGER DEFAULT 0, " // Application-specific flags (pat) No one is using this so I took it out. "A5_SUPPORT INTEGER, " // encryption support "POWER_CLASS INTEGER, " // power class "OLD_TMSI INTEGER, " // previous TMSI in old network "OLD_MCC INTEGER, " // previous network MCC "OLD_MNC INTEGER, " // previous network MNC "OLD_LAC INTEGER, " // previous network LAC #if CACHE_AUTH // This code is eroded and no longer functional. // (pat 9-2013): We are not storing the authentication info in the TMSI table. // We need cached authorization for the case where we are using open registration and TMSIs are being sent, // to verify the TMSI to avoid TMSI collisions, however, this should never have been the in the BTS - // needs to be in sipauthserve if anywhere, so we always call the Registrar to get fresh challenges // For UMTS this is a requirement, so there is no point putting in a special case for 2G GSM to cache a single RAND here. // Cached authentication could also be used in the case where the Registrar is unreachable; but in that case we // should fall back to using IMSIs and fail open. We dont handle that well at the moment. //"RANDUPPER INTEGER, " // authentication token //"RANDLOWER INTEGER, " // authentication token //"SRES INTEGER, " // authentication token #endif "kc varchar(33) default '', " // returned by the Registrar, needed for ciphering. "RRLP_STATUS INTEGER DEFAULT 0, " // Does MS support RRLP? If so, where are we in the RRLP state machine? "DEG_LAT FLOAT, " // RRLP result "DEG_LONG FLOAT, " // RRLP result "ASSOCIATED_URI text default '', " // Saved from the SIP REGISTER message and inserted into MOC SIP INVITE. "ASSERTED_IDENTITY text default '', " // Saved from the SIP REGISTER message and inserted into MOC SIP INVITE. "WELCOME_SENT INTEGER DEFAULT 0," // 0 == welcome message not sent yet; 1 == sent by us; 2 == sent by someone else. // (pat) We keep the unauthorized entries for several reasons: // 1. so we can cache the unauthorized status to reduce loading on the Registrar by using cached unauthorization // 2. to prevent sending the (un)welcome message multiple times. // 3. if a previously authorized MS becomes unauthorized we want to keep the entry here to reserve the TMSI permanently. "AUTH INTEGER DEFAULT 0," // Authorization result, 0 == unauthorized. See enum Authorization for other values. "AUTH_EXPIRY INTEGER DEFAULT 0," // Absolute time in seconds when authorization expires or 0 for single-use. "REJECT_CODE INTEGER DEFAULT 0," // Reject code, or 0 if authorized. "TMSI_ASSIGNED INTEGER DEFAULT 0," // Set when the TMSI has been successfully assigned to the MS, ie, the MS knows it. "PTMSI_ASSIGNED INTEGER DEFAULT 0" // Set when the P-TMSI has been successfully assigned to the MS by the SGSN, ie, the MS knows it. // TODO: A bunch of stuff for GPRS. GPRS could use the TMSI as the P-TMSI if it is limited to 30 bits. ")"; }; // These are the mappings of allocated tmsis to/from the TMSI_TABLE. static int tmsi2table(uint32_t tmsi) { return (int) tmsi; } static uint32_t table2tmsi(int storedValue) { return storedValue; } static bool tmsiIsValid(int rawTmsi) { return rawTmsi != 0 && rawTmsi <= MAX_VALID_TMSI; } bool configTmsiTestMode() { return gConfig.getNum("Control.LUR.TestMode"); } bool configSendTmsis() { return gConfig.getBool("Control.LUR.SendTMSIs"); } // return true on success bool TMSITable::runQuery(const char *query, int checkChanges) const { int resultCode, changes=0; LOG(DEBUG)<= 99999) {return;} // punt if disabled or ridiculous. unsigned oldest_allowed = time(NULL) - (maxage * 60*60); char query[102]; snprintf(query,100,"DELETE FROM TMSI_TABLE WHERE ACCESSED <= %u",oldest_allowed); runQuery(query,false); int changes = sqlite3_changes(mTmsiDB); if (changes) { LOG(INFO) << "Deleted "< (int)sHighestFakeTmsi) { sHighestFakeTmsi = rawtmsi; } if (! qinit.sqlStep()) break; } } } int TMSITable::tmsiTabOpen(const char* wPath) { // FIXME -- We can't call the logger here because it has not been initialized yet. // (pat) I think this has been fixed - the TMSITable initialization is now in main(). //printf("TMSITable::open(%s)\n",wPath); fflush(stdout); LOG(INFO) << "TMSITable::open "<append(","); this->appendf("%s='%s'",name,sval); } void addc(const char *name,unsigned ival) { if (cnt++) this->append(","); this->appendf("%s=%u",name,ival); } void finish() {} }; struct TSqlInsert : public TSqlQuery { TSqlString values; Int_z cnt; // Count of commas. void addc(const char *name, string sval) { if (cnt++) { this->append(","); values.append(","); } this->append(name); values.appendf("'%s'",sval); } void addc(const char *name, unsigned ival) { if (cnt++) { this->append(","); values.append(","); } this->append(name); values.appendf("%u",ival); } void finish() { append(") VALUES ("); append(this->values); append(")"); } }; // Flush anything that has changed out to the TMSI table in this SQL query. void TSqlQuery::addStore(TmsiTableStore *store) { if (store->imei_changed) { addc("IMEI",store->imei); store->imei_changed = false; } if (store->auth_changed) { addc("AUTH",store->auth); store->auth_changed = false; } if (store->authExpiry_changed) { addc("AUTH_EXPIRY",store->authExpiry); store->authExpiry_changed = false; } if (store->rejectCode_changed) { addc("REJECT_CODE",store->rejectCode); store->rejectCode_changed = false; } if (store->assigned_changed) { addc("TMSI_ASSIGNED",store->assigned); store->assigned_changed = false; } if (store->a5support_changed) { addc("A5_SUPPORT",store->a5support); store->a5support_changed = false; } if (store->powerClass_changed) { addc("POWER_CLASS",store->powerClass); store->powerClass_changed = false; } if (store->kc_changed) { addc("kc",store->kc); store->kc_changed = false; } if (store->associatedUri_changed) { addc("ASSOCIATED_URI",store->associatedUri); store->associatedUri_changed = false; } if (store->assertedIdentity_changed) { addc("ASSERTED_IDENTITY",store->assertedIdentity); store->assertedIdentity_changed = false; } if (store->welcomeSent_changed) { addc("WELCOME_SENT",store->welcomeSent); store->welcomeSent_changed = false; } } unsigned TMSITable::allocateTmsi() { int tmsi; // For testing, we will deliberately screw something up. if (configTmsiTestMode()) { // Deliberately over-write an existing entry to create a TMSI collision. // Must use at least two phones - the second one will be assigned the same tmsi as the first. sqlQuery q8(mTmsiDB,"TMSI_TABLE","TMSI",""); // Returns all the table rows one by one. if (q8.sqlSuccess() == false) { WATCH("TMSI table is empty"); } else { int rawtmsi = q8.getResultInt(); if (tmsiIsValid(rawtmsi)) { unsigned tmsi = table2tmsi(rawtmsi); WATCH("TMSI table test mode: created deliberate tmsi collision for"<auth_changed)<auth)<assigned_changed)<assigned); ScopedLock lock(sTmsiMutex,__FILE__,__LINE__); // This lock should be redundant - sql serializes access, but it may prevent sql retry failures. TSqlUpdate q1; q1.append("UPDATE TMSI_TABLE SET "); q1.addStore(store); if (! q1.cnt) { return; } // Nothing changed. q1.addc("ACCESSED",(unsigned)time(NULL)); q1.appendf(" WHERE IMSI='%s'",imsi); // You must include the quotes around IMSI runQuery(q1.c_str(),true); } // Update or create a new entry in the TMSI table. // If we are assigning TMSIs to the MS, return the new TMSI, else 0. // This routine checks to see if the record for this IMSI already exists; the caller actually knows this and could inform us, // however there is a great deal of logic with many options between the original TMSITable lookup and updating // the TMSITable entry here, so I deemed it more robust and resistant to future changes if this routine ignores that // and does another lookup of the IMSI to see if the record already exists. uint32_t TMSITable::tmsiTabCreateOrUpdate( const string imsi, TmsiTableStore *store, const GSM::L3LocationAreaIdentity * lai, uint32_t oldTmsi) { // Create or find an entry based on IMSI. // Return assigned TMSI. assert(mTmsiDB); bool sendTmsis = configSendTmsis(); unsigned now = (unsigned)time(NULL); ScopedLock lock(sTmsiMutex,__FILE__,__LINE__); // This lock should be redundant - sql serializes access, but it may prevent sql retry failures. unsigned oldRawTmsi = tmsiTabGetTMSI(imsi,false); bool isNewRecord = (oldRawTmsi == 0); TSqlQuery *queryp; // dufus language TSqlInsert foo; TSqlUpdate bar; if (isNewRecord) { // Create a new record. queryp = &foo; queryp->reserve(200); queryp->append("INSERT INTO TMSI_TABLE ("); queryp->addc("IMSI",imsi); queryp->addc("CREATED",now); queryp->addc("ACCESSED",now); } else { queryp = &bar; queryp->reserve(200); // Update existing record. queryp->append("UPDATE TMSI_TABLE SET "); } uint32_t tmsi = 0; if (sendTmsis) { // We are handling several cases here. If it is a new record we are allocating a new tmsi for the first time, // or if it is an existing record that was created without an assigned tmsi, ie, it has a fake tmsi, // then we are updating it to a real tmsi. // We never go backwards, ie, once we allocate a real tmsi, we never change it back to a fake tmsi, // because we want to reserve the tmsi for this handset effectively forever. if (! tmsiIsValid(oldRawTmsi)) { gReports.incr("OpenBTS.GSM.MM.TMSI.Assigned"); tmsi = allocateTmsi(); queryp->addc("TMSI",tmsi2table(tmsi)); } else { tmsi = oldRawTmsi; } } else { if (isNewRecord) { // We are creating a new record. // sqlite3 would auto-assign a unique TMSI higher than any other in // the table, but we need to handle the initial case for the first fake tmsi. tmsi = ++sHighestFakeTmsi; queryp->addc("TMSI",tmsi2table(tmsi)); } } queryp->addStore(store); if (isNewRecord && lai) { queryp->addc("OLD_MCC",lai->MCC()); queryp->addc("OLD_MNC",lai->MNC()); queryp->addc("OLD_LAC",lai->LAC()); queryp->addc("OLD_TMSI",oldTmsi); queryp->finish(); } else { queryp->append(format(" WHERE IMSI=='%s'",imsi)); } if (!runQuery(queryp->c_str(),1)) { LOG(ALERT) << "TMSI creation failed for"<MCC()); query.addc("OLD_MNC",lai->MNC()); query.addc("OLD_LAC",lai->LAC()); query.addc("OLD_TMSI",oldTmsi); } query.finish(); if (!runQuery(query.c_str(),1)) { LOG(ALERT) << "TMSI creation failed, query:"<MCC()); query.add("OLD_MNC",lai->MNC()); query.add("OLD_LAC",lai->LAC()); query.add("OLD_TMSI",oldTmsi); } query.append(") VALUES ("); query.append(values); query.append(")"); if (!runQuery(query.c_str(),1)) { LOG(ALERT) << "TMSI creation failed, query:"<store_valid = true; // We have either updated the store or confirmed the imsi does not exist in the TMSI_TABLE. sqlQuery q11(mTmsiDB,"TMSI_TABLE","AUTH,AUTH_EXPIRY,TMSI_ASSIGNED,REJECT_CODE,WELCOME_SENT", "IMSI",imsi.c_str()); if (!q11.sqlSuccess()) { LOG(INFO) << "No TMSI_TABLE table entry for"<auth = (Authorization) q11.getResultInt(0); //store->auth_valid = true; store->authExpiry = q11.getResultInt(1); //store->authExpiry_valid = true; store->assigned = q11.getResultInt(2); //store->assigned_valid = true; store->rejectCode = q11.getResultInt(3); //store->rejectCode_valid = true; store->welcomeSent = q11.getResultInt(4); //store->welcomeSent_valid = true; LOG(DEBUG) <auth)<authExpiry) <assigned)<rejectCode)<welcomeSent); return true; } string TMSITable::tmsiTabGetIMSI(unsigned tmsi, unsigned *pAuthorizationResult) const { string imsi; if (pAuthorizationResult) tmsiTabTouchTmsi(tmsi); sqlQuery q4(mTmsiDB,"TMSI_TABLE","IMSI,AUTH", "TMSI",tmsi2table(tmsi)); if (!q4.sqlSuccess()) { if (pAuthorizationResult) { *pAuthorizationResult = 0; } } else { imsi = q4.getResultText(0); int auth = q4.getResultInt(1); if (pAuthorizationResult) { *pAuthorizationResult = auth; } LOG(DEBUG) < > TMSITable::tmsiTabView(int verbosity, bool rawFlag, unsigned maxrows) const { vector< vector > view; vector vh1; string header1 = verbosity >= 2 ? TmsiTableDefinition::longerHeader : verbosity == 1 ? TmsiTableDefinition::longHeader : TmsiTableDefinition::shortHeader; vector headers = stringSplit(vh1,header1.c_str()); view.push_back(headers); string columns = replaceAll(header1," ",","); sqlQuery query(mTmsiDB,"TMSI_TABLE",columns.c_str(),"ORDER BY ACCESSED DESC"); // descending unsigned nrows = 1; time_t now = time(NULL); // If the table is completely empty this returns a row of all empty values. while (query.sqlResultSize() && (nrows++ < maxrows)) { // Print out the columns. vector row; row.clear(); unsigned col = 0; for (col = 0; col < vh1.size(); col++) { string header = headers[col]; //if (header.find("TMSI") != string::npos) // Needed if we add PTMSI if (header == "TMSI") { // TMSI. Use hex and fix negative numbers. int rawtmsi = query.getResultInt(col); if (rawFlag || tmsiIsValid(rawtmsi)) { unsigned tmsi = table2tmsi(rawtmsi); row.push_back(format("0x%x",(unsigned) tmsi)); } else { // This is a dummy tmsi created by sqlite. row.push_back("-"); } } else if (header == "CREATED" || header == "ACCESSED") { // Print seconds as a time value. row.push_back(prettyAge(now - query.getResultInt(col))); } else if (header == "AUTH_EXPIRY") { // The expiry is the absolute time when it expires, or 0 if unknown. int expiry = query.getResultInt(col); int remaining = expiry - now; if (remaining < 0) remaining = 0; row.push_back(expiry ? prettyAge(remaining) : "-"); } else { // All other column types. If they are integer they are converted to "" if NULL else decimal. row.push_back(query.getResultText(col)); } } view.push_back(row); if (! query.sqlStep()) break; } return view; } void TMSITable::tmsiTabDump(int verbosity,bool rawFlag, ostream& os, bool showAll, bool taboption) const { // Dump the TMSI table. unsigned maxrows = showAll ? 2^31 : 100; vector< vector > view = tmsiTabView(verbosity, rawFlag, maxrows); #if unused // Add the IMSI authorization failures. They dont have TMSIs, or any other information. vector failList; getAuthFailures(failList); for (vector::iterator it = failList.begin(); it != failList.end(); it++) { if (view.size() >= maxrows) break; vector failureRow; failureRow.push_back(*it); // The IMSI; failureRow.push_back(string("-")); // TMSI failureRow.push_back(string("0")); // AUTH view.push_back(failureRow); } #endif if (view.size() >= maxrows) { vector tmp; tmp.push_back(string("...")); view.push_back(tmp); } printPrettyTable(view,os,taboption); #if 0 // previous tmsi table dumping code. sqlite3_stmt *stmt; if (sqlite3_prepare_statement(mTmsiDB,&stmt,"SELECT TMSI,IMSI,CREATED,ACCESSED FROM TMSI_TABLE ORDER BY ACCESSED DESC")) { LOG(ERR) << "sqlite3_prepare_statement failed"; return; } time_t now = time(NULL); while (sqlite3_run_query(mTmsiDB,stmt)==SQLITE_ROW) { os << hex << setw(8) << sqlite3_column_int64(stmt,0) << ' ' << dec; os << sqlite3_column_text(stmt,1) << ' '; printAge(now-sqlite3_column_int(stmt,2),os); os << ' '; printAge(now-sqlite3_column_int(stmt,3),os); os << ' '; os << endl; } sqlite3_finalize(stmt); #endif } #if UNUSED void TMSITable::setIMEI(string IMSI, string IMEI) { // If the IMEI has changed, update it and also reset RRLP_STATUS. // The A5_SUPPORT and POWER_CLASS have to change too, but that is done by classmark(). if (IMEI.size()) { sqlQuery qu(mTmsiDB,"TMSI_TABLE","IMEI","IMSI",IMSI.c_str()); string oldIMEI = qu.getResultText(0); if (oldIMEI != IMEI) { LOG(INFO) << "Updating" <