576 lines
15 KiB
C
576 lines
15 KiB
C
/* OsmoUPF: Verify that skipping used ids works for: UP-SEID, GTP local TEID, nft ruleset chain_id. */
|
|
|
|
/* (C) 2023 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
|
|
*
|
|
* All Rights Reserved
|
|
*
|
|
* Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0+
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include <getopt.h>
|
|
|
|
#include <nftables/libnftables.h>
|
|
|
|
#include <osmocom/core/sockaddr_str.h>
|
|
#include <osmocom/core/application.h>
|
|
|
|
#include <osmocom/pfcp/pfcp_endpoint.h>
|
|
|
|
#include <osmocom/upf/upf.h>
|
|
#include <osmocom/upf/netinst.h>
|
|
#include <osmocom/upf/up_endpoint.h>
|
|
#include <osmocom/upf/up_peer.h>
|
|
#include <osmocom/upf/up_session.h>
|
|
#include <osmocom/upf/up_gtp_action.h>
|
|
|
|
#define log(FMT, ARGS...) fprintf(stderr, FMT, ##ARGS)
|
|
#define log_assert(COND) do { \
|
|
log("assert(" #COND ")\n"); \
|
|
OSMO_ASSERT(COND); \
|
|
} while (0)
|
|
|
|
#define log_assert_expect_failure(COND) do { \
|
|
log("assert(" #COND ") <-- EXPECTED TO FAIL (known error)\n"); \
|
|
OSMO_ASSERT(!(COND)); \
|
|
} while (0)
|
|
|
|
|
|
void *main_ctx;
|
|
void *ctx;
|
|
|
|
/* The override of osmo_pfcp_endpoint_tx() stores any Session Establishment Response's UP-SEID here, so that this test
|
|
* can reference specific sessions later.
|
|
*/
|
|
uint64_t last_up_seid = 0;
|
|
|
|
void select_poll(void)
|
|
{
|
|
while (osmo_select_main_ctx(1));
|
|
}
|
|
|
|
static void setup(const char *name)
|
|
{
|
|
log("\n===== START of %s\n", name);
|
|
ctx = talloc_named_const(main_ctx, 0, name);
|
|
g_upf_alloc(ctx);
|
|
osmo_talloc_replace_string(g_upf, &g_upf->pfcp.vty_cfg.local_addr, "1.1.1.1");
|
|
|
|
OSMO_ASSERT(netinst_add(g_upf, &g_upf->netinst, "default", "1.1.1.1", NULL));
|
|
|
|
/* PFCP endpoint recovery timestamp overridden by time() below */
|
|
upf_pfcp_init();
|
|
/* but do not upf_pfcp_listen() */
|
|
|
|
upf_nft_init();
|
|
|
|
select_poll();
|
|
log("\n");
|
|
}
|
|
|
|
static void cleanup(void)
|
|
{
|
|
up_endpoint_free(&g_upf->pfcp.ep);
|
|
upf_gtp_devs_close();
|
|
|
|
upf_gtp_genl_close();
|
|
|
|
upf_nft_free();
|
|
|
|
log("\n===== END of %s\n", talloc_get_name(ctx));
|
|
talloc_free(ctx);
|
|
}
|
|
|
|
static struct osmo_sockaddr *str2addr(const char *addr, uint16_t port)
|
|
{
|
|
static struct osmo_sockaddr osa;
|
|
struct osmo_sockaddr_str str;
|
|
osmo_sockaddr_str_from_str(&str, addr, port);
|
|
osmo_sockaddr_str_to_sockaddr(&str, &osa.u.sas);
|
|
return &osa;
|
|
}
|
|
|
|
static struct up_peer *have_peer(const char *remote_addr, uint16_t port)
|
|
{
|
|
return up_peer_find_or_add(g_upf->pfcp.ep, str2addr(remote_addr, port));
|
|
}
|
|
|
|
static struct osmo_pfcp_msg *new_pfcp_msg_for_osmo_upf_rx(struct up_peer *from_peer, enum osmo_pfcp_message_type msg_type)
|
|
{
|
|
/* pfcp_endpoint discards received messages immediately after dispatching; in this test, allocate them in
|
|
* OTC_SELECT so they get discarded on the next select_poll().
|
|
* osmo_pfcp_msg_alloc_rx() is not useful here, it creates a blank struct to be decoded from raw data; instead,
|
|
* use osmo_pfcp_msg_alloc_tx_req() which properly sets up the internal structures to match the given msg_type,
|
|
* and when that is done set m->rx = true to indicate it is a message received by osmo-upf. */
|
|
struct osmo_pfcp_msg *m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &from_peer->remote_addr, msg_type);
|
|
m->rx = true;
|
|
return m;
|
|
}
|
|
|
|
static void peer_assoc(struct up_peer *peer)
|
|
{
|
|
struct osmo_pfcp_msg *m = new_pfcp_msg_for_osmo_upf_rx(peer, OSMO_PFCP_MSGT_ASSOC_SETUP_REQ);
|
|
m->ies.assoc_setup_req.recovery_time_stamp = 1234;
|
|
osmo_fsm_inst_dispatch(peer->fi, UP_PEER_EV_RX_ASSOC_SETUP_REQ, m);
|
|
select_poll();
|
|
}
|
|
|
|
static int next_teid = 0x100;
|
|
static int next_cp_seid = 0x100;
|
|
|
|
/* Send a PFCP Session Establishment Request, and return the created session */
|
|
static struct up_session *session_est_tunmap(struct up_peer *peer)
|
|
{
|
|
struct osmo_pfcp_msg *m;
|
|
|
|
struct osmo_pfcp_ie_f_seid cp_f_seid;
|
|
|
|
struct osmo_pfcp_ie_f_teid f_teid_access_local;
|
|
struct osmo_pfcp_ie_outer_header_creation ohc_access;
|
|
|
|
struct osmo_pfcp_ie_f_teid f_teid_core_local;
|
|
struct osmo_pfcp_ie_outer_header_creation ohc_core;
|
|
|
|
struct osmo_pfcp_ie_apply_action aa = {};
|
|
|
|
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_FORW, true);
|
|
|
|
f_teid_access_local = (struct osmo_pfcp_ie_f_teid){
|
|
.choose_flag = true,
|
|
.choose = {
|
|
.ipv4_addr = true,
|
|
},
|
|
};
|
|
|
|
ohc_access = (struct osmo_pfcp_ie_outer_header_creation){
|
|
.teid_present = true,
|
|
.teid = next_teid++,
|
|
.ip_addr = {
|
|
.v4_present = true,
|
|
.v4 = *str2addr("5.6.7.8", 0),
|
|
},
|
|
};
|
|
osmo_pfcp_bits_set(ohc_access.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
|
|
|
|
f_teid_core_local = (struct osmo_pfcp_ie_f_teid){
|
|
.choose_flag = true,
|
|
.choose = {
|
|
.ipv4_addr = true,
|
|
},
|
|
};
|
|
|
|
ohc_core = (struct osmo_pfcp_ie_outer_header_creation){
|
|
.teid_present = true,
|
|
.teid = next_teid++,
|
|
.ip_addr = {
|
|
.v4_present = true,
|
|
.v4 = *str2addr("13.14.15.16", 0),
|
|
},
|
|
};
|
|
osmo_pfcp_bits_set(ohc_core.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
|
|
|
|
cp_f_seid = (struct osmo_pfcp_ie_f_seid){
|
|
.seid = next_cp_seid++,
|
|
};
|
|
osmo_pfcp_ip_addrs_set(&cp_f_seid.ip_addr, osmo_pfcp_endpoint_get_local_addr(g_upf->pfcp.ep->pfcp_ep));
|
|
|
|
m = new_pfcp_msg_for_osmo_upf_rx(peer, OSMO_PFCP_MSGT_SESSION_EST_REQ);
|
|
m->h.seid_present = true;
|
|
m->h.seid = 0;
|
|
/* GTP tunmap: remove header from both directions, and add header in both directions */
|
|
m->ies.session_est_req = (struct osmo_pfcp_msg_session_est_req){
|
|
.node_id = m->ies.session_est_req.node_id,
|
|
.cp_f_seid_present = true,
|
|
.cp_f_seid = cp_f_seid,
|
|
.create_pdr_count = 2,
|
|
.create_pdr = {
|
|
{
|
|
.pdr_id = 1,
|
|
.precedence = 255,
|
|
.pdi = {
|
|
.source_iface = OSMO_PFCP_SOURCE_IFACE_CORE,
|
|
.local_f_teid_present = true,
|
|
.local_f_teid = f_teid_core_local,
|
|
},
|
|
.outer_header_removal_present = true,
|
|
.outer_header_removal = {
|
|
.desc = OSMO_PFCP_OUTER_HEADER_REMOVAL_GTP_U_UDP_IPV4,
|
|
},
|
|
.far_id_present = true,
|
|
.far_id = 1,
|
|
},
|
|
{
|
|
.pdr_id = 2,
|
|
.precedence = 255,
|
|
.pdi = {
|
|
.source_iface = OSMO_PFCP_SOURCE_IFACE_ACCESS,
|
|
.local_f_teid_present = true,
|
|
.local_f_teid = f_teid_access_local,
|
|
},
|
|
.outer_header_removal_present = true,
|
|
.outer_header_removal = {
|
|
.desc = OSMO_PFCP_OUTER_HEADER_REMOVAL_GTP_U_UDP_IPV4,
|
|
},
|
|
.far_id_present = true,
|
|
.far_id = 2,
|
|
},
|
|
},
|
|
.create_far_count = 2,
|
|
.create_far = {
|
|
{
|
|
.far_id = 1,
|
|
.forw_params_present = true,
|
|
.forw_params = {
|
|
.destination_iface = OSMO_PFCP_DEST_IFACE_ACCESS,
|
|
.outer_header_creation_present = true,
|
|
.outer_header_creation = ohc_access,
|
|
},
|
|
.apply_action = aa,
|
|
},
|
|
{
|
|
.far_id = 2,
|
|
.forw_params_present = true,
|
|
.forw_params = {
|
|
.destination_iface = OSMO_PFCP_DEST_IFACE_CORE,
|
|
.outer_header_creation_present = true,
|
|
.outer_header_creation = ohc_core,
|
|
},
|
|
.apply_action = aa,
|
|
},
|
|
},
|
|
};
|
|
|
|
osmo_fsm_inst_dispatch(peer->fi, UP_PEER_EV_RX_SESSION_EST_REQ, m);
|
|
select_poll();
|
|
|
|
return up_session_find_by_up_seid(peer, last_up_seid);
|
|
}
|
|
|
|
static void session_del(struct up_session *session)
|
|
{
|
|
struct osmo_pfcp_msg *m;
|
|
|
|
log_assert(session);
|
|
|
|
m = new_pfcp_msg_for_osmo_upf_rx(session->up_peer, OSMO_PFCP_MSGT_SESSION_DEL_REQ);
|
|
m->h.seid_present = true;
|
|
m->h.seid = session->up_seid;
|
|
|
|
osmo_fsm_inst_dispatch(session->fi, UP_SESSION_EV_RX_SESSION_DEL_REQ, m);
|
|
select_poll();
|
|
}
|
|
|
|
static void dump_state(void)
|
|
{
|
|
struct up_peer *peer;
|
|
log("\n state:\n");
|
|
llist_for_each_entry(peer, &g_upf->pfcp.ep->peers, entry) {
|
|
struct up_session *session;
|
|
int bkt;
|
|
log(" | peer %s %s\n", peer->fi->name, osmo_fsm_inst_state_name(peer->fi));
|
|
hash_for_each(peer->sessions_by_up_seid, bkt, session, node_by_up_seid) {
|
|
struct up_gtp_action *a;
|
|
llist_for_each_entry(a, &session->active_gtp_actions, entry) {
|
|
if (a->kind != UP_GTP_U_TUNMAP)
|
|
continue;
|
|
log(" | session[%s]: UP-SEID 0x%"PRIx64"; chain_id access=%u core=%u;"
|
|
" local TEID access=0x%x core=0x%x\n",
|
|
osmo_fsm_inst_state_name(session->fi),
|
|
session->up_seid,
|
|
a->tunmap.access.chain_id, a->tunmap.core.chain_id,
|
|
a->tunmap.access.tun.local.teid, a->tunmap.core.tun.local.teid);
|
|
}
|
|
}
|
|
}
|
|
log("\n");
|
|
}
|
|
|
|
static void test_skip_used_id(void)
|
|
{
|
|
struct up_peer *peer;
|
|
struct up_session *s1;
|
|
uint64_t s1_up_seid;
|
|
struct up_session *s2;
|
|
struct up_session *s3;
|
|
struct up_session *s4;
|
|
struct up_gtp_action *a;
|
|
|
|
setup(__func__);
|
|
|
|
log("PFCP Associate peer\n");
|
|
peer = have_peer("1.2.3.4", 1234);
|
|
peer_assoc(peer);
|
|
dump_state();
|
|
|
|
/* Make sure to start out all IDs with 1 */
|
|
g_upf->pfcp.ep->next_up_seid_state = 0;
|
|
g_upf->gtp.next_local_teid_state = 0;
|
|
g_upf->tunmap.next_chain_id_state = 0;
|
|
|
|
log("set up tunmap, which assigns first UP-SEID 0x1, local-TEID 0x1 and 0x2, chain_ids 1 and 2\n");
|
|
s1 = session_est_tunmap(peer);
|
|
dump_state();
|
|
|
|
log_assert(s1->up_seid == 1);
|
|
a = llist_first_entry_or_null(&s1->active_gtp_actions, struct up_gtp_action, entry);
|
|
log_assert(a);
|
|
log_assert(a->kind == UP_GTP_U_TUNMAP);
|
|
log_assert(a->tunmap.core.tun.local.teid == 1);
|
|
log_assert(a->tunmap.access.tun.local.teid == 2);
|
|
log_assert(a->tunmap.access.chain_id == 1);
|
|
log_assert(a->tunmap.core.chain_id == 2);
|
|
log("\n");
|
|
|
|
log("simulate wrapping of IDs back to 1\n");
|
|
g_upf->pfcp.ep->next_up_seid_state = 0;
|
|
g_upf->gtp.next_local_teid_state = 0;
|
|
g_upf->tunmap.next_chain_id_state = 0;
|
|
|
|
log("set up second tunmap, should use distinct IDs\n");
|
|
s2 = session_est_tunmap(peer);
|
|
dump_state();
|
|
|
|
log_assert(s2->up_seid == 2);
|
|
a = llist_first_entry_or_null(&s2->active_gtp_actions, struct up_gtp_action, entry);
|
|
log_assert(a);
|
|
log_assert(a->kind == UP_GTP_U_TUNMAP);
|
|
log_assert(a->tunmap.core.tun.local.teid == 3);
|
|
log_assert(a->tunmap.access.tun.local.teid == 4);
|
|
log_assert(a->tunmap.access.chain_id == 3);
|
|
log_assert(a->tunmap.core.chain_id == 4);
|
|
log("\n");
|
|
|
|
log("drop first tunmap (%s)\n", s1->fi->name);
|
|
s1_up_seid = s1->up_seid;
|
|
session_del(s1);
|
|
dump_state();
|
|
log_assert(up_session_find_by_up_seid(peer, s1_up_seid) == NULL);
|
|
log("\n");
|
|
|
|
log("again wrap all ID state back to 1\n");
|
|
g_upf->pfcp.ep->next_up_seid_state = 0;
|
|
g_upf->gtp.next_local_teid_state = 0;
|
|
g_upf->tunmap.next_chain_id_state = 0;
|
|
|
|
log("set up third tunmap, should now re-use same IDs as the first session\n");
|
|
s3 = session_est_tunmap(peer);
|
|
dump_state();
|
|
|
|
log_assert(s3->up_seid == 1);
|
|
a = llist_first_entry_or_null(&s3->active_gtp_actions, struct up_gtp_action, entry);
|
|
log_assert(a);
|
|
log_assert(a->kind == UP_GTP_U_TUNMAP);
|
|
log_assert(a->tunmap.core.tun.local.teid == 1);
|
|
log_assert(a->tunmap.access.tun.local.teid == 2);
|
|
log_assert(a->tunmap.access.chain_id == 1);
|
|
log_assert(a->tunmap.core.chain_id == 2);
|
|
log("\n");
|
|
|
|
log("set up 4th tunmap; chain_id state would use 3 and 4, but they are in use, so should assign 5 and 6\n");
|
|
s4 = session_est_tunmap(peer);
|
|
dump_state();
|
|
|
|
log_assert(s4->up_seid == 3);
|
|
a = llist_first_entry_or_null(&s4->active_gtp_actions, struct up_gtp_action, entry);
|
|
log_assert(a);
|
|
log_assert(a->kind == UP_GTP_U_TUNMAP);
|
|
log_assert(a->tunmap.core.tun.local.teid == 5);
|
|
log_assert(a->tunmap.access.tun.local.teid == 6);
|
|
log_assert(a->tunmap.access.chain_id == 5);
|
|
log_assert(a->tunmap.core.chain_id == 6);
|
|
log("\n");
|
|
|
|
cleanup();
|
|
}
|
|
|
|
static const struct log_info_cat test_default_categories[] = {
|
|
[DREF] = {
|
|
.name = "DREF",
|
|
.description = "Reference Counting",
|
|
.enabled = 1, .loglevel = LOGL_DEBUG,
|
|
.color = OSMO_LOGCOLOR_DARKGREY,
|
|
},
|
|
[DPEER] = {
|
|
.name = "DPEER",
|
|
.description = "PFCP peer association",
|
|
.enabled = 1, .loglevel = LOGL_DEBUG,
|
|
.color = OSMO_LOGCOLOR_YELLOW,
|
|
},
|
|
[DSESSION] = {
|
|
.name = "DSESSION",
|
|
.description = "PFCP sessions",
|
|
.enabled = 1, .loglevel = LOGL_DEBUG,
|
|
.color = OSMO_LOGCOLOR_BLUE,
|
|
},
|
|
[DGTP] = {
|
|
.name = "DGTP",
|
|
.description = "GTP tunneling",
|
|
.enabled = 1, .loglevel = LOGL_DEBUG,
|
|
.color = OSMO_LOGCOLOR_PURPLE,
|
|
},
|
|
[DNFT] = {
|
|
.name = "DNFT",
|
|
.description = "GTP forwarding rules via linux netfilter",
|
|
.enabled = 1, .loglevel = LOGL_DEBUG,
|
|
.color = OSMO_LOGCOLOR_PURPLE,
|
|
},
|
|
};
|
|
|
|
const struct log_info log_info = {
|
|
.cat = test_default_categories,
|
|
.num_cat = ARRAY_SIZE(test_default_categories),
|
|
};
|
|
|
|
static struct {
|
|
bool verbose;
|
|
} cmdline_opts = {
|
|
.verbose = false,
|
|
};
|
|
|
|
static void print_help(const char *program)
|
|
{
|
|
printf("Usage:\n"
|
|
" %s [-v]\n"
|
|
"Options:\n"
|
|
" -h --help show this text.\n"
|
|
" -v --verbose print source file and line numbers\n",
|
|
program
|
|
);
|
|
}
|
|
|
|
static void handle_options(int argc, char **argv)
|
|
{
|
|
while (1) {
|
|
int option_index = 0, c;
|
|
static struct option long_options[] = {
|
|
{"help", 0, 0, 'h'},
|
|
{"verbose", 1, 0, 'v'},
|
|
{0, 0, 0, 0}
|
|
};
|
|
|
|
c = getopt_long(argc, argv, "hv",
|
|
long_options, &option_index);
|
|
if (c == -1)
|
|
break;
|
|
|
|
switch (c) {
|
|
case 'h':
|
|
print_help(argv[0]);
|
|
exit(0);
|
|
case 'v':
|
|
cmdline_opts.verbose = true;
|
|
break;
|
|
default:
|
|
/* catch unknown options *as well as* missing arguments. */
|
|
fprintf(stderr, "Error in command line options. Exiting.\n");
|
|
exit(-1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
handle_options(argc, argv);
|
|
|
|
main_ctx = talloc_named_const(NULL, 0, "main");
|
|
|
|
msgb_talloc_ctx_init(main_ctx, 0);
|
|
|
|
osmo_fsm_set_dealloc_ctx(OTC_SELECT);
|
|
|
|
osmo_init_logging2(main_ctx, &log_info);
|
|
log_set_print_category_hex(osmo_stderr_target, 0);
|
|
log_set_print_category(osmo_stderr_target, 1);
|
|
log_set_print_level(osmo_stderr_target, 1);
|
|
log_set_print_timestamp(osmo_stderr_target, 0);
|
|
log_set_print_extended_timestamp(osmo_stderr_target, 0);
|
|
log_set_all_filter(osmo_stderr_target, 1);
|
|
|
|
if (cmdline_opts.verbose) {
|
|
log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_BASENAME);
|
|
log_set_print_filename_pos(osmo_stderr_target, LOG_FILENAME_POS_LINE_END);
|
|
log_set_use_color(osmo_stderr_target, 1);
|
|
} else {
|
|
log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_NONE);
|
|
log_set_use_color(osmo_stderr_target, 0);
|
|
}
|
|
|
|
osmo_fsm_log_timeouts(true);
|
|
osmo_fsm_log_addr(false);
|
|
|
|
/* actual tests */
|
|
test_skip_used_id();
|
|
|
|
log_fini();
|
|
talloc_free(main_ctx);
|
|
return 0;
|
|
}
|
|
|
|
/* overrides */
|
|
|
|
int osmo_pfcp_endpoint_tx(struct osmo_pfcp_endpoint *ep, struct osmo_pfcp_msg *m)
|
|
{
|
|
enum osmo_pfcp_cause *cause;
|
|
|
|
log("\n[test override] PFCP tx:\n%s\n\n", osmo_pfcp_msg_to_str_c(OTC_SELECT, m));
|
|
|
|
last_up_seid = 0;
|
|
|
|
cause = osmo_pfcp_msg_cause(m);
|
|
switch (m->h.message_type) {
|
|
case OSMO_PFCP_MSGT_SESSION_EST_RESP:
|
|
if (*cause == OSMO_PFCP_CAUSE_REQUEST_ACCEPTED) {
|
|
last_up_seid = m->ies.session_est_resp.up_f_seid.seid;
|
|
log("osmo-upf created session 0x%"PRIx64"\n\n", last_up_seid);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
};
|
|
osmo_pfcp_msg_free(m);
|
|
return 0;
|
|
}
|
|
|
|
static void *fake_nft_ctx = (void *)0x1;
|
|
|
|
struct nft_ctx *nft_ctx_new(uint32_t flags)
|
|
{
|
|
log("[test override] %s()\n", __func__);
|
|
return fake_nft_ctx;
|
|
}
|
|
|
|
void nft_ctx_free(struct nft_ctx *ctx)
|
|
{
|
|
log("[test override] %s()\n", __func__);
|
|
log_assert(ctx == fake_nft_ctx);
|
|
}
|
|
|
|
int nft_run_cmd_from_buffer(struct nft_ctx *nft, const char *buf)
|
|
{
|
|
log("\n[test override] %s():\n%s\n", __func__, buf);
|
|
return 0;
|
|
}
|
|
|
|
/* for deterministic recovery_time_stamp */
|
|
time_t time(time_t *tloc)
|
|
{
|
|
log("[test override] %s()\n", __func__);
|
|
return 0;
|
|
}
|