Add an invitation protocol.
Using the tinc command, an administrator of an existing VPN can generate invitations for new nodes. The invitation is a small URL that can easily be copy&pasted into email or live chat. Another person can have tinc automatically setup the necessary configuration files and exchange keys with the server, by only using the invitation URL. The invitation protocol uses temporary ECDSA keys. The invitation URL consists of the hostname and port of the server, a hash of the server's temporary ECDSA key and a cookie. When the client wants to accept an invitation, it also creates a temporary ECDSA key, connects to the server and says it wants to accept an invitation. Both sides exchange their temporary keys. The client verifies that the server's key matches the hash in the invitation URL. After setting up an SPTPS connection using the temporary keys, the client gives the cookie to the server. If the cookie is valid, the server sends the client an invitation file containing the client's new name and a copy of the server's host config file. If everything is ok, the client will generate a long-term ECDSA key and send it to the server, which will add it to a new host config file for the client. The invitation protocol currently allows multiple host config files to be send from the server to the client. However, the client filters out most configuration variables for its own host configuration file. In particular, it only accepts Name, Mode, Broadcast, ConnectTo, Subnet and AutoConnect. Also, at the moment no tinc-up script is generated. When an invitation has succesfully been accepted, the client needs to start the tinc daemon manually.
This commit is contained in:
parent
12e68b95e6
commit
ced4c1a327
13 changed files with 1297 additions and 54 deletions
|
|
@ -27,10 +27,12 @@
|
|||
#include "cipher.h"
|
||||
#include "crypto.h"
|
||||
#include "digest.h"
|
||||
#include "ecdsa.h"
|
||||
#include "edge.h"
|
||||
#include "graph.h"
|
||||
#include "logger.h"
|
||||
#include "meta.h"
|
||||
#include "names.h"
|
||||
#include "net.h"
|
||||
#include "netutl.h"
|
||||
#include "node.h"
|
||||
|
|
@ -41,6 +43,8 @@
|
|||
#include "utils.h"
|
||||
#include "xalloc.h"
|
||||
|
||||
ecdsa_t *invitation_key = NULL;
|
||||
|
||||
static bool send_proxyrequest(connection_t *c) {
|
||||
switch(proxytype) {
|
||||
case PROXY_HTTP: {
|
||||
|
|
@ -146,6 +150,107 @@ bool send_id(connection_t *c) {
|
|||
return send_request(c, "%d %s %d.%d", ID, myself->connection->name, myself->connection->protocol_major, minor);
|
||||
}
|
||||
|
||||
static bool finalize_invitation(connection_t *c, const char *data, uint16_t len) {
|
||||
if(strchr(data, '\n')) {
|
||||
logger(DEBUG_ALWAYS, LOG_ERR, "Received invalid key from invited node %s (%s)!\n", c->name, c->hostname);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a new host config file
|
||||
char filename[PATH_MAX];
|
||||
snprintf(filename, sizeof filename, "%s" SLASH "hosts" SLASH "%s", confbase, c->name);
|
||||
if(!access(filename, F_OK)) {
|
||||
logger(DEBUG_ALWAYS, LOG_ERR, "Host config file for %s (%s) already exists!\n", c->name, c->hostname);
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE *f = fopen(filename, "w");
|
||||
if(!f) {
|
||||
logger(DEBUG_ALWAYS, LOG_ERR, "Error trying to create %s: %s\n", filename, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
fprintf(f, "ECDSAPublicKey = %s\n", data);
|
||||
fclose(f);
|
||||
|
||||
logger(DEBUG_CONNECTIONS, LOG_INFO, "Key succesfully received from %s (%s)", c->name, c->hostname);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool receive_invitation_sptps(void *handle, uint8_t type, const char *data, uint16_t len) {
|
||||
connection_t *c = handle;
|
||||
|
||||
if(type == 128)
|
||||
return true;
|
||||
|
||||
if(type == 1 && c->status.invitation_used)
|
||||
return finalize_invitation(c, data, len);
|
||||
|
||||
if(type != 0 || len != 18 || c->status.invitation_used)
|
||||
return false;
|
||||
|
||||
char cookie[25];
|
||||
b64encode_urlsafe(data, cookie, 18);
|
||||
|
||||
char filename[PATH_MAX], usedname[PATH_MAX];
|
||||
snprintf(filename, sizeof filename, "%s" SLASH "invitations" SLASH "%s", confbase, cookie);
|
||||
snprintf(usedname, sizeof usedname, "%s" SLASH "invitations" SLASH "%s.used", confbase, cookie);
|
||||
|
||||
// Atomically rename the invitation file
|
||||
if(rename(filename, usedname)) {
|
||||
if(errno == ENOENT)
|
||||
logger(DEBUG_ALWAYS, LOG_ERR, "Peer %s tried to use non-existing invitation %s\n", c->hostname, cookie);
|
||||
else
|
||||
logger(DEBUG_ALWAYS, LOG_ERR, "Error trying to rename invitation %s\n", cookie);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Open the renamed file
|
||||
FILE *f = fopen(usedname, "r");
|
||||
if(!f) {
|
||||
logger(DEBUG_ALWAYS, LOG_ERR, "Error trying to open invitation %s\n", cookie);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the new node's Name from the file
|
||||
char buf[1024];
|
||||
fgets(buf, sizeof buf, f);
|
||||
if(*buf)
|
||||
buf[strlen(buf) - 1] = 0;
|
||||
|
||||
len = strcspn(buf, " \t=");
|
||||
char *name = buf + len;
|
||||
name += strspn(name, " \t");
|
||||
if(*name == '=') {
|
||||
name++;
|
||||
name += strspn(name, " \t");
|
||||
}
|
||||
buf[len] = 0;
|
||||
|
||||
if(!*buf || !*name || strcasecmp(buf, "Name") || !check_id(name)) {
|
||||
logger(DEBUG_ALWAYS, LOG_ERR, "Invalid invitation file %s\n", cookie);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
|
||||
free(c->name);
|
||||
c->name = xstrdup(name);
|
||||
|
||||
// Send the node the contents of the invitation file
|
||||
rewind(f);
|
||||
size_t result;
|
||||
while((result = fread(buf, 1, sizeof buf, f)))
|
||||
sptps_send_record(&c->sptps, 0, buf, result);
|
||||
sptps_send_record(&c->sptps, 1, buf, 0);
|
||||
fclose(f);
|
||||
unlink(usedname);
|
||||
|
||||
c->status.invitation_used = true;
|
||||
|
||||
logger(DEBUG_CONNECTIONS, LOG_INFO, "Invitation %s succesfully sent to %s (%s)", cookie, c->name, c->hostname);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool id_h(connection_t *c, const char *request) {
|
||||
char name[MAX_STRING_SIZE];
|
||||
|
||||
|
|
@ -168,6 +273,31 @@ bool id_h(connection_t *c, const char *request) {
|
|||
return send_request(c, "%d %d %d", ACK, TINC_CTL_VERSION_CURRENT, getpid());
|
||||
}
|
||||
|
||||
if(name[0] == '?') {
|
||||
if(!invitation_key) {
|
||||
logger(DEBUG_ALWAYS, LOG_ERR, "Got invitation from %s but we don't have an invitation key", c->hostname);
|
||||
return false;
|
||||
}
|
||||
|
||||
c->ecdsa = ecdsa_set_base64_public_key(name + 1);
|
||||
if(!c->ecdsa) {
|
||||
logger(DEBUG_ALWAYS, LOG_ERR, "Got bad invitation from %s", c->hostname);
|
||||
return false;
|
||||
}
|
||||
|
||||
c->status.invitation = true;
|
||||
char *mykey = ecdsa_get_base64_public_key(invitation_key);
|
||||
if(!mykey)
|
||||
return false;
|
||||
if(!send_request(c, "%d %s", ACK, mykey))
|
||||
return false;
|
||||
free(mykey);
|
||||
|
||||
c->protocol_minor = 2;
|
||||
|
||||
return sptps_start(&c->sptps, c, false, false, invitation_key, c->ecdsa, "tinc invitation", 15, send_meta_sptps, receive_invitation_sptps);
|
||||
}
|
||||
|
||||
/* Check if identity is a valid name */
|
||||
|
||||
if(!check_id(name)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue