| /* Copyright 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de> |
| * |
| * All Rights Reserved |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU Affero General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * 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. See the |
| * GNU Affero General Public License for more details. |
| * |
| * You should have received a copy of the GNU Affero General Public License |
| * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| * |
| */ |
| |
| #include <string.h> |
| #include <errno.h> |
| #include <osmocom/core/sockaddr_str.h> |
| #include <osmocom/gsm/gsup.h> |
| #include <osmocom/mslookup/mslookup.h> |
| #include <osmocom/hlr/logging.h> |
| #include <osmocom/hlr/hlr.h> |
| #include <osmocom/hlr/db.h> |
| #include <osmocom/hlr/timestamp.h> |
| #include <osmocom/hlr/mslookup_server.h> |
| |
| static const struct osmo_mslookup_result not_found = { |
| .rc = OSMO_MSLOOKUP_RC_NOT_FOUND, |
| }; |
| const struct osmo_ipa_name mslookup_server_msc_wildcard = {}; |
| |
| static void set_result(struct osmo_mslookup_result *result, |
| const struct mslookup_service_host *service_host, |
| uint32_t age) |
| { |
| if (!osmo_sockaddr_str_is_nonzero(&service_host->host_v4) |
| && !osmo_sockaddr_str_is_nonzero(&service_host->host_v6)) { |
| *result = not_found; |
| return; |
| } |
| result->rc = OSMO_MSLOOKUP_RC_RESULT; |
| result->host_v4 = service_host->host_v4; |
| result->host_v6 = service_host->host_v6; |
| result->age = age; |
| } |
| |
| struct mslookup_service_host *mslookup_server_get_local_gsup_addr() |
| { |
| static struct mslookup_service_host gsup_bind = {}; |
| struct mslookup_service_host *host; |
| |
| /* Find a HLR/GSUP service set for the server (no VLR unit name) */ |
| host = mslookup_server_service_get(&mslookup_server_msc_wildcard, OSMO_MSLOOKUP_SERVICE_HLR_GSUP); |
| if (host) |
| return host; |
| |
| /* Try to use the locally configured GSUP bind address */ |
| osmo_sockaddr_str_from_str(&gsup_bind.host_v4, g_hlr->gsup_bind_addr, OSMO_GSUP_PORT); |
| if (gsup_bind.host_v4.af == AF_INET6) { |
| gsup_bind.host_v6 = gsup_bind.host_v4; |
| gsup_bind.host_v4 = (struct osmo_sockaddr_str){}; |
| } |
| return &gsup_bind; |
| } |
| |
| struct mslookup_server_msc_cfg *mslookup_server_msc_get(const struct osmo_ipa_name *msc_name, bool create) |
| { |
| struct llist_head *c = &g_hlr->mslookup.server.local_site_services; |
| struct mslookup_server_msc_cfg *msc; |
| |
| if (!msc_name) |
| return NULL; |
| |
| llist_for_each_entry(msc, c, entry) { |
| if (osmo_ipa_name_cmp(&msc->name, msc_name)) |
| continue; |
| return msc; |
| } |
| if (!create) |
| return NULL; |
| |
| msc = talloc_zero(g_hlr, struct mslookup_server_msc_cfg); |
| OSMO_ASSERT(msc); |
| INIT_LLIST_HEAD(&msc->service_hosts); |
| msc->name = *msc_name; |
| llist_add_tail(&msc->entry, c); |
| return msc; |
| } |
| |
| struct mslookup_service_host *mslookup_server_msc_service_get(struct mslookup_server_msc_cfg *msc, const char *service, |
| bool create) |
| { |
| struct mslookup_service_host *e; |
| if (!msc) |
| return NULL; |
| |
| llist_for_each_entry(e, &msc->service_hosts, entry) { |
| if (!strcmp(e->service, service)) |
| return e; |
| } |
| |
| if (!create) |
| return NULL; |
| |
| e = talloc_zero(msc, struct mslookup_service_host); |
| OSMO_ASSERT(e); |
| OSMO_STRLCPY_ARRAY(e->service, service); |
| llist_add_tail(&e->entry, &msc->service_hosts); |
| return e; |
| } |
| |
| struct mslookup_service_host *mslookup_server_service_get(const struct osmo_ipa_name *msc_name, const char *service) |
| { |
| struct mslookup_server_msc_cfg *msc = mslookup_server_msc_get(msc_name, false); |
| if (!msc) |
| return NULL; |
| return mslookup_server_msc_service_get(msc, service, false); |
| } |
| |
| int mslookup_server_msc_service_set(struct mslookup_server_msc_cfg *msc, const char *service, |
| const struct osmo_sockaddr_str *addr) |
| { |
| struct mslookup_service_host *e; |
| |
| if (!service || !service[0] |
| || strlen(service) > OSMO_MSLOOKUP_SERVICE_MAXLEN) |
| return -EINVAL; |
| if (!addr || !osmo_sockaddr_str_is_nonzero(addr)) |
| return -EINVAL; |
| |
| e = mslookup_server_msc_service_get(msc, service, true); |
| if (!e) |
| return -EINVAL; |
| |
| switch (addr->af) { |
| case AF_INET: |
| e->host_v4 = *addr; |
| break; |
| case AF_INET6: |
| e->host_v6 = *addr; |
| break; |
| default: |
| return -EINVAL; |
| } |
| return 0; |
| } |
| |
| int mslookup_server_msc_service_del(struct mslookup_server_msc_cfg *msc, const char *service, |
| const struct osmo_sockaddr_str *addr) |
| { |
| struct mslookup_service_host *e, *n; |
| int deleted = 0; |
| |
| if (!msc) |
| return -ENOENT; |
| |
| llist_for_each_entry_safe(e, n, &msc->service_hosts, entry) { |
| if (service && strcmp(service, e->service)) |
| continue; |
| |
| if (addr) { |
| if (!osmo_sockaddr_str_cmp(addr, &e->host_v4)) { |
| e->host_v4 = (struct osmo_sockaddr_str){}; |
| /* Removed one addr. If the other is still there, keep the entry. */ |
| if (osmo_sockaddr_str_is_nonzero(&e->host_v6)) |
| continue; |
| } else if (!osmo_sockaddr_str_cmp(addr, &e->host_v6)) { |
| e->host_v6 = (struct osmo_sockaddr_str){}; |
| /* Removed one addr. If the other is still there, keep the entry. */ |
| if (osmo_sockaddr_str_is_nonzero(&e->host_v4)) |
| continue; |
| } else |
| /* No addr match, keep the entry. */ |
| continue; |
| /* Addr matched and none is left. Delete. */ |
| } |
| llist_del(&e->entry); |
| talloc_free(e); |
| deleted++; |
| } |
| return deleted; |
| } |
| |
| /* A remote entity is asking us whether we are the home HLR of the given subscriber. */ |
| static void mslookup_server_rx_hlr_gsup(const struct osmo_mslookup_query *query, |
| struct osmo_mslookup_result *result) |
| { |
| struct mslookup_service_host *host; |
| int rc; |
| switch (query->id.type) { |
| case OSMO_MSLOOKUP_ID_IMSI: |
| rc = db_subscr_exists_by_imsi(g_hlr->dbc, query->id.imsi); |
| break; |
| case OSMO_MSLOOKUP_ID_MSISDN: |
| rc = db_subscr_exists_by_msisdn(g_hlr->dbc, query->id.msisdn); |
| break; |
| default: |
| LOGP(DMSLOOKUP, LOGL_ERROR, "Unknown mslookup ID type: %d\n", query->id.type); |
| *result = not_found; |
| return; |
| } |
| |
| if (rc) { |
| LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: does not exist in local HLR\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL)); |
| *result = not_found; |
| return; |
| } |
| |
| LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: found in local HLR\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL)); |
| |
| host = mslookup_server_get_local_gsup_addr(); |
| |
| set_result(result, host, 0); |
| if (result->rc != OSMO_MSLOOKUP_RC_RESULT) { |
| LOGP(DMSLOOKUP, LOGL_ERROR, |
| "Subscriber found, but error in service '" OSMO_MSLOOKUP_SERVICE_HLR_GSUP "' config:" |
| " v4: " OSMO_SOCKADDR_STR_FMT " v6: " OSMO_SOCKADDR_STR_FMT "\n", |
| OSMO_SOCKADDR_STR_FMT_ARGS(&host->host_v4), |
| OSMO_SOCKADDR_STR_FMT_ARGS(&host->host_v6)); |
| } |
| } |
| |
| /* Look in the local HLR record: If the subscriber is "at home" in this HLR and is also currently located at a local |
| * VLR, we will find a valid location updating with vlr_number, and no vlr_via_proxy entry. */ |
| static bool subscriber_has_done_lu_here_hlr(const struct osmo_mslookup_query *query, |
| uint32_t *lu_age, |
| struct osmo_ipa_name *local_msc_name) |
| { |
| struct hlr_subscriber subscr; |
| int rc; |
| uint32_t age; |
| |
| switch (query->id.type) { |
| case OSMO_MSLOOKUP_ID_IMSI: |
| rc = db_subscr_get_by_imsi(g_hlr->dbc, query->id.imsi, &subscr); |
| break; |
| case OSMO_MSLOOKUP_ID_MSISDN: |
| rc = db_subscr_get_by_msisdn(g_hlr->dbc, query->id.msisdn, &subscr); |
| break; |
| default: |
| LOGP(DMSLOOKUP, LOGL_ERROR, "Unknown mslookup ID type: %d\n", query->id.type); |
| return false; |
| } |
| |
| if (rc) { |
| LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: does not exist in local HLR\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL)); |
| return false; |
| } |
| |
| if (!subscr.vlr_number[0]) { |
| LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: not attached (vlr_number unset)\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL)); |
| } |
| |
| if (subscr.vlr_via_proxy.len) { |
| /* The VLR is behind a proxy, the subscriber is not attached to a local VLR but a remote one. That |
| * remote proxy should instead respond to the service lookup request. */ |
| LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: last attach is not at local VLR, but at VLR '%s' via proxy %s\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL), |
| subscr.vlr_number, |
| osmo_ipa_name_to_str(&subscr.vlr_via_proxy)); |
| return false; |
| } |
| |
| if (!timestamp_age(&subscr.last_lu_seen, &age)) { |
| LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: Invalid last_lu_seen timestamp for subscriber\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL)); |
| return false; |
| } |
| if (age > g_hlr->mslookup.server.local_attach_max_age) { |
| LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: last attach was here, but too long ago: %us > %us\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL), |
| age, g_hlr->mslookup.server.local_attach_max_age); |
| return false; |
| } |
| |
| *lu_age = age; |
| osmo_ipa_name_set_str(local_msc_name, subscr.vlr_number); |
| LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: attached %u seconds ago at local VLR %s\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL), |
| age, osmo_ipa_name_to_str(local_msc_name)); |
| |
| return true; |
| } |
| |
| static bool subscriber_has_done_lu_here(const struct osmo_mslookup_query *query, |
| uint32_t *lu_age_p, |
| struct osmo_ipa_name *local_msc_name) |
| { |
| bool attached_here; |
| uint32_t lu_age = 0; |
| struct osmo_ipa_name msc_name = {}; |
| |
| /* First ask the local HLR db, but if the local proxy record indicates a more recent LU, use that instead. |
| * For all usual cases, only one of these will reflect a LU, even if a subscriber had more than one home HLR: |
| * - if the subscriber is known here, we will never proxy. |
| * - if the subscriber is not known here, this local HLR db will never record a LU. |
| * However, if a subscriber was being proxied to a remote home HLR, and if then the subscriber was also added to |
| * the local HLR database, there might occur a situation where both reflect a LU. So, to be safe against all |
| * situations, compare the two entries. |
| */ |
| attached_here = subscriber_has_done_lu_here_hlr(query, &lu_age, &msc_name); |
| |
| /* Future: If proxy has a younger lu, replace. */ |
| |
| if (attached_here && !msc_name.len) { |
| LOGP(DMSLOOKUP, LOGL_ERROR, "%s: attached here, but no VLR name known\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL)); |
| return false; |
| } |
| |
| if (!attached_here) { |
| /* Already logged "not attached" for both local-db and proxy attach */ |
| return false; |
| } |
| |
| LOGP(DMSLOOKUP, LOGL_INFO, "%s: attached here, at VLR %s\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL), |
| osmo_ipa_name_to_str(&msc_name)); |
| *lu_age_p = lu_age; |
| *local_msc_name = msc_name; |
| return true; |
| } |
| |
| /* A remote entity is asking us whether we are providing the given service for the given subscriber. */ |
| void mslookup_server_rx(const struct osmo_mslookup_query *query, |
| struct osmo_mslookup_result *result) |
| { |
| const struct mslookup_service_host *service_host; |
| uint32_t age; |
| struct osmo_ipa_name msc_name; |
| |
| /* A request for a home HLR: answer exactly if this is the subscriber's home HLR, i.e. the IMSI is listed in the |
| * HLR database. */ |
| if (strcmp(query->service, OSMO_MSLOOKUP_SERVICE_HLR_GSUP) == 0) |
| return mslookup_server_rx_hlr_gsup(query, result); |
| |
| /* All other service types: answer when the subscriber has done a LU that is either listed in the local HLR or |
| * in the GSUP proxy database: i.e. if the subscriber has done a Location Updating at an VLR belonging to this |
| * HLR. Respond with whichever services are configured in the osmo-hlr.cfg. */ |
| if (!subscriber_has_done_lu_here(query, &age, &msc_name)) { |
| *result = not_found; |
| return; |
| } |
| |
| /* We've detected a LU here. The VLR where the LU happened is stored in msc_unit_name, and the LU age is stored |
| * in 'age'. Figure out the address configured for that VLR and service name. */ |
| service_host = mslookup_server_service_get(&msc_name, query->service); |
| |
| if (!service_host) { |
| /* Find such service set globally (no VLR unit name) */ |
| service_host = mslookup_server_service_get(&mslookup_server_msc_wildcard, query->service); |
| } |
| |
| if (!service_host) { |
| LOGP(DMSLOOKUP, LOGL_ERROR, |
| "%s: subscriber found, but no service %s configured, cannot service lookup request\n", |
| osmo_mslookup_result_name_c(OTC_SELECT, query, NULL), |
| osmo_quote_str_c(OTC_SELECT, query->service, -1)); |
| *result = not_found; |
| return; |
| } |
| |
| set_result(result, service_host, age); |
| } |