diff options
-rw-r--r-- | configs/res_stun_monitor.conf.sample | 17 | ||||
-rw-r--r-- | include/asterisk/stun.h | 43 | ||||
-rw-r--r-- | main/stun.c | 126 | ||||
-rw-r--r-- | res/res_stun_monitor.c | 302 |
4 files changed, 311 insertions, 177 deletions
diff --git a/configs/res_stun_monitor.conf.sample b/configs/res_stun_monitor.conf.sample index 9237799c5..12d32a4cd 100644 --- a/configs/res_stun_monitor.conf.sample +++ b/configs/res_stun_monitor.conf.sample @@ -2,11 +2,11 @@ ; Configuration file for the res_stun_monitor module ; ; The res_stun_monitor module sends STUN requests to a configured STUN server -; periodically. If the monitor detects a change in the external ip or port +; periodically. If the monitor detects a change in the external IP address or port ; provided by the STUN server an event is sent out internally within Asterisk ; to alert all listeners to that event of the change. -; The current default listeners for the netork change event include chan_sip +; The current default listeners for the network change event include chan_sip ; and chan_iax. Both of these channel drivers by default react to this event ; by renewing all outbound registrations. This allows the endpoints Asterisk ; is registering with to become aware of the address change and know the new @@ -15,8 +15,13 @@ [general] ; ; ---- STUN Server configuration --- -; Setting the 'stunaddr' option to a valid address enables the stun monitor. -; -; stunaddr = mystunserver.com ; address of the stun server to query. -; stunrefresh = 30 ; number of seconds between stun refreshes. default is 30 +; Setting the 'stunaddr' option to a valid address enables the STUN monitor. ; +;stunaddr = mystunserver.com ; Address of the STUN server to query. + ; Valid form: + ; [(hostname | IP-address) [':' port]] + ; The port defaults to the standard STUN port (3478). + ; Set to an empty value to disable STUN monitoring. + ; Default is disabled. +;stunrefresh = 30 ; Number of seconds between STUN refreshes. + ; Default is 30. diff --git a/include/asterisk/stun.h b/include/asterisk/stun.h index 1fda0c33d..f0f9d0e34 100644 --- a/include/asterisk/stun.h +++ b/include/asterisk/stun.h @@ -41,28 +41,53 @@ enum ast_stun_result { struct stun_attr; -/*! \brief Generic STUN request - * send a generic stun request to the server specified. - * \param s the socket used to send the request - * \param dst the address of the STUN server - * \param username if non null, add the username in the request - * \param answer if non null, the function waits for a response and +/*! + * \brief Generic STUN request. + * + * \param s The socket used to send the request. + * \param dst If non null, the address of the STUN server. + * Only needed if the socket is not bound or connected. + * \param username If non null, add the username in the request. + * \param answer If non null, the function waits for a response and * puts here the externally visible address. - * \return 0 on success, other values on error. - * The interface it may change in the future. + * + * \details + * Send a generic STUN request to the server specified, possibly + * waiting for a reply and filling the answer parameter with the + * externally visible address. Note that in this case the + * request will be blocking. + * + * \note The interface may change slightly in the future. + * + * \retval 0 on success. + * \retval <0 on error. + * \retval >0 on timeout. */ int ast_stun_request(int s, struct sockaddr_in *dst, const char *username, struct sockaddr_in *answer); /*! \brief callback type to be invoked on stun responses. */ typedef int (stun_cb_f)(struct stun_attr *attr, void *arg); -/*! \brief handle an incoming STUN message. +/*! + * \brief handle an incoming STUN message. * + * \param s Socket to send any response to. + * \param src Address where packet came from. + * \param data STUN packet buffer to process. + * \param len Length of packet + * \param stun_cb If not NULL, callback for each STUN attribute. + * \param arg Arg to pass to callback. + * + * \details * Do some basic sanity checks on packet size and content, * try to extract a bit of information, and possibly reply. * At the moment this only processes BIND requests, and returns * the externally visible address of the request. * If a callback is specified, invoke it with the attribute. + * + * \retval AST_STUN_ACCEPT if responed to a STUN request + * \retval AST_STUN_IGNORE + * \retval -1 on error */ int ast_stun_handle_packet(int s, struct sockaddr_in *src, unsigned char *data, size_t len, stun_cb_f *stun_cb, void *arg); diff --git a/main/stun.c b/main/stun.c index 3ad326ee8..a1474156f 100644 --- a/main/stun.c +++ b/main/stun.c @@ -234,6 +234,21 @@ static int stun_send(int s, struct sockaddr_in *dst, struct stun_header *resp) (struct sockaddr *)dst, sizeof(*dst)); } +/*! + * \internal + * \brief Compare the STUN tranaction IDs. + * + * \param left Transaction ID. + * \param right Transaction ID. + * + * \retval 0 if match. + * \retval non-zero if not match. + */ +static int stun_id_cmp(stun_trans_id *left, stun_trans_id *right) +{ + return memcmp(left, right, sizeof(*left)); +} + /*! \brief helper function to generate a random request id */ static void stun_req_id(struct stun_header *req) { @@ -242,14 +257,6 @@ static void stun_req_id(struct stun_header *req) req->id.id[x] = ast_random(); } -/*! \brief handle an incoming STUN message. - * - * Do some basic sanity checks on packet size and content, - * try to extract a bit of information, and possibly reply. - * At the moment this only processes BIND requests, and returns - * the externally visible address of the request. - * If a callback is specified, invoke it with the attribute. - */ int ast_stun_handle_packet(int s, struct sockaddr_in *src, unsigned char *data, size_t len, stun_cb_f *stun_cb, void *arg) { struct stun_header *hdr = (struct stun_header *)data; @@ -359,75 +366,98 @@ static int stun_get_mapped(struct stun_attr *attr, void *arg) return 0; } -/*! \brief Generic STUN request - * Send a generic stun request to the server specified, - * possibly waiting for a reply and filling the 'reply' field with - * the externally visible address. Note that in this case the request - * will be blocking. - * (Note, the interface may change slightly in the future). - * - * \param s the socket used to send the request - * \param dst the address of the STUN server - * \param username if non null, add the username in the request - * \param answer if non null, the function waits for a response and - * puts here the externally visible address. - * \return 0 on success, other values on error. - */ int ast_stun_request(int s, struct sockaddr_in *dst, const char *username, struct sockaddr_in *answer) { struct stun_header *req; - unsigned char reqdata[1024]; + struct stun_header *rsp; + unsigned char req_buf[1024]; + unsigned char rsp_buf[1024]; int reqlen, reqleft; struct stun_attr *attr; - int res = 0; + int res = -1; int retry; - req = (struct stun_header *)reqdata; + if (answer) { + /* Always clear answer in case the request fails. */ + memset(answer, 0, sizeof(struct sockaddr_in)); + } + + /* Create STUN bind request */ + req = (struct stun_header *) req_buf; stun_req_id(req); reqlen = 0; - reqleft = sizeof(reqdata) - sizeof(struct stun_header); + reqleft = sizeof(req_buf) - sizeof(struct stun_header); req->msgtype = 0; req->msglen = 0; - attr = (struct stun_attr *)req->ies; - if (username) + attr = (struct stun_attr *) req->ies; + if (username) { append_attr_string(&attr, STUN_USERNAME, username, &reqlen, &reqleft); + } req->msglen = htons(reqlen); req->msgtype = htons(STUN_BINDREQ); - for (retry = 0; retry < 3; retry++) { /* XXX make retries configurable */ + + for (retry = 0; retry++ < 3;) { /* XXX make retries configurable */ /* send request, possibly wait for reply */ - unsigned char reply_buf[1024]; - struct pollfd pfds = { .fd = s, .events = POLLIN }; struct sockaddr_in src; socklen_t srclen; + /* Send STUN message. */ res = stun_send(s, dst, req); if (res < 0) { - ast_log(LOG_WARNING, "ast_stun_request send #%d failed error %d, retry\n", - retry, res); - continue; + ast_debug(1, "stun_send try %d failed: %s\n", retry, strerror(errno)); + break; } - if (answer == NULL) + if (!answer) { + /* Successful send since we don't care about any response. */ + res = 0; break; - res = ast_poll(&pfds, 1, 3000); - if (res <= 0) /* timeout or error */ - continue; + } + +try_again: + /* Wait for response. */ + { + struct pollfd pfds = { .fd = s, .events = POLLIN }; + + res = ast_poll(&pfds, 1, 3000); + if (res < 0) { + /* Error */ + continue; + } + if (!res) { + /* No response, timeout */ + res = 1; + continue; + } + } + + /* Read STUN response. */ memset(&src, 0, sizeof(src)); srclen = sizeof(src); - /* XXX pass -1 in the size, because stun_handle_packet might + /* XXX pass sizeof(rsp_buf) - 1 in the size, because stun_handle_packet might * write past the end of the buffer. */ - res = recvfrom(s, reply_buf, sizeof(reply_buf) - 1, - 0, (struct sockaddr *)&src, &srclen); + res = recvfrom(s, rsp_buf, sizeof(rsp_buf) - 1, + 0, (struct sockaddr *) &src, &srclen); if (res < 0) { - ast_log(LOG_WARNING, "ast_stun_request recvfrom #%d failed error %d, retry\n", - retry, res); - continue; + ast_debug(1, "recvfrom try %d failed: %s\n", retry, strerror(errno)); + break; } - memset(answer, 0, sizeof(struct sockaddr_in)); - ast_stun_handle_packet(s, &src, reply_buf, res, - stun_get_mapped, answer); - res = 0; /* signal regular exit */ + + /* Process the STUN response. */ + rsp = (struct stun_header *) rsp_buf; + if (ast_stun_handle_packet(s, &src, rsp_buf, res, stun_get_mapped, answer) + || (rsp->msgtype != htons(STUN_BINDRESP) + && rsp->msgtype != htons(STUN_BINDERR)) + || stun_id_cmp(&req->id, &rsp->id)) { + /* Bad STUN packet, not right type, or transaction ID did not match. */ + memset(answer, 0, sizeof(struct sockaddr_in)); + + /* Was not a resonse to our request. */ + goto try_again; + } + /* Success. answer contains the external address if available. */ + res = 0; break; } return res; diff --git a/res/res_stun_monitor.c b/res/res_stun_monitor.c index 64ca73eda..b9854e60f 100644 --- a/res/res_stun_monitor.c +++ b/res/res_stun_monitor.c @@ -38,107 +38,137 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/stun.h" #include "asterisk/netsock2.h" #include "asterisk/lock.h" +#include "asterisk/acl.h" #include <fcntl.h> -static const int DEFAULT_MONITOR_REFRESH = 30; +#define DEFAULT_MONITOR_REFRESH 30 /*!< Default refresh period in seconds */ static const char stun_conf_file[] = "res_stun_monitor.conf"; static struct ast_sched_context *sched; static struct { - struct sockaddr_in stunaddr; /*!< The stun address we send requests to*/ - struct sockaddr_in externaladdr; /*!< current perceived external address. */ + /*! STUN monitor protection lock. */ ast_mutex_t lock; + /*! Current perceived external address. */ + struct sockaddr_in external_addr; + /*! STUN server host name. */ + const char *server_hostname; + /*! Port of STUN server to use */ + unsigned int stun_port; + /*! Number of seconds between polls to the STUN server for the external address. */ unsigned int refresh; - int stunsock; + /*! Monitoring STUN socket. */ + int stun_sock; + /*! TRUE if the STUN monitor is enabled. */ unsigned int monitor_enabled:1; - unsigned int externaladdr_known:1; + /*! TRUE if the perceived external address is valid/known. */ + unsigned int external_addr_known:1; + /*! TRUE if we have already griped about a STUN poll failing. */ + unsigned int stun_poll_failed_gripe:1; } args; -static inline void stun_close_sock(void) +static void stun_close_sock(void) { - if (args.stunsock != -1) { - close(args.stunsock); - args.stunsock = -1; - memset(&args.externaladdr, 0, sizeof(args.externaladdr)); - args.externaladdr_known = 0; + if (0 <= args.stun_sock) { + close(args.stun_sock); + args.stun_sock = -1; } } -/* \brief purge the stun socket's receive buffer before issuing a new request - * - * XXX Note that this is somewhat of a hack. This function is essentially doing - * a cleanup on the socket rec buffer to handle removing any STUN responses we have not - * handled. This is called before sending out a new STUN request so we don't read - * a latent previous response thinking it is new. - */ -static void stun_purge_socket(void) -{ - int flags = fcntl(args.stunsock, F_GETFL); - int res = 0; - unsigned char reply_buf[1024]; - - fcntl(args.stunsock, F_SETFL, flags | O_NONBLOCK); - while (res != -1) { - /* throw away everything in the buffer until we reach the end. */ - res = recv(args.stunsock, reply_buf, sizeof(reply_buf), 0); - } - fcntl(args.stunsock, F_SETFL, flags & ~O_NONBLOCK); -} - /* \brief called by scheduler to send STUN request */ static int stun_monitor_request(const void *blarg) { int res; - int generate_event = 0; - struct sockaddr_in answer = { 0, }; - + struct sockaddr_in answer; + static const struct sockaddr_in no_addr = { 0, }; - /* once the stun socket goes away, this scheduler item will go away as well */ ast_mutex_lock(&args.lock); - if (args.stunsock == -1) { - ast_log(LOG_ERROR, "STUN monitor: can not send STUN request, socket is not open\n"); + if (!args.monitor_enabled) { goto monitor_request_cleanup; } - stun_purge_socket(); - - if (!(ast_stun_request(args.stunsock, &args.stunaddr, NULL, &answer)) && - (memcmp(&args.externaladdr, &answer, sizeof(args.externaladdr)))) { - const char *newaddr = ast_strdupa(ast_inet_ntoa(answer.sin_addr)); - int newport = ntohs(answer.sin_port); - - ast_log(LOG_NOTICE, "STUN MONITOR: Old external address/port %s:%d now seen as %s:%d \n", - ast_inet_ntoa(args.externaladdr.sin_addr), ntohs(args.externaladdr.sin_port), - newaddr, newport); - - memcpy(&args.externaladdr, &answer, sizeof(args.externaladdr)); + if (args.stun_sock < 0) { + struct ast_sockaddr stun_addr; - if (args.externaladdr_known) { - /* the external address was already known, and has changed... generate event. */ - generate_event = 1; + /* STUN socket not open. Refresh the server DNS address resolution. */ + if (!args.server_hostname) { + /* No STUN hostname? */ + goto monitor_request_cleanup; + } - } else { - /* this was the first external address we found, do not alert listeners - * until this address changes to something else. */ - args.externaladdr_known = 1; + /* Lookup STUN address. */ + memset(&stun_addr, 0, sizeof(stun_addr)); + stun_addr.ss.ss_family = AF_INET; + if (ast_get_ip(&stun_addr, args.server_hostname)) { + /* Lookup failed. */ + ast_log(LOG_WARNING, "Unable to lookup STUN server '%s'\n", + args.server_hostname); + goto monitor_request_cleanup; } - } + ast_sockaddr_set_port(&stun_addr, args.stun_port); - if (generate_event) { - struct ast_event *event = ast_event_new(AST_EVENT_NETWORK_CHANGE, AST_EVENT_IE_END); - if (!event) { - ast_log(LOG_ERROR, "STUN monitor: could not create AST_EVENT_NETWORK_CHANGE event.\n"); + /* open socket binding */ + args.stun_sock = socket(AF_INET, SOCK_DGRAM, 0); + if (args.stun_sock < 0) { + ast_log(LOG_WARNING, "Unable to create STUN socket: %s\n", strerror(errno)); goto monitor_request_cleanup; } - if (ast_event_queue(event)) { - ast_event_destroy(event); - event = NULL; - ast_log(LOG_ERROR, "STUN monitor: could not queue AST_EVENT_NETWORK_CHANGE event.\n"); + if (ast_connect(args.stun_sock, &stun_addr)) { + ast_log(LOG_WARNING, "STUN Failed to connect to %s: %s\n", + ast_sockaddr_stringify(&stun_addr), strerror(errno)); + stun_close_sock(); goto monitor_request_cleanup; } } + res = ast_stun_request(args.stun_sock, NULL, NULL, &answer); + if (res) { + /* + * STUN request timed out or errored. + * + * Refresh the server DNS address resolution next time around. + */ + if (!args.stun_poll_failed_gripe) { + args.stun_poll_failed_gripe = 1; + ast_log(LOG_WARNING, "STUN poll %s. Re-evaluating STUN server address.\n", + res < 0 ? "failed" : "got no response"); + } + stun_close_sock(); + } else { + args.stun_poll_failed_gripe = 0; + if (memcmp(&no_addr, &answer, sizeof(no_addr)) + && memcmp(&args.external_addr, &answer, sizeof(args.external_addr))) { + const char *newaddr = ast_strdupa(ast_inet_ntoa(answer.sin_addr)); + int newport = ntohs(answer.sin_port); + + ast_log(LOG_NOTICE, "Old external address/port %s:%d now seen as %s:%d.\n", + ast_inet_ntoa(args.external_addr.sin_addr), + ntohs(args.external_addr.sin_port), newaddr, newport); + + args.external_addr = answer; + + if (args.external_addr_known) { + struct ast_event *event; + + /* + * The external address was already known, and has changed... + * generate event. + */ + event = ast_event_new(AST_EVENT_NETWORK_CHANGE, AST_EVENT_IE_END); + if (!event) { + ast_log(LOG_ERROR, "Could not create AST_EVENT_NETWORK_CHANGE event.\n"); + } else if (ast_event_queue(event)) { + ast_event_destroy(event); + ast_log(LOG_ERROR, "Could not queue AST_EVENT_NETWORK_CHANGE event.\n"); + } + } else { + /* this was the first external address we found, do not alert listeners + * until this address changes to something else. */ + args.external_addr_known = 1; + } + } + } + monitor_request_cleanup: /* always refresh this scheduler item. It will be removed elsewhere when * it is supposed to go away */ @@ -148,46 +178,40 @@ monitor_request_cleanup: return res; } -/* \brief stops the stun monitor thread +/*! + * \internal + * \brief Stops the STUN monitor thread. + * * \note do not hold the args->lock while calling this + * + * \return Nothing */ static void stun_stop_monitor(void) { + ast_mutex_lock(&args.lock); + args.monitor_enabled = 0; + ast_free((char *) args.server_hostname); + args.server_hostname = NULL; + stun_close_sock(); + ast_mutex_unlock(&args.lock); + if (sched) { ast_sched_context_destroy(sched); sched = NULL; ast_log(LOG_NOTICE, "STUN monitor stopped\n"); } - /* it is only safe to destroy the socket without holding arg->lock - * after the sched thread is destroyed */ - stun_close_sock(); } -/* \brief starts the stun monitor thread +/*! + * \internal + * \brief Starts the STUN monitor thread. + * * \note The args->lock MUST be held when calling this function + * + * \return Nothing */ static int stun_start_monitor(void) { - struct ast_sockaddr dst; - /* clean up any previous open socket */ - stun_close_sock(); - - /* create destination ast_sockaddr */ - ast_sockaddr_from_sin(&dst, &args.stunaddr); - - /* open new socket binding */ - args.stunsock = socket(AF_INET, SOCK_DGRAM, 0); - if (args.stunsock < 0) { - ast_log(LOG_WARNING, "Unable to create STUN socket: %s\n", strerror(errno)); - return -1; - } - - if (ast_connect(args.stunsock, &dst) != 0) { - ast_log(LOG_WARNING, "SIP STUN Failed to connect to %s\n", ast_sockaddr_stringify(&dst)); - stun_close_sock(); - return -1; - } - /* if scheduler thread is not started, make sure to start it now */ if (sched) { return 0; /* already started */ @@ -195,7 +219,6 @@ static int stun_start_monitor(void) if (!(sched = ast_sched_context_create())) { ast_log(LOG_ERROR, "Failed to create stun monitor scheduler context\n"); - stun_close_sock(); return -1; } @@ -210,7 +233,6 @@ static int stun_start_monitor(void) ast_log(LOG_ERROR, "Unable to schedule STUN network monitor \n"); ast_sched_context_destroy(sched); sched = NULL; - stun_close_sock(); return -1; } @@ -219,6 +241,65 @@ static int stun_start_monitor(void) return 0; } +/*! + * \internal + * \brief Parse and setup the stunaddr parameter. + * + * \param value Configuration parameter variable value. + * + * \retval 0 on success. + * \retval -1 on error. + */ +static int setup_stunaddr(const char *value) +{ + char *val; + char *host_str; + char *port_str; + unsigned int port; + struct ast_sockaddr stun_addr; + + if (ast_strlen_zero(value)) { + /* Setting to an empty value disables STUN monitoring. */ + args.monitor_enabled = 0; + return 0; + } + + val = ast_strdupa(value); + if (!ast_sockaddr_split_hostport(val, &host_str, &port_str, 0) + || ast_strlen_zero(host_str)) { + return -1; + } + + /* Determine STUN port */ + if (ast_strlen_zero(port_str) + || 1 != sscanf(port_str, "%30u", &port)) { + port = STANDARD_STUN_PORT; + } + + host_str = ast_strdup(host_str); + if (!host_str) { + return -1; + } + + /* Lookup STUN address. */ + memset(&stun_addr, 0, sizeof(stun_addr)); + stun_addr.ss.ss_family = AF_INET; + if (ast_get_ip(&stun_addr, host_str)) { + ast_log(LOG_WARNING, "Unable to lookup STUN server '%s'\n", host_str); + ast_free(host_str); + return -1; + } + + /* Save STUN server information. */ + ast_free((char *) args.server_hostname); + args.server_hostname = host_str; + args.stun_port = port; + + /* Enable STUN monitor */ + args.monitor_enabled = 1; + return 0; +} + static int load_config(int startup) { struct ast_flags config_flags = { 0, }; @@ -229,39 +310,37 @@ static int load_config(int startup) ast_set_flag(&config_flags, CONFIG_FLAG_FILEUNCHANGED); } - if (!(cfg = ast_config_load2(stun_conf_file, "res_stun_monitor", config_flags)) || - cfg == CONFIG_STATUS_FILEINVALID) { + cfg = ast_config_load2(stun_conf_file, "res_stun_monitor", config_flags); + if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) { ast_log(LOG_WARNING, "Unable to load config %s\n", stun_conf_file); return -1; } - - if (cfg == CONFIG_STATUS_FILEUNCHANGED && !startup) { + if (cfg == CONFIG_STATUS_FILEUNCHANGED) { return 0; } + /* clean up any previous open socket */ + stun_close_sock(); + args.stun_poll_failed_gripe = 0; + /* set defaults */ args.monitor_enabled = 0; - memset(&args.stunaddr, 0, sizeof(args.stunaddr)); args.refresh = DEFAULT_MONITOR_REFRESH; for (v = ast_variable_browse(cfg, "general"); v; v = v->next) { if (!strcasecmp(v->name, "stunaddr")) { - args.stunaddr.sin_port = htons(STANDARD_STUN_PORT); - if (ast_parse_arg(v->value, PARSE_INADDR, &args.stunaddr)) { - ast_log(LOG_WARNING, "Invalid STUN server address: %s\n", v->value); - } else { - ast_log(LOG_NOTICE, "STUN monitor enabled: %s\n", v->value); - args.monitor_enabled = 1; + if (setup_stunaddr(v->value)) { + ast_log(LOG_WARNING, "Invalid STUN server address: %s at line %d\n", + v->value, v->lineno); } } else if (!strcasecmp(v->name, "stunrefresh")) { if ((sscanf(v->value, "%30u", &args.refresh) != 1) || !args.refresh) { ast_log(LOG_WARNING, "Invalid stunrefresh value '%s', must be an integer > 0 at line %d\n", v->value, v->lineno); args.refresh = DEFAULT_MONITOR_REFRESH; - } else { - ast_log(LOG_NOTICE, "STUN Monitor set to refresh every %d seconds\n", args.refresh); } } else { - ast_log(LOG_WARNING, "SIP STUN: invalid config option %s at line %d\n", v->value, v->lineno); + ast_log(LOG_WARNING, "Invalid config option %s at line %d\n", + v->value, v->lineno); } } @@ -280,8 +359,7 @@ static int __reload(int startup) } ast_mutex_unlock(&args.lock); - if ((res == -1) || !args.monitor_enabled) { - args.monitor_enabled = 0; + if (res < 0 || !args.monitor_enabled) { stun_stop_monitor(); } @@ -303,12 +381,8 @@ static int unload_module(void) static int load_module(void) { ast_mutex_init(&args.lock); - args.stunsock = -1; - memset(&args.externaladdr, 0, sizeof(args.externaladdr)); - args.externaladdr_known = 0; - sched = NULL; + args.stun_sock = -1; if (__reload(1)) { - stun_stop_monitor(); ast_mutex_destroy(&args.lock); return AST_MODULE_LOAD_DECLINE; } |