diff --git a/.gitignore b/.gitignore index e532534c0bde..54957608219f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,6 @@ contrib/pyln-*/.eggs/ contrib/pyln-*/pyln/*/__version__.py release/ tests/plugins/test_selfdisable_after_getmanifest -.DS_Store # Ignore generated files devtools/features @@ -60,3 +59,7 @@ doc/lightning*.[1578] *_wiregen.[ch] *_printgen.[ch] *_gettextgen.po + +# Ignore unrelated stuff +.DS_Store +.gdb_history diff --git a/common/json_helpers.c b/common/json_helpers.c index 44efdffbd44c..bcde33a6162f 100644 --- a/common/json_helpers.c +++ b/common/json_helpers.c @@ -281,6 +281,10 @@ void json_add_address(struct json_stream *response, const char *fieldname, json_add_string(response, "type", "torv3"); json_add_string(response, "address", fmt_wireaddr_without_port(tmpctx, addr)); json_add_num(response, "port", addr->port); + } else if (addr->type == ADDR_TYPE_DNS) { + json_add_string(response, "type", "dns"); + json_add_string(response, "address", fmt_wireaddr_without_port(tmpctx, addr)); + json_add_num(response, "port", addr->port); } else if (addr->type == ADDR_TYPE_WEBSOCKET) { json_add_string(response, "type", "websocket"); json_add_num(response, "port", addr->port); diff --git a/common/test/run-ip_port_parsing.c b/common/test/run-ip_port_parsing.c index 68b1bdc55d42..090b7f235879 100644 --- a/common/test/run-ip_port_parsing.c +++ b/common/test/run-ip_port_parsing.c @@ -117,6 +117,26 @@ int main(int argc, char *argv[]) common_setup(argv[0]); + /* Check IP/TOR/DNS parser */ + assert(is_ipaddr("192.168.1.2")); + assert(!is_ipaddr("foo.bar.1.2")); + assert(is_toraddr("qubesosfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion")); + assert(is_toraddr("qubesos4rrrrz6n4.onion")); + assert(!is_toraddr("QUBESOSfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion")); + assert(!is_toraddr("QUBESOS4rrrrz6n4.onion")); + assert(!is_toraddr("qubesos-asa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion")); + assert(is_dnsaddr("example.com")); + assert(is_dnsaddr("example.digits123.com")); + assert(is_dnsaddr("example-hyphen.com")); + assert(is_dnsaddr("123example.com")); + assert(is_dnsaddr("example123.com")); + assert(is_dnsaddr("is-valid.3hostname123.com")); + assert(!is_dnsaddr("UPPERCASE.invalid.com")); + assert(!is_dnsaddr("-.invalid.com")); + assert(!is_dnsaddr("invalid.-example.com")); + assert(!is_dnsaddr("invalid.example-.com")); + assert(!is_dnsaddr("invalid..example.com")); + /* Grossly invalid. */ assert(!separate_address_and_port(tmpctx, "[", &ip, &port)); assert(!separate_address_and_port(tmpctx, "[123", &ip, &port)); @@ -148,6 +168,19 @@ int main(int argc, char *argv[]) assert(streq(ip, "192.168.2.255")); assert(port == 0); + /* DNS types */ + assert(separate_address_and_port(tmpctx, "example.com:42", &ip, &port)); + assert(streq(ip, "example.com")); + assert(port == 42); + assert(separate_address_and_port(tmpctx, "sub.example.com:21", &ip, &port)); + assert(streq(ip, "sub.example.com")); + assert(port == 21); + port = 123; + assert(separate_address_and_port(tmpctx, "sub.example.com", &ip, &port)); + assert(streq(ip, "sub.example.com")); + assert(port == 123); + port = 0; + // unusual but possibly valid case assert(separate_address_and_port(tmpctx, "[::1]", &ip, &port)); assert(streq(ip, "::1")); diff --git a/common/wireaddr.c b/common/wireaddr.c index 6638f2e6e72d..8130fee5dd98 100644 --- a/common/wireaddr.c +++ b/common/wireaddr.c @@ -37,6 +37,11 @@ bool fromwire_wireaddr(const u8 **cursor, size_t *max, struct wireaddr *addr) case ADDR_TYPE_TOR_V3: addr->addrlen = TOR_V3_ADDRLEN; break; + case ADDR_TYPE_DNS: + addr->addrlen = fromwire_u8(cursor, max); + memset(&addr->addr, 0, sizeof(addr->addr)); + addr->addr[addr->addrlen] = 0; + break; case ADDR_TYPE_WEBSOCKET: addr->addrlen = 0; break; @@ -52,6 +57,8 @@ bool fromwire_wireaddr(const u8 **cursor, size_t *max, struct wireaddr *addr) void towire_wireaddr(u8 **pptr, const struct wireaddr *addr) { towire_u8(pptr, addr->type); + if (addr->type == ADDR_TYPE_DNS) + towire_u8(pptr, addr->addrlen); towire(pptr, addr->addr, addr->addrlen); towire_u16(pptr, addr->port); } @@ -211,6 +218,7 @@ bool wireaddr_is_wildcard(const struct wireaddr *addr) return memeqzero(addr->addr, addr->addrlen); case ADDR_TYPE_TOR_V2_REMOVED: case ADDR_TYPE_TOR_V3: + case ADDR_TYPE_DNS: case ADDR_TYPE_WEBSOCKET: return false; } @@ -259,6 +267,8 @@ char *fmt_wireaddr_without_port(const tal_t * ctx, const struct wireaddr *a) case ADDR_TYPE_TOR_V3: return tal_fmt(ctx, "%s.onion", b32_encode(tmpctx, a->addr, a->addrlen)); + case ADDR_TYPE_DNS: + return tal_fmt(ctx, "%s", a->addr); case ADDR_TYPE_WEBSOCKET: return tal_strdup(ctx, "websocket"); } @@ -287,8 +297,8 @@ REGISTER_TYPE_TO_STRING(wireaddr, fmt_wireaddr); * Returns false if it wasn't one of these forms. If it returns true, * it only overwrites *port if it was specified by above. */ -static bool separate_address_and_port(const tal_t *ctx, const char *arg, - char **addr, u16 *port) +bool separate_address_and_port(const tal_t *ctx, const char *arg, + char **addr, u16 *port) { char *portcolon; @@ -322,6 +332,81 @@ static bool separate_address_and_port(const tal_t *ctx, const char *arg, return true; } +bool is_ipaddr(const char *arg) +{ + struct in_addr v4; + struct in6_addr v6; + if (inet_pton(AF_INET, arg, &v4)) + return true; + if (inet_pton(AF_INET6, arg, &v6)) + return true; + return false; +} + +bool is_toraddr(const char *arg) +{ + size_t i, arglen; + arglen = strlen(arg); + if (!strends(arg, ".onion")) + return false; + if (arglen != 16 + 6 && arglen != 56 + 6) + return false; + for (i = 0; i < arglen - 6; i++) { + if (arg[i] >= 'a' && arg[i] <= 'z') + continue; + if (arg[i] >= '0' && arg[i] <= '9') + continue; + return false; + } + return true; +} + +/* Rules: + * + * - not longer than 255 + * - segments are separated with . dot + * - segments do not start or end with - hyphen + * - segments must be longer thant zero + * - lowercase a-z and digits 0-9 and - hyphen + */ +bool is_dnsaddr(const char *arg) +{ + size_t i, arglen; + int lastdot; + + if (is_ipaddr(arg) || is_toraddr(arg)) + return false; + + /* now that its not IP or TOR, check its a DNS name */ + arglen = strlen(arg); + if (arglen > 255) + return false; + lastdot = -1; + for (i = 0; i < arglen; i++) { + if (arg[i] == '.') { + /* segment must be longer than zero */ + if (i - lastdot == 1) + return false; + /* last segment can not end with hypen */ + if (i != 0 && arg[i-1] == '-') + return false; + lastdot = i; + continue; + } + /* segment cannot start with hyphen */ + if (i == lastdot + 1 && arg[i] == '-') + return false; + if (arg[i] >= 'a' && arg[i] <= 'z') + continue; + if (arg[i] >= '0' && arg[i] <= '9') + continue; + if (arg[i] == '-') + continue; + return false; + } + return true; +} + struct wireaddr * wireaddr_from_hostname(const tal_t *ctx, const char *hostname, @@ -714,6 +799,7 @@ struct addrinfo *wireaddr_to_addrinfo(const tal_t *ctx, return ai; case ADDR_TYPE_TOR_V2_REMOVED: case ADDR_TYPE_TOR_V3: + case ADDR_TYPE_DNS: case ADDR_TYPE_WEBSOCKET: break; } @@ -766,6 +852,7 @@ bool all_tor_addresses(const struct wireaddr_internal *wireaddr) switch (wireaddr[i].u.wireaddr.type) { case ADDR_TYPE_IPV4: case ADDR_TYPE_IPV6: + case ADDR_TYPE_DNS: return false; case ADDR_TYPE_TOR_V2_REMOVED: case ADDR_TYPE_TOR_V3: diff --git a/common/wireaddr.h b/common/wireaddr.h index 3f9cddc6249d..8b8b79904b72 100644 --- a/common/wireaddr.h +++ b/common/wireaddr.h @@ -37,13 +37,18 @@ struct sockaddr_un; * where `checksum = sha3(".onion checksum" | pubkey || version)[:2]` */ +/* BOLT-hostnames #7: + * * `5`: DNS hostname; data = `[byte:len][len*byte:hostname][u16:port]` (length up to 258) + */ + /* BOLT-websockets #7: * * `6`: WebSocket port; data = `[2:port]` (length 2) */ #define TOR_V2_ADDRLEN 10 #define TOR_V3_ADDRLEN 35 -#define LARGEST_ADDRLEN TOR_V3_ADDRLEN +#define DNS_ADDRLEN 255 +#define LARGEST_ADDRLEN DNS_ADDRLEN #define TOR_V3_BLOBLEN 64 #define STATIC_TOR_MAGIC_STRING "gen-default-toraddress" @@ -52,10 +57,10 @@ enum wire_addr_type { ADDR_TYPE_IPV6 = 2, ADDR_TYPE_TOR_V2_REMOVED = 3, ADDR_TYPE_TOR_V3 = 4, - ADDR_TYPE_WEBSOCKET = 6, + ADDR_TYPE_DNS = 5, + ADDR_TYPE_WEBSOCKET = 6 }; -/* Structure now fit for tor support */ struct wireaddr { enum wire_addr_type type; u8 addrlen; @@ -149,6 +154,16 @@ struct wireaddr_internal { bool wireaddr_internal_eq(const struct wireaddr_internal *a, const struct wireaddr_internal *b); + +bool separate_address_and_port(const tal_t *ctx, const char *arg, + char **addr, u16 *port); + +bool is_ipaddr(const char *arg); + +bool is_toraddr(const char *arg); + +bool is_dnsaddr(const char *arg); + bool parse_wireaddr_internal(const char *arg, struct wireaddr_internal *addr, u16 port, bool wildcard_ok, bool dns_ok, bool unresolved_ok, bool allow_deprecated, diff --git a/connectd/connectd.c b/connectd/connectd.c index 1150f6ee7c5e..9a56abe2fea5 100644 --- a/connectd/connectd.c +++ b/connectd/connectd.c @@ -874,7 +874,10 @@ static struct io_plan *conn_init(struct io_conn *conn, "Can't connect to forproxy address"); break; case ADDR_INTERNAL_WIREADDR: + /* DNS should have been resolved before */ + assert(addr->u.wireaddr.type != ADDR_TYPE_DNS); /* If it was a Tor address, we wouldn't be here. */ + assert(!is_toraddr((char*)addr->u.wireaddr.addr)); ai = wireaddr_to_addrinfo(tmpctx, &addr->u.wireaddr); break; } @@ -925,6 +928,14 @@ static void try_connect_one_addr(struct connecting *connect) bool use_proxy = connect->daemon->always_use_proxy; const struct wireaddr_internal *addr = &connect->addrs[connect->addrnum]; struct io_conn *conn; +#if EXPERIMENTAL_FEATURES /* BOLT7 DNS RFC #911 */ + bool use_dns = connect->daemon->use_dns; + struct addrinfo hints, *ais, *aii; + struct wireaddr_internal addrhint; + int gai_err; + struct sockaddr_in *sa4; + struct sockaddr_in6 *sa6; +#endif /* In case we fail without a connection, make destroy_io_conn happy */ connect->conn = NULL; @@ -974,6 +985,51 @@ static void try_connect_one_addr(struct connecting *connect) case ADDR_TYPE_IPV6: af = AF_INET6; break; + case ADDR_TYPE_DNS: +#if EXPERIMENTAL_FEATURES /* BOLT7 DNS RFC #911 */ + if (use_proxy) /* hand it to the proxy */ + break; + if (!use_dns) /* ignore DNS when we can't use it */ + goto next; + /* Resolve with getaddrinfo */ + memset(&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; + hints.ai_protocol = 0; + hints.ai_flags = AI_ADDRCONFIG; + gai_err = getaddrinfo((char *)addr->u.wireaddr.addr, + tal_fmt(tmpctx, "%d", + addr->u.wireaddr.port), + &hints, &ais); + if (gai_err != 0) { + status_debug("DNS with getaddrinfo gave: %s", + gai_strerror(gai_err)); + goto next; + } + /* create new addrhints on-the-fly per result ... */ + for (aii = ais; aii; aii = aii->ai_next) { + addrhint.itype = ADDR_INTERNAL_WIREADDR; + if (aii->ai_family == AF_INET) { + sa4 = (struct sockaddr_in *) aii->ai_addr; + wireaddr_from_ipv4(&addrhint.u.wireaddr, + &sa4->sin_addr, + addr->u.wireaddr.port); + } else if (aii->ai_family == AF_INET6) { + sa6 = (struct sockaddr_in6 *) aii->ai_addr; + wireaddr_from_ipv6(&addrhint.u.wireaddr, + &sa6->sin6_addr, + addr->u.wireaddr.port); + } else { + /* skip unsupported ai_family */ + continue; + } + tal_arr_expand(&connect->addrs, addrhint); + /* don't forget to update convenience pointer */ + addr = &connect->addrs[connect->addrnum]; + } + freeaddrinfo(ais); +#endif + goto next; case ADDR_TYPE_WEBSOCKET: af = -1; break; @@ -1158,6 +1214,7 @@ static bool handle_wireaddr_listen(struct daemon *daemon, case ADDR_TYPE_WEBSOCKET: case ADDR_TYPE_TOR_V2_REMOVED: case ADDR_TYPE_TOR_V3: + case ADDR_TYPE_DNS: break; } status_failed(STATUS_FAIL_INTERNAL_ERROR, @@ -1613,6 +1670,10 @@ static void add_seed_addrs(struct wireaddr_internal **addrs, NULL, broken_reply, NULL); if (new_addrs) { for (size_t j = 0; j < tal_count(new_addrs); j++) { +#if EXPERIMENTAL_FEATURES /* BOLT7 DNS RFC #911 */ + if (new_addrs[j].type == ADDR_TYPE_DNS) + continue; +#endif struct wireaddr_internal a; a.itype = ADDR_INTERNAL_WIREADDR; a.u.wireaddr = new_addrs[j]; diff --git a/connectd/netaddress.c b/connectd/netaddress.c index dff03e92d308..5f9fb57fd3fa 100644 --- a/connectd/netaddress.c +++ b/connectd/netaddress.c @@ -258,6 +258,7 @@ bool guess_address(struct wireaddr *addr) } case ADDR_TYPE_TOR_V2_REMOVED: case ADDR_TYPE_TOR_V3: + case ADDR_TYPE_DNS: case ADDR_TYPE_WEBSOCKET: status_broken("Cannot guess address type %u", addr->type); break; diff --git a/devtools/gossipwith.c b/devtools/gossipwith.c index 97287103d36b..f356f00089bb 100644 --- a/devtools/gossipwith.c +++ b/devtools/gossipwith.c @@ -324,6 +324,9 @@ int main(int argc, char *argv[]) case ADDR_TYPE_WEBSOCKET: opt_usage_exit_fail("Don't support websockets"); break; + case ADDR_TYPE_DNS: + opt_usage_exit_fail("Don't support DNS"); + break; case ADDR_TYPE_IPV4: af = AF_INET; break; diff --git a/doc/lightning-getinfo.7.md b/doc/lightning-getinfo.7.md index 40e1f98d0174..a3d4b5b118b1 100644 --- a/doc/lightning-getinfo.7.md +++ b/doc/lightning-getinfo.7.md @@ -40,10 +40,10 @@ On success, an object is returned, containing: - **network** (string): represents the type of network on the node are working (e.g: `bitcoin`, `testnet`, or `regtest`) - **fees_collected_msat** (msat): Total routing fees collected by this node - **address** (array of objects, optional): The addresses we announce to the world: - - **type** (string): Type of connection (one of "ipv4", "ipv6", "torv2", "torv3", "websocket") + - **type** (string): Type of connection (one of "dns", "ipv4", "ipv6", "torv2", "torv3", "websocket") - **port** (u16): port number - If **type** is "ipv4", "ipv6", "torv2" or "torv3": + If **type** is "dns", "ipv4", "ipv6", "torv2" or "torv3": - **address** (string): address in expected format for **type** - **binding** (array of objects, optional): The addresses we are listening on: - **type** (string): Type of connection (one of "local socket", "ipv4", "ipv6", "torv2", "torv3") @@ -117,4 +117,4 @@ RESOURCES --------- Main web site: -[comment]: # ( SHA256STAMP:8374064ca0f95ab0c20d3edaf7f3742316af98f4d1e0e8de88922524f1ea3ce5) +[comment]: # ( SHA256STAMP:90a3bacb6cb4456119afee8e60677c29bf5f46c4cd950e660a9f9c8e0433b473) diff --git a/doc/lightning-listnodes.7.md b/doc/lightning-listnodes.7.md index 39d184904ad7..3f36ce07f9cf 100644 --- a/doc/lightning-listnodes.7.md +++ b/doc/lightning-listnodes.7.md @@ -36,10 +36,10 @@ If **last_timestamp** is present: - **color** (hex): The favorite RGB color this node advertized (always 6 characters) - **features** (hex): BOLT #9 features bitmap this node advertized - **addresses** (array of objects): The addresses this node advertized: - - **type** (string): Type of connection (one of "ipv4", "ipv6", "torv2", "torv3", "websocket") + - **type** (string): Type of connection (one of "dns", "ipv4", "ipv6", "torv2", "torv3", "websocket") - **port** (u16): port number - If **type** is "ipv4", "ipv6", "torv2" or "torv3": + If **type** is "dns", "ipv4", "ipv6", "torv2" or "torv3": - **address** (string): address in expected format for **type** If **option_will_fund** is present: @@ -52,9 +52,9 @@ If **option_will_fund** is present: - **compact_lease** (hex): the lease as represented in the node_announcement [comment]: # (GENERATE-FROM-SCHEMA-END) - + On failure, one of the following error codes may be returned: - + - -32602: Error in given parameters. EXAMPLE JSON RESPONSE @@ -89,10 +89,10 @@ Vincenzo Palazzo <> wrote the initial version o SEE ALSO -------- -FIXME: +FIXME: RESOURCES --------- Main web site: -[comment]: # ( SHA256STAMP:4a5cfb1cf3d7fd77e49d6e7e369a9a6d374345b011d7db2fa9b4062156869ca4) +[comment]: # ( SHA256STAMP:85400c9c1741943e2e02935b4f14fd187a7db6056410e42adec07ef3c6772f5f) diff --git a/doc/schemas/getinfo.schema.json b/doc/schemas/getinfo.schema.json index 4e0eda46a409..fc07c5de9bb5 100644 --- a/doc/schemas/getinfo.schema.json +++ b/doc/schemas/getinfo.schema.json @@ -86,6 +86,7 @@ "type": { "type": "string", "enum": [ + "dns", "ipv4", "ipv6", "torv2", @@ -104,6 +105,7 @@ "type": { "type": "string", "enum": [ + "dns", "ipv4", "ipv6", "torv2", diff --git a/doc/schemas/listnodes.schema.json b/doc/schemas/listnodes.schema.json index d88250f663bd..42637bff65f1 100644 --- a/doc/schemas/listnodes.schema.json +++ b/doc/schemas/listnodes.schema.json @@ -74,6 +74,7 @@ "type": { "type": "string", "enum": [ + "dns", "ipv4", "ipv6", "torv2", @@ -92,6 +93,7 @@ "type": { "type": "string", "enum": [ + "dns", "ipv4", "ipv6", "torv2", diff --git a/lightningd/options.c b/lightningd/options.c index 54dde2aa640d..4e79efeb32f2 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -188,29 +188,55 @@ static char *opt_add_addr_withtype(const char *arg, { char const *err_msg; struct wireaddr_internal wi; + bool dns_ok; + char *address; + u16 port; assert(arg != NULL); + dns_ok = !ld->always_use_proxy && ld->config.use_dns; - tal_arr_expand(&ld->proposed_listen_announce, ala); - if (!parse_wireaddr_internal(arg, &wi, - ld->portnum, - wildcard_ok, !ld->always_use_proxy, false, - deprecated_apis, &err_msg)) { - return tal_fmt(NULL, "Unable to parse address '%s': %s", arg, err_msg); - } + if (!separate_address_and_port(tmpctx, arg, &address, &port)) + return tal_fmt(NULL, "Unable to parse address:port '%s'", arg); - /* Sanity check for exact duplicates. */ - for (size_t i = 0; i < tal_count(ld->proposed_wireaddr); i++) { - /* Only compare announce vs announce and bind vs bind */ - if ((ld->proposed_listen_announce[i] & ala) == 0) - continue; + if (is_ipaddr(address) || is_toraddr(address) || ala != ADDR_ANNOUNCE) { + if (!parse_wireaddr_internal(arg, &wi, ld->portnum, + wildcard_ok, dns_ok, false, + deprecated_apis, &err_msg)) { + return tal_fmt(NULL, "Unable to parse address '%s': %s", arg, err_msg); + } - if (wireaddr_internal_eq(&ld->proposed_wireaddr[i], &wi)) - return tal_fmt(NULL, "Duplicate %s address %s", - ala & ADDR_ANNOUNCE ? "announce" : "listen", - type_to_string(tmpctx, struct wireaddr_internal, &wi)); + /* Sanity check for exact duplicates. */ + for (size_t i = 0; i < tal_count(ld->proposed_wireaddr); i++) { + /* Only compare announce vs announce and bind vs bind */ + if ((ld->proposed_listen_announce[i] & ala) == 0) + continue; + + if (wireaddr_internal_eq(&ld->proposed_wireaddr[i], &wi)) + return tal_fmt(NULL, "Duplicate %s address %s", + ala & ADDR_ANNOUNCE ? "announce" : "listen", + type_to_string(tmpctx, struct wireaddr_internal, &wi)); + } + + tal_arr_expand(&ld->proposed_listen_announce, ala); + tal_arr_expand(&ld->proposed_wireaddr, wi); + } + +#if EXPERIMENTAL_FEATURES /* BOLT7 DNS RFC #911 */ + /* Add ADDR_TYPE_DNS to announce DNS hostnames */ + if (is_dnsaddr(address) && ala & ADDR_ANNOUNCE) { + memset(&wi, 0, sizeof(wi)); + wi.itype = ADDR_INTERNAL_WIREADDR; + wi.u.wireaddr.type = ADDR_TYPE_DNS; + wi.u.wireaddr.addrlen = strlen(address); + strncpy((char * restrict)&wi.u.wireaddr.addr, + address, sizeof(wi.u.wireaddr.addr) - 1); + wi.u.wireaddr.port = port; + + tal_arr_expand(&ld->proposed_listen_announce, ADDR_ANNOUNCE); + tal_arr_expand(&ld->proposed_wireaddr, wi); } - tal_arr_expand(&ld->proposed_wireaddr, wi); +#endif + return NULL; } @@ -1049,8 +1075,8 @@ static void register_opts(struct lightningd *ld) "Comma separated list of extra TLV types to accept."); #endif - opt_register_noarg("--disable-dns", opt_set_invbool, &ld->config.use_dns, - "Disable DNS lookups of peers"); + opt_register_early_noarg("--disable-dns", opt_set_invbool, &ld->config.use_dns, + "Disable DNS lookups of peers"); if (deprecated_apis) opt_register_noarg("--enable-autotor-v2-mode", opt_set_invbool, &ld->config.use_v3_autotor, diff --git a/tests/test_gossip.py b/tests/test_gossip.py index 2a4bc7ca361c..30c82cb6f8d7 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -4,7 +4,8 @@ from fixtures import TEST_NETWORK from pyln.client import RpcError, Millisatoshi from utils import ( - DEVELOPER, wait_for, TIMEOUT, only_one, sync_blockheight, expected_node_features, COMPAT + DEVELOPER, wait_for, TIMEOUT, only_one, sync_blockheight, + expected_node_features, COMPAT, EXPERIMENTAL_FEATURES ) import json @@ -110,12 +111,21 @@ def test_announce_address(node_factory, bitcoind): """Make sure our announcements are well formed.""" # We do not allow announcement of duplicates. - opts = {'announce-addr': + opts = {'disable-dns': None, 'announce-addr': ['4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion', '1.2.3.4:1234', + 'localhost:1235', + 'example.com:1236', '::'], 'log-level': 'io', 'dev-allow-localhost': None} + if not EXPERIMENTAL_FEATURES: # BOLT7 DNS RFC #911 + opts = {'disable-dns': None, 'announce-addr': + ['4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion', + '1.2.3.4:1234', + '::'], + 'log-level': 'io', + 'dev-allow-localhost': None} l1, l2 = node_factory.get_nodes(2, opts=[opts, {}]) l1.rpc.connect(l2.info['id'], 'localhost', l2.port) @@ -125,13 +135,104 @@ def test_announce_address(node_factory, bitcoind): l1.wait_channel_active(scid) l2.wait_channel_active(scid) + if not EXPERIMENTAL_FEATURES: # BOLT7 DNS RFC #911 + l1.daemon.wait_for_log(r"\[OUT\] 0101.*47" + "010102030404d2" + "017f000001...." + "02000000000000000000000000000000002607" + "04e00533f3e8f2aedaa8969b3d0fa03a96e857bbb28064dca5e147e934244b9ba50230032607") + return + # We should see it send node announce with all addresses (257 = 0x0101) - # local ephemeral port is masked out. - l1.daemon.wait_for_log(r"\[OUT\] 0101.*47" - "010102030404d2" - "017f000001...." - "02000000000000000000000000000000002607" - "04e00533f3e8f2aedaa8969b3d0fa03a96e857bbb28064dca5e147e934244b9ba50230032607") + # Note: local ephemeral port is masked out. + # Note: Since we `disable-dns` it should not announce a resolved IPv4 + # or IPv6 address for example.com + # + # Also expect the address descriptor types to be sorted! + # BOLT #7: + # - MUST place address descriptors in ascending order. + l1.daemon.wait_for_log(r"\[OUT\] 0101.*0063" + "010102030404d2" # IPv4 01 1.2.3.4:1234 + "017f000001...." # IPv4 01 127.0.0.1:wxyz + "02000000000000000000000000000000002607" # IPv6 02 :::9735 + "04e00533f3e8f2aedaa8969b3d0fa03a96e857bbb28064dca5e147e934244b9ba50230032607" # TORv3 04 + "05096c6f63616c686f737404d3" # DNS 05 len localhost:1235 + "050b6578616d706c652e636f6d04d4") # DNS 05 len example.com:1236 + + # Check other node can parse these + addresses = l2.rpc.listnodes(l1.info['id'])['nodes'][0]['addresses'] + addresses_dns = [address for address in addresses if address['type'] == 'dns'] + assert len(addresses) == 6 + assert len(addresses_dns) == 2 + assert addresses_dns[0]['address'] == 'localhost' + assert addresses_dns[0]['port'] == 1235 + assert addresses_dns[1]['address'] == 'example.com' + assert addresses_dns[1]['port'] == 1236 + + +@unittest.skipIf(not EXPERIMENTAL_FEATURES, "BOLT7 DNS RFC #911") +@pytest.mark.developer("gossip without DEVELOPER=1 is slow") +def test_announce_and_connect_via_dns(node_factory, bitcoind): + """ Test that DNS annoucements propagate and can be used when connecting. + + - First node announces only a FQDN like 'localhost.localdomain'. + - Second node gets a channel with first node. + - Third node just connects to second node. + - Fourth node with DNS disabled also connects to second node. + - Wait for gossip so third and fourth node sees first node. + - Third node must be able to 'resolve' 'localhost.localdomain' + and connect to first node. + - Fourth node must not be able to connect because he has disabled DNS. + + Notes: + - --disable-dns is needed so the first node does not announce 127.0.0.1 itself. + - 'dev-allow-localhost' must not be set, so it does not resolve localhost anyway. + """ + opts1 = {'disable-dns': None, + 'announce-addr': ['localhost.localdomain:12345'], # announce dns + 'bind-addr': ['127.0.0.1:12345', '[::1]:12345']} # and bind local IPs + opts3 = {'may_reconnect': True} + opts4 = {'disable-dns': None} + l1, l2, l3, l4 = node_factory.get_nodes(4, opts=[opts1, {}, opts3, opts4]) + + # In order to enable DNS on a pyln testnode we need to delete the + # 'disable-dns' opt (which is added by pyln test utils) and restart it. + del l3.daemon.opts['disable-dns'] + l3.restart() + + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l3.rpc.connect(l2.info['id'], 'localhost', l2.port) + l4.rpc.connect(l2.info['id'], 'localhost', l2.port) + scid, _ = l1.fundchannel(l2, 10**6) + bitcoind.generate_block(5) + + # wait until l3 and l4 see l1 via gossip with announced addresses + wait_for(lambda: len(l3.rpc.listnodes(l1.info['id'])['nodes']) == 1) + wait_for(lambda: len(l4.rpc.listnodes(l1.info['id'])['nodes']) == 1) + wait_for(lambda: 'addresses' in l3.rpc.listnodes(l1.info['id'])['nodes'][0]) + wait_for(lambda: 'addresses' in l4.rpc.listnodes(l1.info['id'])['nodes'][0]) + addresses = l3.rpc.listnodes(l1.info['id'])['nodes'][0]['addresses'] + assert(len(addresses) == 1) # no other addresses must be announced for this + assert(addresses[0]['type'] == 'dns') + assert(addresses[0]['address'] == 'localhost.localdomain') + assert(addresses[0]['port'] == 12345) + + # now l3 must be able to use DNS to resolve and connect to l1 + result = l3.rpc.connect(l1.info['id']) + assert result['id'] == l1.info['id'] + assert result['direction'] == 'out' + assert result['address']['port'] == 12345 + if result['address']['type'] == 'ipv4': + assert result['address']['address'] == '127.0.0.1' + elif result['address']['type'] == 'ipv6': + assert result['address']['address'] == '::1' + else: + assert False + + # l4 however must not be able to connect because he used '--disable-dns' + # This raises RpcError code 401, currently with an empty error message. + with pytest.raises(RpcError, match=r"401"): + l4.rpc.connect(l1.info['id']) @pytest.mark.developer("needs DEVELOPER=1")