From cb2a3faf3d788b6c26598a55326905dfc4fb5922 Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Wed, 15 Sep 2021 16:19:06 +0200 Subject: [PATCH 1/9] options: fix ld proposed_wireaddr proposed_listen_announce get out of sync Incase the error handling happening after the quted line is non-critical: ``` return tal_fmt(NULL, "Unable to parse address '%s': %s", arg, err_msg); ``` we should not expand the proposed_listen_announce array without adding a proposed_wireaddr. So we move the expand of proposed_listen_announce to the location where we also expand the proposed_wireaddr. Changelog-None --- lightningd/options.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lightningd/options.c b/lightningd/options.c index 54dde2aa640d..6c042caffc3e 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -191,7 +191,6 @@ static char *opt_add_addr_withtype(const char *arg, assert(arg != NULL); - tal_arr_expand(&ld->proposed_listen_announce, ala); if (!parse_wireaddr_internal(arg, &wi, ld->portnum, wildcard_ok, !ld->always_use_proxy, false, @@ -210,6 +209,8 @@ static char *opt_add_addr_withtype(const char *arg, 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); return NULL; From 185f28c158b94099654a787930befb9ab358db17 Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Tue, 21 Sep 2021 15:50:57 +0200 Subject: [PATCH 2/9] options: make --disable-dns an early arg --- lightningd/options.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightningd/options.c b/lightningd/options.c index 6c042caffc3e..57d9ec85f2f7 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -1050,8 +1050,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, From 4bd6573702127461fed5f43677dac87b14fa5dfb Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Tue, 28 Sep 2021 17:20:45 +0200 Subject: [PATCH 3/9] chore: gitignore gdb_history --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 From dbfee2b65b3081342128f66215f6e5ea8af29a12 Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Wed, 29 Sep 2021 11:40:05 +0200 Subject: [PATCH 4/9] options: fix respect --disable-dns when parsing wireaddress Changelog-Fixed: Options: Respect --always-use-proxy AND --disable-dns when parsing wireaddresses to listen on. --- lightningd/options.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lightningd/options.c b/lightningd/options.c index 57d9ec85f2f7..e186b6e1cadf 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -188,12 +188,14 @@ static char *opt_add_addr_withtype(const char *arg, { char const *err_msg; struct wireaddr_internal wi; + bool dns_ok; assert(arg != NULL); + dns_ok = !ld->always_use_proxy && ld->config.use_dns; if (!parse_wireaddr_internal(arg, &wi, ld->portnum, - wildcard_ok, !ld->always_use_proxy, false, + wildcard_ok, dns_ok, false, deprecated_apis, &err_msg)) { return tal_fmt(NULL, "Unable to parse address '%s': %s", arg, err_msg); } From f99aa1dff6cf442cffd6d998aae527e2dda13965 Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Fri, 1 Oct 2021 13:14:46 +0200 Subject: [PATCH 5/9] wireaddr: adds helper is_ipaddr, is_toraddr and is_dnsaddr --- common/test/run-ip_port_parsing.c | 33 +++++++++++++ common/wireaddr.c | 79 ++++++++++++++++++++++++++++++- common/wireaddr.h | 10 ++++ connectd/connectd.c | 1 + 4 files changed, 121 insertions(+), 2 deletions(-) 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..f9afbc48e392 100644 --- a/common/wireaddr.c +++ b/common/wireaddr.c @@ -287,8 +287,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 +322,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, diff --git a/common/wireaddr.h b/common/wireaddr.h index 3f9cddc6249d..e9dc0bf8e7cb 100644 --- a/common/wireaddr.h +++ b/common/wireaddr.h @@ -149,6 +149,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..7cd457d759cb 100644 --- a/connectd/connectd.c +++ b/connectd/connectd.c @@ -875,6 +875,7 @@ static struct io_plan *conn_init(struct io_conn *conn, break; case ADDR_INTERNAL_WIREADDR: /* 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; } From 55825cb35efbb9e938bb6ca743f4a42862697abf Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Fri, 1 Oct 2021 13:47:29 +0200 Subject: [PATCH 6/9] bolt7: allow announcement of ADDR_TYPE_DNS --- common/json_helpers.c | 4 +++ common/wireaddr.c | 12 +++++++ common/wireaddr.h | 11 +++++-- connectd/connectd.c | 6 ++++ connectd/netaddress.c | 1 + devtools/gossipwith.c | 3 ++ doc/lightning-getinfo.7.md | 6 ++-- doc/lightning-listnodes.7.md | 12 +++---- doc/schemas/getinfo.schema.json | 2 ++ doc/schemas/listnodes.schema.json | 2 ++ lightningd/options.c | 55 +++++++++++++++++++++---------- tests/test_gossip.py | 34 +++++++++++++++---- 12 files changed, 112 insertions(+), 36 deletions(-) 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/wireaddr.c b/common/wireaddr.c index f9afbc48e392..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"); } @@ -789,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; } @@ -841,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 e9dc0bf8e7cb..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; diff --git a/connectd/connectd.c b/connectd/connectd.c index 7cd457d759cb..b92058f2c71d 100644 --- a/connectd/connectd.c +++ b/connectd/connectd.c @@ -975,6 +975,9 @@ static void try_connect_one_addr(struct connecting *connect) case ADDR_TYPE_IPV6: af = AF_INET6; break; + case ADDR_TYPE_DNS: + // TODO: resolve with getaddrinfo and set af + break; case ADDR_TYPE_WEBSOCKET: af = -1; break; @@ -1159,6 +1162,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, @@ -1614,6 +1618,8 @@ 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 (new_addrs[j].type == ADDR_TYPE_DNS) + continue; 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 e186b6e1cadf..8e903cfb3c94 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -189,31 +189,52 @@ 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; - 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 (!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); + } + + /* 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 (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)); + /* 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_listen_announce, ala); - tal_arr_expand(&ld->proposed_wireaddr, wi); return NULL; } diff --git a/tests/test_gossip.py b/tests/test_gossip.py index 2a4bc7ca361c..785eaeaaae8c 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -110,9 +110,11 @@ 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} @@ -126,12 +128,30 @@ def test_announce_address(node_factory, bitcoind): l2.wait_channel_active(scid) # 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 @pytest.mark.developer("needs DEVELOPER=1") From 5cfcc6cac092a3bd4c7a427ea7719098f4f84606 Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Thu, 7 Oct 2021 13:01:05 +0200 Subject: [PATCH 7/9] connectd: resolve ADDR_TYPE_DNS This will resolve ADDR_TYPE_DNS wireaddr by expanding connect->addrs with one new wireaddr ADDR_INTERNAL_WIREADDR per DNS result and calling recursion --- connectd/connectd.c | 52 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/connectd/connectd.c b/connectd/connectd.c index b92058f2c71d..fd682788ff37 100644 --- a/connectd/connectd.c +++ b/connectd/connectd.c @@ -874,6 +874,8 @@ 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); @@ -926,6 +928,12 @@ 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; + 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; /* In case we fail without a connection, make destroy_io_conn happy */ connect->conn = NULL; @@ -976,8 +984,48 @@ static void try_connect_one_addr(struct connecting *connect) af = AF_INET6; break; case ADDR_TYPE_DNS: - // TODO: resolve with getaddrinfo and set af - break; + 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); + goto next; case ADDR_TYPE_WEBSOCKET: af = -1; break; From 6bc3f5704c50215192b527e83503e799324efb33 Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Fri, 8 Oct 2021 15:37:56 +0200 Subject: [PATCH 8/9] pytest: test connecting to a DNS only announced node --- tests/test_gossip.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/test_gossip.py b/tests/test_gossip.py index 785eaeaaae8c..b7913cb98733 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -154,6 +154,70 @@ def test_announce_address(node_factory, bitcoind): assert addresses_dns[1]['port'] == 1236 +@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") def test_gossip_timestamp_filter(node_factory, bitcoind, chainparams): # Updates get backdated 5 seconds with --dev-fast-gossip. From 930d25bf3865c8d9229034b25552fb1825b9648d Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Tue, 19 Oct 2021 11:32:52 +0200 Subject: [PATCH 9/9] chore: use EXPERIMENTAL for BOLT7 DNS #911 Changelog-EXPERIMENTAL: Ability to announce DNS addresses --- connectd/connectd.c | 6 ++++++ lightningd/options.c | 2 ++ tests/test_gossip.py | 19 ++++++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/connectd/connectd.c b/connectd/connectd.c index fd682788ff37..9a56abe2fea5 100644 --- a/connectd/connectd.c +++ b/connectd/connectd.c @@ -928,12 +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; @@ -984,6 +986,7 @@ static void try_connect_one_addr(struct connecting *connect) 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 */ @@ -1025,6 +1028,7 @@ static void try_connect_one_addr(struct connecting *connect) addr = &connect->addrs[connect->addrnum]; } freeaddrinfo(ais); +#endif goto next; case ADDR_TYPE_WEBSOCKET: af = -1; @@ -1666,8 +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/lightningd/options.c b/lightningd/options.c index 8e903cfb3c94..4e79efeb32f2 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -221,6 +221,7 @@ static char *opt_add_addr_withtype(const char *arg, 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)); @@ -234,6 +235,7 @@ static char *opt_add_addr_withtype(const char *arg, tal_arr_expand(&ld->proposed_listen_announce, ADDR_ANNOUNCE); tal_arr_expand(&ld->proposed_wireaddr, wi); } +#endif return NULL; diff --git a/tests/test_gossip.py b/tests/test_gossip.py index b7913cb98733..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 @@ -118,6 +119,13 @@ def test_announce_address(node_factory, bitcoind): '::'], '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) @@ -127,6 +135,14 @@ 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) # Note: local ephemeral port is masked out. # Note: Since we `disable-dns` it should not announce a resolved IPv4 @@ -154,6 +170,7 @@ def test_announce_address(node_factory, bitcoind): 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.