Skip to content

Commit 6707ebc

Browse files
committed
Add private/backend DNS FQDNs and route SDK via HTTPS over WG tunnel
DNS records for encrypted private API access over WireGuard: - private.<id>.<region>.seip.<domain> → server tunnel IP (client→server HTTPS) - backend.<id>.<region>.seip.<domain> → server backbone IP (server→server mesh) - private.<id>.ep.<domain> → client tunnel IP (registered on join) SDK: after join, stores server_private_fqdn from response and routes all private API calls (tree, IPAM, mesh, relay, certs) through HTTPS to the private FQDN over the WG tunnel. Falls back to public API if tunnel is not available. Server: registers private + backend DNS A records on startup and client private DNS on join. Passes dns + server_private_fqdn to ApiContext for use by request handlers.
1 parent 4e891d9 commit 6707ebc

File tree

4 files changed

+136
-14
lines changed

4 files changed

+136
-14
lines changed

projects/LemonadeNexus/include/LemonadeNexus/Api/IRequestHandler.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace nexus::acme { class AcmeService; }
3333
namespace nexus::network {
3434
class HttpServer;
3535
class DdnsService;
36+
class DnsService;
3637
}
3738
namespace nexus::relay {
3839
class RelayService;
@@ -65,7 +66,9 @@ struct ApiContext {
6566
core::TrustPolicyService& trust_policy;
6667
core::GovernanceService& governance;
6768
wireguard::WireGuardService* wireguard{nullptr};
69+
network::DnsService* dns{nullptr};
6870
std::string server_fqdn;
71+
std::string server_private_fqdn; // private.<id>.<region>.seip.<domain>
6972
std::string server_public_ip;
7073
std::string tunnel_bind_ip;
7174
};

projects/LemonadeNexus/src/Api/TreeApiHandler.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <LemonadeNexus/Crypto/SodiumCryptoService.hpp>
1111
#include <LemonadeNexus/Core/ServerConfig.hpp>
1212
#include <LemonadeNexus/WireGuard/WireGuardService.hpp>
13+
#include <LemonadeNexus/Network/DnsService.hpp>
1314
#include <LemonadeNexus/ACL/Permission.hpp>
1415

1516
#include <spdlog/spdlog.h>
@@ -177,20 +178,35 @@ void TreeApiHandler::do_register_routes(httplib::Server& pub, httplib::Server& p
177178
wg_endpoint = ctx_.server_public_ip + ":" + std::to_string(ctx_.config.udp_port);
178179
}
179180

181+
// Register private DNS for this client: private.<node_id>.ep.<domain> -> tunnel IP
182+
std::string client_tunnel_ip_bare = alloc.base_network;
183+
if (auto slash = client_tunnel_ip_bare.find('/'); slash != std::string::npos) {
184+
client_tunnel_ip_bare = client_tunnel_ip_bare.substr(0, slash);
185+
}
186+
std::string client_private_fqdn;
187+
if (ctx_.dns && !client_tunnel_ip_bare.empty()) {
188+
client_private_fqdn = "private." + node_id + ".ep." + ctx_.config.dns_base_domain;
189+
ctx_.dns->set_record(client_private_fqdn, "A", client_tunnel_ip_bare, 300);
190+
spdlog::info("[Join] registered DNS: {} -> {}", client_private_fqdn, client_tunnel_ip_bare);
191+
}
192+
180193
nlohmann::json resp = {
181194
{"token", auth_result.session_token},
182195
{"node_id", node_id},
183196
{"tunnel_ip", alloc.base_network},
184197
{"tunnel_subnet", "10.64.0.0/10"},
185198
{"server_tunnel_ip", server_tunnel},
199+
{"server_private_fqdn", ctx_.server_private_fqdn},
200+
{"client_private_fqdn", client_private_fqdn},
186201
{"private_api_port", !ctx_.tunnel_bind_ip.empty()
187202
? ctx_.config.private_http_port
188203
: ctx_.config.http_port},
189204
{"wg_server_pubkey", wg_server_pubkey},
190205
{"wg_endpoint", wg_endpoint},
191206
{"dns_servers", nlohmann::json::array({server_tunnel})},
192207
};
193-
spdlog::info("[Join] node={} tunnel_ip={} wg_endpoint={}", node_id, alloc.base_network, wg_endpoint);
208+
spdlog::info("[Join] node={} tunnel_ip={} wg_endpoint={} private_fqdn={}",
209+
node_id, alloc.base_network, wg_endpoint, client_private_fqdn);
194210
json_response(res, resp);
195211
});
196212

projects/LemonadeNexus/src/main.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,12 +432,17 @@ int main(int argc, char* argv[]) {
432432

433433
// Publish SEIP records: <id>.<region>.seip.<domain> for geo-aware discovery
434434
dns.set_server_region(config.region);
435+
std::string server_private_fqdn;
435436
{
436437
auto seip_id = nexus::core::resolve_server_node_id(storage);
437438
if (!seip_id.empty() && !config.region.empty() && !server_public_ip.empty()) {
438439
dns.publish_seip_records(seip_id, config.region, server_public_ip);
439440
spdlog::info("SEIP: published {}.{}.seip.{} -> {}",
440441
seip_id, config.region, config.dns_base_domain, server_public_ip);
442+
443+
// Register private FQDN for HTTPS over WG tunnel
444+
server_private_fqdn = "private." + seip_id + "." + config.region +
445+
".seip." + config.dns_base_domain;
441446
}
442447
}
443448

@@ -582,6 +587,32 @@ int main(int argc, char* argv[]) {
582587
spdlog::warn("Backbone: skipping (no server node ID or WG key)");
583588
}
584589

590+
// ========================================================================
591+
// Private DNS records: private/backend FQDNs → tunnel/backbone IPs
592+
// ========================================================================
593+
{
594+
auto seip_id = nexus::core::resolve_server_node_id(storage);
595+
if (!seip_id.empty() && !config.region.empty()) {
596+
// private.<id>.<region>.seip.<domain> → server tunnel IP (for client HTTPS)
597+
if (!tunnel_bind_ip.empty()) {
598+
server_private_fqdn = "private." + seip_id + "." + config.region +
599+
".seip." + config.dns_base_domain;
600+
dns.set_record(server_private_fqdn, "A", tunnel_bind_ip, 300);
601+
spdlog::info("DNS: private {} -> {}", server_private_fqdn, tunnel_bind_ip);
602+
}
603+
604+
// backend.<id>.<region>.seip.<domain> → server backbone IP (for server-to-server)
605+
auto slash = backbone_ip.find('/');
606+
auto bb_bare = (slash != std::string::npos) ? backbone_ip.substr(0, slash) : backbone_ip;
607+
if (!bb_bare.empty()) {
608+
auto backend_fqdn = "backend." + seip_id + "." + config.region +
609+
".seip." + config.dns_base_domain;
610+
dns.set_record(backend_fqdn, "A", bb_bare, 300);
611+
spdlog::info("DNS: backend {} -> {}", backend_fqdn, bb_bare);
612+
}
613+
}
614+
}
615+
585616
// ========================================================================
586617
// HTTP Control Plane -- Dual-server architecture
587618
// ========================================================================
@@ -642,7 +673,9 @@ int main(int argc, char* argv[]) {
642673
.trust_policy = trust_policy,
643674
.governance = governance,
644675
.wireguard = &wireguard_service,
676+
.dns = &dns,
645677
.server_fqdn = server_fqdn,
678+
.server_private_fqdn = server_private_fqdn,
646679
.server_public_ip = server_public_ip,
647680
.tunnel_bind_ip = tunnel_bind_ip,
648681
};

projects/LemonadeNexusSDK/src/LemonadeNexusClient.cpp

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ struct LemonadeNexusClient::Impl {
2323
Identity identity;
2424
std::string session_token;
2525
std::string node_id;
26+
std::string server_private_fqdn; // e.g. "private.<id>.<region>.seip.lemonade-nexus.io"
27+
uint16_t private_port{9101};
2628
mutable std::mutex mutex;
2729

2830
// Server pool with health state
@@ -221,6 +223,65 @@ struct LemonadeNexusClient::Impl {
221223
return std::nullopt;
222224
}
223225

226+
// --- Private API (HTTPS over WG tunnel via private FQDN) ---
227+
228+
std::string private_base_url() const {
229+
if (!server_private_fqdn.empty()) {
230+
return "https://" + server_private_fqdn + ":" + std::to_string(private_port);
231+
}
232+
return current_base_url();
233+
}
234+
235+
std::optional<json> private_http_get(const std::string& path, int& status_out) {
236+
if (server_private_fqdn.empty()) {
237+
return http_get(path, status_out);
238+
}
239+
try {
240+
httplib::Client cli(private_base_url());
241+
cli.set_connection_timeout(config.connect_timeout_sec);
242+
cli.set_read_timeout(config.read_timeout_sec);
243+
cli.enable_server_certificate_verification(true);
244+
auto res = cli.Get(path, auth_headers());
245+
if (!res) {
246+
spdlog::debug("[LemonadeNexusClient] private GET {} failed, falling back", path);
247+
return http_get(path, status_out);
248+
}
249+
status_out = res->status;
250+
if (res->status < 200 || res->status >= 300) {
251+
try { return json::parse(res->body); } catch (...) { return std::nullopt; }
252+
}
253+
return json::parse(res->body);
254+
} catch (...) {
255+
return http_get(path, status_out);
256+
}
257+
}
258+
259+
std::optional<json> private_http_post(const std::string& path, const json& body, int& status_out) {
260+
if (server_private_fqdn.empty()) {
261+
return http_post(path, body, status_out);
262+
}
263+
try {
264+
httplib::Client cli(private_base_url());
265+
cli.set_connection_timeout(config.connect_timeout_sec);
266+
cli.set_read_timeout(config.read_timeout_sec);
267+
cli.enable_server_certificate_verification(true);
268+
auto res = cli.Post(path, auth_headers(), body.dump(), "application/json");
269+
if (!res) {
270+
spdlog::debug("[LemonadeNexusClient] private POST {} failed, falling back", path);
271+
return http_post(path, body, status_out);
272+
}
273+
status_out = res->status;
274+
if (res->status < 200 || res->status >= 300) {
275+
try { return json::parse(res->body); } catch (...) {
276+
json err; err["error"] = "HTTP " + std::to_string(res->status); return err;
277+
}
278+
}
279+
return json::parse(res->body);
280+
} catch (...) {
281+
return http_post(path, body, status_out);
282+
}
283+
}
284+
224285
// Discover additional servers via /api/servers
225286
void discover_servers() {
226287
if (has_discovered) return;
@@ -767,7 +828,7 @@ Result<AuthResponse> LemonadeNexusClient::register_passkey_credential(const std:
767828
Result<TreeNode> LemonadeNexusClient::get_tree_node(const std::string& node_id) {
768829
Result<TreeNode> result;
769830
int status = 0;
770-
auto resp = impl_->http_get("/api/tree/node/" + node_id, status);
831+
auto resp = impl_->private_http_get("/api/tree/node/" + node_id, status);
771832
result.http_status = status;
772833

773834
if (!resp) {
@@ -798,7 +859,7 @@ Result<DeltaResult> LemonadeNexusClient::submit_delta(const TreeDelta& delta) {
798859
json body = signed_delta;
799860

800861
int status = 0;
801-
auto resp = impl_->http_post("/api/tree/delta", body, status);
862+
auto resp = impl_->private_http_post("/api/tree/delta", body, status);
802863
result.http_status = status;
803864

804865
if (!resp) {
@@ -830,7 +891,7 @@ Result<DeltaResult> LemonadeNexusClient::create_child_node(const std::string& pa
830891
if (!child.hostname.empty()) body["hostname"] = child.hostname;
831892

832893
int status = 0;
833-
auto resp = impl_->http_post("/api/tree/node", body, status);
894+
auto resp = impl_->private_http_post("/api/tree/node", body, status);
834895
result.http_status = status;
835896

836897
if (!resp) {
@@ -851,7 +912,7 @@ Result<DeltaResult> LemonadeNexusClient::update_node(const std::string& node_id,
851912
Result<DeltaResult> result;
852913

853914
int status = 0;
854-
auto resp = impl_->http_post("/api/tree/node/update/" + node_id, updates, status);
915+
auto resp = impl_->private_http_post("/api/tree/node/update/" + node_id, updates, status);
855916
result.http_status = status;
856917

857918
if (!resp) {
@@ -872,7 +933,7 @@ Result<DeltaResult> LemonadeNexusClient::delete_node(const std::string& node_id)
872933

873934
int status = 0;
874935
json body = json::object(); // empty body
875-
auto resp = impl_->http_post("/api/tree/node/delete/" + node_id, body, status);
936+
auto resp = impl_->private_http_post("/api/tree/node/delete/" + node_id, body, status);
876937
result.http_status = status;
877938

878939
if (!resp) {
@@ -890,7 +951,7 @@ Result<DeltaResult> LemonadeNexusClient::delete_node(const std::string& node_id)
890951
Result<std::vector<TreeNode>> LemonadeNexusClient::get_children(const std::string& parent_id) {
891952
Result<std::vector<TreeNode>> result;
892953
int status = 0;
893-
auto resp = impl_->http_get("/api/tree/children/" + parent_id, status);
954+
auto resp = impl_->private_http_get("/api/tree/children/" + parent_id, status);
894955
result.http_status = status;
895956

896957
if (!resp) {
@@ -930,7 +991,7 @@ Result<AllocationResponse> LemonadeNexusClient::allocate_ip(const AllocationRequ
930991
}
931992

932993
int status = 0;
933-
auto resp = impl_->http_post("/api/ipam/allocate", body, status);
994+
auto resp = impl_->private_http_post("/api/ipam/allocate", body, status);
934995
result.http_status = status;
935996

936997
if (!resp) {
@@ -964,7 +1025,7 @@ Result<std::vector<RelayNodeInfo>> LemonadeNexusClient::list_relays() {
9641025
Result<std::vector<RelayNodeInfo>> result;
9651026

9661027
int status = 0;
967-
auto resp = impl_->http_get("/api/relay/list", status);
1028+
auto resp = impl_->private_http_get("/api/relay/list", status);
9681029
result.http_status = status;
9691030

9701031
if (!resp) {
@@ -1001,7 +1062,7 @@ Result<RelayTicket> LemonadeNexusClient::request_relay_ticket(const std::string&
10011062
body["relay_id"] = relay_id;
10021063

10031064
int status = 0;
1004-
auto resp = impl_->http_post("/api/relay/ticket", body, status);
1065+
auto resp = impl_->private_http_post("/api/relay/ticket", body, status);
10051066
result.http_status = status;
10061067

10071068
if (!resp) {
@@ -1032,7 +1093,7 @@ Result<RelayRegisterResult> LemonadeNexusClient::register_relay(const RelayRegis
10321093
body["supports_relay"] = reg.supports_relay;
10331094

10341095
int status = 0;
1035-
auto resp = impl_->http_post("/api/relay/register", body, status);
1096+
auto resp = impl_->private_http_post("/api/relay/register", body, status);
10361097
result.http_status = status;
10371098

10381099
if (!resp) {
@@ -1058,7 +1119,7 @@ Result<CertStatus> LemonadeNexusClient::get_cert_status(const std::string& domai
10581119
Result<CertStatus> result;
10591120

10601121
int status = 0;
1061-
auto resp = impl_->http_get("/api/certs/" + domain, status);
1122+
auto resp = impl_->private_http_get("/api/certs/" + domain, status);
10621123
result.http_status = status;
10631124

10641125
if (!resp) {
@@ -1340,6 +1401,15 @@ Result<JoinResult> LemonadeNexusClient::join_network(const std::string& username
13401401
{
13411402
std::lock_guard lock(impl_->mutex);
13421403
impl_->node_id = node_id;
1404+
// Store server private FQDN for HTTPS over WG tunnel
1405+
auto srv_priv_fqdn = resp->value("server_private_fqdn", std::string{});
1406+
if (!srv_priv_fqdn.empty()) {
1407+
impl_->server_private_fqdn = srv_priv_fqdn;
1408+
impl_->private_port = static_cast<uint16_t>(
1409+
resp->value("private_api_port", 9101));
1410+
spdlog::info("[LemonadeNexusClient] private API via HTTPS: {}:{}",
1411+
srv_priv_fqdn, impl_->private_port);
1412+
}
13431413
}
13441414

13451415
// Step 4: Configure the WireGuard tunnel (bring up if we have a tunnel IP)
@@ -1769,7 +1839,7 @@ void LemonadeNexusClient::set_mesh_callback(MeshStateCallback cb) {
17691839

17701840
Result<std::vector<MeshPeer>> LemonadeNexusClient::fetch_mesh_peers(const std::string& nid) {
17711841
int status = 0;
1772-
auto resp = impl_->http_get("/api/mesh/peers/" + nid, status);
1842+
auto resp = impl_->private_http_get("/api/mesh/peers/" + nid, status);
17731843
if (!resp) {
17741844
return {false, {}, status, "mesh peers request failed"};
17751845
}
@@ -1834,7 +1904,7 @@ StatusResult LemonadeNexusClient::mesh_heartbeat(const std::string& nid,
18341904
body["endpoint"] = endpoint;
18351905

18361906
int status = 0;
1837-
auto resp = impl_->http_post("/api/mesh/heartbeat", body, status);
1907+
auto resp = impl_->private_http_post("/api/mesh/heartbeat", body, status);
18381908
if (!resp) {
18391909
return {false, {}, status, "heartbeat request failed"};
18401910
}

0 commit comments

Comments
 (0)