Compare commits

..

6 commits

6 changed files with 114 additions and 36 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
zig-*/
env

22
README.md Normal file
View file

@ -0,0 +1,22 @@
# local-etc-hosts-updater
Queries the mDNS record for a given .local address, and maps it to the given subdomains inside a hosts file
## Usage
### CLI Args
`local-etc-hosts-updater <local_domain> <ip_version> <interface>`
- `local_domain` is the local domain to be queried
- `ip_version` is an enum.
- 4 will query for an IPv4 address in the `192.168.0.0/16` subnet
- 6 will query for a ULA IPv6 address in the `fd00::/8` subnet
- 0 will do both
- `interface` is the interface name to query, it is required for ipv6
### ENV vars
- `OLD_HOSTS_PATH` path to the current hosts file
- `TARGET_DOMAIN` public domain for the subdomains
- `SUBDOMAIN_LIST` space separated list of subdomains to map

7
src/consts.zig Normal file
View file

@ -0,0 +1,7 @@
pub const ENV_KEY_OLD_HOSTS_PATH = "OLD_HOSTS_PATH";
pub const TMP_HOSTS_FILE = "tmp_hosts";
pub const ENV_KEY_TARGET_DOMAIN = "TARGET_DOMAIN";
pub const ENV_KEY_SUBDOMAIN_LIST = "SUBDOMAIN_LIST";
pub const FILE_LINE_BUFF_SIZE = 100;
pub const MSG_BUFF_SIZE = 200;

View file

@ -3,7 +3,7 @@ const util = @import("util.zig");
const hosts_header = "# local-etc-hosts-updater"; const hosts_header = "# local-etc-hosts-updater";
const FILE_LINE_BUFF_SIZE = 100; const FILE_LINE_BUFF_SIZE = util.consts.FILE_LINE_BUFF_SIZE;
pub fn update_hosts(ip: util.IP, domain: util.Domain) !void { pub fn update_hosts(ip: util.IP, domain: util.Domain) !void {
try create_tmp_hosts(ip, domain); try create_tmp_hosts(ip, domain);
@ -22,7 +22,7 @@ inline fn check_purge(line: []u8, purging: *bool, skip_last_line: *bool) void {
} }
fn purge_existing(old_hosts: std.fs.File, tmp_hosts: std.fs.File) !void { fn purge_existing(old_hosts: std.fs.File, tmp_hosts: std.fs.File) !void {
var buff = [_]u8{0x00} ** FILE_LINE_BUFF_SIZE; var buff: [FILE_LINE_BUFF_SIZE]u8 = undefined;
var buff_stream = std.io.fixedBufferStream(&buff); var buff_stream = std.io.fixedBufferStream(&buff);
const buff_reader = buff_stream.reader(); const buff_reader = buff_stream.reader();
const buff_writer = buff_stream.writer(); const buff_writer = buff_stream.writer();
@ -61,20 +61,11 @@ fn purge_existing(old_hosts: std.fs.File, tmp_hosts: std.fs.File) !void {
} }
} }
fn append_new(tmp_hosts: std.fs.File, addr: std.net.Address, local_domain: util.Domain) !void { fn write_subdomains_map(writer: std.fs.File.Writer, addr: std.net.Address, local_domain: util.Domain) !void {
const writer = tmp_hosts.writer();
var buff = [_]u8{0x00} ** FILE_LINE_BUFF_SIZE;
// write output
const header_len = hosts_header.*.len;
@memcpy(buff[0..header_len], hosts_header);
try writer.writeAll(buff[0..header_len]);
try writer.writeByte('\n');
var subdomains = try get_subdomains(); var subdomains = try get_subdomains();
const domain = try util.getenv("TARGET_DOMAIN"); const domain = try util.getenv(util.consts.ENV_KEY_TARGET_DOMAIN);
var addr_buff = [_]u8{0x00} ** 50; var addr_buff: [50]u8 = undefined; // max is ~43
var addr_str = try std.fmt.bufPrint(&addr_buff, "{}", .{addr}); var addr_str = try std.fmt.bufPrint(&addr_buff, "{}", .{addr});
if (addr_str[0] == '[') { if (addr_str[0] == '[') {
addr_str = addr_str[1 .. addr_str.len - 3]; // [fd00::0000]:0 addr_str = addr_str[1 .. addr_str.len - 3]; // [fd00::0000]:0
@ -86,33 +77,49 @@ fn append_new(tmp_hosts: std.fs.File, addr: std.net.Address, local_domain: util.
try std.fmt.format(writer, "{s} {s}.{s}\n", .{ addr_str, subdomain, domain }); try std.fmt.format(writer, "{s} {s}.{s}\n", .{ addr_str, subdomain, domain });
} }
try std.fmt.format(writer, "{s} {s}\n", .{ addr_str, local_domain.name }); try std.fmt.format(writer, "{s} {s}\n", .{ addr_str, local_domain.name });
}
fn append_new(tmp_hosts: std.fs.File, ip: util.IP, local_domain: util.Domain) !void {
const writer = tmp_hosts.writer();
var buff: [FILE_LINE_BUFF_SIZE]u8 = undefined;
// write output
const header_len = hosts_header.*.len;
@memcpy(buff[0..header_len], hosts_header);
try writer.writeAll(buff[0..header_len]);
try writer.writeByte('\n');
if (ip.v6) |addr| {
try write_subdomains_map(writer, addr, local_domain);
}
if (ip.v4) |addr| {
try write_subdomains_map(writer, addr, local_domain);
}
// add extra final newline // add extra final newline
try writer.writeByte('\n'); try writer.writeByte('\n');
} }
fn get_subdomains() !std.mem.SplitIterator(u8, .scalar) { fn get_subdomains() !std.mem.SplitIterator(u8, .scalar) {
const subdomain_list = try util.getenv("SUBDOMAIN_LIST"); const subdomain_list = try util.getenv(util.consts.ENV_KEY_SUBDOMAIN_LIST);
return std.mem.splitScalar(u8, std.mem.span(subdomain_list), ' '); return std.mem.splitScalar(u8, std.mem.span(subdomain_list), ' ');
} }
fn create_tmp_hosts(ip: util.IP, domain: util.Domain) !void { fn create_tmp_hosts(ip: util.IP, domain: util.Domain) !void {
var old_hosts = try std.fs.openFileAbsoluteZ(try util.getenv("OLD_HOSTS_PATH"), .{ .mode = .read_only }); var old_hosts = try std.fs.Dir.openFileZ(util.cwd, try util.getenv(util.consts.ENV_KEY_OLD_HOSTS_PATH), .{ .mode = .read_only });
defer old_hosts.close(); defer old_hosts.close();
var tmp_hosts = try std.fs.createFileAbsoluteZ(try util.getenv("TMP_HOSTS_PATH"), .{ .truncate = true }); var tmp_hosts = try std.fs.Dir.createFileZ(util.cwd, util.consts.TMP_HOSTS_FILE, .{ .truncate = true });
defer tmp_hosts.close(); defer tmp_hosts.close();
try purge_existing(old_hosts, tmp_hosts); try purge_existing(old_hosts, tmp_hosts);
if (ip) |addr| { try append_new(tmp_hosts, ip, domain);
try append_new(tmp_hosts, addr, domain);
}
} }
fn move_tmp_hosts() !void { fn move_tmp_hosts() !void {
var target_hosts = try std.fs.createFileAbsoluteZ(try util.getenv("OLD_HOSTS_PATH"), .{ .lock = .exclusive }); var target_hosts = try std.fs.Dir.createFileZ(util.cwd, try util.getenv(util.consts.ENV_KEY_OLD_HOSTS_PATH), .{ .lock = .exclusive });
defer target_hosts.close(); defer target_hosts.close();
const target_writer = target_hosts.writer(); const target_writer = target_hosts.writer();
var tmp_hosts = try std.fs.openFileAbsoluteZ(try util.getenv("TMP_HOSTS_PATH"), .{}); var tmp_hosts = try std.fs.Dir.openFileZ(util.cwd, util.consts.TMP_HOSTS_FILE, .{});
defer tmp_hosts.close(); defer tmp_hosts.close();
const source_reader = tmp_hosts.reader(); const source_reader = tmp_hosts.reader();
@ -121,4 +128,6 @@ fn move_tmp_hosts() !void {
return err; return err;
} }
}; };
try std.fs.Dir.deleteFileZ(util.cwd, util.consts.TMP_HOSTS_FILE);
} }

View file

@ -2,7 +2,6 @@ const std = @import("std");
const util = @import("util.zig"); const util = @import("util.zig");
const MDNSError = error{ const MDNSError = error{
Unimplemented,
SocketInitFail, SocketInitFail,
UDPConnectFail, UDPConnectFail,
UDPSendFail, UDPSendFail,
@ -14,17 +13,30 @@ const MDNSError = error{
}; };
pub fn get_mdns(domain: util.Domain, ip_info: util.IPInfo) !util.IP { pub fn get_mdns(domain: util.Domain, ip_info: util.IPInfo) !util.IP {
if (ip_info.version != .Both) {
const sock = try send_query(domain, ip_info); const sock = try send_query(domain, ip_info);
return receive_response(sock, ip_info); return receive_response(sock, ip_info);
}
const ip_info4 = util.IPInfo{ .version = .IPv4, .interface = ip_info.interface };
var sock = try send_query(domain, ip_info4);
const addr_v4 = try receive_response(sock, ip_info4);
const ip_info6 = util.IPInfo{ .version = .IPv4, .interface = ip_info.interface };
sock = try send_query(domain, ip_info6);
const addr_v6 = try receive_response(sock, ip_info6);
return util.merge_addrs(addr_v4, addr_v6);
} }
const socket = c_int; const socket = c_int;
const MSG_BUFF_SIZE = 200; const MSG_BUFF_SIZE = util.consts.MSG_BUFF_SIZE;
fn get_mdns_socket(ip_info: util.IPInfo) !socket { fn get_mdns_socket(ip_info: util.IPInfo) !socket {
const sock = std.c.socket(switch (ip_info.version) { const sock = std.c.socket(switch (ip_info.version) {
util.IP_VER_ENUM.IPv4 => std.c.AF.INET, util.IP_VER_ENUM.IPv4 => std.c.AF.INET,
util.IP_VER_ENUM.IPv6 => std.c.AF.INET6, util.IP_VER_ENUM.IPv6 => std.c.AF.INET6,
else => unreachable,
}, std.c.SOCK.DGRAM, std.c.IPPROTO.UDP); }, std.c.SOCK.DGRAM, std.c.IPPROTO.UDP);
if (sock == -1) { if (sock == -1) {
return MDNSError.SocketInitFail; return MDNSError.SocketInitFail;
@ -59,6 +71,7 @@ fn construct_mdns_query(domain: util.Domain, ip_info: util.IPInfo, buff: []u8) !
[_]u8{ 0x00, switch (ip_info.version) { [_]u8{ 0x00, switch (ip_info.version) {
util.IP_VER_ENUM.IPv4 => 0x01, util.IP_VER_ENUM.IPv4 => 0x01,
util.IP_VER_ENUM.IPv6 => 0x1c, util.IP_VER_ENUM.IPv6 => 0x1c,
else => unreachable,
} } ++ // A or AAAA record } } ++ // A or AAAA record
[_]u8{ 0x00, 0x01 } // IN query [_]u8{ 0x00, 0x01 } // IN query
; ;
@ -71,12 +84,13 @@ fn get_target_address(ip_info: util.IPInfo) !std.net.Address {
const target_addr: []const u8 = switch (ip_info.version) { const target_addr: []const u8 = switch (ip_info.version) {
util.IP_VER_ENUM.IPv4 => "224.0.0.251", util.IP_VER_ENUM.IPv4 => "224.0.0.251",
util.IP_VER_ENUM.IPv6 => blk: { util.IP_VER_ENUM.IPv6 => blk: {
var buf = [_]u8{0x00} ** MSG_BUFF_SIZE; var buf: [50]u8 = undefined;
var byte_buf = std.io.fixedBufferStream(&buf); var byte_buf = std.io.fixedBufferStream(&buf);
const writer = byte_buf.writer(); const writer = byte_buf.writer();
try std.fmt.format(writer, "ff02::fb%{s}", .{ip_info.interface.?}); try std.fmt.format(writer, "ff02::fb%{s}", .{ip_info.interface.?});
break :blk buf[0..writer.context.pos]; break :blk buf[0..writer.context.pos];
}, },
else => unreachable,
}; };
return std.net.Address.resolveIp(target_addr, 5353); return std.net.Address.resolveIp(target_addr, 5353);
} }
@ -87,7 +101,7 @@ fn send_query(domain: util.Domain, ip_info: util.IPInfo) !socket {
const addr = try get_target_address(ip_info); const addr = try get_target_address(ip_info);
var buff = [_]u8{0x00} ** MSG_BUFF_SIZE; var buff: [MSG_BUFF_SIZE]u8 = undefined;
const n = try construct_mdns_query(domain, ip_info, &buff); const n = try construct_mdns_query(domain, ip_info, &buff);
if (std.c.sendto(sock, &buff, n, std.c.MSG.DONTWAIT, &addr.any, addr.getOsSockLen()) == -1) { if (std.c.sendto(sock, &buff, n, std.c.MSG.DONTWAIT, &addr.any, addr.getOsSockLen()) == -1) {
@ -99,7 +113,7 @@ fn send_query(domain: util.Domain, ip_info: util.IPInfo) !socket {
fn receive_response(sock: socket, ip_info: util.IPInfo) !util.IP { fn receive_response(sock: socket, ip_info: util.IPInfo) !util.IP {
defer _ = std.c.close(sock); defer _ = std.c.close(sock);
var buff = [_]u8{0x00} ** MSG_BUFF_SIZE; var buff: [MSG_BUFF_SIZE]u8 = undefined;
const n: usize = blk: { const n: usize = blk: {
const ret = std.c.recv(sock, &buff, MSG_BUFF_SIZE, 0); const ret = std.c.recv(sock, &buff, MSG_BUFF_SIZE, 0);
if (ret < 0) { if (ret < 0) {
@ -153,22 +167,30 @@ fn parse_mdns_response(response: []u8, ip_info: util.IPInfo) !util.IP {
if (switch (ip_info.version) { if (switch (ip_info.version) {
util.IP_VER_ENUM.IPv4 => ip_len != 4, util.IP_VER_ENUM.IPv4 => ip_len != 4,
util.IP_VER_ENUM.IPv6 => ip_len != 16, util.IP_VER_ENUM.IPv6 => ip_len != 16,
else => unreachable,
}) { }) {
continue; continue;
} }
if (ip_len == 4) { if (ip_len == 4) {
if (ip_bytes[0] == 192 and ip_bytes[1] == 168) { if (ip_bytes[0] == 192 and ip_bytes[1] == 168) {
var addr_buff = [_]u8{0x00} ** 4; var addr_buff: [4]u8 = undefined;
@memcpy(&addr_buff, ip_bytes); @memcpy(&addr_buff, ip_bytes);
addr = std.net.Address.initIp4(addr_buff, 0); addr = std.net.Address.initIp4(addr_buff, 0);
} }
} else if (ip_len == 16) { } else if (ip_len == 16) {
if (ip_bytes[0] == 0xfd) { if (ip_bytes[0] == 0xfd) {
var addr_buff = [_]u8{0x00} ** 16; var addr_buff: [16]u8 = undefined;
@memcpy(&addr_buff, ip_bytes); @memcpy(&addr_buff, ip_bytes);
addr = std.net.Address.initIp6(addr_buff, 0, 0, @intCast(std.c.if_nametoindex(ip_info.interface.?))); addr = std.net.Address.initIp6(addr_buff, 0, 0, @intCast(std.c.if_nametoindex(ip_info.interface.?)));
} }
} }
} }
return addr; if (addr == null) {
return MDNSError.NoMatchingAddress;
}
return switch (ip_info.version) {
.IPv4 => util.IP{ .v4 = addr },
.IPv6 => util.IP{ .v6 = addr },
else => unreachable,
};
} }

View file

@ -1,5 +1,7 @@
const std = @import("std"); const std = @import("std");
pub const consts = @import("consts.zig");
const ArgError = error{ const ArgError = error{
NotEnoughArgs, NotEnoughArgs,
BadDomain, BadDomain,
@ -7,14 +9,26 @@ const ArgError = error{
InterfaceRequired, InterfaceRequired,
InvalidInterface, InvalidInterface,
EnvVarNotSet, EnvVarNotSet,
InvalidOldHostsPath,
}; };
pub const IP_VER_ENUM = enum(u3) { pub const IP_VER_ENUM = enum(u3) {
Both = 0,
IPv4 = 4, IPv4 = 4,
IPv6 = 6, IPv6 = 6,
}; };
pub const IP = ?std.net.Address; pub const IP = struct {
v4: ?std.net.Address = null,
v6: ?std.net.Address = null,
};
pub fn merge_addrs(v4: IP, v6: IP) IP {
return IP{
.v4 = v4.v4,
.v6 = v6.v6,
};
}
pub const IPInfo = struct { pub const IPInfo = struct {
version: IP_VER_ENUM, version: IP_VER_ENUM,
@ -31,10 +45,12 @@ pub fn getenv(key: [*:0]const u8) ![*:0]const u8 {
} }
pub fn check_perms() !void { pub fn check_perms() !void {
var f = try std.fs.openFileAbsoluteZ(try getenv("OLD_HOSTS_PATH"), .{ .mode = .write_only }); var f = try std.fs.Dir.openFileZ(cwd, try getenv(consts.ENV_KEY_OLD_HOSTS_PATH), .{ .mode = .write_only });
f.close(); f.close();
} }
pub const cwd = std.fs.cwd();
pub fn get_input() !struct { pub fn get_input() !struct {
Domain, Domain,
IPInfo, IPInfo,
@ -49,7 +65,7 @@ pub fn get_input() !struct {
const ip_ver = std.meta.intToEnum(IP_VER_ENUM, std.fmt.parseInt(u3, ip_ver_str, 10) catch return ArgError.InvalidAddressVer) catch return ArgError.InvalidAddressVer; const ip_ver = std.meta.intToEnum(IP_VER_ENUM, std.fmt.parseInt(u3, ip_ver_str, 10) catch return ArgError.InvalidAddressVer) catch return ArgError.InvalidAddressVer;
var iface: ?[:0]const u8 = null; var iface: ?[:0]const u8 = null;
if (ip_ver == IP_VER_ENUM.IPv6) { if (ip_ver != IP_VER_ENUM.IPv4) {
iface = args.next() orelse return ArgError.InterfaceRequired; iface = args.next() orelse return ArgError.InterfaceRequired;
if (std.c.if_nametoindex(iface.?) == 0) { if (std.c.if_nametoindex(iface.?) == 0) {
return ArgError.InvalidInterface; return ArgError.InvalidInterface;
@ -62,7 +78,7 @@ pub fn get_input() !struct {
fn check_domain(domain_str: [:0]const u8) !Domain { fn check_domain(domain_str: [:0]const u8) !Domain {
var domain = Domain{ var domain = Domain{
.name = domain_str, .name = domain_str,
.labels = [_][]const u8{&[_]u8{}} ** 5, .labels = undefined,
}; };
var labels = std.mem.splitScalar(u8, domain_str, '.'); var labels = std.mem.splitScalar(u8, domain_str, '.');
var last: []const u8 = ""; var last: []const u8 = "";