From c97c47da47ce59536bd4cdf8915b34b0cbe1769e Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Tue, 27 Sep 2016 18:26:35 +0200 Subject: [PATCH 1/4] irc: Expose more IRC functionality Now exposing the connection event and raw incoming commands. --- irc.c | 14 +++++++++++--- irc.h | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/irc.c b/irc.c index d1eebac1c..af440f9fc 100644 --- a/irc.c +++ b/irc.c @@ -3,6 +3,8 @@ #include "daemon/log.h" void (*irc_privmsg_cb)(struct ircstate *, const struct privmsg *) = NULL; +void (*irc_command_cb)(struct ircstate *, const struct irccommand *) = NULL; +void (*irc_connect_cb)(struct ircstate *) = NULL; void (*irc_disconnect_cb)(struct ircstate *) = NULL; static struct io_plan *irc_connected(struct io_conn *conn, struct lightningd_state *dstate, struct ircstate *state); @@ -63,7 +65,7 @@ static struct io_plan *irc_write_loop(struct io_conn *conn, struct ircstate *sta ); } -/* +/* * Called by the read loop to handle individual lines. This splits the * line into a struct irccommand and passes it on to the specific * handlers for the irccommand type. It silently drops any irccommand @@ -94,10 +96,14 @@ static void handle_irc_command(struct ircstate *state, const char *line) pm->msg = tal_strjoin(m, splits + 2, " ", STR_NO_TRAIL); irc_privmsg_cb(state, pm); } + + if (irc_command_cb != NULL) + irc_command_cb(state, m); + tal_free(m); } -/* +/* * Read incoming data and split it along the newline boundaries. Takes * care of buffering incomplete lines and passes the lines to the * handle_irc_command handler. @@ -168,7 +174,9 @@ static struct io_plan *irc_connected(struct io_conn *conn, struct lightningd_sta state->connected = true; irc_send(state, "USER", "%s 0 * :A lightning node", state->nick); irc_send(state, "NICK", "%s", state->nick); - irc_send(state, "JOIN", "#lightning-nodes"); + + if (irc_connect_cb != NULL) + irc_connect_cb(state); return io_duplex(conn, io_read_partial(conn, diff --git a/irc.h b/irc.h index 6d9cf5f46..e1590b271 100644 --- a/irc.h +++ b/irc.h @@ -53,8 +53,10 @@ struct ircstate { struct timerel reconnect_timeout; }; -/* Callback to register for incoming messages */ +/* Callbacks to register for incoming messages, events and raw commands */ extern void (*irc_privmsg_cb)(struct ircstate *, const struct privmsg *); +extern void (*irc_command_cb)(struct ircstate *, const struct irccommand *); +extern void (*irc_connect_cb)(struct ircstate *); extern void (*irc_disconnect_cb)(struct ircstate *); /* Send messages to IRC */ From b2126375e01c873f8bd5b492f9bc147783af9166 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 28 Sep 2016 16:52:03 +0200 Subject: [PATCH 2/4] irc: Add contact information to nodes The routing table now includes hostnames and ports for the node as well as a helper to add/update the nodes we learn about. --- daemon/routing.c | 21 +++++++++++++++++++++ daemon/routing.h | 11 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/daemon/routing.c b/daemon/routing.c index 1b63a5669..dec8dc7fe 100644 --- a/daemon/routing.c +++ b/daemon/routing.c @@ -28,6 +28,7 @@ static bool node_eq(const struct node *n, const secp256k1_pubkey *key) { return structeq(&n->id.pubkey, key); } + HTABLE_DEFINE_TYPE(struct node, keyof_node, hash_key, node_eq, node_map); struct node_map *empty_node_map(struct lightningd_state *dstate) @@ -69,6 +70,26 @@ struct node *new_node(struct lightningd_state *dstate, return n; } +struct node *add_node( + struct lightningd_state *dstate, + const struct pubkey *pk, + char *hostname, + int port) +{ + struct node *n = get_node(dstate, pk); + if (!n) { + n = new_node(dstate, pk); + log_debug_struct(dstate->base_log, "Creating new node %s", + struct pubkey, pk); + } else { + log_debug_struct(dstate->base_log, "Update existing node %s", + struct pubkey, pk); + } + n->hostname = tal_steal(n, hostname); + n->port = port; + return n; +} + static bool remove_conn_from_array(struct node_connection ***conns, struct node_connection *nc) { diff --git a/daemon/routing.h b/daemon/routing.h index faeede061..5430c47ba 100644 --- a/daemon/routing.h +++ b/daemon/routing.h @@ -20,6 +20,11 @@ struct node_connection { struct node { struct pubkey id; + + /* IP/Hostname and port of this node */ + char *hostname; + int port; + /* Routes connecting to us, from us. */ struct node_connection **in, **out; @@ -46,6 +51,12 @@ struct node *get_node(struct lightningd_state *dstate, * If it returns more than msatoshi, it overflowed. */ s64 connection_fee(const struct node_connection *c, u64 msatoshi); +/* Updates existing node, or creates a new one as required. */ +struct node *add_node(struct lightningd_state *dstate, + const struct pubkey *pk, + char *hostname, + int port); + /* Updates existing connection, or creates new one as required. */ struct node_connection *add_connection(struct lightningd_state *dstate, const struct pubkey *from, From 2a5a114f27fa464761456a95a50ba5e8024facd9 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 28 Sep 2016 16:54:16 +0200 Subject: [PATCH 3/4] irc: Handle node announcements lightningd now uses a WHOIS query on itself to learn its external IP address and announces that on the channel with the NODE message. It also tracks other nodes in the routing table. Refactored the signature verification to reuse it for both CHAN and NODE messages. --- daemon/irc_announce.c | 173 +++++++++++++++++++++++++++++++++--------- daemon/lightningd.h | 3 + 2 files changed, 140 insertions(+), 36 deletions(-) diff --git a/daemon/irc_announce.c b/daemon/irc_announce.c index 1b2255aae..4c04cffa3 100644 --- a/daemon/irc_announce.c +++ b/daemon/irc_announce.c @@ -13,12 +13,20 @@ #include #include -static bool announce_channel(const tal_t *ctx, struct ircstate *state, struct peer *p) +/* Sign a privmsg by prepending the signature to the message */ +static void sign_privmsg(struct ircstate *state, struct privmsg *msg) { - char txid[65]; int siglen; u8 der[72]; struct signature sig; + privkey_sign(state->dstate, msg->msg, strlen(msg->msg), &sig); + siglen = signature_to_der(state->dstate->secpctx, der, &sig); + msg->msg = tal_fmt(msg, "%s %s", tal_hexstr(msg, der, siglen), msg->msg); +} + +static bool announce_channel(const tal_t *ctx, struct ircstate *state, struct peer *p) +{ + char txid[65]; struct privmsg *msg = talz(ctx, struct privmsg); struct txlocator *loc = locate_tx(ctx, state->dstate, &p->anchor.txid); @@ -38,20 +46,45 @@ static bool announce_channel(const tal_t *ctx, struct ircstate *state, struct pe state->dstate->config.fee_per_satoshi, p->remote.locktime.locktime ); - - privkey_sign(state->dstate, msg->msg, strlen(msg->msg), &sig); - siglen = signature_to_der(state->dstate->secpctx, der, &sig); - msg->msg = tal_fmt(msg, "%s %s", tal_hexstr(ctx, der, siglen), msg->msg); - + sign_privmsg(state, msg); irc_send_msg(state, msg); return true; } -static void announce_channels(struct ircstate *state) +/* Send an announcement for this node to the channel, including its + * hostname, port and ID */ +static void announce_node(const tal_t *ctx, struct ircstate *state) { + char *hostname = state->dstate->external_ip; + int port = state->dstate->portnum; + struct privmsg *msg = talz(ctx, struct privmsg); + + if (hostname == NULL) { + //FIXME: log that we don't know our IP yet. + return; + } + + msg->channel = "#lightning-nodes"; + msg->msg = tal_fmt( + msg, "NODE %s %s %d", + pubkey_to_hexstr(msg, state->dstate->secpctx, &state->dstate->id), + hostname, + port + ); + + sign_privmsg(state, msg); + irc_send_msg(state, msg); +} + +/* Announce the node's contact information and all of its channels */ +static void announce(struct ircstate *state) +{ + tal_t *ctx = tal(state, tal_t); struct peer *p; + announce_node(ctx, state); + list_for_each(&state->dstate->peers, p, list) { if (!state_is_normal(p->state)) @@ -60,7 +93,7 @@ static void announce_channels(struct ircstate *state) } tal_free(ctx); - new_reltimer(state->dstate, state, time_from_sec(60), announce_channels, state); + new_reltimer(state->dstate, state, time_from_sec(60), announce, state); } /* Reconnect to IRC server upon disconnection. */ @@ -69,42 +102,41 @@ static void handle_irc_disconnect(struct ircstate *state) new_reltimer(state->dstate, state, state->reconnect_timeout, irc_connect, state); } -/* - * Handle an incoming message by checking if it is a channel - * announcement, parse it and add the channel to the topology if yes. - * - * The format for a valid announcement is: - * CHAN - * - */ -static void handle_irc_privmsg(struct ircstate *istate, const struct privmsg *msg) +/* Verify a signed privmsg */ +static bool verify_signed_privmsg( + struct ircstate *istate, + const struct pubkey *pk, + const struct privmsg *msg) { - int blkheight; - char **splits = tal_strsplit(msg, msg->msg + 1, " ", STR_NO_EMPTY); - - if (tal_count(splits) != 11 || !streq(splits[1], "CHAN")) - return; - - int siglen = hex_data_size(strlen(splits[0])); - u8 *der = tal_hexdata(msg, splits[0], strlen(splits[0])); - if (der == NULL) - return; - struct signature sig; struct sha256_double hash; - char *content = strchr(msg->msg, ' ') + 1; + const char *m = msg->msg + 1; + int siglen = strchr(m, ' ') - m; + const char *content = m + siglen + 1; + u8 *der = tal_hexdata(msg, m, siglen); + + siglen = hex_data_size(siglen); + if (der == NULL) + return false; + if (!signature_from_der(istate->dstate->secpctx, der, siglen, &sig)) - return; - + return false; sha256_double(&hash, content, strlen(content)); - splits++; + return check_signed_hash(istate->dstate->secpctx, &hash, &sig, pk); +} +static void handle_channel_announcement( + struct ircstate *istate, + const struct privmsg *msg, + char **splits) +{ struct pubkey *pk1 = talz(msg, struct pubkey); struct pubkey *pk2 = talz(msg, struct pubkey); struct sha256_double *txid = talz(msg, struct sha256_double); int index; - bool ok = true; + int blkheight; + ok &= pubkey_from_hexstr(istate->dstate->secpctx, splits[1], strlen(splits[1]), pk1); ok &= pubkey_from_hexstr(istate->dstate->secpctx, splits[2], strlen(splits[2]), pk2); ok &= bitcoin_txid_from_hex(splits[3], strlen(splits[3]), txid); @@ -115,7 +147,7 @@ static void handle_irc_privmsg(struct ircstate *istate, const struct privmsg *ms return; } - if (!check_signed_hash(istate->dstate->secpctx, &hash, &sig, pk1)) { + if (!verify_signed_privmsg(istate, pk1, msg)) { log_debug(istate->log, "Ignoring announcement from %s, signature check failed.", splits[1]); @@ -131,11 +163,80 @@ static void handle_irc_privmsg(struct ircstate *istate, const struct privmsg *ms atoi(splits[7]), atoi(splits[8]), 6); } +static void handle_node_announcement( + struct ircstate *istate, + const struct privmsg *msg, + char **splits) +{ + struct pubkey *pk = talz(msg, struct pubkey); + char *hostname = tal_strdup(msg, splits[2]); + int port = atoi(splits[3]); + + if (!pubkey_from_hexstr(istate->dstate->secpctx, splits[1], strlen(splits[1]), pk) || port < 1) + return; + + if (!verify_signed_privmsg(istate, pk, msg)) { + log_debug(istate->log, "Ignoring node announcement from %s, signature check failed.", + splits[1]); + return; + } + + add_node(istate->dstate, pk, hostname, port); +} + +/* + * Handle an incoming message by checking if it is a channel + * announcement, parse it and add the channel to the topology if yes. + * + * The format for a valid announcement is: + * CHAN + * + */ +static void handle_irc_privmsg(struct ircstate *istate, const struct privmsg *msg) +{ + char **splits = tal_strsplit(msg, msg->msg + 1, " ", STR_NO_EMPTY); + int splitcount = tal_count(splits) - 1; + + if (splitcount < 2) + return; + + char *type = splits[1]; + + if (splitcount == 10 && streq(type, "CHAN")) + handle_channel_announcement(istate, msg, splits + 1); + else if (splitcount == 5 && streq(type, "NODE")) + handle_node_announcement(istate, msg, splits + 1); +} + +static void handle_irc_command(struct ircstate *istate, const struct irccommand *cmd) +{ + struct lightningd_state *dstate = istate->dstate; + char **params = tal_strsplit(cmd, cmd->params, " ", STR_NO_EMPTY); + int numparams = tal_count(params) - 1; + + if (streq(cmd->command, "378")) { + dstate->external_ip = tal_strdup( + istate->dstate, params[numparams - 1]); + + // Add our node to the node_map for completeness + add_node(istate->dstate, &dstate->id, + dstate->external_ip, dstate->portnum); + } +} + +static void handle_irc_connected(struct ircstate *istate) +{ + irc_send(istate, "JOIN", "#lightning-nodes"); + irc_send(istate, "WHOIS", "%s", istate->nick); +} + void setup_irc_connection(struct lightningd_state *dstate) { // Register callback irc_privmsg_cb = *handle_irc_privmsg; + irc_connect_cb = *handle_irc_connected; irc_disconnect_cb = *handle_irc_disconnect; + irc_command_cb = *handle_irc_command; struct ircstate *state = talz(dstate, struct ircstate); state->dstate = dstate; @@ -151,5 +252,5 @@ void setup_irc_connection(struct lightningd_state *dstate) pubkey_to_hexstr(state, dstate->secpctx, &dstate->id) + 1); irc_connect(state); - announce_channels(state); + announce(state); } diff --git a/daemon/lightningd.h b/daemon/lightningd.h index f8d1e479f..2751c77a2 100644 --- a/daemon/lightningd.h +++ b/daemon/lightningd.h @@ -130,5 +130,8 @@ struct lightningd_state { /* Re-exec hack for testing. */ char **reexec; + + /* IP/hostname to be announced for incoming connections */ + char *external_ip; }; #endif /* LIGHTNING_DAEMON_LIGHTNING_H */ From 594eb8109cd956be13aae4b9413e9d1d20544be0 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Fri, 30 Sep 2016 15:30:19 +0200 Subject: [PATCH 4/4] jsonrpc: Added 'getnodes' to list known nodes. getnodes returns an object containing a single array of 'nodes'. Each element contains the node's ID, its hostname and its port. If unknown (because we haven't seen a node announcement yet) then the port is 0 and the hostname is null. --- daemon/jsonrpc.c | 1 + daemon/jsonrpc.h | 1 + daemon/routing.c | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/daemon/jsonrpc.c b/daemon/jsonrpc.c index 6a1369b22..04decb9d8 100644 --- a/daemon/jsonrpc.c +++ b/daemon/jsonrpc.c @@ -285,6 +285,7 @@ static const struct json_command *cmdlist[] = { &getlog_command, &connect_command, &getpeers_command, + &getnodes_command, &gethtlcs_command, &close_command, &newaddr_command, diff --git a/daemon/jsonrpc.h b/daemon/jsonrpc.h index 4c5ad6159..72b4dbebc 100644 --- a/daemon/jsonrpc.h +++ b/daemon/jsonrpc.h @@ -61,6 +61,7 @@ extern const struct json_command newaddr_command; extern const struct json_command connect_command; extern const struct json_command close_command; extern const struct json_command getpeers_command; +extern const struct json_command getnodes_command; /* Invoice management. */ extern const struct json_command invoice_command; diff --git a/daemon/routing.c b/daemon/routing.c index dec8dc7fe..e74724dc9 100644 --- a/daemon/routing.c +++ b/daemon/routing.c @@ -64,6 +64,7 @@ struct node *new_node(struct lightningd_state *dstate, n->id = *id; n->in = tal_arr(n, struct node_connection *, 0); n->out = tal_arr(n, struct node_connection *, 0); + n->port = 0; node_map_add(dstate->nodes, n); tal_add_destructor(n, destroy_node); @@ -511,4 +512,40 @@ const struct json_command dev_routefail_command = { "Returns an empty result on success" }; +static void json_getnodes(struct command *cmd, + const char *buffer, const jsmntok_t *params) +{ + struct json_result *response = new_json_result(cmd); + struct node *n; + struct node_map_iter i; + n = node_map_first(cmd->dstate->nodes, &i); + + json_object_start(response, NULL); + json_array_start(response, "nodes"); + + while (n != NULL) { + json_object_start(response, NULL); + json_add_pubkey(response, cmd->dstate->secpctx, + "nodeid", &n->id); + json_add_num(response, "port", n->port); + if (!n->port) + json_add_null(response, "hostname"); + else + json_add_string(response, "hostname", n->hostname); + + json_object_end(response); + n = node_map_next(cmd->dstate->nodes, &i); + } + + json_array_end(response); + json_object_end(response); + command_success(cmd, response); +} + +const struct json_command getnodes_command = { + "getnodes", + json_getnodes, + "List all known nodes in the network.", + "Returns a 'nodes' array" +};