nd,ui: add a new facility to be able to change node name
the "nodename" encompasses lnd alias and OS hostname. while the former may be seen by lightning node peers as a node name, the latter is how the device is seen on a local network such as WiFi. upon receiving a comm message set_nodename, nd sets both lightning node alias and hostname to that new name while applying restrictions such as RFC 1123 for hostnames. the lightning alias is written to lnd config file, regenerated and persistent, after which the lnd daemon is restarted to pick up the changes. network host name is changed by writing the name to /etc/hostname and issuing "hostname <newname>" shell command. while persisting operations are atomic, the whole sequence isn't. in the latter case an inconsistency can be eliminated by sending a set_nodename msg again. the nd daemon also includes the OS hostname in the settings message when sending it to ngui.master v0.7.0
parent
836f196a44
commit
a080e1ac79
@ -0,0 +1,28 @@
|
||||
//! operating system related helper functions.
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
|
||||
const types = @import("types.zig");
|
||||
const sysimpl = @import("sys/sysimpl.zig");
|
||||
|
||||
pub const Service = @import("sys/Service.zig");
|
||||
|
||||
pub usingnamespace if (builtin.is_test) struct {
|
||||
// stubs, mocks and overrides for testing.
|
||||
|
||||
pub fn hostname(allocator: std.mem.Allocator) ![]const u8 {
|
||||
return allocator.dupe(u8, "testhost");
|
||||
}
|
||||
|
||||
pub fn setHostname(allocator: std.mem.Allocator, name: []const u8) !void {
|
||||
_ = allocator;
|
||||
_ = name;
|
||||
}
|
||||
} else sysimpl; // real implementation for production code.
|
||||
|
||||
test {
|
||||
_ = @import("sys/Service.zig");
|
||||
_ = @import("sys/sysimpl.zig");
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
//! real implementation of the sys module for production code.
|
||||
|
||||
const std = @import("std");
|
||||
const types = @import("../types.zig");
|
||||
|
||||
/// caller owns memory; must dealloc using `allocator`.
|
||||
pub fn hostname(allocator: std.mem.Allocator) ![]const u8 {
|
||||
var buf: [std.os.HOST_NAME_MAX]u8 = undefined;
|
||||
const name = try std.os.gethostname(&buf);
|
||||
return allocator.dupe(u8, name);
|
||||
}
|
||||
|
||||
/// a variable for tests; must not mutate at runtime otherwise.
|
||||
var hostname_filepath: []const u8 = "/etc/hostname";
|
||||
|
||||
/// removes all non-alphanumeric ascii and utf8 codepoints when setting hostname,
|
||||
/// as well as leading digits.
|
||||
pub fn setHostname(allocator: std.mem.Allocator, name: []const u8) !void {
|
||||
// sanitize the new input.
|
||||
var sanitized = try std.ArrayList(u8).initCapacity(allocator, name.len);
|
||||
defer sanitized.deinit();
|
||||
var it = (try std.unicode.Utf8View.init(name)).iterator();
|
||||
while (it.nextCodepointSlice()) |s| {
|
||||
if (s.len != 1) continue;
|
||||
switch (s[0]) {
|
||||
'A'...'Z', 'a'...'z' => |c| try sanitized.append(c),
|
||||
'0'...'9' => |c| {
|
||||
if (sanitized.items.len == 0) {
|
||||
// ignore leading digits
|
||||
continue;
|
||||
}
|
||||
try sanitized.append(c);
|
||||
},
|
||||
else => {}, // ignore non-alphanumeric
|
||||
}
|
||||
}
|
||||
if (sanitized.items.len == 0) {
|
||||
return error.SetHostnameEmptyName;
|
||||
}
|
||||
const newname = sanitized.items;
|
||||
|
||||
// need not continue if current name matches the new one.
|
||||
var buf: [std.os.HOST_NAME_MAX]u8 = undefined;
|
||||
const currname = try std.os.gethostname(&buf);
|
||||
if (std.mem.eql(u8, currname, newname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make persistent change first
|
||||
const opt = .{ .mode = 0o644 };
|
||||
const file = try std.io.BufferedAtomicFile.create(allocator, std.fs.cwd(), hostname_filepath, opt);
|
||||
defer file.destroy(); // releases resources; does NOT deletes the file
|
||||
try file.writer().writeAll(newname);
|
||||
try file.finish();
|
||||
|
||||
// rename hostname on the running system
|
||||
var proc = types.ChildProcess.init(&.{ "hostname", newname }, allocator);
|
||||
switch (try proc.spawnAndWait()) {
|
||||
.Exited => |code| if (code != 0) return error.SetHostnameBadExitCode,
|
||||
else => return error.SetHostnameBadTerm,
|
||||
}
|
||||
}
|
||||
|
||||
test "setHostname" {
|
||||
const t = std.testing;
|
||||
const tt = @import("../test.zig");
|
||||
// need to manual free resources because no way to deinit the child process spawn in setHostname.
|
||||
var arena_state = std.heap.ArenaAllocator.init(t.allocator);
|
||||
defer arena_state.deinit();
|
||||
const arena = arena_state.allocator();
|
||||
|
||||
var tmp = try tt.TempDir.create();
|
||||
defer tmp.cleanup();
|
||||
hostname_filepath = try tmp.join(&.{"hostname"});
|
||||
try tmp.dir.writeFile(hostname_filepath, "dummy");
|
||||
|
||||
try setHostname(arena, "123_-newhostname$%/3-4hello5\xef\x83\xa7end");
|
||||
|
||||
var buf: [128]u8 = undefined;
|
||||
const cont = try tmp.dir.readFile(hostname_filepath, &buf);
|
||||
try t.expectEqualStrings("newhostname34hello5end", cont);
|
||||
}
|