#define _POSIX_C_SOURCE 200112L #include "cess_compat.h" #include "cesspool.h" #include #include #ifdef _WIN32 #include #include #include #else #include #include #include #endif #include #include #include #include #ifndef _WIN32 #include #endif #ifdef CESSPOOL_HAVE_SODIUM #include #endif #define CESSPOOL_PACKET_BYTES 72u #ifndef CESSPOOL_CONFIG_DIRECTORY #define CESSPOOL_CONFIG_DIRECTORY "libcess" #endif #define CESSPOOL_MAX_RENDEZVOUS_PEERS 34u #define CESSPOOL_PEER_TTL_MS 600000u #define CESSPOOL_BOOTSTRAP_PROBE_MS 5000u #define CESSPOOL_BOOTSTRAP_TIMEOUT_MS 15000u /* Cesspool owns its compact protobuf-style varint framing; it has no SLEEP * dependency even though Dat also uses the same wire primitive. */ static int cess_varint_encode(uint64_t value, uint8_t *out, size_t capacity, size_t *written) { size_t n = 0; if (!out) { errno = EINVAL; return -1; } do { if (n >= capacity) { errno = ENOSPC; return -1; } out[n] = (uint8_t)(value & 0x7fu); value >>= 7; if (value) out[n] |= 0x80u; n++; } while (value); if (written) *written = n; return 0; } static int cess_varint_decode(const uint8_t *in, size_t length, uint64_t *value, size_t *used) { uint64_t result = 0; size_t i; if (!in || !value) { errno = EINVAL; return -1; } for (i = 0; i < length && i < 10; i++) { uint8_t byte = in[i]; if (i == 9 && byte > 1) { errno = EOVERFLOW; return -1; } result |= (uint64_t)(byte & 0x7fu) << (i * 7); if (!(byte & 0x80u)) { *value = result; if (used) *used = i + 1; return 0; } } errno = length < 10 ? EAGAIN : EOVERFLOW; return -1; } static uint64_t monotonic_ms(void) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) return 0; return (uint64_t)now.tv_sec * 1000u + (uint64_t)now.tv_nsec / 1000000u; } static int pb_var(uint8_t tag,uint64_t value,uint8_t*out,size_t cap,size_t*off){size_t n;if(*off>=cap){errno=ENOSPC;return-1;}out[(*off)++]=tag;if(cess_varint_encode(value,out+*off,cap-*off,&n))return-1;*off+=n;return 0;} static int pb_bytes(uint8_t tag,const uint8_t*data,size_t len,uint8_t*out,size_t cap,size_t*off){size_t n;if(*off>=cap){errno=ENOSPC;return-1;}out[(*off)++]=tag;if(cess_varint_encode(len,out+*off,cap-*off,&n))return-1;*off+=n;if(len>cap-*off){errno=ENOSPC;return-1;}memcpy(out+*off,data,len);*off+=len;return 0;} int cesspool_request_encode(const cesspool_request_t*r,uint8_t*out,size_t cap,size_t*written){size_t off=0;if(!r||!out){errno=EINVAL;return-1;}if(pb_var(0x08,r->type,out,cap,&off))return-1;if(r->port&&pb_var(0x10,r->port,out,cap,&off))return-1;if(r->local_address_len&&pb_bytes(0x1a,r->local_address,r->local_address_len,out,cap,&off))return-1;if(written)*written=off;return 0;} int cesspool_request_decode(cesspool_request_t*r,const uint8_t*in,size_t len){size_t off=0,n;uint64_t v;if(!r||!in){errno=EINVAL;return-1;}memset(r,0,sizeof(*r));while(offtype=(uint32_t)v;else r->port=(uint16_t)v;}else if(tag==0x1a){if(cess_varint_decode(in+off,len-off,&v,&n))return-1;off+=n;if(v>sizeof(r->local_address)||v>len-off){errno=EPROTO;return-1;}memcpy(r->local_address,in+off,(size_t)v);r->local_address_len=(size_t)v;off+=(size_t)v;}else{errno=EPROTO;return-1;}}return 0;} int cesspool_response_encode(const cesspool_response_t *r, uint8_t *out, size_t cap, size_t *written) { size_t off = 0; if (!r || !out) { errno = EINVAL; return -1; } if (r->peers_len && pb_bytes(0x0a, r->peers, r->peers_len, out, cap, &off)) return -1; if (r->local_peers_len && pb_bytes(0x12, r->local_peers, r->local_peers_len, out, cap, &off)) return -1; if (r->peer_info_len && pb_bytes(0x1a, r->peer_info, r->peer_info_len, out, cap, &off)) return -1; if (written) *written = off; return 0; } int cesspool_response_decode(cesspool_response_t *r, const uint8_t *in, size_t len) { size_t off = 0, n; uint64_t v; if (!r || !in) { errno = EINVAL; return -1; } memset(r, 0, sizeof(*r)); while (off < len) { uint8_t tag = in[off++], *dst; size_t *dstlen, capacity; if (cess_varint_decode(in + off, len - off, &v, &n)) return -1; off += n; if (v > len - off) { errno = EPROTO; return -1; } if (tag == 0x0a) { dst = r->peers; dstlen = &r->peers_len; capacity = sizeof(r->peers); } else if (tag == 0x12) { dst = r->local_peers; dstlen = &r->local_peers_len; capacity = sizeof(r->local_peers); } else if (tag == 0x1a) { dst = r->peer_info; dstlen = &r->peer_info_len; capacity = sizeof(r->peer_info); } else { off += (size_t)v; continue; } if (v > capacity) { errno = EOVERFLOW; return -1; } memcpy(dst, in + off, (size_t)v); *dstlen = (size_t)v; off += (size_t)v; } return 0; } int cesspool_process_response(cesspool_t*p,const cesspool_response_t*r,const uint8_t prefix[2]){size_t off;cesspool_peer_t peer;struct sockaddr_in*address;if(!p||!r){errno=EINVAL;return-1;}if(r->peers_len%6||r->local_peers_len%4){errno=EPROTO;return-1;}for(off=0;offpeers_len;off+=6){memset(&peer,0,sizeof(peer));address=(struct sockaddr_in*)&peer.address.addr;address->sin_family=AF_INET;memcpy(&address->sin_addr,r->peers+off,4);memcpy(&address->sin_port,r->peers+off+4,2);peer.address.addrlen=sizeof(*address);memcpy(peer.public_key,r->peers+off,6);cesspool_add_peer(p,&peer);}if(prefix)for(off=0;offlocal_peers_len;off+=4){memset(&peer,0,sizeof(peer));address=(struct sockaddr_in*)&peer.address.addr;address->sin_family=AF_INET;((uint8_t*)&address->sin_addr)[0]=prefix[0];((uint8_t*)&address->sin_addr)[1]=prefix[1];((uint8_t*)&address->sin_addr)[2]=r->local_peers[off];((uint8_t*)&address->sin_addr)[3]=r->local_peers[off+1];memcpy(&address->sin_port,r->local_peers+off+2,2);peer.address.addrlen=sizeof(*address);peer.local=1;memcpy(peer.public_key,&address->sin_addr,4);memcpy(peer.public_key+4,&address->sin_port,2);cesspool_add_peer(p,&peer);}return 0;} int cesspool_discovery_key(const uint8_t key[32], uint8_t topic[32]) { if (!key || !topic) { errno = EINVAL; return -1; } #ifdef CESSPOOL_HAVE_SODIUM /* The root NETWORK_PROTOCOL token selects the discovery namespace. * With "hypercore", this matches legacy Dat's discoveryKey derivation. */ return crypto_generichash(topic, 32, (const unsigned char *)CESSPOOL_NETWORK_PROTOCOL, strlen(CESSPOOL_NETWORK_PROTOCOL), key, 32); #else memcpy(topic, key, 32); return 0; #endif } int cesspool_encode(const cesspool_message_t *m, uint8_t *out, size_t capacity, size_t *written) { size_t total = CESSPOOL_PACKET_BYTES; if (!m || !out) { errno = EINVAL; return -1; } if (m->type > CESSPOOL_HOLEPUNCH) { errno = EINVAL; return -1; } if (m->type == CESSPOOL_REPLY) { if (capacity < CESSPOOL_PACKET_BYTES + 2 || m->payload_len > UINT16_MAX || m->payload_len > capacity - CESSPOOL_PACKET_BYTES - 2) { errno = ENOSPC; return -1; } total += 2 + m->payload_len; } if (capacity < total) { errno = ENOSPC; return -1; } memcpy(out, "CSP0", 4); out[4] = m->type; out[5] = m->type == CESSPOOL_REPLY ? 1 : 0; out[6] = (uint8_t)(m->port >> 8); out[7] = (uint8_t)m->port; memcpy(out + 8, m->topic, 32); memcpy(out + 40, m->peer_id, 32); if (m->type == CESSPOOL_REPLY) { out[72] = (uint8_t)(m->payload_len >> 8); out[73] = (uint8_t)m->payload_len; memcpy(out + 74, m->payload, m->payload_len); } if (written) *written = total; return 0; } int cesspool_decode(cesspool_message_t *m, const uint8_t *in, size_t len) { if (!m || !in) { errno = EINVAL; return -1; } if (len < CESSPOOL_PACKET_BYTES || memcmp(in, "CSP0", 4) || in[4] > CESSPOOL_HOLEPUNCH) { errno = EPROTO; return -1; } memset(m, 0, sizeof(*m)); m->type = in[4]; m->port = ((uint16_t)in[6] << 8) | in[7]; memcpy(m->topic, in + 8, 32); memcpy(m->peer_id, in + 40, 32); if (m->type == CESSPOOL_REPLY) { if (in[5] != 1 || len < CESSPOOL_PACKET_BYTES + 2) { errno = EPROTO; return -1; } m->payload_len = ((size_t)in[72] << 8) | in[73]; if (m->payload_len > sizeof(m->payload) || len != CESSPOOL_PACKET_BYTES + 2 + m->payload_len) { errno = EPROTO; return -1; } memcpy(m->payload, in + 74, m->payload_len); } else if (in[5] != 0 || len != CESSPOOL_PACKET_BYTES) { errno = EPROTO; return -1; } return 0; } static int send_message(cesspool_t *pool,const cess_peer_t *peer,uint8_t type,const uint8_t topic[32],uint16_t port) { cesspool_message_t message;uint8_t packet[CESSNET_MAX_PACKET];size_t len; memset(&message,0,sizeof(message));message.type=type;message.port=port;if(topic)memcpy(message.topic,topic,32);memcpy(message.peer_id,pool->net.peerid,32); if(cesspool_encode(&message,packet,sizeof(packet),&len))return-1; return cessnet_send(&pool->net,peer,packet,len); } static int send_reply(cesspool_t *pool, const cess_peer_t *peer, const uint8_t topic[32], const cesspool_response_t *response) { cesspool_message_t message; uint8_t packet[CESSNET_MAX_PACKET]; size_t payload_len, packet_len; memset(&message, 0, sizeof(message)); message.type = CESSPOOL_REPLY; memcpy(message.topic, topic, CESSPOOL_TOPIC_BYTES); memcpy(message.peer_id, pool->net.peerid, CESSPOOL_PEER_BYTES); if (cesspool_response_encode(response, message.payload, sizeof(message.payload), &payload_len)) return -1; message.payload_len = payload_len; if (cesspool_encode(&message, packet, sizeof(packet), &packet_len)) return -1; return cessnet_send(&pool->net, peer, packet, packet_len); } static int send_bootstrap(cesspool_t *pool,uint8_t type,const uint8_t topic[32],uint16_t port) { static const uint16_t lan_ports[] = {CESSNET_PORT,3000,3002,3004,2001,2003,2005}; size_t i;int sent=0,lan_sent=0; if (!pool) { errno = EINVAL; return -1; } for(i=0;ibootstrap_count;i++) if(send_message(pool,&pool->bootstrap_nodes[i],type,topic,port)==0)sent++; /* Always keep zero-configuration LAN discovery active, even when a * rendezvous node is configured. Routers do not forward these broadcasts; * this complements Internet discovery and lets same-LAN peers connect * without requiring them to share bootstrap configuration. */ for (i = 0; i < sizeof(lan_ports) / sizeof(lan_ports[0]); i++) { cess_peer_t target; struct sockaddr_in *address; memset(&target, 0, sizeof(target)); address = (struct sockaddr_in *)&target.addr; address->sin_family = AF_INET; address->sin_addr.s_addr = htonl(INADDR_BROADCAST); address->sin_port = htons(lan_ports[i]); target.addrlen = sizeof(*address); if (send_message(pool, &target, type, topic, port) == 0) lan_sent++; } if (pool->bootstrap_count) return (sent || lan_sent) ? 0 : -1; return 0; /* LAN broadcasts are best-effort, never a join failure. */ } static int has_topic(const cesspool_t *pool,const uint8_t topic[32],int announcing) { size_t i; for(i=0;itopic_count;i++)if(!memcmp(pool->topics[i].key,topic,32)&&(!announcing||pool->topics[i].announcing))return 1; return 0; } static int same_peer_address(const cess_peer_t *a, const cess_peer_t *b) { const struct sockaddr_in *aa, *bb; if (!a || !b || a->addrlen < (socklen_t)sizeof(struct sockaddr_in) || b->addrlen < (socklen_t)sizeof(struct sockaddr_in)) return 0; aa = (const struct sockaddr_in *)&a->addr; bb = (const struct sockaddr_in *)&b->addr; return aa->sin_family == AF_INET && bb->sin_family == AF_INET && aa->sin_addr.s_addr == bb->sin_addr.s_addr && aa->sin_port == bb->sin_port; } static int announcement_add(cesspool_t *pool, const uint8_t topic[32], const uint8_t peer_id[32], const cess_peer_t *address, uint64_t now) { size_t i; for (i = 0; i < pool->announcement_count; i++) { cesspool_announcement_t *entry = &pool->announcements[i]; if (!memcmp(entry->topic, topic, 32) && !memcmp(entry->peer_id, peer_id, 32)) { entry->address = *address; entry->last_seen = now; return 0; } } if (pool->announcement_count >= CESSPOOL_MAX_ANNOUNCEMENTS) { errno = ENOSPC; return -1; } { cesspool_announcement_t *entry = &pool->announcements[pool->announcement_count++]; memcpy(entry->topic, topic, 32); memcpy(entry->peer_id, peer_id, 32); entry->address = *address; entry->last_seen = now; } return 0; } static void announcement_remove(cesspool_t *pool, const uint8_t topic[32], const uint8_t peer_id[32]) { size_t i = 0; while (i < pool->announcement_count) { cesspool_announcement_t *entry = &pool->announcements[i]; if (!memcmp(entry->topic, topic, 32) && !memcmp(entry->peer_id, peer_id, 32)) { pool->announcements[i] = pool->announcements[--pool->announcement_count]; continue; } i++; } } static int peer_has_announcement(const cesspool_t *pool, const uint8_t peer_id[32]) { size_t i; for (i = 0; i < pool->announcement_count; i++) if (!memcmp(pool->announcements[i].peer_id, peer_id, 32)) return 1; return 0; } static int send_rendezvous_reply(cesspool_t *pool, cess_peer_t *requester, const uint8_t topic[32]) { cesspool_response_t response; size_t i; memset(&response, 0, sizeof(response)); for (i = 0; i < pool->announcement_count; i++) { const cesspool_announcement_t *peer = &pool->announcements[i]; const struct sockaddr_in *address; uint8_t *record; if (memcmp(peer->topic, topic, 32) || same_peer_address(&peer->address, requester) || peer->address.addrlen < (socklen_t)sizeof(struct sockaddr_in)) continue; address = (const struct sockaddr_in *)&peer->address.addr; if (address->sin_family != AF_INET || response.peer_info_len / 38 >= CESSPOOL_MAX_RENDEZVOUS_PEERS || response.peer_info_len + 38 > sizeof(response.peer_info)) continue; record = response.peer_info + response.peer_info_len; memcpy(record, &address->sin_addr, 4); memcpy(record + 4, &address->sin_port, 2); memcpy(record + 6, peer->peer_id, 32); response.peer_info_len += 38; } return send_reply(pool, requester, topic, &response); } static int receive_rendezvous_reply(cesspool_t *pool, const cesspool_message_t *message) { cesspool_response_t response; size_t i; if (cesspool_response_decode(&response, message->payload, message->payload_len)) return -1; if (response.peer_info_len % 38) { errno = EPROTO; return -1; } for (i = 0; i < response.peer_info_len; i += 38) { const uint8_t *record = response.peer_info + i; cesspool_peer_t peer; struct sockaddr_in *address; memset(&peer, 0, sizeof(peer)); address = (struct sockaddr_in *)&peer.address.addr; address->sin_family = AF_INET; memcpy(&address->sin_addr, record, 4); memcpy(&address->sin_port, record + 4, 2); peer.address.addrlen = sizeof(*address); memcpy(peer.public_key, record + 6, 32); memcpy(peer.discovery_topic, message->topic, 32); peer.discovered = 1; peer.last_seen = monotonic_ms(); if (announcement_add(pool, message->topic, peer.public_key, &peer.address, peer.last_seen)) continue; if (cesspool_add_peer(pool, &peer)) return -1; } return 0; } static void cesspool_on_message(cessnet_t *net,cess_peer_t *source,const uint8_t *packet,size_t length) { cesspool_t *pool=(cesspool_t *)net->context; cesspool_message_t message; cesspool_peer_t peer; if(!pool)return; if(cesspool_decode(&message,packet,length)){ if(pool->on_transport_packet)pool->on_transport_packet(pool,source,packet,length); return; } if(message.type>CESSPOOL_HOLEPUNCH||!memcmp(message.peer_id,net->peerid,32))return; if (message.type == CESSPOOL_REPLY) { size_t i; int from_bootstrap = 0; for (i = 0; i < pool->bootstrap_count; i++) if (same_peer_address(&pool->bootstrap_nodes[i], source)) { from_bootstrap = 1; break; } if (!from_bootstrap) return; /* A malformed datagram is not proof that the configured rendezvous * endpoint is speaking Cesspool, so validate before refreshing online. */ if (receive_rendezvous_reply(pool, &message)) return; pool->bootstrap_last_seen = monotonic_ms(); pool->bootstrap_reachable = 1; return; } if (message.type == CESSPOOL_PONG) { size_t i; for (i = 0; i < pool->bootstrap_count; i++) { if (same_peer_address(&pool->bootstrap_nodes[i], source)) { pool->bootstrap_last_seen = monotonic_ms(); pool->bootstrap_reachable = 1; break; } } return; } memset(&peer,0,sizeof(peer));peer.address=*source;memcpy(peer.public_key,message.peer_id,32); peer.last_seen = monotonic_ms(); if (message.type == CESSPOOL_ANNOUNCE) { peer.discovered = 1; memcpy(peer.discovery_topic, message.topic, sizeof(peer.discovery_topic)); if (announcement_add(pool, message.topic, message.peer_id, source, peer.last_seen)) return; } cesspool_add_peer(pool,&peer); if (message.type == CESSPOOL_UNANNOUNCE) { cesspool_peer_t *known = cesspool_find_peer(pool, message.peer_id); announcement_remove(pool, message.topic, message.peer_id); if (known) known->discovered = peer_has_announcement(pool, message.peer_id); } else if(message.type==CESSPOOL_PING)send_message(pool,source,CESSPOOL_PONG,NULL,pool->port); else if(message.type==CESSPOOL_LOOKUP&&pool->bootstrap_server) (void)send_rendezvous_reply(pool, source, message.topic); else if(message.type==CESSPOOL_LOOKUP&&has_topic(pool,message.topic,1)) send_message(pool,source,CESSPOOL_ANNOUNCE,message.topic,pool->port); else if(message.type==CESSPOOL_HOLEPUNCH&&pool->on_holepunch)pool->on_holepunch(pool,source); } int cesspool_init(cesspool_t *pool,const uint8_t peer_id[32]) { uint8_t empty[32]={0};if(!pool||!peer_id){errno=EINVAL;return-1;}memset(pool,0,sizeof(*pool));if(cessnet_init(&pool->net,empty,peer_id))return-1;pool->net.context=pool;pool->net.on_message=cesspool_on_message;return 0; } int cesspool_add_bootstrap(cesspool_t *pool,const char *host,uint16_t port) { struct addrinfo hints, *resolved = NULL; struct sockaddr_in *address; cess_peer_t *peer; if(!pool||!host||!port||pool->bootstrap_count>=CESSPOOL_MAX_BOOTSTRAP){errno=EINVAL;return-1;} memset(&hints,0,sizeof(hints));hints.ai_family=AF_INET;hints.ai_socktype=SOCK_DGRAM; if(getaddrinfo(host,NULL,&hints,&resolved)!=0||!resolved){errno=EHOSTUNREACH;return-1;} peer=&pool->bootstrap_nodes[pool->bootstrap_count];memset(peer,0,sizeof(*peer)); memcpy(&peer->addr,resolved->ai_addr,resolved->ai_addrlen);peer->addrlen=(socklen_t)resolved->ai_addrlen; address=(struct sockaddr_in*)&peer->addr;address->sin_port=htons(port); freeaddrinfo(resolved); { size_t i; for (i = 0; i < pool->bootstrap_count; i++) if (same_peer_address(&pool->bootstrap_nodes[i], peer)) return 0; } if (pool->bootstrap_count >= CESSPOOL_MAX_BOOTSTRAP) { errno = ENOSPC; return -1; } pool->bootstrap_count++;return 0; } static int add_endpoint_list(cesspool_t *pool, const char *setting) { char list[2048], *item, *next; int added = 0; if (!setting || !*setting) return 0; if (strlen(setting) >= sizeof(list)) { errno = EOVERFLOW; return -1; } strcpy(list, setting); for (item = list; item && *item; item = next) { char *separator, *end; unsigned long port; next = strpbrk(item, ",;\r\n"); if (next) *next++ = '\0'; separator = strrchr(item, ':'); if (!separator || separator == item || !separator[1]) continue; *separator++ = '\0'; port = strtoul(separator, &end, 10); if (*end || port == 0 || port > 65535) continue; if (cesspool_add_bootstrap(pool, item, (uint16_t)port) != 0) return -1; added++; } return added; } static int bootstrap_config_path(char *path, size_t capacity, int make_dirs) { const char *home = getenv("HOME"); const char *config = getenv("XDG_CONFIG_HOME"); char directory[1024], config_directory[1024]; if (!home || !*home) home = getenv("USERPROFILE"); #ifdef _WIN32 if ((!home || !*home) && getenv("HOMEDRIVE") && getenv("HOMEPATH")) { static char combined[1024]; if (snprintf(combined, sizeof(combined), "%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH")) < (int)sizeof(combined)) home = combined; } #endif if (!home || !*home) home = "."; if (!config || !*config) { if (snprintf(config_directory, sizeof(config_directory), "%s/.config", home) >= (int)sizeof(config_directory)) { errno = ENAMETOOLONG; return -1; } config = config_directory; } if (snprintf(directory, sizeof(directory), "%s/%s", config, CESSPOOL_CONFIG_DIRECTORY) >= (int)sizeof(directory) || snprintf(path, capacity, "%s/bootstrap", directory) >= (int)capacity) { errno = ENAMETOOLONG; return -1; } if (make_dirs) { char parent[1024]; if (snprintf(parent, sizeof(parent), "%s", config) >= (int)sizeof(parent)) { errno = ENAMETOOLONG; return -1; } if (mkdir(parent, 0700) && errno != EEXIST) return -1; if (mkdir(directory, 0700) && errno != EEXIST) return -1; } return 0; } int cesspool_configure_bootstraps(cesspool_t *pool) { char path[1200], line[512]; FILE *file; int count = 0, loaded; if (!pool) { errno = EINVAL; return -1; } loaded = add_endpoint_list(pool, getenv("CESSPOOL_BOOTSTRAP")); if (loaded < 0) return -1; count += loaded; loaded = add_endpoint_list(pool, getenv("DDAT_BOOTSTRAP")); if (loaded < 0) return -1; count += loaded; loaded = add_endpoint_list(pool, getenv("CABLE_BOOTSTRAP")); if (loaded < 0) return -1; count += loaded; if (bootstrap_config_path(path, sizeof(path), 0)) return count; file = fopen(path, "r"); if (!file) return errno == ENOENT ? count : -1; while (fgets(line, sizeof(line), file)) { line[strcspn(line, "\r\n#")] = '\0'; loaded = add_endpoint_list(pool, line); if (loaded < 0) { fclose(file); return -1; } count += loaded; } if (ferror(file)) { fclose(file); return -1; } fclose(file); return count; } int cesspool_bootstrap_config_add(const char *endpoint) { char path[1200], copy[512], *separator, *end; unsigned long port; FILE *file; if (!endpoint || strlen(endpoint) >= sizeof(copy)) { errno = EINVAL; return -1; } strcpy(copy, endpoint); separator = strrchr(copy, ':'); if (!separator || separator == copy || !separator[1]) { errno = EINVAL; return -1; } *separator++ = '\0'; port = strtoul(separator, &end, 10); if (*end || port == 0 || port > 65535) { errno = EINVAL; return -1; } if (bootstrap_config_path(path, sizeof(path), 1)) return -1; file = fopen(path, "a"); if (!file) return -1; { int failed = fprintf(file, "%s:%lu\n", copy, port) < 0; if (fclose(file)) failed = 1; return failed ? -1 : 0; } } int cesspool_bootstrap_config_set(const char *endpoint) { char path[1200], temporary[1240], copy[512], *separator, *end; unsigned long port; FILE *file; int n, failed; if (!endpoint || strlen(endpoint) >= sizeof(copy)) { errno = EINVAL; return -1; } strcpy(copy, endpoint); separator = strrchr(copy, ':'); if (!separator || separator == copy || !separator[1]) { errno = EINVAL; return -1; } *separator++ = '\0'; port = strtoul(separator, &end, 10); if (*end || port == 0 || port > 65535) { errno = EINVAL; return -1; } if (bootstrap_config_path(path, sizeof(path), 1)) return -1; n = snprintf(temporary, sizeof(temporary), "%s.tmp", path); if (n < 0 || (size_t)n >= sizeof(temporary)) { errno = ENAMETOOLONG; return -1; } file = fopen(temporary, "w"); if (!file) return -1; failed = fprintf(file, "%s:%lu\n", copy, port) < 0; if (fclose(file)) failed = 1; if (failed || chmod(temporary, S_IRUSR | S_IWUSR)) { int saved = errno; (void)remove(temporary); errno = saved; return -1; } #ifdef _WIN32 if (!MoveFileExA(temporary, path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { (void)remove(temporary); errno = EIO; return -1; } #else if (rename(temporary, path)) { int saved = errno; (void)remove(temporary); errno = saved; return -1; } #endif return 0; } int cesspool_bootstrap_config_get(char *endpoint, size_t capacity) { char path[1200], line[512]; FILE *file; size_t length; if (!endpoint || !capacity) { errno = EINVAL; return -1; } endpoint[0] = '\0'; if (bootstrap_config_path(path, sizeof(path), 0)) return -1; file = fopen(path, "r"); if (!file) return -1; while (fgets(line, sizeof(line), file)) { line[strcspn(line, "\r\n#")] = '\0'; if (!line[0]) continue; length = strlen(line); if (length >= capacity) { fclose(file); errno = ENOSPC; return -1; } memcpy(endpoint, line, length + 1); fclose(file); return 0; } fclose(file); errno = ENOENT; return -1; } int cesspool_bootstrap_config_list(char *contents, size_t capacity) { char path[1200]; FILE *file; size_t length; if (!contents || !capacity) { errno = EINVAL; return -1; } contents[0] = '\0'; if (bootstrap_config_path(path, sizeof(path), 0)) return -1; file = fopen(path, "r"); if (!file) return -1; length = fread(contents, 1, capacity - 1, file); if (ferror(file)) { int saved = errno; fclose(file); errno = saved; return -1; } if (!feof(file)) { fclose(file); errno = ENOSPC; return -1; } contents[length] = '\0'; return fclose(file) == 0 ? 0 : -1; } int cesspool_bootstrap(cesspool_t *pool){if(!pool||!pool->bootstrap_count){errno=EINVAL;return-1;}return send_bootstrap(pool,CESSPOOL_PING,NULL,pool->port);} int cesspool_listen(cesspool_t *pool,const char *host,uint16_t port){ static const uint16_t defaults[]={CESSNET_PORT,3000,3002,3004,2001,2003,2005}; struct sockaddr_in bound;socklen_t length=sizeof(bound);size_t i,count; if(!pool){errno=EINVAL;return-1;} count=port?1:sizeof(defaults)/sizeof(defaults[0]); for(i=0;inet,host,candidate)==0)break; if(port||errno!=EADDRINUSE)return-1; } if(i==count){errno=EADDRINUSE;return-1;} if(getsockname(pool->net.sock,(struct sockaddr*)&bound,&length)){cessnet_close(&pool->net);return-1;} pool->port=ntohs(bound.sin_port);pool->ready=1;return 0; } int cesspool_set_bootstrap_server(cesspool_t *pool, int enabled) { if (!pool || !pool->ready) { errno = EINVAL; return -1; } pool->bootstrap_server = !!enabled; return 0; } int cesspool_announce(cesspool_t *pool,const uint8_t topic[32],uint16_t port){if(!pool||!topic){errno=EINVAL;return-1;}return send_bootstrap(pool,CESSPOOL_ANNOUNCE,topic,port?port:pool->port);} int cesspool_lookup(cesspool_t *pool,const uint8_t topic[32]){if(!pool||!topic){errno=EINVAL;return-1;}return send_bootstrap(pool,CESSPOOL_LOOKUP,topic,0);} int cesspool_unannounce(cesspool_t *pool,const uint8_t topic[32],uint16_t port){if(!pool||!topic){errno=EINVAL;return-1;}return send_bootstrap(pool,CESSPOOL_UNANNOUNCE,topic,port);} int cesspool_ping(cesspool_t *pool,const cess_peer_t *peer){if(!pool||!peer){errno=EINVAL;return-1;}return send_message(pool,peer,CESSPOOL_PING,NULL,pool->port);} int cesspool_holepunch(cesspool_t *pool,const cess_peer_t *peer){if(!pool||!peer){errno=EINVAL;return-1;}return send_message(pool,peer,CESSPOOL_HOLEPUNCH,NULL,pool->port);} int cesspool_poll(cesspool_t *pool,int timeout_ms){if(!pool){errno=EINVAL;return-1;}return cessnet_poll(&pool->net,timeout_ms);} cesspool_peer_t *cesspool_find_peer(cesspool_t*p,const uint8_t key[32]){size_t i;if(!p||!key)return NULL;for(i=0;ipeer_count;i++)if(!memcmp(p->peers[i].public_key,key,32))return&p->peers[i];return NULL;} int cesspool_add_peer(cesspool_t*p,const cesspool_peer_t*peer){cesspool_peer_t*old;int newly_discovered;if(!p||!peer){errno=EINVAL;return-1;}old=cesspool_find_peer(p,peer->public_key);if(old){newly_discovered=peer->discovered&&(!old->discovered||memcmp(peer->discovery_topic,old->discovery_topic,32));{cesspool_peer_t merged=*peer;if(!peer->discovered&&old->discovered){merged.discovered=1;memcpy(merged.discovery_topic,old->discovery_topic,32);}*old=merged;}if(newly_discovered&&p->on_peer)p->on_peer(p,old);return 0;}if(p->peer_count>=CESSPOOL_MAX_PEERS){errno=ENOSPC;return-1;}p->peers[p->peer_count++]=*peer;if(peer->discovered&&p->on_peer)p->on_peer(p,peer);return 0;} int cesspool_remove_peer(cesspool_t*p,const uint8_t key[32]){size_t i;if(!p||!key){errno=EINVAL;return-1;}for(i=0;ipeer_count;i++)if(!memcmp(p->peers[i].public_key,key,32)){p->peers[i]=p->peers[--p->peer_count];return 0;}errno=ENOENT;return-1;} int cesspool_join(cesspool_t *p, const uint8_t topic[32], uint16_t port, int announce, int lookup) { cesspool_topic_t entry; int announced_now = 0; if (!p || !topic || (!announce && !lookup)) { errno = EINVAL; return -1; } if (p->topic_count >= CESSPOOL_MAX_TOPICS) { errno = ENOSPC; return -1; } memset(&entry, 0, sizeof(entry)); memcpy(entry.key, topic, CESSPOOL_TOPIC_BYTES); entry.port = port ? port : p->port; entry.announcing = !!announce; entry.looking_up = !!lookup; /* No configured rendezvous falls back to best-effort LAN broadcast. */ if (announce) { if (cesspool_announce(p, topic, entry.port)) return -1; announced_now = 1; } if (lookup && cesspool_lookup(p, topic)) { if (announced_now) (void)cesspool_unannounce(p, topic, entry.port); return -1; } p->topics[p->topic_count++] = entry; return 0; } int cesspool_leave(cesspool_t*p,const uint8_t topic[32]){size_t i;if(!p||!topic){errno=EINVAL;return-1;}for(i=0;itopic_count;i++)if(!memcmp(p->topics[i].key,topic,32)){if(p->topics[i].announcing)cesspool_unannounce(p,topic,p->topics[i].port);p->topics[i]=p->topics[--p->topic_count];return 0;}errno=ENOENT;return-1;} int cesspool_tick(cesspool_t *p, uint64_t now) { size_t i = 0; uint64_t wall_now; if (!p) { errno = EINVAL; return -1; } for (i = 0; i < p->topic_count; i++) if (now >= p->topics[i].refresh_at) { if (p->topics[i].announcing) (void)cesspool_announce(p, p->topics[i].key, p->topics[i].port); if (p->topics[i].looking_up) (void)cesspool_lookup(p, p->topics[i].key); p->topics[i].refresh_at = now + 240000u; } if (!p->bootstrap_server && p->bootstrap_count && now >= p->bootstrap_probe_at) { int probed = 0; for (i = 0; i < p->topic_count; i++) if (p->topics[i].looking_up) { (void)cesspool_lookup(p, p->topics[i].key); probed = 1; } if (!probed) (void)cesspool_bootstrap(p); p->bootstrap_probe_at = now + CESSPOOL_BOOTSTRAP_PROBE_MS; } for (i = 0; i < CESSPOOL_MAX_TRANSACTIONS; i++) if (p->transactions[i].active && now >= p->transactions[i].expires_at) p->transactions[i].active = 0; i = 0; wall_now = monotonic_ms(); if (p->bootstrap_count && p->bootstrap_last_seen && wall_now >= p->bootstrap_last_seen) p->bootstrap_reachable = wall_now - p->bootstrap_last_seen < CESSPOOL_BOOTSTRAP_TIMEOUT_MS; else if (p->bootstrap_count) p->bootstrap_reachable = 0; while (i < p->announcement_count) { cesspool_announcement_t *entry = &p->announcements[i]; if (!entry->last_seen || (wall_now >= entry->last_seen && wall_now - entry->last_seen >= CESSPOOL_PEER_TTL_MS)) { p->announcements[i] = p->announcements[--p->announcement_count]; continue; } i++; } /* Client peer rows are a view of live topic announcements. Without * reconciling this bit after pruning, a peer remains "discovered" forever * even though its rendezvous record has expired. */ for (i = 0; i < p->peer_count; i++) { cesspool_peer_t *peer = &p->peers[i]; size_t announcement; peer->discovered = 0; for (announcement = 0; announcement < p->announcement_count; announcement++) { const cesspool_announcement_t *entry = &p->announcements[announcement]; if (memcmp(peer->public_key, entry->peer_id, CESSPOOL_PEER_BYTES)) continue; peer->discovered = 1; memcpy(peer->discovery_topic, entry->topic, CESSPOOL_TOPIC_BYTES); if (entry->last_seen > peer->last_seen) peer->last_seen = entry->last_seen; break; } } if (p->bootstrap_server) { i = 0; while (i < p->peer_count) { cesspool_peer_t *peer = &p->peers[i]; if (!peer->last_seen || (wall_now >= peer->last_seen && wall_now - peer->last_seen >= CESSPOOL_PEER_TTL_MS)) { p->peers[i] = p->peers[--p->peer_count]; continue; } i++; } } return 0; } static int distance_less(const uint8_t*a,const uint8_t*b,const uint8_t target[32]){size_t i;for(i=0;i<32;i++){uint8_t da=a[i]^target[i],db=b[i]^target[i];if(da!=db)return dapeer_count;i++){if(npeers[i];else if(cap&&distance_less(p->peers[i].public_key,out[n-1]->public_key,target))out[n-1]=&p->peers[i];for(j=n;j>1&&distance_less(out[j-1]->public_key,out[j-2]->public_key,target);j--){cesspool_peer_t*t=out[j-1];out[j-1]=out[j-2];out[j-2]=t;}}return n;} size_t cesspool_topic_peers(cesspool_t *p, const uint8_t topic[32], cesspool_peer_t **out, size_t cap) { size_t i, n = 0; if (!p || !topic || (!out && cap)) return 0; for (i = 0; i < p->announcement_count && n < cap; i++) { cesspool_announcement_t *entry = &p->announcements[i]; cesspool_peer_t *peer; size_t j; if (memcmp(entry->topic, topic, 32)) continue; for (j = 0; j < n; j++) if (!memcmp(out[j]->public_key, entry->peer_id, 32)) break; if (j < n) continue; peer = cesspool_find_peer(p, entry->peer_id); if (peer) out[n++] = peer; } return n; } size_t cesspool_announced_peer_count(const cesspool_t *p) { size_t i, j, count = 0; if (!p) return 0; for (i = 0; i < p->announcement_count; i++) { for (j = 0; j < i; j++) if (!memcmp(p->announcements[i].peer_id, p->announcements[j].peer_id, 32)) break; if (j == i) count++; } return count; } size_t cesspool_announced_topic_count(const cesspool_t *p) { size_t i, j, count = 0; if (!p) return 0; for (i = 0; i < p->announcement_count; i++) { for (j = 0; j < i; j++) if (!memcmp(p->announcements[i].topic, p->announcements[j].topic, 32)) break; if (j == i) count++; } return count; } int cesspool_transaction_begin(cesspool_t*p,uint8_t type,const uint8_t target[32],uint64_t expires,uint16_t*out){size_t i,j,attempt;uint16_t candidate;if(!p||!target||!out){errno=EINVAL;return-1;}for(i=0;itransactions[i].active){for(attempt=0;attemptnext_transaction;if(!candidate)candidate=++p->next_transaction;for(j=0;jtransactions[j].active&&p->transactions[j].id==candidate)break;if(j==CESSPOOL_MAX_TRANSACTIONS){cesspool_transaction_t*t=&p->transactions[i];memset(t,0,sizeof(*t));t->active=1;t->type=type;t->id=candidate;t->expires_at=expires;memcpy(t->target,target,32);*out=t->id;return 0;}}errno=ENOSPC;return-1;}errno=ENOSPC;return-1;} int cesspool_transaction_finish(cesspool_t*p,uint16_t id){size_t i;if(!p){errno=EINVAL;return-1;}for(i=0;itransactions[i].active&&p->transactions[i].id==id){p->transactions[i].active=0;return 0;}errno=ENOENT;return-1;} int cesspool_destroy(cesspool_t *pool){if(!pool){errno=EINVAL;return-1;}pool->ready=0;return cessnet_close(&pool->net);}