diff --git a/CHANGES b/CHANGES
index 173a41127c721d80a5c0a03f1c10f721218e3669..d24f840e57a53a6666a413494f2d7ea64eb69fb4 100644
--- a/CHANGES
+++ b/CHANGES
@@ -257,6 +257,13 @@ res_pjsip_sdp_rtp
    originate from the media address instead of the operating system's "primary"
    ip address.
 
+res_rtp_asterisk
+------------------
+ * A new configuration section - ice_host_candidates - has been added to
+   rtp.conf, allowing automatically discovered ICE host candidates to be
+   overriden. This allows an Asterisk server behind a 1:1 NAT to send its
+   external IP as a host candidate rather than relying on STUN to discover it.
+
 ------------------------------------------------------------------------------
 --- Functionality changes from Asterisk 13.6.0 to Asterisk 13.7.0 ------------
 ------------------------------------------------------------------------------
diff --git a/configs/samples/rtp.conf.sample b/configs/samples/rtp.conf.sample
index c22acaa9fa57f05921998e9406aac033a8cf8262..2ef5dd28a7cfcbe95c3078792eb7546888c2fcd1 100644
--- a/configs/samples/rtp.conf.sample
+++ b/configs/samples/rtp.conf.sample
@@ -58,3 +58,30 @@ rtpend=20000
 ;
 ; Password used to authenticate with TURN relay server.
 ; turnpassword=
+;
+[ice_host_candidates]
+;
+; When Asterisk is behind a static one-to-one NAT and ICE is in use, ICE will
+; expose the server's internal IP address as one of the host candidates.
+; Although using STUN (see the 'stunaddr' configuration option) will provide a
+; publicly accessible IP, the internal IP will still be sent to the remote
+; peer. To help hide the topology of your internal network, you can override
+; the host candidates that Asterisk will send to the remote peer.
+;
+; IMPORTANT: Only use this functionality when your Asterisk server is behind a
+; one-to-one NAT and you know what you're doing. If you do define anything
+; here, you almost certainly will NOT want to specify 'stunaddr' or 'turnaddr'
+; above.
+;
+; The format for these overrides is:
+;
+;    <local address> => <advertised address>
+;
+; The following will replace 192.168.1.10 with 1.2.3.4 during ICE
+; negotiation:
+;
+;192.168.1.10 => 1.2.3.4
+;
+; You can define an override for more than 1 interface if you have a multihomed
+; server. Any local interface that is not matched will be passed through
+; unaltered. Both IPv4 and IPv6 addresses are supported.
diff --git a/res/res_rtp_asterisk.c b/res/res_rtp_asterisk.c
index 85d997fc2ffaa2bf882eb388b44d69677ed02a9c..9c7432e8c0743bbd0f742627b35c59c4c45b667c 100644
--- a/res/res_rtp_asterisk.c
+++ b/res/res_rtp_asterisk.c
@@ -182,6 +182,16 @@ struct ast_rtp_ioqueue_thread {
 /*! \brief List of ioqueue threads */
 static AST_LIST_HEAD_STATIC(ioqueues, ast_rtp_ioqueue_thread);
 
+/*! \brief Structure which contains ICE host candidate mapping information */
+struct ast_ice_host_candidate {
+	pj_sockaddr local;
+	pj_sockaddr advertised;
+	AST_RWLIST_ENTRY(ast_ice_host_candidate) next;
+};
+
+/*! \brief List of ICE host candidate mappings */
+static AST_RWLIST_HEAD_STATIC(host_candidates, ast_ice_host_candidate);
+
 #endif
 
 #define FLAG_3389_WARNING               (1 << 0)
@@ -451,6 +461,38 @@ static void dtls_srtp_stop_timeout_timer(struct ast_rtp_instance *instance, stru
 static int __rtp_sendto(struct ast_rtp_instance *instance, void *buf, size_t size, int flags, struct ast_sockaddr *sa, int rtcp, int *ice, int use_srtp);
 
 #ifdef HAVE_PJPROJECT
+/*! \brief Helper function which clears the ICE host candidate mapping */
+static void host_candidate_overrides_clear(void)
+{
+	struct ast_ice_host_candidate *candidate;
+
+	AST_RWLIST_WRLOCK(&host_candidates);
+	AST_RWLIST_TRAVERSE_SAFE_BEGIN(&host_candidates, candidate, next) {
+		AST_RWLIST_REMOVE_CURRENT(next);
+		ast_free(candidate);
+	}
+	AST_RWLIST_TRAVERSE_SAFE_END;
+	AST_RWLIST_UNLOCK(&host_candidates);
+}
+
+/*! \brief Applies the ICE host candidate mapping */
+static void host_candidate_overrides_apply(unsigned int count, pj_sockaddr addrs[])
+{
+	int pos;
+	struct ast_ice_host_candidate *candidate;
+
+	AST_RWLIST_RDLOCK(&host_candidates);
+	for (pos = 0; pos < count; pos++) {
+		AST_LIST_TRAVERSE(&host_candidates, candidate, next) {
+			if (!pj_sockaddr_cmp(&candidate->local, &addrs[pos])) {
+				pj_sockaddr_copy_addr(&addrs[pos], &candidate->advertised);
+				break;
+			}
+		}
+	}
+	AST_RWLIST_UNLOCK(&host_candidates);
+}
+
 /*! \brief Helper function which updates an ast_sockaddr with the candidate used for the component */
 static void update_address_with_ice_candidate(struct ast_rtp *rtp, enum ast_rtp_ice_component_type component,
 	struct ast_sockaddr *cand_address)
@@ -2368,6 +2410,8 @@ static void rtp_add_candidates_to_ice(struct ast_rtp_instance *instance, struct
 		pj_enum_ip_interface(pj_AF_INET6(), &count, address);
 	}
 
+	host_candidate_overrides_apply(count, address);
+
 	for (pos = 0; pos < count; pos++) {
 		pj_sockaddr_set_port(&address[pos], port);
 		ast_rtp_ice_add_cand(rtp, component, transport, PJ_ICE_CAND_TYPE_HOST, 65535, &address[pos], &address[pos], NULL,
@@ -5256,6 +5300,11 @@ static int rtp_reload(int reload)
 	const char *s;
 	struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
 
+#ifdef HAVE_PJPROJECT
+	struct ast_variable *var;
+	struct ast_ice_host_candidate *candidate;
+#endif
+
 	cfg = ast_config_load2("rtp.conf", "rtp", config_flags);
 	if (cfg == CONFIG_STATUS_FILEMISSING || cfg == CONFIG_STATUS_FILEUNCHANGED || cfg == CONFIG_STATUS_FILEINVALID) {
 		return 0;
@@ -5282,6 +5331,7 @@ static int rtp_reload(int reload)
 	turnaddr = pj_str(NULL);
 	turnusername = pj_str(NULL);
 	turnpassword = pj_str(NULL);
+	host_candidate_overrides_clear();
 #endif
 
 	if (cfg) {
@@ -5361,6 +5411,36 @@ static int rtp_reload(int reload)
 		if ((s = ast_variable_retrieve(cfg, "general", "turnpassword"))) {
 			pj_strdup2_with_null(pool, &turnpassword, s);
 		}
+
+		AST_RWLIST_WRLOCK(&host_candidates);
+		for (var = ast_variable_browse(cfg, "ice_host_candidates"); var; var = var->next) {
+			struct ast_sockaddr local_addr, advertised_addr;
+			pj_str_t address;
+
+			ast_sockaddr_setnull(&local_addr);
+			ast_sockaddr_setnull(&advertised_addr);
+
+			if (ast_parse_arg(var->name, PARSE_ADDR | PARSE_PORT_IGNORE, &local_addr)) {
+				ast_log(LOG_WARNING, "Invalid local ICE host address: %s\n", var->name);
+				continue;
+			}
+
+			if (ast_parse_arg(var->value, PARSE_ADDR | PARSE_PORT_IGNORE, &advertised_addr)) {
+				ast_log(LOG_WARNING, "Invalid advertised ICE host address: %s\n", var->value);
+				continue;
+			}
+
+			if (!(candidate = ast_calloc(1, sizeof(*candidate)))) {
+				ast_log(LOG_ERROR, "Failed to allocate ICE host candidate mapping.\n");
+				break;
+			}
+
+			pj_sockaddr_parse(pj_AF_UNSPEC(), 0, pj_cstr(&address, ast_sockaddr_stringify(&local_addr)), &candidate->local);
+			pj_sockaddr_parse(pj_AF_UNSPEC(), 0, pj_cstr(&address, ast_sockaddr_stringify(&advertised_addr)), &candidate->advertised);
+
+			AST_RWLIST_INSERT_TAIL(&host_candidates, candidate, next);
+		}
+		AST_RWLIST_UNLOCK(&host_candidates);
 #endif
 		ast_config_destroy(cfg);
 	}
@@ -5463,6 +5543,7 @@ static int unload_module(void)
 	ast_cli_unregister_multiple(cli_rtp, ARRAY_LEN(cli_rtp));
 
 #ifdef HAVE_PJPROJECT
+	host_candidate_overrides_clear();
 	pj_thread_register_check();
 	rtp_terminate_pjproject();
 #endif