Skip to content

Commit 4e35018

Browse files
committed
connectd: don't stall peer reads when lightningd attaches no subd
When connectd reads a peer message whose channel_id has no attached subd, it creates a subd with conn == NULL and sends connectd_peer_spoke to lightningd, asking it to start up a subdaemon and respond with connectd_peer_connect_subd and the file descriptor that should be assigned to conn. While connectd is waiting for lightningd's response, it stops reading *all* messages from the peer. There were six cases in lightningd's handle_peer_spoke where lightningd would fail to respond, so when those occurred connectd would end up waiting indefinitely while never reading or responding to the peer's messages. Essentially the connection would become a "zombie" until either the next gossip flush (if any) or the ping timeout caused the connection to be dropped. Add a new connectd_peer_no_subd message for lightningd to send connectd when it will not be creating a subdaemon, so connectd can then continue servicing the connection as usual. Notably the connectd_peer_no_subd message uses a new spoke_id to uniquely identify the connectd_peer_spoke message that it is responding to, since multiple connectd_peer_spoke messages may be in flight at a time and because the channel_id (which is the more natural choice) can be changed by connectd before lightningd's response is received, thereby causing the wrong subd to be freed. Fixes: #9369 Changelog-Fixed: connectd: no longer stop servicing a peer connection for ~80s when a channel fails.
1 parent ae53e87 commit 4e35018

10 files changed

Lines changed: 204 additions & 10 deletions

File tree

connectd/connectd.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,6 +2016,27 @@ static void peer_disconnect(struct daemon *daemon, const u8 *msg)
20162016
disconnect_peer(peer);
20172017
}
20182018

2019+
/* lightningd tells us it's not attaching a subd for a peer_spoke we sent. */
2020+
static void peer_no_subd(struct daemon *daemon, const u8 *msg)
2021+
{
2022+
struct node_id id;
2023+
u64 counter, spoke_id;
2024+
struct peer *peer;
2025+
2026+
if (!fromwire_connectd_peer_no_subd(msg, &id, &counter, &spoke_id))
2027+
master_badmsg(WIRE_CONNECTD_PEER_NO_SUBD, msg);
2028+
2029+
peer = peer_htable_get(daemon->peers, &id);
2030+
if (!peer)
2031+
return;
2032+
2033+
/* If it's reconnected already, that subd is long gone. */
2034+
if (peer->counter != counter)
2035+
return;
2036+
2037+
resume_read_without_subd(peer, spoke_id);
2038+
}
2039+
20192040
/* lightningd tells us a peer is no longer "important". */
20202041
static void peer_downgrade(struct daemon *daemon, const u8 *msg)
20212042
{
@@ -2408,6 +2429,10 @@ static struct io_plan *recv_req(struct io_conn *conn,
24082429
return daemon_conn_read_with_fd(conn, daemon->master,
24092430
recv_peer_connect_subd, daemon);
24102431

2432+
case WIRE_CONNECTD_PEER_NO_SUBD:
2433+
peer_no_subd(daemon, msg);
2434+
goto out;
2435+
24112436
case WIRE_CONNECTD_START_SHUTDOWN:
24122437
start_shutdown(daemon, msg);
24132438
goto out;
@@ -2537,6 +2562,7 @@ int main(int argc, char *argv[])
25372562
daemon = tal(NULL, struct daemon);
25382563
daemon->developer = developer;
25392564
daemon->connection_counter = 1;
2565+
daemon->spoke_counter = 1;
25402566
/* htable_new is our helper which allocates a htable, initializes it
25412567
* and set up the memleak callback so our memleak code can see objects
25422568
* inside it */

connectd/connectd.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ struct daemon {
291291
/* Counter from which we derive connection identifiers. */
292292
u64 connection_counter;
293293

294+
/* Counter from which we derive connectd_peer_spoke identifiers, so
295+
* lightningd's answer can be matched to the subd it is about. */
296+
u64 spoke_counter;
297+
294298
/* Base for timeout timers, and how long to wait for init msg */
295299
struct timers timers;
296300
u32 timeout_secs;

connectd/connectd_wire.csv

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ msgdata,connectd_peer_connect_subd,channel_id,channel_id,
116116
msgtype,connectd_peer_spoke,2005
117117
msgdata,connectd_peer_spoke,id,node_id,
118118
msgdata,connectd_peer_spoke,counter,u64,
119+
# Identifies the subd we made for this: quote it back in connectd_peer_no_subd.
120+
msgdata,connectd_peer_spoke,spoke_id,u64,
119121
msgdata,connectd_peer_spoke,msgtype,u16,
120122
msgdata,connectd_peer_spoke,channel_id,channel_id,
121123
# If msgtype == WIRE_ERROR, this is the string.
@@ -130,6 +132,13 @@ msgtype,connectd_disconnect_peer,2016
130132
msgdata,connectd_disconnect_peer,id,node_id,
131133
msgdata,connectd_disconnect_peer,counter,u64,
132134

135+
# master -> connectd: no subd is coming for the connectd_peer_spoke you sent,
136+
# so discard the one you created and resume reading from the peer.
137+
msgtype,connectd_peer_no_subd,2017
138+
msgdata,connectd_peer_no_subd,id,node_id,
139+
msgdata,connectd_peer_no_subd,counter,u64,
140+
msgdata,connectd_peer_no_subd,spoke_id,u64,
141+
133142
# master -> connectd: give message to peer.
134143
msgtype,connectd_peer_send_msg,2003
135144
msgdata,connectd_peer_send_msg,id,node_id,

connectd/multiplex.c

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ struct subd {
4444
/* The actual connection to talk to it (NULL if it's not connected yet) */
4545
struct io_conn *conn;
4646

47+
/* Identifies the connectd_peer_spoke we sent for this subd, so
48+
* lightningd's connectd_peer_no_subd names this exact subd rather
49+
* than whatever happens to own channel_id by then. */
50+
u64 spoke_id;
51+
4752
/* Input buffer */
4853
u8 *in;
4954

@@ -52,8 +57,19 @@ struct subd {
5257

5358
/* After we've told it to tx_abort, we don't send anything else. */
5459
bool rcvd_tx_abort;
60+
61+
/* Lightningd answered our peer_spoke without attaching a conn. We
62+
* keep queueing (it may still attach one later, and then it wants
63+
* these messages), but we count them: see MAX_UNDELIVERED_MSGS. */
64+
bool no_conn_coming;
65+
size_t undelivered;
5566
};
5667

68+
/* Until a conn is attached to a subd, nothing reads the messages queued to
69+
* it, so limit the number of messages queued. Hang up if the limit is
70+
* reached. */
71+
#define MAX_UNDELIVERED_MSGS 100
72+
5773
/* FIXME: reorder! */
5874
static bool is_urgent(enum peer_wire type);
5975
static void destroy_connected_subd(struct subd *subd);
@@ -1383,6 +1399,44 @@ static void destroy_connected_subd(struct subd *subd)
13831399
}
13841400
}
13851401

1402+
/* Lightningd answered the connectd_peer_spoke we sent for spoke_id with "no
1403+
* subd for you". Start reading from the peer again so that we don't cause
1404+
* an unnecessary disconnect due to ping timeout.
1405+
*
1406+
* We deliberately keep the subd and everything queued on it. Lightningd may
1407+
* yet attach a conn via a different path (e.g., connect_activate_subd()),
1408+
* and we can then deliver the queued messages to the subd.
1409+
*
1410+
* We match on spoke_id, not channel_id: by now maybe_update_channelid() may
1411+
* have re-keyed this subd, or a later message may have created a different
1412+
* subd for the same channel_id. */
1413+
void resume_read_without_subd(struct peer *peer, u64 spoke_id)
1414+
{
1415+
/* If we're tearing down, we're not reading from the peer anyway. */
1416+
if (!peer->to_peer || peer->draining_state != NOT_DRAINING)
1417+
return;
1418+
1419+
for (size_t i = 0; i < tal_count(peer->subds); i++) {
1420+
struct subd *subd = peer->subds[i];
1421+
1422+
if (subd->spoke_id != spoke_id)
1423+
continue;
1424+
1425+
/* It got a conn after all: lightningd had already sent a
1426+
* connectd_peer_connect_subd, and that reached us first, so
1427+
* write_to_subd() wakes us in the normal way. */
1428+
if (subd->conn)
1429+
return;
1430+
1431+
status_peer_debug(&peer->id,
1432+
"No subd for %s: resuming read",
1433+
fmt_channel_id(tmpctx, &subd->channel_id));
1434+
subd->no_conn_coming = true;
1435+
io_wake(&peer->peer_in);
1436+
return;
1437+
}
1438+
}
1439+
13861440
static struct subd *new_subd(struct peer *peer,
13871441
const struct channel_id *channel_id)
13881442
{
@@ -1396,6 +1450,9 @@ static struct subd *new_subd(struct peer *peer,
13961450
subd->opener_revocation_basepoint = NULL;
13971451
subd->conn = NULL;
13981452
subd->rcvd_tx_abort = false;
1453+
subd->spoke_id = peer->daemon->spoke_counter++;
1454+
subd->no_conn_coming = false;
1455+
subd->undelivered = 0;
13991456

14001457
/* Connect it to the peer */
14011458
tal_arr_expand(&peer->subds, subd);
@@ -1506,10 +1563,13 @@ static struct io_plan *read_body_from_peer_done(struct io_conn *peer_conn,
15061563
status_peer_debug(&peer->id, "Activating for message %s",
15071564
peer_wire_name(t));
15081565
subd = new_subd(peer, &channel_id);
1509-
/* We tell lightningd to fire up a subdaemon to handle this! */
1566+
/* We tell lightningd to fire up a subdaemon to handle this!
1567+
* It must answer with either connectd_peer_connect_subd or
1568+
* connectd_peer_no_subd: until it does, we stop reading. */
15101569
daemon_conn_send(peer->daemon->master,
15111570
take(towire_connectd_peer_spoke(NULL, &peer->id,
15121571
peer->counter,
1572+
subd->spoke_id,
15131573
t,
15141574
&channel_id,
15151575
is_peer_error(tmpctx, decrypted))));
@@ -1521,6 +1581,17 @@ static struct io_plan *read_body_from_peer_done(struct io_conn *peer_conn,
15211581
/* Tell them to write. */
15221582
msg_enqueue(subd->outq, take(decrypted));
15231583

1584+
/* Disconnect if too many messages queue up on a subd without a conn
1585+
* attached. Otherwise the queue can grow unbounded. */
1586+
if (subd->no_conn_coming && ++subd->undelivered > MAX_UNDELIVERED_MSGS) {
1587+
status_peer_unusual(&peer->id,
1588+
CI_UNEXPECTED "%zu messages queued with no"
1589+
" subd: hanging up", subd->undelivered);
1590+
/* This frees subd: don't touch it again. */
1591+
disconnect_peer(peer);
1592+
return io_wait(peer_conn, &peer->peer_in, next_read, peer);
1593+
}
1594+
15241595
/* Is this a tx_abort? Ignore from now on, and close after sending! */
15251596
if (type == WIRE_TX_ABORT) {
15261597
subd->rcvd_tx_abort = true;
@@ -1611,6 +1682,11 @@ static struct io_plan *subd_conn_init(struct io_conn *subd_conn,
16111682
{
16121683
subd->conn = subd_conn;
16131684

1685+
/* A conn arrived after all, so stop counting against
1686+
* MAX_UNDELIVERED_MSGS. */
1687+
subd->no_conn_coming = false;
1688+
subd->undelivered = 0;
1689+
16141690
/* subd is a child of the conn: free when it closes! */
16151691
tal_steal(subd->conn, subd);
16161692
tal_add_destructor(subd, destroy_connected_subd);

connectd/multiplex.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ void set_custommsgs(struct daemon *daemon, const u8 *msg);
3737
/* Lightningd wants to talk to you. */
3838
void peer_connect_subd(struct daemon *daemon, const u8 *msg, int fd);
3939

40+
/* Lightningd isn't attaching a subd for the peer_spoke we sent with this
41+
* spoke_id, so start reading from the peer again. */
42+
void resume_read_without_subd(struct peer *peer, u64 spoke_id);
43+
4044
/* Disconnect peer: give outgoing msgs time to drain though. */
4145
void disconnect_peer(struct peer *peer);
4246

lightningd/connect_control.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ static unsigned connectd_msg(struct subd *connectd, const u8 *msg, const int *fd
548548
case WIRE_CONNECTD_DEV_REPORT_FDS:
549549
case WIRE_CONNECTD_PEER_SEND_MSG:
550550
case WIRE_CONNECTD_PEER_CONNECT_SUBD:
551+
case WIRE_CONNECTD_PEER_NO_SUBD:
551552
case WIRE_CONNECTD_PING:
552553
case WIRE_CONNECTD_SEND_ONIONMSG:
553554
case WIRE_CONNECTD_INJECT_ONIONMSG:

lightningd/peer_control.c

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2021,8 +2021,9 @@ void handle_peer_spoke(struct lightningd *ld, const u8 *msg)
20212021
int other_fd;
20222022
struct peer_fd *pfd;
20232023
char *errmsg;
2024+
u64 spoke_id;
20242025

2025-
if (!fromwire_connectd_peer_spoke(msg, msg, &id, &connectd_counter, &msgtype, &channel_id, &errmsg))
2026+
if (!fromwire_connectd_peer_spoke(msg, msg, &id, &connectd_counter, &spoke_id, &msgtype, &channel_id, &errmsg))
20262027
fatal("Connectd gave bad CONNECTD_PEER_SPOKE message %s",
20272028
tal_hex(msg, msg));
20282029

@@ -2068,7 +2069,7 @@ void handle_peer_spoke(struct lightningd *ld, const u8 *msg)
20682069
if (errmsg) {
20692070
channel_fail_permanent(channel, REASON_REMOTE,
20702071
"They sent %s", errmsg);
2071-
return;
2072+
goto no_subd;
20722073
}
20732074

20742075
/* If channel is active there are two possibilities:
@@ -2077,7 +2078,7 @@ void handle_peer_spoke(struct lightningd *ld, const u8 *msg)
20772078
* 2. subd exited */
20782079
if (channel->owner) {
20792080
/* We raced... */
2080-
return;
2081+
goto no_subd;
20812082
}
20822083

20832084
if (msgtype == WIRE_CHANNEL_REESTABLISH
@@ -2093,7 +2094,7 @@ void handle_peer_spoke(struct lightningd *ld, const u8 *msg)
20932094
take(towire_connectd_peer_send_msg(NULL, &peer->id,
20942095
peer->connectd_counter,
20952096
error)));
2096-
return;
2097+
goto no_subd;
20972098
}
20982099

20992100
log_debug(channel->log, "channel already active");
@@ -2106,7 +2107,7 @@ void handle_peer_spoke(struct lightningd *ld, const u8 *msg)
21062107
/* FIXME: Send informative error? */
21072108
close(other_fd);
21082109
}
2109-
return;
2110+
goto no_subd;
21102111
}
21112112

21122113
/* Send generic error. */
@@ -2142,7 +2143,7 @@ void handle_peer_spoke(struct lightningd *ld, const u8 *msg)
21422143
goto tell_connectd;
21432144
/* FIXME: Send informative error? */
21442145
close(other_fd);
2145-
return;
2146+
goto no_subd;
21462147

21472148
case WIRE_OPEN_CHANNEL2:
21482149
if (!dual_fund) {
@@ -2162,7 +2163,7 @@ void handle_peer_spoke(struct lightningd *ld, const u8 *msg)
21622163
goto tell_connectd;
21632164
/* FIXME: Send informative error? */
21642165
close(other_fd);
2165-
return;
2166+
goto no_subd;
21662167

21672168
case WIRE_CHANNEL_REESTABLISH:
21682169
/* Maybe a previously closed channel? */
@@ -2202,6 +2203,17 @@ void handle_peer_spoke(struct lightningd *ld, const u8 *msg)
22022203
peer->connectd_counter)));
22032204
return;
22042205

2206+
no_subd:
2207+
/* connectd made a subd for this message and stopped reading from the
2208+
* peer until we answer. We're not attaching one, so tell it: nothing
2209+
* else will ever wake it. Use our own copies of id/counter, since
2210+
* channel_fail_permanent() above may have freed peer and channel. */
2211+
subd_send_msg(ld->connectd,
2212+
take(towire_connectd_peer_no_subd(NULL, &id,
2213+
connectd_counter,
2214+
spoke_id)));
2215+
return;
2216+
22052217
tell_connectd:
22062218
subd_send_msg(ld->connectd,
22072219
take(towire_connectd_peer_connect_subd(NULL, &id,

lightningd/test/run-invoice-select-inchan.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ bool fromwire_connectd_peer_disconnected(const void *p UNNEEDED, struct node_id
291291
bool fromwire_connectd_peer_reconnected(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, struct node_id *id UNNEEDED, u64 *prev_counter UNNEEDED, u64 *counter UNNEEDED, struct wireaddr_internal *addr UNNEEDED, struct wireaddr **remote_addr UNNEEDED, bool *incoming UNNEEDED, u8 **features UNNEEDED, u64 *connected_time_nsec UNNEEDED)
292292
{ fprintf(stderr, "fromwire_connectd_peer_reconnected called!\n"); abort(); }
293293
/* Generated stub for fromwire_connectd_peer_spoke */
294-
bool fromwire_connectd_peer_spoke(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, struct node_id *id UNNEEDED, u64 *counter UNNEEDED, u16 *msgtype UNNEEDED, struct channel_id *channel_id UNNEEDED, wirestring **error UNNEEDED)
294+
bool fromwire_connectd_peer_spoke(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, struct node_id *id UNNEEDED, u64 *counter UNNEEDED, u64 *spoke_id UNNEEDED, u16 *msgtype UNNEEDED, struct channel_id *channel_id UNNEEDED, wirestring **error UNNEEDED)
295295
{ fprintf(stderr, "fromwire_connectd_peer_spoke called!\n"); abort(); }
296296
/* Generated stub for fromwire_dualopend_dev_memleak_reply */
297297
bool fromwire_dualopend_dev_memleak_reply(const void *p UNNEEDED, bool *leak UNNEEDED)
@@ -636,6 +636,9 @@ u8 *towire_connectd_disconnect_peer(const tal_t *ctx UNNEEDED, const struct node
636636
/* Generated stub for towire_connectd_peer_connect_subd */
637637
u8 *towire_connectd_peer_connect_subd(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 counter UNNEEDED, const struct channel_id *channel_id UNNEEDED)
638638
{ fprintf(stderr, "towire_connectd_peer_connect_subd called!\n"); abort(); }
639+
/* Generated stub for towire_connectd_peer_no_subd */
640+
u8 *towire_connectd_peer_no_subd(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 counter UNNEEDED, u64 spoke_id UNNEEDED)
641+
{ fprintf(stderr, "towire_connectd_peer_no_subd called!\n"); abort(); }
639642
/* Generated stub for towire_connectd_peer_send_msg */
640643
u8 *towire_connectd_peer_send_msg(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 counter UNNEEDED, const u8 *msg UNNEEDED)
641644
{ fprintf(stderr, "towire_connectd_peer_send_msg called!\n"); abort(); }

tests/test_connection.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3157,6 +3157,62 @@ def test_dataloss_protection(node_factory, bitcoind):
31573157
assert (closetxid, "confirmed") in set([(o['txid'], o['status']) for o in l2.rpc.listfunds()['outputs']])
31583158

31593159

3160+
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "sqlite3-specific DB rollback")
3161+
@pytest.mark.openchannel('v1')
3162+
@pytest.mark.openchannel('v2')
3163+
def test_dataloss_protection_still_reads(node_factory, bitcoind, executor):
3164+
"""Ensure we keep servicing the peer connection after data-loss recovery.
3165+
3166+
l2 comes back with an old database, so l1's channeld sends a warning and
3167+
deliberately exits *without* disconnecting so that l2 can send an error and
3168+
have l1 force close for them. By the time l2 sends the error, l1 has no
3169+
subd for the channel, so connectd creates one, sends connectd_peer_spoke and
3170+
parks the peer read until lightningd answers. lightningd's
3171+
handle_peer_spoke() must fail the channel and respond with
3172+
connectd_peer_no_subd, so that connectd continues to service the peer
3173+
connection."""
3174+
l1 = node_factory.get_node(may_reconnect=True, allow_warning=True,
3175+
feerates=(7500, 7500, 7500, 7500),
3176+
options={'dev-force-features': -7})
3177+
l2 = node_factory.get_node(may_reconnect=True,
3178+
broken_log='Cannot broadcast our commitment tx: they have a future one',
3179+
feerates=(7500, 7500, 7500, 7500),
3180+
options={'dev-force-features': -7})
3181+
3182+
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
3183+
l1.fundchannel(l2, 10**6)
3184+
l2.stop()
3185+
3186+
# Save copy of the db, then move on.
3187+
dbpath = os.path.join(l2.daemon.lightning_dir, TEST_NETWORK, "lightningd.sqlite3")
3188+
orig_db = Path(dbpath).read_bytes()
3189+
l2.start()
3190+
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
3191+
l1.pay(l2, 200000000)
3192+
3193+
# Make sure both sides consider it completely settled.
3194+
l1.daemon.wait_for_logs(["peer_in WIRE_REVOKE_AND_ACK"] * 2)
3195+
l2.daemon.wait_for_logs(["peer_in WIRE_REVOKE_AND_ACK"] * 2)
3196+
3197+
# Now, move l2 back in time.
3198+
l2.stop()
3199+
Path(dbpath).write_bytes(orig_db)
3200+
l2.start()
3201+
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
3202+
3203+
# l2 freaks out and sends an error; l1 takes handle_peer_spoke()'s errmsg
3204+
# exit and never creates the requested subdaemon.
3205+
l1.daemon.wait_for_log("They sent ERROR.*Awaiting unilateral close")
3206+
3207+
# connectd made a subd for that error and is waiting on lightningd's
3208+
# response.
3209+
assert l1.daemon.is_in_log('Activating for message WIRE_ERROR')
3210+
3211+
# l1 must still be reading from l2. Ensure l1 responds to l2's ping message.
3212+
fut = executor.submit(l2.rpc.ping, l1.info['id'])
3213+
fut.result(20)
3214+
3215+
31603216
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "sqlite3-specific DB rollback")
31613217
@pytest.mark.openchannel('v1')
31623218
@pytest.mark.openchannel('v2')

0 commit comments

Comments
 (0)