aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Justin Whear <justin.whear+github@gmail.com> 2023-02-27 09:58:45 -0800
committerGravatar GitHub <noreply@github.com> 2023-02-27 09:58:45 -0800
commit18cce50390694231e4f235f14091f0153c1b8449 (patch)
tree27beb7b9af3b560a2bcf87d8cc95d2988997079b
parentc8be1e9a5dd36f5c6883a0d7058d1c6e6da2a5f8 (diff)
downloadbun-18cce50390694231e4f235f14091f0153c1b8449.tar.gz
bun-18cce50390694231e4f235f14091f0153c1b8449.tar.zst
bun-18cce50390694231e4f235f14091f0153c1b8449.zip
Feat/os.network interfaces (#2142)
-rw-r--r--src/bun.js/bindings/bindings.zig4
-rw-r--r--src/bun.js/node/node_os.zig260
-rw-r--r--src/darwin_c.zig33
-rw-r--r--src/linux_c.zig12
-rw-r--r--test/bun.js/os.test.js3
5 files changed, 307 insertions, 5 deletions
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index ebe5f83ed..1479ef977 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -3629,12 +3629,12 @@ pub const JSValue = enum(JSValueReprInt) {
return zig_str;
}
- pub fn get(this: JSValue, global: *JSGlobalObject, comptime property: []const u8) ?JSValue {
+ pub fn get(this: JSValue, global: *JSGlobalObject, property: []const u8) ?JSValue {
const value = getIfPropertyExistsImpl(this, global, property.ptr, @intCast(u32, property.len));
return if (@enumToInt(value) != 0) value else return null;
}
- pub fn getTruthy(this: JSValue, global: *JSGlobalObject, comptime property: []const u8) ?JSValue {
+ pub fn getTruthy(this: JSValue, global: *JSGlobalObject, property: []const u8) ?JSValue {
if (get(this, global, property)) |prop| {
if (prop.isEmptyOrUndefinedOrNull()) return null;
return prop;
diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig
index eb7ae3c2d..9bf1bbb86 100644
--- a/src/bun.js/node/node_os.zig
+++ b/src/bun.js/node/node_os.zig
@@ -381,10 +381,193 @@ pub const Os = struct {
pub fn networkInterfaces(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
JSC.markBinding(@src());
- // TODO:
- return JSC.JSValue.createEmptyObject(globalThis, 0);
+ // getifaddrs sets a pointer to a linked list
+ var interface_start: ?*C.ifaddrs = null;
+ const rc = C.getifaddrs(&interface_start);
+ if (rc != 0) {
+ const err = JSC.SystemError{
+ .message = JSC.ZigString.init("A system error occurred: getifaddrs returned an error"),
+ .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))),
+ .errno = @enumToInt(std.os.errno(rc)),
+ .syscall = JSC.ZigString.init("getifaddrs"),
+ };
+
+ globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis));
+ return JSC.JSValue.jsUndefined();
+ }
+ defer C.freeifaddrs(interface_start);
+
+
+ const helpers = struct {
+ // We'll skip interfaces that aren't actually available
+ pub fn skip(iface: *C.ifaddrs) bool {
+ // Skip interfaces that aren't actually available
+ if (iface.ifa_flags & C.IFF_RUNNING == 0) return true;
+ if (iface.ifa_flags & C.IFF_UP == 0) return true;
+ if (iface.ifa_addr == null) return true;
+
+ return false;
+ }
+
+ // We won't actually return link-layer interfaces but we need them for
+ // extracting the MAC address
+ pub fn isLinkLayer(iface: *C.ifaddrs) bool {
+ if (iface.ifa_addr == null) return false;
+ return if (comptime Environment.isLinux)
+ return iface.ifa_addr.*.sa_family == std.os.AF.PACKET
+ else if (comptime Environment.isMac)
+ return iface.ifa_addr.?.*.family == std.os.AF.LINK
+ else unreachable;
+ }
+
+ pub fn isLoopback(iface: *C.ifaddrs) bool {
+ return iface.ifa_flags & C.IFF_LOOPBACK == C.IFF_LOOPBACK;
+ }
+ };
+
+
+ // The list currently contains entries for link-layer interfaces
+ // and the IPv4, IPv6 interfaces. We only want to return the latter two
+ // but need the link-layer entries to determine MAC address.
+ // So, on our first pass through the linked list we'll count the number of
+ // INET interfaces only.
+ var num_inet_interfaces: usize = 0;
+ var it = interface_start;
+ while (it) |iface| : (it = iface.ifa_next) {
+ if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue;
+ num_inet_interfaces += 1;
+ }
+
+ var ret = JSC.JSValue.createEmptyObject(globalThis, 8);
+
+ // Second pass through, populate each interface object
+ it = interface_start;
+ while (it) |iface| : (it = iface.ifa_next) {
+ if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue;
+
+ const interface_name = std.mem.sliceTo(iface.ifa_name, 0);
+ const addr = std.net.Address.initPosix(@alignCast(4, @ptrCast(*std.os.sockaddr, iface.ifa_addr)));
+ const netmask = std.net.Address.initPosix(@alignCast(4, @ptrCast(*std.os.sockaddr, iface.ifa_netmask)));
+
+ var interface = JSC.JSValue.createEmptyObject(globalThis, 7);
+
+ // address <string> The assigned IPv4 or IPv6 address
+ // cidr <string> The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null.
+ {
+ // Compute the CIDR suffix; returns null if the netmask cannot
+ // be converted to a CIDR suffix
+ const maybe_suffix: ?u8 = switch (addr.any.family) {
+ std.os.AF.INET => netmaskToCIDRSuffix(netmask.in.sa.addr),
+ std.os.AF.INET6 => netmaskToCIDRSuffix(@bitCast(u128, netmask.in6.sa.addr)),
+ else => null
+ };
+
+ // Format the address and then, if valid, the CIDR suffix; both
+ // the address and cidr values can be slices into this same buffer
+ // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24"
+ var buf: [64]u8 = undefined;
+ const addr_str = formatAddress(addr, &buf) catch unreachable;
+ var cidr = JSC.JSValue.null;
+ if (maybe_suffix) |suffix| {
+ //NOTE addr_str might not start at buf[0] due to slicing in formatAddress
+ const start = @ptrToInt(addr_str.ptr) - @ptrToInt(&buf[0]);
+ // Start writing the suffix immediately after the address
+ const suffix_str = std.fmt.bufPrint(buf[start + addr_str.len..], "/{}", .{ suffix }) catch unreachable;
+ // The full cidr value is the address + the suffix
+ const cidr_str = buf[start..start + addr_str.len + suffix_str.len];
+ cidr = JSC.ZigString.init(cidr_str).withEncoding().toValueGC(globalThis);
+ }
+
+ interface.put(globalThis, JSC.ZigString.static("address"),
+ JSC.ZigString.init(addr_str).withEncoding().toValueGC(globalThis));
+ interface.put(globalThis, JSC.ZigString.static("cidr"), cidr);
+ }
+
+ // netmask <string> The IPv4 or IPv6 network mask
+ {
+ var buf: [64]u8 = undefined;
+ const str = formatAddress(netmask, &buf) catch unreachable;
+ interface.put(globalThis, JSC.ZigString.static("netmask"),
+ JSC.ZigString.init(str).withEncoding().toValueGC(globalThis));
+ }
+
+ // family <string> Either IPv4 or IPv6
+ interface.put(globalThis, JSC.ZigString.static("family"),
+ (switch (addr.any.family) {
+ std.os.AF.INET => JSC.ZigString.static("IPv4"),
+ std.os.AF.INET6 => JSC.ZigString.static("IPv6"),
+ else => JSC.ZigString.static("unknown"),
+ }).toValue(globalThis)
+ );
+
+ // mac <string> The MAC address of the network interface
+ {
+ // We need to search for the link-layer interface whose name matches this one
+ var ll_it = interface_start;
+ const maybe_ll_addr = while (ll_it) |ll_iface| : (ll_it = ll_iface.ifa_next) {
+ if (helpers.skip(ll_iface) or !helpers.isLinkLayer(ll_iface)) continue;
+
+ const ll_name = bun.sliceTo(ll_iface.ifa_name, 0);
+ if (!strings.hasPrefix(ll_name, interface_name)) continue;
+ if (ll_name.len > interface_name.len and ll_name[interface_name.len] != ':') continue;
+
+ // This is the correct link-layer interface entry for the current interface,
+ // cast to a link-layer socket address
+ if (comptime Environment.isLinux) {
+ break @ptrCast(?*std.os.sockaddr.ll, @alignCast(4, ll_iface.ifa_addr));
+ } else if (comptime Environment.isMac) {
+ break @ptrCast(?*C.sockaddr_dl, @alignCast(2, ll_iface.ifa_addr));
+ } else unreachable;
+ } else null;
+
+ if (maybe_ll_addr) |ll_addr| {
+ // Encode its link-layer address. We need 2*6 bytes for the
+ // hex characters and 5 for the colon separators
+ var mac_buf: [17]u8 = undefined;
+ var addr_data = if (comptime Environment.isLinux) ll_addr.addr
+ else if (comptime Environment.isMac) ll_addr.sdl_data[ll_addr.sdl_nlen..]
+ else unreachable;
+ const mac = std.fmt.bufPrint(&mac_buf,
+ "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}",
+ .{
+ addr_data[0], addr_data[1], addr_data[2],
+ addr_data[3], addr_data[4], addr_data[5],
+ }
+ ) catch unreachable;
+ interface.put(globalThis, JSC.ZigString.static("mac"),
+ JSC.ZigString.init(mac).withEncoding().toValueGC(globalThis));
+ }
+ }
+
+ // internal <boolean> true if the network interface is a loopback or similar interface that is not remotely accessible; otherwise false
+ interface.put(globalThis, JSC.ZigString.static("internal"),
+ JSC.JSValue.jsBoolean(helpers.isLoopback(iface)));
+
+ // scopeid <number> The numeric IPv6 scope ID (only specified when family is IPv6)
+ if (addr.any.family == std.os.AF.INET6) {
+ interface.put(globalThis, JSC.ZigString.static("scope_id"),
+ JSC.JSValue.jsNumber(addr.in6.sa.scope_id));
+ }
+
+
+ // Does this entry already exist?
+ if (ret.get(globalThis, interface_name)) |array| {
+ // Add this interface entry to the existing array
+ const next_index = @intCast(u32, array.getLengthOfArray(globalThis));
+ array.putIndex(globalThis, next_index, interface);
+ } else {
+ // Add it as an array with this interface as an element
+ const member_name = JSC.ZigString.init(interface_name);
+ var array = JSC.JSValue.createEmptyArray(globalThis, 1);
+ array.putIndex(globalThis, 0, interface);
+ ret.put(globalThis, &member_name, array);
+ }
+ }
+
+ return ret;
}
+
pub fn platform(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
JSC.markBinding(@src());
@@ -561,3 +744,76 @@ pub const Os = struct {
return JSC.ZigString.static(comptime getMachineName()).toValue(globalThis);
}
};
+
+fn formatAddress(address: std.net.Address, into: []u8) ![]u8 {
+ // std.net.Address.format includes `:<port>` and square brackets (IPv6)
+ // while Node does neither. This uses format then strips these to bring
+ // the result into conformance with Node.
+ var result = try std.fmt.bufPrint(into, "{}", .{address});
+
+ // Strip `:<port>`
+ if (std.mem.lastIndexOfScalar(u8, result, ':')) |colon| {
+ result = result[0..colon];
+ }
+ // Strip brackets
+ if (result[0] == '[' and result[result.len-1] == ']') {
+ result = result[1..result.len-1];
+ }
+ return result;
+}
+
+/// Given a netmask returns a CIDR suffix. Returns null if the mask is not valid.
+/// `@TypeOf(mask)` must be one of u32 (IPv4) or u128 (IPv6)
+fn netmaskToCIDRSuffix(mask: anytype) ?u8 {
+ const T = @TypeOf(mask);
+ comptime std.debug.assert(T == u32 or T == u128);
+
+ const mask_bits = @byteSwap(mask);
+
+ // Validity check: set bits should be left-contiguous
+ const first_zero = @clz(~mask_bits);
+ const last_one = @bitSizeOf(T) - @ctz(mask_bits);
+ if (first_zero < @bitSizeOf(T) and first_zero < last_one) return null;
+ return first_zero;
+}
+test "netmaskToCIDRSuffix" {
+ const ipv4_tests = .{
+ .{ "255.255.255.255", 32 },
+ .{ "255.255.255.254", 31 },
+ .{ "255.255.255.252", 30 },
+ .{ "255.255.255.128", 25 },
+ .{ "255.255.255.0", 24 },
+ .{ "255.255.128.0", 17 },
+ .{ "255.255.0.0", 16 },
+ .{ "255.128.0.0", 9 },
+ .{ "255.0.0.0", 8 },
+ .{ "224.0.0.0", 3 },
+ .{ "192.0.0.0", 2 },
+ .{ "128.0.0.0", 1 },
+ .{ "0.0.0.0", 0 },
+
+ // invalid masks
+ .{ "255.0.0.255", null },
+ .{ "128.0.0.255", null },
+ .{ "128.0.0.1", null },
+ };
+ inline for (ipv4_tests) |t| {
+ const addr = try std.net.Address.parseIp4(t[0], 0);
+ try std.testing.expectEqual(@as(?u8, t[1]), netmaskToCIDRSuffix(addr.in.sa.addr));
+ }
+
+ const ipv6_tests = .{
+ .{ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 128 },
+ .{ "ffff:ffff:ffff:ffff::", 64 },
+ .{ "::", 0 },
+
+ // invalid masks
+ .{ "ff00:1::", null },
+ .{ "0:1::", null },
+ };
+ inline for (ipv6_tests) |t| {
+ const addr = try std.net.Address.parseIp6(t[0], 0);
+ const bits = @bitCast(u128, addr.in6.sa.addr);
+ try std.testing.expectEqual(@as(?u8, t[1]), netmaskToCIDRSuffix(bits));
+ }
+}
diff --git a/src/darwin_c.zig b/src/darwin_c.zig
index f2e22c85e..d62e665b9 100644
--- a/src/darwin_c.zig
+++ b/src/darwin_c.zig
@@ -751,3 +751,36 @@ pub const RemoveFileFlags = struct {
};
pub const removefile_state_t = opaque {};
pub extern fn removefileat(fd: c_int, path: [*c]const u8, state: ?*removefile_state_t, flags: u32) c_int;
+
+// As of Zig v0.11.0-dev.1393+38eebf3c4, ifaddrs.h is not included in the headers
+pub const ifaddrs = extern struct {
+ ifa_next: ?*ifaddrs,
+ ifa_name: [*:0]u8,
+ ifa_flags: c_uint,
+ ifa_addr: ?*std.os.sockaddr,
+ ifa_netmask: ?*std.os.sockaddr,
+ ifa_dstaddr: ?*std.os.sockaddr,
+ ifa_data: *anyopaque,
+};
+pub extern fn getifaddrs(*?*ifaddrs) c_int;
+pub extern fn freeifaddrs(?*ifaddrs) void;
+
+const net_if_h = @cImport({ @cInclude("net/if.h"); });
+pub const IFF_RUNNING = net_if_h.IFF_RUNNING;
+pub const IFF_UP = net_if_h.IFF_UP;
+pub const IFF_LOOPBACK = net_if_h.IFF_LOOPBACK;
+pub const sockaddr_dl = extern struct {
+ sdl_len: u8, // Total length of sockaddr */
+ sdl_family: u8, // AF_LINK */
+ sdl_index: u16, // if != 0, system given index for interface */
+ sdl_type: u8, // interface type */
+ sdl_nlen: u8, // interface name length, no trailing 0 reqd. */
+ sdl_alen: u8, // link level address length */
+ sdl_slen: u8, // link layer selector length */
+ sdl_data: [12]u8, // minimum work area, can be larger; contains both if name and ll address */
+ //#ifndef __APPLE__
+ // /* For TokenRing */
+ // u_short sdl_rcf; /* source routing control */
+ // u_short sdl_route[16]; /* source routing information */
+ //#endif
+};
diff --git a/src/linux_c.zig b/src/linux_c.zig
index 04faf210b..ae9300477 100644
--- a/src/linux_c.zig
+++ b/src/linux_c.zig
@@ -480,3 +480,15 @@ pub fn posix_spawn_file_actions_addchdir_np(actions: *posix_spawn_file_actions_t
}
pub extern fn vmsplice(fd: c_int, iovec: [*]const std.os.iovec, iovec_count: usize, flags: u32) isize;
+
+
+const net_c = @cImport({
+ @cInclude("ifaddrs.h"); // getifaddrs, freeifaddrs
+ @cInclude("net/if.h"); // IFF_RUNNING, IFF_UP
+});
+pub const ifaddrs = net_c.ifaddrs;
+pub const getifaddrs = net_c.getifaddrs;
+pub const freeifaddrs = net_c.freeifaddrs;
+pub const IFF_RUNNING = net_c.IFF_RUNNING;
+pub const IFF_UP = net_c.IFF_UP;
+pub const IFF_LOOPBACK = net_c.IFF_LOOPBACK;
diff --git a/test/bun.js/os.test.js b/test/bun.js/os.test.js
index 87c03d5d4..122969337 100644
--- a/test/bun.js/os.test.js
+++ b/test/bun.js/os.test.js
@@ -110,7 +110,8 @@ it("networkInterfaces", () => {
expect(typeof nI.family === "string").toBe(true);
expect(typeof nI.mac === "string").toBe(true);
expect(typeof nI.internal === "boolean").toBe(true);
- expect(typeof nI.cidr).toBe("string");
+ if (nI.cidr) // may be null
+ expect(typeof nI.cidr).toBe("string");
}
}
});