Browse Source

manually start merging core_net

Colin Davidson 2 years ago
parent
commit
28f7f57247

+ 818 - 0
core/net/addr.odin

@@ -0,0 +1,818 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, IPv4 + IPv6 parsers, documentation.
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:strconv"
+import "core:strings"
+import "core:fmt"
+
+/*
+	Expects an IPv4 address with no leading or trailing whitespace:
+	- a.b.c.d
+	- a.b.c.d:port
+	- [a.b.c.d]:port
+
+	If the IP address is bracketed, the port must be present and valid (though it will be ignored):
+	- [a.b.c.d] will be treated as a parsing failure.
+
+	The port, if present, is required to be a base 10 number in the range 0-65535, inclusive.
+
+	If `non_decimal_address` is true, `aton` is told each component must be decimal and max 255.
+*/
+parse_ip4_address :: proc(address_and_maybe_port: string, non_decimal_address := false) -> (addr: IP4_Address, ok: bool) {
+	res, res_ok := aton(address_and_maybe_port, .IP4, !non_decimal_address)
+	if ip4, ip4_ok := res.(IP4_Address); ip4_ok {
+		return ip4, res_ok
+	}
+	return {}, false
+}
+
+/*
+	Parses an IP address in "non-decimal" `inet_aton` form.
+
+	e.g."00377.0x0ff.65534" = 255.255.255.254
+		00377 = 255 in octal
+		0x0ff = 255 in hexadecimal
+		This leaves 16 bits worth of address
+		.65534 then accounts for the last two digits
+
+	For the address part the allowed forms are:
+		a.b.c.d - where each part represents a byte
+		a.b.c   - where `a` & `b` represent a byte and `c` a u16
+		a.b     - where `a` represents a byte and `b` supplies the trailing 24 bits
+		a       - where `a` gives the entire 32-bit value
+
+	The port, if present, is required to be a base 10 number in the range 0-65535, inclusive.
+*/
+aton :: proc(address_and_maybe_port: string, family: Address_Family, is_decimal_only := false) -> (addr: Address, ok: bool) {
+	switch family {
+	case .IP4:
+		/*
+			There is no valid address shorter than `0.0.0.0`.
+		*/
+		if len(address_and_maybe_port) < 7 {
+			return {}, false
+		}
+
+		address, _ := split_port(address_and_maybe_port) or_return // This call doesn't allocate
+
+		buf: [4]u64 = {}
+		i := 0
+
+		max_value := u64(max(u32))
+		bases     := DEFAULT_DIGIT_BASES
+
+		if is_decimal_only {
+			max_value = 255
+			bases     = {.Dec}
+		}
+
+		for len(address) > 0 {
+			if i == 4 {
+				return {}, false
+			}
+
+			/*
+				Decimal-only addresses may not have a leading zero.
+			*/
+			if is_decimal_only && len(address) > 1 && address[0] == '0' && address[1] != '.' {
+				return
+			}
+
+			number, consumed, number_ok := parse_ip_component(address, max_value, bases)
+			if !number_ok || consumed == 0 {
+				return {}, false
+			}
+
+			buf[i] = number
+
+			address = address[consumed:]
+
+			if len(address) > 0 && address[0] == '.' {
+				address = address[1:]
+			}
+			i += 1
+		}
+
+		/*
+			Distribute parts.
+		*/
+		switch i {
+		case 1:
+			buf[1] = buf[0] & 0xffffff
+			buf[0] >>= 24
+			fallthrough
+		case 2:
+			buf[2] = buf[1] & 0xffff
+			buf[1] >>= 16
+			fallthrough
+		case 3:
+			buf[3] = buf[2] & 0xff
+			buf[2] >>= 8
+		}
+
+		a: [4]u8 = ---
+		for v, i in buf {
+			if v > 255 { return {}, false }
+			a[i] = u8(v)
+		}
+		return IP4_Address(a), true
+
+	case .IP6:
+		return parse_ip6_address(address_and_maybe_port)
+
+	case:
+		return nil, false
+	}
+}
+
+/*
+	The minimum length of a valid IPv6 address string is 2, e.g. `::`
+
+	The maximum length of a valid IPv6 address string is 45, when it embeds an IPv4,
+	e.g. `0000:0000:0000:0000:0000:ffff:255.255.255.255`
+
+	An IPv6 address must contain at least 3 pieces, e.g. `::`,
+	and at most 9 (using `::` for a trailing or leading 0)
+*/
+IPv6_MIN_STRING_LENGTH :: 2
+IPv6_MAX_STRING_LENGTH :: 45
+IPv6_MIN_COLONS        :: 2
+IPv6_PIECE_COUNT       :: 8
+
+parse_ip6_address :: proc(address_and_maybe_port: string) -> (addr: IP6_Address, ok: bool) {
+	/*
+		If we have an IPv6 address of the form [IP]:Port, first get us just the IP.
+	*/
+	address, _ := split_port(address_and_maybe_port) or_return
+
+	/*
+		Early bailouts based on length and number of pieces.
+	*/
+	if len(address) < IPv6_MIN_STRING_LENGTH || len(address) > IPv6_MAX_STRING_LENGTH { return }
+
+	/*
+		Do a pre-pass on the string that checks how many `:` and `.` we have,
+		if they're in the right order, and if the things between them are digits as expected.
+
+		It's not strictly necessary considering we could use `strings.split`,
+		but this way we can avoid using an allocator and return earlier on bogus input. Win-win.
+	*/
+	colon_count  := 0
+	dot_count    := 0
+
+	pieces_temp:  [IPv6_PIECE_COUNT + 1]string
+
+	piece_start := 0
+	piece_end   := 0
+
+	for ch, i in address {
+		switch ch {
+		case '0'..'9', 'a'..'f', 'A'..'F':
+			piece_end += 1
+
+		case ':':
+			/*
+				If we see a `:` after a `.`, it means an IPv4 part was sandwiched between IPv6,
+				instead of it being the tail: invalid.
+			*/
+			if dot_count > 0 { return }
+
+			pieces_temp[colon_count] = address[piece_start:piece_end]
+
+			colon_count += 1
+			if colon_count > IPv6_PIECE_COUNT { return }
+
+			/*
+				If there's anything left, put it in the next piece.
+			*/
+			piece_start = i + 1
+			piece_end   = piece_start
+
+		case '.':
+			/*
+				IPv4 address is treated as one piece. No need to update `piece_*`.
+			*/
+			dot_count += 1
+
+
+		case: // Invalid character, return early
+			return
+		}
+	}
+
+	if colon_count < IPv6_MIN_COLONS { return }
+
+	/*
+		Assign the last piece string.
+	*/
+	pieces_temp[colon_count] = address[piece_start:]
+
+	/*
+		`pieces` now holds the same output as it would if had used `strings.split`.
+	*/
+	pieces := pieces_temp[:colon_count + 1]
+
+	/*
+		Check if we have what looks like an embedded IPv4 address.
+	*/
+	ipv4:      IP4_Address
+	have_ipv4: bool
+
+	if dot_count > 0 {
+		/*
+			If we have an IPv4 address accounting for the last 32 bits,
+			this means we can have at most 6 IPv6 pieces, like so: `x:x:X:x:x:x:d.d.d.d`
+
+			Or, put differently: 6 pieces IPv6 (5 colons), a colon, 1 piece IPv4 (3 dots),
+			for a total of 6 colons and 3 dots.
+		*/
+		if dot_count != 3 || colon_count > 6 { return }
+
+		/*
+			Try to parse IPv4 address.
+			If successful, we have our least significant 32 bits.
+			If not, it invalidates the whole address and we can bail.
+		*/
+		ipv4, have_ipv4 = parse_ip4_address(pieces_temp[colon_count])
+		if !have_ipv4 { return }
+	}
+
+	/*
+		Check for `::` being used more than once, and save the skip.
+	*/
+	zero_skip := -1
+	for i in 1..<len(pieces) - 1 {
+		if pieces[i] == "" {
+			/*
+				Return if skip has already been set.
+			*/
+			if zero_skip != -1 {
+				return
+			}
+
+			zero_skip = i
+		}
+	}
+
+	/*
+		Now check if we have the necessary number pieces, accounting for any `::`,
+		and how many were skipped by it if applicable.
+	*/
+	before_skip := 0
+	after_skip  := 0
+	num_skipped := 0
+
+	if zero_skip != -1 {
+		before_skip = zero_skip
+		after_skip  = len(pieces) - zero_skip - 1
+
+		/*
+			An IPv4 "piece" accounts for 2 IPv6 pieces we haven't added to the pieces slice, so add 1.
+		*/
+		if have_ipv4 {
+			after_skip += 1
+		}
+
+		/*
+			Adjust for leading `::`.
+		*/
+		if pieces[0] == "" {
+			before_skip -= 1
+			// Leading `:` can only be part of `::`.
+			if before_skip > 0 { return }
+		}
+
+		/*
+			Adjust for trailing `::`.
+		*/
+		if pieces[len(pieces) - 1] == "" {
+			after_skip -= 1
+			// Trailing `:` can only be part of `::`.
+			if after_skip > 0 { return }
+		}
+
+		/*
+			Calculate how many zero pieces we skipped.
+			It should be at least one, considering we encountered a `::`.
+		*/
+		num_skipped = IPv6_PIECE_COUNT - before_skip - after_skip
+		if num_skipped < 1 { return }
+
+	} else {
+		/*
+			No zero skip means everything is part of "before the skip".
+			An IPv4 "piece" accounts for 2 IPv6 pieces we haven't added to the pieces slice, so add 1.
+		*/
+		piece_count := len(pieces)
+		if have_ipv4 {
+			piece_count += 1
+		}
+
+		/*
+			Do we have the complete set?
+		*/
+		if piece_count != IPv6_PIECE_COUNT { return }
+
+		/*
+			Validate leading and trailing empty parts, as they can only be part of a `::`.
+		*/
+		if pieces[0] == "" || pieces[len(pieces) - 1] == "" { return }
+
+
+		before_skip = piece_count
+		after_skip  = 0
+		num_skipped = 0
+	}
+
+	/*
+		Now try to parse the pieces into a 8 16-bit pieces.
+	*/
+	piece_values: [IPv6_PIECE_COUNT]u16be
+
+	idx     := 0
+	val_idx := 0
+
+	for _ in 0..<before_skip {
+		/*
+			An empty piece is the default zero. Otherwise, try to parse as an IPv6 hex piece.
+			If we have an IPv4 address, stop on the penultimate index.
+		*/
+		if have_ipv4 && val_idx == 6 {
+			break
+		}
+
+		piece := pieces[idx]
+
+		/*
+			An IPv6 piece can at most contain 4 hex digits.
+		*/
+		if len(piece) > 4 { return }
+
+		if piece != "" {
+			val, _ := parse_ip_component(piece, 65535, {.IPv6}) or_return
+			piece_values[val_idx] = u16be(val)
+		}
+
+		idx     += 1
+		val_idx += 1
+	}
+
+	if before_skip == 0 {
+		idx += 1
+	}
+
+	if num_skipped > 0 {
+		idx     += 1
+		val_idx += num_skipped
+	}
+
+	if after_skip > 0 {
+		for _ in 0..<after_skip {
+			/*
+				An empty piece is the default zero. Otherwise, try to parse as an IPv6 hex piece.
+				If we have an IPv4 address, stop on the penultimate index.
+			*/
+			if have_ipv4 && val_idx == 6 {
+				break
+			}
+
+			piece := pieces[idx]
+
+			/*
+				An IPv6 piece can at most contain 4 hex digits.
+			*/
+			if len(piece) > 4 { return }
+
+			if piece != "" {
+				val, _ := parse_ip_component(piece, 65535, {.IPv6}) or_return
+				piece_values[val_idx] = u16be(val)
+			}
+
+			idx     += 1
+			val_idx += 1
+		}
+	}
+
+	/*
+		Distribute IPv4 address into last two pieces, if applicable.
+	*/
+	if have_ipv4 {
+		val := u16(ipv4[0]) << 8
+		val |= u16(ipv4[1])
+		piece_values[6] = u16be(val)
+
+
+		val  = u16(ipv4[2]) << 8
+		val |= u16(ipv4[3])
+		piece_values[7] = u16be(val)
+	}
+
+	return transmute(IP6_Address)piece_values, true
+}
+
+/*
+	Try parsing as an IPv6 address.
+	If it's determined not to be, try as an IPv4 address, optionally in non-decimal format.
+*/
+parse_address :: proc(address_and_maybe_port: string, non_decimal_address := false) -> Address {
+	addr6, ok6 := parse_ip6_address(address_and_maybe_port)
+	if ok6 do return addr6
+	addr4, ok4 := parse_ip4_address(address_and_maybe_port, non_decimal_address)
+	if ok4 do return addr4
+	return nil
+}
+
+parse_endpoint :: proc(endpoint_str: string) -> (ep: Endpoint, ok: bool) {
+	addr_str, port, split_ok := split_port(endpoint_str)
+	if !split_ok do return
+
+	addr := parse_address(addr_str)
+	if addr == nil do return
+
+	ep = Endpoint { address = addr, port = port }
+	ok = true
+	return
+}
+
+Host :: struct {
+	hostname: string,
+	port:     int,
+}
+Host_Or_Endpoint :: union {
+	Host,
+	Endpoint,
+}
+Parse_Endpoint_Error :: enum {
+	Bad_Port = 1,
+	Bad_Address,
+	Bad_Hostname,
+}
+
+// Takes a string consisting of a hostname or IP address, and an optional port,
+// and return the component parts in a useful form.
+parse_hostname_or_endpoint :: proc(endpoint_str: string) -> (target: Host_Or_Endpoint, err: Network_Error) {
+	host, port, port_ok := split_port(endpoint_str)
+	if !port_ok {
+		return nil, .Bad_Port
+	}
+	if addr := parse_address(host); addr != nil {
+		return Endpoint{addr, port}, nil
+	}
+	if !validate_hostname(host) {
+		return nil, .Bad_Hostname
+	}
+	return Host{host, port}, nil
+}
+
+
+// Takes an endpoint string and returns its parts.
+// Returns ok=false if port is not a number.
+split_port :: proc(endpoint_str: string) -> (addr_or_host: string, port: int, ok: bool) {
+	// IP6 [addr_or_host]:port
+	if i := strings.last_index(endpoint_str, "]:"); i != -1 {
+		addr_or_host = endpoint_str[1:i]
+		port, ok = strconv.parse_int(endpoint_str[i+2:], 10)
+
+		if port > 65535 {
+			ok = false
+		}
+		return
+	}
+
+	if n := strings.count(endpoint_str, ":"); n == 1 {
+		// IP4 addr_or_host:port
+		i := strings.last_index(endpoint_str, ":")
+		assert(i != -1)
+
+		addr_or_host = endpoint_str[:i]
+		port, ok = strconv.parse_int(endpoint_str[i+1:], 10)
+
+		if port > 65535 {
+			ok = false
+		}
+		return
+	} else if n > 1 {
+		// IP6 address without port
+	}
+
+	// No port
+	addr_or_host = endpoint_str
+	port = 0
+	ok = true
+	return
+}
+
+// Joins an address or hostname with a port.
+join_port :: proc(address_or_host: string, port: int, allocator := context.allocator) -> string {
+	addr_or_host, _, ok := split_port(address_or_host)
+	if !ok do return addr_or_host
+
+	b := strings.make_builder(allocator)
+
+	addr := parse_address(addr_or_host)
+	if addr == nil {
+		// hostname
+		fmt.sbprintf(&b, "%v:%v", addr_or_host, port)
+	} else {
+		switch in addr {
+		case IP4_Address:
+			fmt.sbprintf(&b, "%v:%v", address_to_string(addr), port)
+		case IP6_Address:
+			fmt.sbprintf(&b, "[%v]:%v", address_to_string(addr), port)
+		}
+	}
+	return strings.to_string(b)
+}
+
+
+
+// TODO(tetra): Do we need this?
+map_to_ip6 :: proc(addr: Address) -> Address {
+	if addr6, ok := addr.(IP6_Address); ok {
+		return addr6
+	}
+	addr4 := addr.(IP4_Address)
+	addr4_u16 := transmute([2]u16be) addr4
+	addr6: IP6_Address
+	addr6[4] = 0xffff
+	copy(addr6[5:], addr4_u16[:])
+	return addr6
+}
+
+/*
+	Returns a temporarily-allocated string representation of the address.
+
+	See RFC 5952 section 4 for IPv6 representation recommendations.
+*/
+address_to_string :: proc(addr: Address, allocator := context.temp_allocator) -> string {
+	b := strings.make_builder(allocator)
+	switch v in addr {
+	case IP4_Address:
+		fmt.sbprintf(&b, "%v.%v.%v.%v", v[0], v[1], v[2], v[3])
+	case IP6_Address:
+		/*
+			First find the longest run of zeroes.
+		*/
+		Zero_Run :: struct {
+			start: int,
+			end:   int,
+		}
+
+		/*
+			We're dealing with 0-based indices, appropriately enough for runs of zeroes.
+			Still, it means we need to initialize runs with some value outside of the possible range.
+		*/
+		run  := Zero_Run{-1, -1}
+		best := Zero_Run{-1, -1}
+
+		addr := transmute([8]u16be)v
+
+		last := u16be(1)
+		for val, i in addr {
+			/*
+				If we encounter adjacent zeroes, then start a new run if not already in one.
+				Also remember the rightmost index regardless, because it'll be the new
+				frontier of both new and existing runs.
+			*/
+			if last == 0 && val == 0 {
+				run.end = i
+				if run.start == -1 {
+					run.start = i - 1
+				}
+			}
+
+			/*
+				If we're in a run check if its length is better than the best recorded so far.
+				If so, update the best run's start and end.
+			*/
+			if run.start != -1 {
+				length_to_beat := best.end - best.start
+				length         := run.end  - run.start
+
+				if length > length_to_beat {
+					best = run
+				}
+			}
+
+			/*
+				If we were in a run, this is where we reset it.
+			*/
+			if val != 0 {
+				run = {-1, -1}
+			}
+
+			last = val
+		}
+
+		for val, i in addr {
+			if best.start == i || best.end == i {
+				/*
+					For the left and right side of the best zero run, print a `:`.
+				*/
+				fmt.sbprint(&b, ":")
+			} else if i < best.start {
+				/*
+					If we haven't made it to the best run yet, print the digit.
+					Make sure we only print a `:` after the digit if it's not
+					immediately followed by the run's own leftmost `:`.
+				*/
+				fmt.sbprintf(&b, "%x", val)
+				if i < best.start - 1 {
+					fmt.sbprintf(&b, ":")
+				}
+			} else if i > best.end {
+				/*
+					If there are any digits after the zero run, print them.
+					But don't print the `:` at the end of the IP number.
+				*/
+				fmt.sbprintf(&b, "%x", val)
+				if i != 7 {
+					fmt.sbprintf(&b, ":")
+				}
+			}
+		}
+	}
+	return strings.to_string(b)
+}
+
+// Returns a temporarily-allocated string representation of the endpoint.
+// If there's a port, uses the `[address]:port` format.
+endpoint_to_string :: proc(ep: Endpoint, allocator := context.temp_allocator) -> string {
+	if ep.port == 0 {
+		return address_to_string(ep.address, allocator)
+	} else {
+		s := address_to_string(ep.address, context.temp_allocator)
+		b := strings.make_builder(allocator)
+		switch a in ep.address {
+		case IP4_Address:  fmt.sbprintf(&b, "%v:%v",   s, ep.port)
+		case IP6_Address:  fmt.sbprintf(&b, "[%v]:%v", s, ep.port)
+		}
+		return strings.to_string(b)
+	}
+}
+
+to_string :: proc{address_to_string, endpoint_to_string}
+
+
+family_from_address :: proc(addr: Address) -> Address_Family {
+	switch in addr {
+	case IP4_Address: return .IP4
+	case IP6_Address: return .IP6
+	case:
+		unreachable()
+	}
+}
+family_from_endpoint :: proc(ep: Endpoint) -> Address_Family {
+	return family_from_address(ep.address)
+}
+
+
+Digit_Parse_Base :: enum u8 {
+	Dec  = 0, // No prefix
+	Oct  = 1, // Leading zero
+	Hex  = 2, // 0x prefix
+	IPv6 = 3, // Unprefixed IPv6 piece hex. Can't be used with other bases.
+}
+Digit_Parse_Bases :: bit_set[Digit_Parse_Base; u8]
+DEFAULT_DIGIT_BASES :: Digit_Parse_Bases{.Dec, .Oct, .Hex}
+
+/*
+	Parses a single unsigned number in requested `bases` from `input`.
+	`max_value` represents the maximum allowed value for this number.
+
+	Returns the `value`, the `bytes_consumed` so far, and `ok` to signal success or failure.
+
+	An out-of-range or invalid number will return the accumulated value so far (which can be out of range),
+	the number of bytes consumed leading up the error, and `ok = false`.
+
+	When `.` or `:` are encountered, they'll be considered valid separators and will stop parsing,
+	returning the valid number leading up to it.
+
+	Other non-digit characters are treated as an error.
+
+	Octal numbers are expected to have a leading zero, with no 'o' format specifier.
+	Hexadecimal numbers are expected to be preceded by '0x' or '0X'.
+	Numbers will otherwise be considered to be in base 10.
+*/
+parse_ip_component :: proc(input: string, max_value := u64(max(u32)), bases := DEFAULT_DIGIT_BASES) -> (value: u64, bytes_consumed: int, ok: bool) {
+	/*
+		Default to base 10
+	*/
+	base         := u64(10)
+	input        := input
+
+	/*
+		We keep track of the number of prefix bytes and digit bytes separately.
+		This way if a prefix is consumed and we encounter a separator or the end of the string,
+		the number is only considered valid if at least 1 digit byte has been consumed and the value is within range.
+	*/
+	prefix_bytes := 0
+	digit_bytes  := 0
+
+	/*
+		IPv6 hex bytes are unprefixed and can't be disambiguated from octal or hex unless the digit is out of range.
+		If we got the `.IPv6` option, skip prefix scanning and other flags aren't also used.
+	*/
+	if .IPv6 in bases {
+		if bases != {.IPv6} { return } // Must be used on its own.
+		base = 16
+	} else {
+		/*
+			Scan for and consume prefix, if applicable.
+		*/
+		if len(input) >= 2 && input[0] == '0' {
+			if .Hex in bases && (input[1] == 'x' || input[1] == 'X') {
+				base         = 16
+				input        = input[2:]
+				prefix_bytes = 2
+			}
+			if prefix_bytes == 0 && .Oct in bases {
+				base         = 8
+				input        = input[1:]
+				prefix_bytes = 1
+			}
+		}
+	}
+
+	parse_loop: for ch in input {
+		switch ch {
+		case '0'..'7':
+			digit_bytes += 1
+			value = value * base + u64(ch - '0')
+
+		case '8'..'9':
+			digit_bytes += 1
+
+			if base == 8 {
+				/*
+					Out of range for octal numbers.
+				*/
+				return value, digit_bytes + prefix_bytes, false
+			}
+			value = value * base + u64(ch - '0')
+
+		case 'a'..'f':
+			digit_bytes += 1
+
+			if base == 8 || base == 10 {
+				/*
+					Out of range for octal and decimal numbers.
+				*/
+				return value, digit_bytes + prefix_bytes, false
+			}
+			value = value * base + (u64(ch - 'a') + 10)
+
+		case 'A'..'F':
+			digit_bytes += 1
+
+			if base == 8 || base == 10 {
+				/*
+					Out of range for octal and decimal numbers.
+				*/
+				return value, digit_bytes + prefix_bytes, false
+			}
+			value = value * base + (u64(ch - 'A') + 10)
+
+		case '.', ':':
+			/*
+				Number separator. Return early.
+				We don't need to check if the number is in range.
+				We do that each time through the loop.
+			*/
+			break parse_loop
+
+		case:
+			/*
+				Invalid character encountered.
+			*/
+			return value, digit_bytes + prefix_bytes, false
+		}
+
+		if value > max_value {
+			/*
+				Out-of-range number.
+			*/
+			return value, digit_bytes + prefix_bytes, false
+		}
+	}
+
+	/*
+		If we consumed at least 1 digit byte, `value` *should* continue a valid number in an appropriate base in the allowable range.
+	*/
+	return value, digit_bytes + prefix_bytes, digit_bytes >= 1
+}

+ 71 - 0
core/net/addr_darwin.odin

@@ -0,0 +1,71 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:os"
+
+// Returns an address for each interface that can be bound to.
+get_network_interfaces :: proc() -> []Address {
+	// TODO
+	return nil
+}
+
+@private
+endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: os.SOCKADDR_STORAGE_LH) {
+	switch a in ep.address {
+	case IP4_Address:
+		(^os.sockaddr_in)(&sockaddr)^ = os.sockaddr_in {
+			sin_port = u16be(ep.port),
+			sin_addr = transmute(os.in_addr) a,
+			sin_family = u8(os.AF_INET),
+			sin_len = size_of(os.sockaddr_in),
+		}
+		return
+	case IP6_Address:
+		(^os.sockaddr_in6)(&sockaddr)^ = os.sockaddr_in6 {
+			sin6_port = u16be(ep.port),
+			sin6_addr = transmute(os.in6_addr) a,
+			sin6_family = u8(os.AF_INET6),
+			sin6_len = size_of(os.sockaddr_in6),
+		}
+		return
+	}
+	unreachable()
+}
+
+@private
+sockaddr_to_endpoint :: proc(native_addr: ^os.SOCKADDR_STORAGE_LH) -> (ep: Endpoint) {
+	switch native_addr.family {
+	case u8(os.AF_INET):
+		addr := cast(^os.sockaddr_in) native_addr
+		port := int(addr.sin_port)
+		ep = Endpoint {
+			address = IP4_Address(transmute([4]byte) addr.sin_addr),
+			port = port,
+		}
+	case u8(os.AF_INET6):
+		addr := cast(^os.sockaddr_in6) native_addr
+		port := int(addr.sin6_port)
+		ep = Endpoint {
+			address = IP6_Address(transmute([8]u16be) addr.sin6_addr),
+			port = port,
+		}
+	case:
+		panic("native_addr is neither IP4 or IP6 address")
+	}
+	return
+}

+ 93 - 0
core/net/addr_linux.odin

@@ -0,0 +1,93 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:os"
+
+// Returns an address for each interface that can be bound to.
+get_network_interfaces :: proc() -> []Address {
+	// TODO
+	return nil
+}
+
+@private
+endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: os.SOCKADDR_STORAGE_LH) {
+	switch a in ep.address {
+	case IP4_Address:
+		(^os.sockaddr_in)(&sockaddr)^ = os.sockaddr_in {
+			sin_port = u16be(ep.port),
+			sin_addr = transmute(os.in_addr) a,
+			sin_family = u16(os.AF_INET),
+		}
+		return
+	case IP6_Address:
+		(^os.sockaddr_in6)(&sockaddr)^ = os.sockaddr_in6 {
+			sin6_port = u16be(ep.port),
+			sin6_addr = transmute(os.in6_addr) a,
+			sin6_family = u16(os.AF_INET6),
+		}
+		return
+	}
+	unreachable()
+}
+
+sockaddr_storage_to_endpoint :: proc(native_addr: ^os.SOCKADDR_STORAGE_LH) -> (ep: Endpoint) {
+	switch native_addr.ss_family {
+	case u16(os.AF_INET):
+		addr := cast(^os.sockaddr_in) native_addr
+		port := int(addr.sin_port)
+		ep = Endpoint {
+			address = IP4_Address(transmute([4]byte) addr.sin_addr),
+			port = port,
+		}
+	case u16(os.AF_INET6):
+		addr := cast(^os.sockaddr_in6) native_addr
+		port := int(addr.sin6_port)
+		ep = Endpoint {
+			address = IP6_Address(transmute([8]u16be) addr.sin6_addr),
+			port = port,
+		}
+	case:
+		panic("native_addr is neither IP4 or IP6 address")
+	}
+	return
+}
+
+sockaddr_basic_to_endpoint :: proc(native_addr: ^os.SOCKADDR) -> (ep: Endpoint) {
+	switch native_addr.sa_family {
+	case u16(os.AF_INET):
+		addr := cast(^os.sockaddr_in) native_addr
+		port := int(addr.sin_port)
+		ep = Endpoint {
+			address = IP4_Address(transmute([4]byte) addr.sin_addr),
+			port = port,
+		}
+	case u16(os.AF_INET6):
+		addr := cast(^os.sockaddr_in6) native_addr
+		port := int(addr.sin6_port)
+		ep = Endpoint {
+			address = IP6_Address(transmute([8]u16be) addr.sin6_addr),
+			port = port,
+		}
+	case:
+		//panic("native_addr is neither IP4 or IP6 address")
+		return {}
+	}
+	return
+}
+
+sockaddr_to_endpoint :: proc { sockaddr_basic_to_endpoint, sockaddr_storage_to_endpoint }

+ 69 - 0
core/net/addr_openbsd.odin

@@ -0,0 +1,69 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:os"
+
+// Returns an address for each interface that can be bound to.
+get_network_interfaces :: proc() -> []Address {
+	// TODO
+	return nil
+}
+
+@private
+endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: os.SOCKADDR_STORAGE_LH) {
+	switch a in ep.address {
+	case IP4_Address:
+		(^os.sockaddr_in)(&sockaddr)^ = os.sockaddr_in {
+			sin_port = u16be(ep.port),
+			sin_addr = transmute(os.in_addr) a,
+			sin_family = u8(os.AF_INET),
+		}
+		return
+	case IP6_Address:
+		(^os.sockaddr_in6)(&sockaddr)^ = os.sockaddr_in6 {
+			sin6_port = u16be(ep.port),
+			sin6_addr = transmute(os.in6_addr) a,
+			sin6_family = u8(os.AF_INET6),
+		}
+		return
+	}
+	unreachable()
+}
+
+@private
+sockaddr_to_endpoint :: proc(native_addr: ^os.SOCKADDR_STORAGE_LH) -> (ep: Endpoint) {
+	switch native_addr.ss_family {
+	case u8(os.AF_INET):
+		addr := cast(^os.sockaddr_in)native_addr
+		port := int(addr.sin_port)
+		ep = Endpoint {
+			address = IP4_Address(transmute([4]byte) addr.sin_addr),
+			port = port,
+		}
+	case u8(os.AF_INET6):
+		addr := cast(^os.sockaddr_in6)native_addr
+		port := int(addr.sin6_port)
+		ep = Endpoint {
+			address = IP6_Address(transmute([8]u16be) addr.sin6_addr),
+			port = port,
+		}
+	case:
+		panic("native_addr is neither IP4 or IP6 address")
+	}
+	return
+}

+ 69 - 0
core/net/addr_windows.odin

@@ -0,0 +1,69 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import win "core:sys/windows"
+
+// Returns an address for each interface that can be bound to.
+get_network_interfaces :: proc() -> []Address {
+	// TODO
+	return nil
+}
+
+@private
+endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: win.SOCKADDR_STORAGE_LH) {
+	switch a in ep.address {
+	case IP4_Address:
+		(^win.sockaddr_in)(&sockaddr)^ = win.sockaddr_in {
+			sin_port = u16be(win.USHORT(ep.port)),
+			sin_addr = transmute(win.in_addr) a,
+			sin_family = u16(win.AF_INET),
+		}
+		return
+	case IP6_Address:
+		(^win.sockaddr_in6)(&sockaddr)^ = win.sockaddr_in6 {
+			sin6_port = u16be(win.USHORT(ep.port)),
+			sin6_addr = transmute(win.in6_addr) a,
+			sin6_family = u16(win.AF_INET6),
+		}
+		return
+	}
+	unreachable()
+}
+
+@private
+sockaddr_to_endpoint :: proc(native_addr: ^win.SOCKADDR_STORAGE_LH) -> (ep: Endpoint) {
+	switch native_addr.ss_family {
+	case u16(win.AF_INET):
+		addr := cast(^win.sockaddr_in) native_addr
+		port := int(addr.sin_port)
+		ep = Endpoint {
+			address = IP4_Address(transmute([4]byte) addr.sin_addr),
+			port = port,
+		}
+	case u16(win.AF_INET6):
+		addr := cast(^win.sockaddr_in6) native_addr
+		port := int(addr.sin6_port)
+		ep = Endpoint {
+			address = IP6_Address(transmute([8]u16be) addr.sin6_addr),
+			port = port,
+		}
+	case:
+		panic("native_addr is neither IP4 or IP6 address")
+	}
+	return
+}

+ 437 - 0
core/net/common.odin

@@ -0,0 +1,437 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+
+	This file collects structs, enums and settings applicable to the entire package in one handy place.
+	Platform-specific ones can be found in their respective `*_windows.odin` and similar files.
+*/
+package net
+
+import "core:runtime"
+
+/*
+	TUNEABLES
+*/
+
+/*
+	Determines the default value for whether dial_tcp() and accept_tcp() will set TCP_NODELAY on the new
+	socket, and the client socket, respectively.
+	This can also be set on a per-socket basis using the 'options' optional parameter to those procedures.
+
+	When TCP_NODELAY is set, data will be sent out to the peer as quickly as possible, rather than being
+	coalesced into fewer network packets.
+
+	This makes the networking layer more eagerly send data when you ask it to,
+	which can reduce latency by up to 200ms.
+
+	This does mean that a lot of small writes will negatively effect throughput however,
+	since the Nagle algorithm will be disabled, and each write becomes one
+	IP packet. This will increase traffic by a factor of 40, with IP and TCP
+	headers for each payload.
+
+	However, you can avoid this by buffering things up yourself if you wish to send a lot of
+	short data chunks, when TCP_NODELAY is enabled on that socket.
+*/
+ODIN_NET_TCP_NODELAY_DEFAULT :: #config(ODIN_NET_TCP_NODELAY_DEFAULT, true)
+
+/*
+	See also top of `dns.odin` for DNS configuration.
+*/
+
+/*
+	COMMON DEFINITIONS
+*/
+
+Maybe :: runtime.Maybe
+
+General_Error :: enum {
+	Unable_To_Enumerate_Network_Interfaces = 1,
+}
+
+/*
+	`Platform_Error` is used to wrap errors returned by the different platforms that defy translation into a common error.
+*/
+Platform_Error :: enum u32 {}
+
+/*
+	NOTE(tetra): Enums in Network_Error should not have a named zero value.
+	If you have a proc that returns an enum with an Ok=0 value, using or_return from the callsite, when the caller returns a union, works as expected.
+	However, if that proc returns the union directly, returning the Ok value will NOT work with the caller's or_return, as it will treat Error{.Ok} as != nil, and early-return with it.
+
+	The approach currently taken to avoid this is:
+	- Remove the named zero values for the enums
+	- Use the union everywhere
+*/
+Network_Error :: union {
+	General_Error,
+	Platform_Error,
+	Create_Socket_Error,
+	Dial_Error,
+	Listen_Error,
+	Accept_Error,
+	Bind_Error,
+	TCP_Send_Error,
+	UDP_Send_Error,
+	TCP_Recv_Error,
+	UDP_Recv_Error,
+	Shutdown_Error,
+	Socket_Option_Error,
+	Parse_Endpoint_Error,
+	Resolve_Error,
+	DNS_Error,
+}
+
+
+Resolve_Error :: enum {
+	Unable_To_Resolve = 1,
+}
+
+DNS_Error :: enum {
+	Invalid_Hostname_Error = 1,
+	Invalid_Hosts_Config_Error,
+	Invalid_Resolv_Config_Error,
+	Connection_Error,
+	Server_Error,
+	System_Error,
+}
+
+/*
+	SOCKET OPTIONS & DEFINITIONS
+*/
+
+TCP_Options :: struct {
+	no_delay: bool,
+}
+
+default_tcp_options := TCP_Options {
+	no_delay = ODIN_NET_TCP_NODELAY_DEFAULT,
+}
+
+/*
+	To allow freely using `Socket` in your own data structures in a cross-platform manner,
+	we treat it as a handle large enough to accomodate OS-specific notions of socket handles.
+
+	The platform code will perform the cast so you don't have to.
+*/
+Socket     :: distinct i64
+
+TCP_Socket :: distinct Socket
+UDP_Socket :: distinct Socket
+
+Socket_Protocol :: enum {
+	TCP,
+	UDP,
+}
+
+Any_Socket :: union {
+	TCP_Socket,
+	UDP_Socket,
+}
+
+/*
+	ADDRESS DEFINITIONS
+*/
+
+IP4_Address :: distinct [4]u8
+IP6_Address :: distinct [8]u16be
+Address :: union {IP4_Address, IP6_Address}
+
+IP4_Loopback := IP4_Address{127, 0, 0, 1}
+IP6_Loopback := IP6_Address{0, 0, 0, 0, 0, 0, 0, 1}
+
+IP4_Any := IP4_Address{}
+IP6_Any := IP6_Address{}
+
+Endpoint :: struct {
+	address: Address,
+	port:    int,
+}
+
+Address_Family :: enum {
+	IP4,
+	IP6,
+}
+
+Netmask :: distinct Address
+
+/*
+	INTERFACE / LINK STATE
+*/
+Network_Interface :: struct {
+	adapter_name:     string, // On Windows this is a GUID that we could parse back into its u128 for more compact storage.
+	friendly_name:    string,
+	description:      string,
+	dns_suffix:       string,
+
+	physical_address: string, // MAC address, etc.
+	mtu:              u32,
+
+	unicast:          [dynamic]Lease,
+	multicast:        [dynamic]Address,
+	anycast:          [dynamic]Address,
+
+	gateways:         [dynamic]Address,
+	dhcp_v4:          Address,
+	dhcp_v6:          Address,
+
+	tunnel_type:      Tunnel_Type,
+
+	link: struct {
+		state:          Link_State,
+		transmit_speed: u64,
+		receive_speed:  u64,
+	},
+}
+
+/*
+	Empty bit set is unknown state.
+*/
+Link_States :: enum u32 {
+	Up               = 1,
+	Down             = 2,
+	Testing          = 3,
+	Dormant          = 4,
+	Not_Present      = 5,
+	Lower_Layer_Down = 6,
+	Loopback         = 7,
+}
+Link_State :: bit_set[Link_States; u32]
+
+Lease :: struct {
+	address:  Address,
+	netmask:  Netmask,
+	lifetime: struct {
+		valid:     u32,
+		preferred: u32,
+		lease:     u32,
+	},
+	origin: struct {
+		prefix: Prefix_Origin,
+		suffix: Suffix_Origin,
+	},
+	address_duplication: Address_Duplication,
+}
+
+Tunnel_Type :: enum i32 {
+	None         = 0,
+	Other        = 1,
+	Direct       = 2,
+	IPv4_To_IPv6 = 11,
+	ISA_TAP      = 13,
+	Teredo       = 14,
+	IP_HTTPS     = 15,
+}
+
+Prefix_Origin :: enum i32 {
+	Other                = 0,
+	Manual               = 1,
+	Well_Known           = 2,
+	DHCP                 = 3,
+	Router_Advertisement = 4,
+	Unchanged            = 16,
+}
+
+Suffix_Origin :: enum i32 {
+	Other                = 0,
+	Manual               = 1,
+	Well_Known           = 2,
+	DHCP                 = 3,
+	Link_Layer_Address   = 4,
+	Random               = 5,
+	Unchanged            = 16,
+}
+
+Address_Duplication :: enum i32 {
+	Invalid    = 0,
+	Tentative  = 1,
+	Duplicate  = 2,
+	Deprecated = 3,
+	Preferred  = 4,
+}
+
+/*
+	DNS DEFINITIONS
+*/
+
+DNS_Configuration :: struct {
+	/*
+		Configuration files.
+	*/
+	resolv_conf: string,
+	hosts_file:  string,
+
+	// TODO: Allow loading these up with `reload_configuration()` call or the like so we don't have to do it each call.
+	name_servers:       []Endpoint,
+	hosts_file_entries: []DNS_Record,
+}
+
+DNS_Record_Type :: enum u16 {
+	DNS_TYPE_A     = 0x1,  // IP4 address.
+	DNS_TYPE_NS    = 0x2,  // IP6 address.
+	DNS_TYPE_CNAME = 0x5,  // Another host name.
+	DNS_TYPE_MX    = 0xf,  // Arbitrary binary data or text.
+	DNS_TYPE_AAAA  = 0x1c, // Address of a name (DNS) server.
+	DNS_TYPE_TEXT  = 0x10, // Address and preference priority of a mail exchange server.
+	DNS_TYPE_SRV   = 0x21, // Address, port, priority, and weight of a host that provides a particular service.
+
+	IP4            = DNS_TYPE_A,
+	IP6            = DNS_TYPE_AAAA,
+	CNAME          = DNS_TYPE_CNAME,
+	TXT            = DNS_TYPE_TEXT,
+	NS             = DNS_TYPE_NS,
+	MX             = DNS_TYPE_MX,
+	SRV            = DNS_TYPE_SRV,
+}
+
+/*
+	Base DNS Record. All DNS responses will carry a hostname and TTL (time to live) field.
+*/
+DNS_Record_Base :: struct {
+	record_name: string,
+	ttl_seconds: u32,    	// The time in seconds that this service will take to update, after the record is updated.
+}
+
+/*
+	An IP4 address that the domain name maps to. There can be any number of these.
+*/
+DNS_Record_IP4 :: struct {
+	using base: DNS_Record_Base,
+	address:    IP4_Address,
+}
+
+/*
+	An IPv6 address that the domain name maps to. There can be any number of these.
+*/
+DNS_Record_IP6 :: struct {
+	using base: DNS_Record_Base,
+	address:    IP6_Address,
+}
+
+/*
+	Another domain name that the domain name maps to.
+	Domains can be pointed to another domain instead of directly to an IP address.
+	`get_dns_records` will recursively follow these if you request this type of record.
+*/
+DNS_Record_CNAME :: struct {
+	using base: DNS_Record_Base,
+	host_name:  string,
+}
+
+/*
+	Arbitrary string data that is associated with the domain name.
+	Commonly of the form `key=value` to be parsed, though there is no specific format for them.
+	These can be used for any purpose.
+*/
+DNS_Record_TXT :: struct {
+	using base: DNS_Record_Base,
+	value:      string,
+}
+
+/*
+	Domain names of other DNS servers that are associated with the domain name.
+	TODO(tetra): Expand on what these records are used for, and when you should use pay attention to these.
+*/
+DNS_Record_NS :: struct {
+	using base: DNS_Record_Base,
+	host_name:  string,
+}
+
+// Domain names for email servers that are associated with the domain name.
+// These records also have values which ranks them in the order they should be preferred. Lower is more-preferred.
+DNS_Record_MX :: struct {
+	using base: DNS_Record_Base,
+	host_name:  string,
+	preference: int,
+}
+
+// An endpoint for a service that is available through the domain name.
+// This is the way to discover the services that a domain name provides.
+//
+// Clients MUST attempt to contact the host with the lowest priority that they can reach.
+// If two hosts have the same priority, they should be contacted in the order according to their weight.
+// Hosts with larger weights should have a proportionally higher chance of being contacted by clients.
+// A weight of zero indicates a very low weight, or, when there is no choice (to reduce visual noise).
+//
+// The host may be "." to indicate that it is "decidedly not available" on this domain.
+DNS_Record_SRV :: struct {
+	// base contains the full name of this record.
+	// e.g: _sip._tls.example.com
+	using base: DNS_Record_Base,
+
+	// The hostname or address where this service can be found.
+	target:        string,
+	// The port on which this service can be found.
+	port:          int,
+
+	service_name:  string, // NOTE(tetra): These are substrings of 'record_name'
+	protocol_name: string, // NOTE(tetra): These are substrings of 'record_name'
+
+	// Lower is higher priority
+	priority:      int,
+	// Relative weight of this host compared to other of same priority; the chance of using this host should be proporitional to this weight.
+	// The number of seconds that it will take to update the record.
+	weight:        int,
+}
+
+DNS_Record :: union {
+	DNS_Record_IP4,
+	DNS_Record_IP6,
+	DNS_Record_CNAME,
+	DNS_Record_TXT,
+	DNS_Record_NS,
+	DNS_Record_MX,
+	DNS_Record_SRV,
+}
+
+DNS_Response_Code :: enum u16be {
+	No_Error,
+	Format_Error,
+	Server_Failure,
+	Name_Error,
+	Not_Implemented,
+	Refused,
+}
+
+DNS_Query :: enum u16be {
+	Host_Address              = 1,
+	Authoritative_Name_Server = 2,
+	Mail_Destination          = 3,
+	Mail_Forwarder            = 4,
+	CNAME                     = 5,
+	All                       = 255,
+}
+
+DNS_Header :: struct {
+	id:                     u16be,
+	is_response:            bool,
+	opcode:                 u16be,
+	is_authoritative:       bool,
+	is_truncated:           bool,
+	is_recursion_desired:   bool,
+	is_recursion_available: bool,
+	response_code:          DNS_Response_Code,
+}
+
+DNS_Record_Header :: struct #packed {
+	type:   u16be,
+	class:  u16be,
+	ttl:    u32be,
+	length: u16be,
+}
+
+DNS_Host_Entry :: struct {
+	name: string,
+	addr: Address,
+}

+ 873 - 0
core/net/dns.odin

@@ -0,0 +1,873 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:mem"
+import "core:strings"
+import "core:time"
+import "core:os"
+
+/*
+	Default configuration for DNS resolution.
+*/
+when ODIN_OS == .Windows {
+	getenv :: proc(key: string) -> (val: string) {
+		return os.get_env(key)
+	}
+
+	DEFAULT_DNS_CONFIGURATION :: DNS_Configuration{
+		resolv_conf        = "",
+		hosts_file         = "%WINDIR%\\system32\\drivers\\etc\\hosts",
+	}
+} else when ODIN_OS == .Linux || ODIN_OS == .Darwin || ODIN_OS == .OpenBSD {
+	getenv :: proc(key: string) -> (val: string) {
+		val, _ = os.getenv(key)
+		return
+	}
+
+	DEFAULT_DNS_CONFIGURATION :: DNS_Configuration{
+		resolv_conf        = "/etc/resolv.conf",
+		hosts_file         = "/etc/hosts",
+	}
+} else {
+	#panic("Please add a configuration for this OS.")
+}
+
+@(init)
+init_dns_configuration :: proc() {
+	/*
+		Resolve %ENVIRONMENT% placeholders in their paths.
+	*/
+	dns_configuration.resolv_conf, _ = replace_environment_path(dns_configuration.resolv_conf)
+	dns_configuration.hosts_file,  _ = replace_environment_path(dns_configuration.hosts_file)
+}
+
+destroy_dns_configuration :: proc() {
+	delete(dns_configuration.resolv_conf)
+	delete(dns_configuration.hosts_file)
+}
+
+dns_configuration := DEFAULT_DNS_CONFIGURATION
+
+/*
+	Always allocates for consistency.
+*/
+replace_environment_path :: proc(path: string, allocator := context.allocator) -> (res: string, ok: bool) {
+	/*
+		Nothing to replace. Return a clone of the original.
+	*/
+	if strings.count(path, "%") != 2 {
+		return strings.clone(path), true
+	}
+
+	left  := strings.index(path, "%") + 1
+	assert(left > 0 && left <= len(path)) // should be covered by there being two %
+
+	right := strings.index(path[left:], "%") + 1
+	assert(right > 0 && right <= len(path)) // should be covered by there being two %
+
+	env_key := path[left: right]
+	env_val := getenv(env_key)
+	defer delete(env_val)
+
+	res, _ = strings.replace(path, path[left - 1: right + 1], env_val, 1)
+
+	return res, true
+}
+
+
+/*
+	Resolves a hostname to exactly one IP4 and IP6 endpoint.
+	It's then up to you which one you use.
+	Note that which address you use to open a socket, determines the type of the socket you get.
+
+	Returns `ok=false` if the host name could not be resolved to any endpoints.
+
+	Returned endpoints have the same port as provided in the string, or 0 if absent.
+	If you want to use a specific port, just modify the field after the call to this procedure.
+
+	If the hostname part of the endpoint is actually a string representation of an IP address, DNS resolution will be skipped.
+	This allows you to pass both strings like "example.com:9000" and "1.2.3.4:9000" to this function end reliably get
+	back an endpoint in both cases.
+*/
+resolve :: proc(hostname_and_maybe_port: string) -> (ep4, ep6: Endpoint, err: Network_Error) {
+	target := parse_hostname_or_endpoint(hostname_and_maybe_port) or_return
+	switch t in target {
+	case Endpoint:
+		// NOTE(tetra): The hostname was actually an IP address; nothing to resolve, so just return it.
+		switch in t.address {
+		case IP4_Address: ep4 = t
+		case IP6_Address: ep6 = t
+		case:             unreachable()
+		}
+		return
+
+	case Host:
+		err4, err6: Network_Error = ---, ---
+		ep4, err4 = resolve_ip4(t.hostname)
+		ep6, err6 = resolve_ip6(t.hostname)
+		if err4 != nil && err6 != nil {
+			err = err4
+		}
+		return
+	}
+	unreachable()
+}
+resolve_ip4 :: proc(hostname_and_maybe_port: string) -> (ep4: Endpoint, err: Network_Error) {
+	target := parse_hostname_or_endpoint(hostname_and_maybe_port) or_return
+	switch t in target {
+	case Endpoint:
+		// NOTE(tetra): The hostname was actually an IP address; nothing to resolve, so just return it.
+		switch in t.address {
+		case IP4_Address:
+			return t, nil
+		case IP6_Address:
+			err = .Unable_To_Resolve
+			return
+		}
+	case Host:
+		recs, _ := get_dns_records_from_os(t.hostname, .IP4, context.temp_allocator)
+		if len(recs) == 0 {
+			err = .Unable_To_Resolve
+			return
+		}
+		ep4 = {
+			address = recs[0].(DNS_Record_IP4).address,
+			port = t.port,
+		}
+		return
+	}
+	unreachable()
+}
+resolve_ip6 :: proc(hostname_and_maybe_port: string) -> (ep6: Endpoint, err: Network_Error) {
+	target := parse_hostname_or_endpoint(hostname_and_maybe_port) or_return
+	switch t in target {
+	case Endpoint:
+		// NOTE(tetra): The hostname was actually an IP address; nothing to resolve, so just return it.
+		switch in t.address {
+		case IP4_Address:
+			err = .Unable_To_Resolve
+			return
+		case IP6_Address:
+			return t, nil
+		}
+	case Host:
+		recs, _ := get_dns_records_from_os(t.hostname, .IP6, context.temp_allocator)
+		if len(recs) == 0 {
+			err = .Unable_To_Resolve
+			return
+		}
+		ep6 = {
+			address = recs[0].(DNS_Record_IP6).address,
+			port = t.port,
+		}
+		return
+	}
+	unreachable()
+}
+
+/*
+	`get_dns_records` uses OS-specific methods to query DNS records.
+*/
+when ODIN_OS == .Windows {
+	get_dns_records_from_os :: get_dns_records_windows
+} else when ODIN_OS == .Linux || ODIN_OS == .Darwin || ODIN_OS == .OpenBSD {
+	get_dns_records_from_os :: get_dns_records_unix
+} else {
+	#panic("get_dns_records_from_os not implemented on this OS")
+}
+
+/*
+	A generic DNS client usable on any platform.
+	Performs a recursive DNS query for records of a particular type for the hostname.
+
+	NOTE: This procedure instructs the DNS resolver to recursively perform CNAME requests on our behalf,
+	meaning that DNS queries for a hostname will resolve through CNAME records until an
+	IP address is reached.
+*/
+get_dns_records_from_nameservers :: proc(hostname: string, type: DNS_Record_Type, name_servers: []Endpoint, host_overrides: []DNS_Record, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) {
+	context.allocator = allocator
+
+	if type != .SRV {
+		// NOTE(tetra): 'hostname' can contain underscores when querying SRV records
+		ok := validate_hostname(hostname)
+		if !ok {
+			return nil, .Invalid_Hostname_Error
+		}
+	}
+
+	hdr := DNS_Header{
+		id = 0,
+		is_response = false,
+		opcode = 0,
+		is_authoritative = false,
+		is_truncated = false,
+		is_recursion_desired = true,
+		is_recursion_available = false,
+		response_code = DNS_Response_Code.No_Error,
+	}
+
+	id, bits := pack_dns_header(hdr)
+	dns_hdr := [6]u16be{}
+	dns_hdr[0] = id
+	dns_hdr[1] = bits
+	dns_hdr[2] = 1
+
+	dns_query := [2]u16be{ u16be(type), 1 }
+
+	output := [(size_of(u16be) * 6) + NAME_MAX + (size_of(u16be) * 2)]u8{}
+	b := strings.builder_from_slice(output[:])
+
+	strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_hdr[:]))
+	ok := encode_hostname(&b, hostname)
+	if !ok {
+		return nil, .Invalid_Hostname_Error
+	}
+	strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_query[:]))
+
+	dns_packet := output[:strings.builder_len(b)]
+
+	dns_response_buf := [4096]u8{}
+	dns_response: []u8
+	for name_server in name_servers {
+		conn, sock_err := make_unbound_udp_socket(family_from_endpoint(name_server))
+		if sock_err != nil {
+			return nil, .Connection_Error
+		}
+		defer close(conn)
+
+		_, send_err := send(conn, dns_packet[:], name_server)
+		if send_err != nil {
+			continue
+		}
+
+		set_err := set_option(conn, .Receive_Timeout, time.Second * 1)
+		if set_err != nil {
+			return nil, .Connection_Error
+		}
+
+		recv_sz, _, recv_err := recv_udp(conn, dns_response_buf[:])
+		if recv_err == UDP_Recv_Error.Timeout {
+			continue
+		} else if recv_err != nil {
+			continue
+		}
+
+		if recv_sz == 0 {
+			continue
+		}
+
+		dns_response = dns_response_buf[:recv_sz]
+
+		rsp, _ok := parse_response(dns_response, type)
+		if !_ok {
+			return nil, .Server_Error
+		}
+
+		if len(rsp) == 0 {
+			continue
+		}
+
+		return rsp[:], nil
+	}
+
+	return
+}
+
+// `records` slice is also destroyed.
+destroy_dns_records :: proc(records: []DNS_Record, allocator := context.allocator) {
+	context.allocator = allocator
+
+	for rec in records {
+		switch r in rec {
+		case DNS_Record_IP4:
+			delete(r.base.record_name)
+
+		case DNS_Record_IP6:
+			delete(r.base.record_name)
+
+		case DNS_Record_CNAME:
+			delete(r.base.record_name)
+			delete(r.host_name)
+
+		case DNS_Record_TXT:
+			delete(r.base.record_name)
+			delete(r.value)
+
+		case DNS_Record_NS:
+			delete(r.base.record_name)
+			delete(r.host_name)
+
+		case DNS_Record_MX:
+			delete(r.base.record_name)
+			delete(r.host_name)
+
+		case DNS_Record_SRV:
+			delete(r.record_name)
+			delete(r.target)
+		}
+	}
+
+	delete(records, allocator)
+}
+
+/*
+	TODO(cloin): Does the DNS Resolver need to recursively hop through CNAMEs to get the IP
+	or is that what recursion desired does? Do we need to handle recursion unavailable?
+	How do we deal with is_authoritative / is_truncated?
+*/
+
+NAME_MAX  :: 255
+LABEL_MAX :: 63
+
+pack_dns_header :: proc(hdr: DNS_Header) -> (id: u16be, bits: u16be) {
+	id = hdr.id
+	bits = hdr.opcode << 1 | u16be(hdr.response_code)
+	if hdr.is_response {
+		bits |= 1 << 15
+	}
+	if hdr.is_authoritative {
+		bits |= 1 << 10
+	}
+	if hdr.is_truncated {
+		bits |= 1 << 9
+	}
+	if hdr.is_recursion_desired {
+		bits |= 1 << 8
+	}
+	if hdr.is_recursion_available {
+		bits |= 1 << 7
+	}
+
+	return id, bits
+}
+
+unpack_dns_header :: proc(id: u16be, bits: u16be) -> (hdr: DNS_Header) {
+	hdr.id = id
+	hdr.is_response            = (bits & (1 << 15)) != 0
+	hdr.opcode                 = (bits >> 11) & 0xF
+	hdr.is_authoritative       = (bits & (1 << 10)) != 0
+	hdr.is_truncated           = (bits & (1 <<  9)) != 0
+	hdr.is_recursion_desired   = (bits & (1 <<  8)) != 0
+	hdr.is_recursion_available = (bits & (1 <<  7)) != 0
+	hdr.response_code = DNS_Response_Code(bits & 0xF)
+
+	return hdr
+}
+
+load_resolv_conf :: proc(resolv_conf_path: string, allocator := context.allocator) -> (name_servers: []Endpoint, ok: bool) {
+	context.allocator = allocator
+
+	res, success := os.read_entire_file_from_filename(resolv_conf_path)
+	if !success {
+		return
+	}
+	defer delete(res)
+	resolv_str := string(res)
+
+	_name_servers := make([dynamic]Endpoint, 0, allocator)
+	for line in strings.split_lines_iterator(&resolv_str) {
+		if len(line) == 0 || line[0] == '#' {
+			continue
+		}
+
+		id_str := "nameserver"
+		if strings.compare(line[:len(id_str)], id_str) != 0 {
+			continue
+		}
+
+		server_ip_str := strings.trim_left_space(line[len(id_str):])
+		if len(server_ip_str) == 0 {
+			continue
+		}
+
+		addr := parse_address(server_ip_str)
+		endpoint := Endpoint{
+			addr,
+			53,
+		}
+		append(&_name_servers, endpoint)
+	}
+
+	return _name_servers[:], true
+}
+
+load_hosts :: proc(hosts_file_path: string, allocator := context.allocator) -> (hosts: []DNS_Host_Entry, ok: bool) {
+	context.allocator = allocator
+
+	res, success := os.read_entire_file_from_filename(hosts_file_path, allocator)
+	if !success {
+		return
+	}
+	defer delete(res)
+
+	_hosts := make([dynamic]DNS_Host_Entry, 0, allocator)
+	hosts_str := string(res)
+	for line in strings.split_lines_iterator(&hosts_str) {
+		if len(line) == 0 || line[0] == '#' {
+			continue
+		}
+
+		splits := strings.fields(line)
+		defer delete(splits)
+
+		ip_str := splits[0]
+		addr := parse_address(ip_str)
+		if addr == nil {
+			continue
+		}
+
+		for hostname in splits[1:] {
+			if len(hostname) == 0 {
+				continue
+			}
+
+			append(&_hosts, DNS_Host_Entry{hostname, addr})
+		}
+	}
+
+	return _hosts[:], true
+}
+
+/*
+	www.google.com -> 3www6google3com0
+*/
+encode_hostname :: proc(b: ^strings.Builder, hostname: string, allocator := context.allocator) -> (ok: bool) {
+	_hostname := hostname
+	for section in strings.split_iterator(&_hostname, ".") {
+		if len(section) > LABEL_MAX {
+			return
+		}
+
+		strings.write_byte(b, u8(len(section)))
+		strings.write_string(b, section)
+	}
+	strings.write_byte(b, 0)
+
+	return true
+}
+
+skip_hostname :: proc(packet: []u8, start_idx: int, allocator := context.allocator) -> (encode_size: int, ok: bool) {
+	out_size := 0
+
+	cur_idx := start_idx
+	iteration_max := 0
+	top: for cur_idx < len(packet) {
+		if packet[cur_idx] == 0 {
+			out_size += 1
+			break
+		}
+
+		if iteration_max > 255 {
+			return
+		}
+
+		if packet[cur_idx] > 63 && packet[cur_idx] != 0xC0 {
+			return
+		}
+
+		switch packet[cur_idx] {
+		case 0xC0:
+			out_size += 2
+			break top
+		case:
+			label_size := int(packet[cur_idx]) + 1
+			idx2 := cur_idx + label_size
+
+			if idx2 < cur_idx + 1 || idx2 > len(packet) {
+				return
+			}
+
+			out_size += label_size
+			cur_idx = idx2
+		}
+
+		iteration_max += 1
+	}
+
+	if start_idx + out_size > len(packet) {
+		return
+	}
+
+	return out_size, true
+}
+
+decode_hostname :: proc(packet: []u8, start_idx: int, allocator := context.allocator) -> (hostname: string, encode_size: int, ok: bool) {
+	output := [NAME_MAX]u8{}
+	b := strings.builder_from_slice(output[:])
+
+	// If you're on level 0, update out_bytes, everything through a pointer
+	// doesn't count towards this hostname's packet length
+
+	// Evaluate tokens to generate the hostname
+	out_size := 0
+	level := 0
+	print_size := 0
+	cur_idx := start_idx
+	iteration_max := 0
+	labels_added := 0
+	for cur_idx < len(packet) {
+		if packet[cur_idx] == 0 {
+
+			if (level == 0) {
+				out_size += 1
+			}
+
+			break
+		}
+
+		if iteration_max > 255 {
+			return
+		}
+
+		if packet[cur_idx] > 63 && packet[cur_idx] != 0xC0 {
+			return
+		}
+
+		switch packet[cur_idx] {
+
+		// This is a offset to more data in the packet, jump to it
+		case 0xC0:
+			pkt := packet[cur_idx:cur_idx+2]
+			val := (^u16be)(raw_data(pkt))^
+			offset := int(val & 0x3FFF)
+			if offset > len(packet) {
+				return
+			}
+
+			cur_idx = offset
+
+			if (level == 0) {
+				out_size += 2
+				level += 1
+			}
+
+		// This is a label, insert it into the hostname
+		case:
+			label_size := int(packet[cur_idx])
+			idx2 := cur_idx + label_size + 1
+			if idx2 < cur_idx + 1 || idx2 > len(packet) {
+				return
+			}
+
+			if print_size + label_size + 1 > NAME_MAX {
+				return
+			}
+
+			if labels_added > 0 {
+				strings.write_byte(&b, '.')
+			}
+			strings.write_bytes(&b, packet[cur_idx+1:idx2])
+			print_size += label_size + 1
+			labels_added += 1
+
+			cur_idx = idx2
+
+			if (level == 0) {
+				out_size += label_size + 1
+			}
+		}
+
+		iteration_max += 1
+	}
+
+	if start_idx + out_size > len(packet) {
+		return
+	}
+
+	return strings.clone(strings.to_string(b), allocator), out_size, true
+}
+
+// Uses RFC 952 & RFC 1123
+validate_hostname :: proc(hostname: string) -> (ok: bool) {
+	if len(hostname) > 255 || len(hostname) == 0 {
+		return
+	}
+
+	if hostname[0] == '-' {
+		return
+	}
+
+	_hostname := hostname
+	for label in strings.split_iterator(&_hostname, ".") {
+		if len(label) > 63 || len(label) == 0 {
+			return
+		}
+
+		for ch in label {
+			switch ch {
+			case:
+				return
+			case 'a'..'z', 'A'..'Z', '0'..'9', '-':
+				continue
+			}
+		}
+	}
+
+	return true
+}
+
+parse_record :: proc(packet: []u8, cur_off: ^int, filter: DNS_Record_Type = nil) -> (record: DNS_Record, ok: bool) {
+	record_buf := packet[cur_off^:]
+
+	srv_record_name, hn_sz := decode_hostname(packet, cur_off^, context.temp_allocator) or_return
+	// TODO(tetra): Not sure what we should call this.
+	// Is it really only used in SRVs?
+	// Maybe some refactoring is required?
+
+	ahdr_sz := size_of(DNS_Record_Header)
+	if len(record_buf) - hn_sz < ahdr_sz {
+		return
+	}
+
+	record_hdr_bytes := record_buf[hn_sz:hn_sz+ahdr_sz]
+	record_hdr := cast(^DNS_Record_Header)raw_data(record_hdr_bytes)
+
+	data_sz := record_hdr.length
+	data_off := cur_off^ + int(hn_sz) + int(ahdr_sz)
+	data := packet[data_off:data_off+int(data_sz)]
+	cur_off^ += int(hn_sz) + int(ahdr_sz) + int(data_sz)
+
+	if u16be(filter) != record_hdr.type {
+		return nil, true
+	}
+
+	_record: DNS_Record
+	#partial switch DNS_Record_Type(record_hdr.type) {
+		case .IP4:
+			if len(data) != 4 {
+				return
+			}
+
+			addr := (^IP4_Address)(raw_data(data))^
+
+			_record = DNS_Record_IP4{
+				base = DNS_Record_Base{
+					record_name = strings.clone(srv_record_name),
+					ttl_seconds = u32(record_hdr.ttl),
+				},
+				address = addr,
+			}
+
+		case .IP6:
+			if len(data) != 16 {
+				return
+			}
+
+			addr := (^IP6_Address)(raw_data(data))^
+
+			_record = DNS_Record_IP6{
+				base = DNS_Record_Base{
+					record_name = strings.clone(srv_record_name),
+					ttl_seconds = u32(record_hdr.ttl),
+				},
+				address = addr,
+			}
+
+		case .CNAME:
+			hostname, _ := decode_hostname(packet, data_off) or_return
+
+			_record = DNS_Record_CNAME{
+				base = DNS_Record_Base{
+					record_name = strings.clone(srv_record_name),
+					ttl_seconds = u32(record_hdr.ttl),
+				},
+				host_name = hostname,
+			}
+
+		case .TXT:
+			_record = DNS_Record_TXT{
+				base = DNS_Record_Base{
+					record_name = strings.clone(srv_record_name),
+					ttl_seconds = u32(record_hdr.ttl),
+				},
+				value = strings.clone(string(data)),
+			}
+
+		case .NS:
+			name, _ := decode_hostname(packet, data_off) or_return
+
+			_record = DNS_Record_NS{
+				base = DNS_Record_Base{
+					record_name = strings.clone(srv_record_name),
+					ttl_seconds = u32(record_hdr.ttl),
+				},
+				host_name = name,
+			}
+
+		case .SRV:
+			if len(data) <= 6 {
+				return
+			}
+
+			priority: u16be = mem.slice_data_cast([]u16be, data)[0]
+			weight:   u16be = mem.slice_data_cast([]u16be, data)[1]
+			port:     u16be = mem.slice_data_cast([]u16be, data)[2]
+			target, _ := decode_hostname(packet, data_off + (size_of(u16be) * 3)) or_return
+
+			// NOTE(tetra): Srv record name should be of the form '_servicename._protocol.hostname'
+			// The record name is the name of the record.
+			// Not to be confused with the _target_ of the record, which is--in combination with the port--what we're looking up
+			// by making this request in the first place.
+
+			// NOTE(Jeroen): Service Name and Protocol Name can probably just be string slices into the record name.
+			// It's already cloned, after all. I wouldn't put them on the temp allocator like this.
+
+			parts := strings.split_n(srv_record_name, ".", 3, context.temp_allocator)
+			if len(parts) != 3 {
+				return
+			}
+			service_name, protocol_name := parts[0], parts[1]
+
+			_record = DNS_Record_SRV{
+				base = DNS_Record_Base{
+					record_name = strings.clone(srv_record_name),
+					ttl_seconds = u32(record_hdr.ttl),
+				},
+				target        = target,
+				service_name  = service_name,
+				protocol_name = protocol_name,
+				priority      = int(priority),
+				weight        = int(weight),
+				port          = int(port),
+			}
+
+		case .MX:
+			if len(data) <= 2 {
+				return
+			}
+
+			preference: u16be = mem.slice_data_cast([]u16be, data)[0]
+			hostname, _ := decode_hostname(packet, data_off + size_of(u16be)) or_return
+
+			_record = DNS_Record_MX{
+				base = DNS_Record_Base{
+					record_name = strings.clone(srv_record_name),
+					ttl_seconds = u32(record_hdr.ttl),
+				},
+				host_name  = hostname,
+				preference = int(preference),
+			}
+
+		case:
+			return
+
+	}
+
+	return _record, true
+}
+
+/*
+	DNS Query Response Format:
+	- DNS_Header (packed)
+	- Query Count
+	- Answer Count
+	- Authority Count
+	- Additional Count
+	- Query[]
+		- Hostname -- encoded
+		- Type
+		- Class
+	- Answer[]
+		- DNS Record Data
+	- Authority[]
+		- DNS Record Data
+	- Additional[]
+		- DNS Record Data
+
+	DNS Record Data:
+	- DNS_Record_Header
+	- Data[]
+*/
+
+parse_response :: proc(response: []u8, filter: DNS_Record_Type = nil, allocator := context.allocator) -> (records: []DNS_Record, ok: bool) {
+	header_size_bytes :: 12
+	if len(response) < header_size_bytes {
+		return
+	}
+
+	_records := make([dynamic]DNS_Record, 0)
+
+	dns_hdr_chunks := mem.slice_data_cast([]u16be, response[:header_size_bytes])
+	hdr := unpack_dns_header(dns_hdr_chunks[0], dns_hdr_chunks[1])
+	if !hdr.is_response {
+		return
+	}
+
+	question_count := int(dns_hdr_chunks[2])
+	if question_count != 1 {
+		return
+	}
+	answer_count := int(dns_hdr_chunks[3])
+	authority_count := int(dns_hdr_chunks[4])
+	additional_count := int(dns_hdr_chunks[5])
+
+	cur_idx := header_size_bytes
+
+	for i := 0; i < question_count; i += 1 {
+		if cur_idx == len(response) {
+			continue
+		}
+
+		dq_sz :: 4
+		hn_sz := skip_hostname(response, cur_idx, context.temp_allocator) or_return
+		dns_query := mem.slice_data_cast([]u16be, response[cur_idx+hn_sz:cur_idx+hn_sz+dq_sz])
+
+		cur_idx += hn_sz + dq_sz
+	}
+
+	for i := 0; i < answer_count; i += 1 {
+		if cur_idx == len(response) {
+			continue
+		}
+
+		rec := parse_record(response, &cur_idx, filter) or_return
+		if rec == nil {
+			continue
+		}
+
+		append(&_records, rec)
+	}
+
+	for i := 0; i < authority_count; i += 1 {
+		if cur_idx == len(response) {
+			continue
+		}
+
+		rec := parse_record(response, &cur_idx, filter) or_return
+		if rec == nil {
+			continue
+		}
+
+		append(&_records, rec)
+	}
+
+	for i := 0; i < additional_count; i += 1 {
+		if cur_idx == len(response) {
+			continue
+		}
+
+		rec := parse_record(response, &cur_idx, filter) or_return
+		if rec == nil {
+			continue
+		}
+
+		append(&_records, rec)
+	}
+
+	return _records[:], true
+}

+ 83 - 0
core/net/dns_unix.odin

@@ -0,0 +1,83 @@
+//+build linux, darwin, freebsd, openbsd, !windows
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:strings"
+
+get_dns_records_unix :: proc(hostname: string, type: DNS_Record_Type, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) {
+	context.allocator = allocator
+
+	if type != .SRV {
+		// NOTE(tetra): 'hostname' can contain underscores when querying SRV records
+		ok := validate_hostname(hostname)
+		if !ok {
+			return nil, .Invalid_Hostname_Error
+		}
+	}
+
+	name_servers, resolve_ok := load_resolv_conf(dns_configuration.resolv_conf)
+	defer delete(name_servers)
+	if !resolve_ok {
+		return nil, .Invalid_Resolv_Config_Error
+	}
+	if len(name_servers) == 0 {
+		return
+	}
+
+	hosts, hosts_ok := load_hosts(dns_configuration.hosts_file)
+	defer delete(hosts)
+	if !hosts_ok {
+		return nil, .Invalid_Hosts_Config_Error
+	}
+	if len(hosts) == 0 {
+		return
+	}
+
+	host_overrides := make([dynamic]DNS_Record)
+	for host in hosts {
+		if strings.compare(host.name, hostname) != 0 {
+			continue
+		}
+
+		if type == .IP4 && family_from_address(host.addr) == .IP4 {
+			record := DNS_Record_IP4{
+				base = {
+					record_name = strings.clone(hostname),
+					ttl_seconds = 0,
+				},
+				address = host.addr.(IP4_Address),
+			}
+			append(&host_overrides, record)
+		} else if type == .IP6 && family_from_address(host.addr) == .IP6 {
+			record := DNS_Record_IP6{
+				base = {
+					record_name = strings.clone(hostname),
+					ttl_seconds = 0,
+				},
+				address = host.addr.(IP6_Address),
+			}
+			append(&host_overrides, record)
+		}
+	}
+
+	if len(host_overrides) > 0 {
+		return host_overrides[:], nil
+	}
+
+	return get_dns_records_from_nameservers(hostname, type, name_servers, host_overrides[:])
+}

+ 166 - 0
core/net/dns_windows.odin

@@ -0,0 +1,166 @@
+//+build windows
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:strings"
+import "core:mem"
+
+import win "core:sys/windows"
+
+// Performs a recursive DNS query for records of a particular type for the hostname.
+//
+// NOTE: This procedure instructs the DNS resolver to recursively perform CNAME requests on our behalf,
+// meaning that DNS queries for a hostname will resolve through CNAME records until an
+// IP address is reached.
+//
+// WARNING: This procedure allocates memory for each record returned; deleting just the returned slice is not enough!
+// See `destroy_records`.
+get_dns_records_windows :: proc(hostname: string, type: DNS_Record_Type, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) {
+	context.allocator = allocator
+
+	host_cstr := strings.clone_to_cstring(hostname, context.temp_allocator)
+	rec: ^win.DNS_RECORD
+	res := win.DnsQuery_UTF8(host_cstr, u16(type), 0, nil, &rec, nil)
+
+	switch u32(res) {
+	case 0:
+		// okay
+	case win.ERROR_INVALID_NAME:
+		return nil, .Invalid_Hostname_Error
+	case win.DNS_INFO_NO_RECORDS:
+		return
+	case:
+		return nil, .System_Error
+	}
+	defer win.DnsRecordListFree(rec, 1) // 1 means that we're freeing a list... because the proc name isn't enough.
+
+	count := 0
+	for r := rec; r != nil; r = r.pNext {
+		if r.wType != u16(type) do continue // NOTE(tetra): Should never happen, but...
+		count += 1
+	}
+
+
+	recs := make([dynamic]DNS_Record, 0, count)
+	if recs == nil do return nil, .System_Error // return no results if OOM.
+
+	for r := rec; r != nil; r = r.pNext {
+		if r.wType != u16(type) do continue // NOTE(tetra): Should never happen, but...
+
+		base_record := DNS_Record_Base{
+			record_name = strings.clone(string(r.pName)),
+			ttl_seconds = r.dwTtl,
+		}
+
+		switch DNS_Record_Type(r.wType) {
+		case .IP4:
+			addr := IP4_Address(transmute([4]u8)r.Data.A)
+			record := DNS_Record_IP4{
+				base    = base_record,
+				address = addr,
+			}
+			append(&recs, record)
+
+		case .IP6:
+			addr := IP6_Address(transmute([8]u16be) r.Data.AAAA)
+			record := DNS_Record_IP6{
+				base    = base_record,
+				address = addr,
+			}
+			append(&recs, record)
+
+		case .CNAME:
+
+			hostname := strings.clone(string(r.Data.CNAME))
+			record := DNS_Record_CNAME{
+				base      = base_record,
+				host_name = hostname,
+			}
+			append(&recs, record)
+
+		case .TXT:
+			n := r.Data.TXT.dwStringCount
+			ptr := &r.Data.TXT.pStringArray
+			c_strs := mem.slice_ptr(ptr, int(n))
+
+			for cstr in c_strs {
+				record := DNS_Record_TXT{
+					base  = base_record,
+					value = strings.clone(string(cstr)),
+				}
+				append(&recs, record)
+			}
+
+		case .NS:
+			hostname := strings.clone(string(r.Data.NS))
+			record := DNS_Record_NS{
+				base      = base_record,
+				host_name = hostname,
+			}
+			append(&recs, record)
+
+		case .MX:
+			/*
+				TODO(tetra): Order by preference priority? (Prefer hosts with lower preference values.)
+				Or maybe not because you're supposed to just use the first one that works
+				and which order they're in changes every few calls.
+			*/
+
+			record := DNS_Record_MX{
+				base       = base_record,
+				host_name  = strings.clone(string(r.Data.MX.pNameExchange)),
+				preference = int(r.Data.MX.wPreference),
+			}
+			append(&recs, record)
+
+		case .SRV:
+			target   := strings.clone(string(r.Data.SRV.pNameTarget)) // The target hostname/address that the service can be found on
+			priority := int(r.Data.SRV.wPriority)
+			weight   := int(r.Data.SRV.wWeight)
+			port     := int(r.Data.SRV.wPort)
+
+			// NOTE(tetra): Srv record name should be of the form '_servicename._protocol.hostname'
+			// The record name is the name of the record.
+			// Not to be confused with the _target_ of the record, which is--in combination with the port--what we're looking up
+			// by making this request in the first place.
+
+			// NOTE(Jeroen): Service Name and Protocol Name can probably just be string slices into the record name.
+			// It's already cloned, after all. I wouldn't put them on the temp allocator like this.
+
+			parts := strings.split_n(base_record.record_name, ".", 3, context.temp_allocator)
+			if len(parts) != 3 {
+				continue
+			}
+			service_name, protocol_name := parts[0], parts[1]
+
+			append(&recs, DNS_Record_SRV {
+				base          = base_record,
+				target        = target,
+				port          = port,
+				service_name  = service_name,
+				protocol_name = protocol_name,
+				priority      = priority,
+				weight        = weight,
+
+			})
+		}
+	}
+
+	records = recs[:]
+	return
+}

+ 47 - 0
core/net/doc.odin

@@ -0,0 +1,47 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+
+	Features:
+		- Supports Windows, Linux and OSX.
+		- Opening and closing of TCP and UDP sockets.
+		- Sending to and receiving from these sockets.
+		- DNS name lookup, using either the OS or our own resolver.
+
+	Planned:
+		- Nonblocking IO
+		- `Connection` struct
+			A "fat socket" struct that remembers how you opened it, etc, instead of just being a handle.
+		- IP Range structs, CIDR/class ranges, netmask calculator and associated helper procedures.
+		- Use `context.temp_allocator` instead of stack-based arenas?
+			And check it's the default temp allocator or can give us 4 MiB worth of memory
+			without punting to the main allocator by comparing their addresses in an @(init) procedure.
+			Panic if this assumption is not met.
+
+		- Document assumptions about libc usage (or avoidance thereof) for each platform.
+
+	Assumptions:
+		- For performance reasons this package relies on the `context.temp_allocator` in some places.
+
+		  You can replace the default `context.temp_allocator` with your own as long as it meets
+		  this requirement: A minimum of 4 MiB of scratch space that's expected not to be freed.
+
+		  If this expectation is not met, the package's @(init) procedure will attempt to detect
+		  this and panic to avoid temp allocations prematurely overwriting data and garbling results,
+		  or worse. This means that should you replace the temp allocator with an insufficient one,
+		  we'll do our best to loudly complain the first time you try it.
+
+*/
+package net

+ 68 - 0
core/net/interface.odin

@@ -0,0 +1,68 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:strings"
+
+/*
+	`destroy_interfaces` cleans up a list of network interfaces retrieved by e.g. `enumerate_interfaces`.
+*/
+destroy_interfaces :: proc(interfaces: []Network_Interface, allocator := context.allocator) {
+	context.allocator = allocator
+
+	for i in interfaces {
+		delete(i.adapter_name)
+		delete(i.friendly_name)
+		delete(i.description)
+		delete(i.dns_suffix)
+
+		delete(i.physical_address)
+
+		delete(i.unicast)
+		delete(i.multicast)
+		delete(i.anycast)
+		delete(i.gateways)
+	}
+	delete(interfaces, allocator)
+}
+
+/*
+	Turns a slice of bytes (from e.g. `get_adapters_addresses`) into a "XX:XX:XX:..." string.
+*/
+physical_address_to_string :: proc(phy_addr: []u8, allocator := context.allocator) -> (phy_string: string) {
+	context.allocator = allocator
+
+	MAC_HEX := "0123456789ABCDEF"
+
+	if len(phy_addr) == 0 {
+		return ""
+	}
+
+	buf: strings.Builder
+
+	for b, i in phy_addr {
+		if i > 0 {
+			strings.write_rune_builder(&buf, ':')
+		}
+
+		hi := rune(MAC_HEX[b >> 4])
+		lo := rune(MAC_HEX[b & 15])
+		strings.write_rune_builder(&buf, hi)
+		strings.write_rune_builder(&buf, lo)
+	}
+	return strings.to_string(buf)
+}

+ 23 - 0
core/net/interface_darwin.odin

@@ -0,0 +1,23 @@
+//+build darwin
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+/*
+	TODO: Implement. Can probably use the (current) Linux implementation,
+	which will itself be switched over to talking to the kernel via NETLINK protocol once we have raw sockets.
+*/

+ 147 - 0
core/net/interface_linux.odin

@@ -0,0 +1,147 @@
+//+build linux, darwin, openbsd, !windows
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+/*
+	This file uses `getifaddrs` libc call to enumerate interfaces.
+
+	TODO: When we have raw sockets, split off into its own file for Linux so we can use the NETLINK protocol and bypass libc.
+*/
+
+import "core:os"
+import "core:strings"
+
+/*
+	`enumerate_interfaces` retrieves a list of network interfaces with their associated properties.
+*/
+enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Network_Error) {
+	context.allocator = allocator
+
+	head: ^os.ifaddrs
+
+	if res := os._getifaddrs(&head); res < 0 {
+		return {}, .Unable_To_Enumerate_Network_Interfaces
+	}
+
+	/*
+		Unlike Windows, *nix regrettably doesn't return all it knows about an interface in one big struct.
+		We're going to have to iterate over a list and coalesce information as we go.
+	*/
+
+	ifaces: map[string]^Network_Interface
+	defer delete(ifaces)
+
+	for ifaddr := head; ifaddr != nil; ifaddr = ifaddr.next {
+		adapter_name := string(ifaddr.name)
+
+		/*
+			Check if we have seen this interface name before so we can reuse the `Network_Interface`.
+			Else, create a new one.
+		*/
+		if adapter_name not_in ifaces {
+			ifaces[adapter_name] = new(Network_Interface)
+			ifaces[adapter_name].adapter_name = strings.clone(adapter_name)
+		}
+		iface := ifaces[adapter_name]
+
+		address: Address
+		netmask: Netmask
+
+		if ifaddr.address != nil {
+			switch int(ifaddr.address.sa_family) {
+			case os.AF_INET, os.AF_INET6:
+				address = sockaddr_to_endpoint(ifaddr.address).address
+
+			case os.AF_PACKET:
+				/*
+					For some obscure reason the 64-bit `getifaddrs` calls returns a pointer to a
+					32-bit `RTNL_LINK_STATS` structure, which of course means that tx/rx byte count
+					is truncated beyond usefulness.
+
+					We're not going to retrieve stats now. Instead this serves as a reminder to use
+					the NETLINK protocol for this purpose.
+
+					But in case you were curious:
+						stats := transmute(^os.rtnl_link_stats)ifaddr.data
+						fmt.println(stats)
+				*/
+			case:
+			}
+		}
+
+		if ifaddr.netmask != nil {
+			switch int(ifaddr.netmask.sa_family) {
+			case os.AF_INET, os.AF_INET6:
+			 	netmask = Netmask(sockaddr_to_endpoint(ifaddr.netmask).address)
+			case:
+			}
+		}
+
+		if ifaddr.broadcast_or_dest != nil && .BROADCAST in ifaddr.flags {
+			switch int(ifaddr.broadcast_or_dest.sa_family) {
+			case os.AF_INET, os.AF_INET6:
+			 	broadcast := sockaddr_to_endpoint(ifaddr.broadcast_or_dest).address
+			 	append(&iface.multicast, broadcast)
+			case:
+			}
+		}
+
+		if address != nil {
+			lease := Lease{
+				address = address,
+				netmask = netmask,
+			}
+			append(&iface.unicast, lease)
+		}
+
+		/*
+			TODO: Refine this based on the type of adapter.
+		*/
+ 		state := Link_State{}
+
+ 		if .UP in ifaddr.flags {
+ 			state |= {.Up}
+ 		}
+
+ 		if .DORMANT in ifaddr.flags {
+ 			state |= {.Dormant}
+ 		}
+
+ 		if .LOOPBACK in ifaddr.flags {
+ 			state |= {.Loopback}
+ 		}
+
+		iface.link.state = state
+	}
+
+	/*
+		Free the OS structures.
+	*/
+	os._freeifaddrs(head)
+
+	/*
+		Turn the map into a slice to return.
+	*/
+	_interfaces := make([dynamic]Network_Interface, 0, allocator)
+	for _, iface in ifaces {
+		append(&_interfaces, iface^)
+		free(iface)
+	}
+
+	return _interfaces[:], {}
+}

+ 182 - 0
core/net/interface_windows.odin

@@ -0,0 +1,182 @@
+//+build windows
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import sys     "core:sys/windows"
+import win32   "core:sys/win32"
+import strings "core:strings"
+
+MAX_INTERFACE_ENUMERATION_TRIES :: 3
+
+/*
+	`enumerate_interfaces` retrieves a list of network interfaces with their associated properties.
+*/
+enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Network_Error) {
+	context.allocator = allocator
+
+ 	buf:      []u8
+ 	defer delete(buf)
+ 	buf_size: u32
+
+ 	res: u32
+
+ 	gaa: for _ in 1..=MAX_INTERFACE_ENUMERATION_TRIES {
+ 	 	res = sys.get_adapters_addresses(
+ 			.Unspecified, // Return both IPv4 and IPv6 adapters.
+			sys.GAA_Flags{
+				.Include_Prefix,               // (XP SP1+) Return a list of IP address prefixes on this adapter. When this flag is set, IP address prefixes are returned for both IPv6 and IPv4 addresses.
+				.Include_Gateways,             // (Vista+) Return the addresses of default gateways.
+				.Include_Tunnel_Binding_Order, // (Vista+) Return the adapter addresses sorted in tunnel binding order.
+			},
+ 			nil,          // Reserved
+ 			(^sys.IP_Adapter_Addresses)(raw_data(buf)),
+ 			&buf_size,
+ 		)
+
+ 	 	switch res {
+ 	 	case 111: // ERROR_BUFFER_OVERFLOW:
+ 	 		delete(buf)
+ 	 		buf = make([]u8, buf_size)
+ 	 	case 0:
+ 	 		break gaa
+ 	 	case:
+ 	 		return {}, Platform_Error(res)
+ 	 	}
+ 	}
+
+ 	if res != 0 {
+ 		return {}, .Unable_To_Enumerate_Network_Interfaces
+ 	}
+
+ 	_interfaces := make([dynamic]Network_Interface, 0, allocator)
+
+ 	for adapter := (^sys.IP_Adapter_Addresses)(raw_data(buf)); adapter != nil; adapter = adapter.Next {
+		interface := Network_Interface{
+			adapter_name  = strings.clone(string(adapter.AdapterName)),
+ 			friendly_name = wstring_to_string(adapter.FriendlyName),
+ 			description   = wstring_to_string(adapter.Description),
+ 			dns_suffix    = wstring_to_string(adapter.DnsSuffix),
+
+ 			mtu  = adapter.MTU,
+
+ 			link = {
+				transmit_speed = adapter.TransmitLinkSpeed,
+				receive_speed  = adapter.ReceiveLinkSpeed,
+ 			},
+ 		}
+
+ 		if adapter.PhysicalAddressLength > 0 && adapter.PhysicalAddressLength <= len(adapter.PhysicalAddress) {
+ 			interface.physical_address = physical_address_to_string(adapter.PhysicalAddress[:adapter.PhysicalAddressLength])
+ 		}
+
+ 		for u_addr := (^sys.IP_ADAPTER_UNICAST_ADDRESS_LH)(adapter.FirstUnicastAddress); u_addr != nil; u_addr = u_addr.Next {
+ 			win_addr := parse_socket_address(u_addr.Address)
+
+ 			lease := Lease{
+ 				address = win_addr.address,
+ 				origin  = {
+ 					prefix = Prefix_Origin(u_addr.PrefixOrigin),
+ 					suffix = Suffix_Origin(u_addr.SuffixOrigin),
+ 				},
+ 				lifetime = {
+ 					valid     = u_addr.ValidLifetime,
+ 					preferred = u_addr.PreferredLifetime,
+ 					lease     = u_addr.LeaseLifetime,
+ 				},
+ 				address_duplication = Address_Duplication(u_addr.DadState),
+ 			}
+ 			append(&interface.unicast, lease)
+ 		}
+
+ 		for a_addr := (^sys.IP_ADAPTER_ANYCAST_ADDRESS_XP)(adapter.FirstAnycastAddress); a_addr != nil; a_addr = a_addr.Next {
+ 			addr := parse_socket_address(a_addr.Address)
+ 			append(&interface.anycast, addr.address)
+ 		}
+
+ 		for m_addr := (^sys.IP_ADAPTER_MULTICAST_ADDRESS_XP)(adapter.FirstMulticastAddress); m_addr != nil; m_addr = m_addr.Next {
+ 			addr := parse_socket_address(m_addr.Address)
+ 			append(&interface.multicast, addr.address)
+ 		}
+
+ 		for g_addr := (^sys.IP_ADAPTER_GATEWAY_ADDRESS_LH)(adapter.FirstGatewayAddress); g_addr != nil; g_addr = g_addr.Next {
+ 			addr := parse_socket_address(g_addr.Address)
+ 			append(&interface.gateways, addr.address)
+ 		}
+
+		interface.dhcp_v4 = parse_socket_address(adapter.Dhcpv4Server).address
+		interface.dhcp_v6 = parse_socket_address(adapter.Dhcpv6Server).address
+
+ 		switch adapter.OperStatus {
+ 		case .Up:             interface.link.state = {.Up}
+ 		case .Down:           interface.link.state = {.Down}
+ 		case .Testing:        interface.link.state = {.Testing}
+ 		case .Dormant:        interface.link.state = {.Dormant}
+ 		case .NotPresent:     interface.link.state = {.Not_Present}
+ 		case .LowerLayerDown: interface.link.state = {.Lower_Layer_Down}
+ 		case .Unknown:        fallthrough
+ 		case:                 interface.link.state = {}
+ 		}
+
+ 		interface.tunnel_type = Tunnel_Type(adapter.TunnelType)
+
+ 		append(&_interfaces, interface)
+ 	}
+
+	return _interfaces[:], {}
+}
+
+/*
+	Takes a UTF-16 Wstring and clones it.
+*/
+wstring_to_string :: proc(s: ^u16, max_size := 256, allocator := context.allocator) -> (res: string) {
+	temp := win32.wstring_to_utf8((win32.Wstring)(s), max_size, context.temp_allocator)
+	return strings.clone(temp[:len(temp)], allocator)
+}
+
+/*
+	Interpret SOCKET_ADDRESS as an Address
+*/
+parse_socket_address :: proc(addr_in: sys.SOCKET_ADDRESS) -> (addr: Endpoint) {
+	if addr_in.lpSockaddr == nil {
+		return // Empty or invalid address type
+	}
+
+	sock := addr_in.lpSockaddr^
+
+	switch sock.sa_family {
+	case u16(sys.AF_INET):
+		win_addr := cast(^sys.sockaddr_in)addr_in.lpSockaddr
+		port     := int(win_addr.sin_port)
+		return Endpoint {
+			address = IP4_Address(transmute([4]byte)win_addr.sin_addr),
+			port    = port,
+		}
+
+	case u16(sys.AF_INET6):
+		win_addr := cast(^sys.sockaddr_in6)addr_in.lpSockaddr
+		port     := int(win_addr.sin6_port)
+		return Endpoint {
+			address = IP6_Address(transmute([8]u16be)win_addr.sin6_addr),
+			port = port,
+		}
+
+
+	case: return // Empty or invalid address type
+	}
+	unreachable()
+}

+ 87 - 0
core/net/socket.odin

@@ -0,0 +1,87 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+//
+// TODO(tetra): Bluetooth, Raw
+//
+
+any_socket_to_socket :: proc(any_socket: Any_Socket) -> Socket {
+	switch s in any_socket {
+	case TCP_Socket:  return Socket(s)
+	case UDP_Socket:  return Socket(s)
+	case:
+		return Socket({})
+	}
+}
+
+/*
+    Expects both hostname and port to be present in the `hostname_and_port` parameter, either as:
+    `a.host.name:9999`, or as `1.2.3.4:9999`, or IP6 equivalent.
+
+    Calls `parse_hostname_or_endpoint` and `resolve`, then `dial_tcp_from_endpoint`.
+*/
+dial_tcp_from_hostname_and_port_string :: proc(hostname_and_port: string, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) {
+	target := parse_hostname_or_endpoint(hostname_and_port) or_return
+	switch t in target {
+	case Endpoint:
+		return dial_tcp_from_endpoint(t, options)
+	case Host:
+		if t.port == 0 {
+			return 0, .Port_Required
+		}
+		ep4, ep6 := resolve(t.hostname) or_return
+		ep := ep4 if ep4.address != nil else ep6 // NOTE(tetra): We don't know what family the server uses, so we just default to IP4.
+		ep.port = t.port
+		return dial_tcp_from_endpoint(ep, options)
+	}
+	unreachable()
+}
+
+/*
+    Expects the `hostname` as a string and `port` as a `int`.
+    `parse_hostname_or_endpoint` is called and the `hostname` will be resolved into an IP.
+
+    If a `hostname` of form `a.host.name:9999` is given, the port will be ignored in favor of the explicit `port` param.
+*/
+dial_tcp_from_hostname_string_and_explicit_port :: proc(hostname: string, port: int, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) {
+	target := parse_hostname_or_endpoint(hostname) or_return
+	switch t in target {
+	case Endpoint:
+		return dial_tcp_from_endpoint({t.address, port}, options)
+	case Host:
+		if port == 0 {
+			return 0, .Port_Required
+		}
+		ep4, ep6 := resolve(t.hostname) or_return
+		ep := ep4 if ep4.address != nil else ep6 // NOTE(tetra): We don't know what family the server uses, so we just default to IP4.
+		ep.port = port
+		return dial_tcp_from_endpoint(ep, options)
+	}
+	unreachable()
+}
+
+dial_tcp_from_address_and_port :: proc(address: Address, port: int, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) {
+	return dial_tcp_from_endpoint({address, port}, options)
+}
+
+dial_tcp :: proc{
+	dial_tcp_from_endpoint,
+	dial_tcp_from_address_and_port,
+	dial_tcp_from_hostname_and_port_string,
+	dial_tcp_from_hostname_string_and_explicit_port,
+}

+ 513 - 0
core/net/socket_darwin.odin

@@ -0,0 +1,513 @@
+// +build darwin
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:c"
+import "core:os"
+import "core:time"
+
+Platform_Socket :: os.Socket
+
+Create_Socket_Error :: enum c.int {
+	Family_Not_Supported_For_This_Socket = c.int(os.EAFNOSUPPORT),
+	No_Socket_Descriptors_Available = c.int(os.EMFILE),
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	No_Memory_Available_Available = c.int(os.ENOMEM),
+	Protocol_Unsupported_By_System = c.int(os.EPROTONOSUPPORT),
+	Wrong_Protocol_For_Socket = c.int(os.EPROTONOSUPPORT),
+	Family_And_Socket_Type_Mismatch = c.int(os.EPROTONOSUPPORT),
+}
+
+create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) {
+	c_type, c_protocol, c_family: int
+
+	switch family {
+	case .IP4:  c_family = os.AF_INET
+	case .IP6:  c_family = os.AF_INET6
+	case:
+		unreachable()
+	}
+
+	switch protocol {
+	case .TCP:  c_type = os.SOCK_STREAM; c_protocol = os.IPPROTO_TCP
+	case .UDP:  c_type = os.SOCK_DGRAM;  c_protocol = os.IPPROTO_UDP
+	case:
+		unreachable()
+	}
+
+	sock, ok := os.socket(c_family, c_type, c_protocol)
+	if ok != os.ERROR_NONE {
+		err = Create_Socket_Error(ok)
+		return
+	}
+
+	switch protocol {
+	case .TCP:  return TCP_Socket(sock), nil
+	case .UDP:  return UDP_Socket(sock), nil
+	case:
+		unreachable()
+	}
+}
+
+
+Dial_Error :: enum c.int {
+	Port_Required = -1,
+
+	Address_In_Use = c.int(os.EADDRINUSE),
+	In_Progress = c.int(os.EINPROGRESS),
+	Cannot_Use_Any_Address = c.int(os.EADDRNOTAVAIL),
+	Wrong_Family_For_Socket = c.int(os.EAFNOSUPPORT),
+	Refused = c.int(os.ECONNREFUSED),
+	Is_Listening_Socket = c.int(os.EACCES),
+	Already_Connected = c.int(os.EISCONN),
+	Network_Unreachable = c.int(os.ENETUNREACH), // Device is offline
+	Host_Unreachable = c.int(os.EHOSTUNREACH), // Remote host cannot be reached
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Timeout = c.int(os.ETIMEDOUT),
+	Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata?
+}
+
+dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) {
+	if endpoint.port == 0 {
+		return 0, .Port_Required
+	}
+
+	family := family_from_endpoint(endpoint)
+	sock := create_socket(family, .TCP) or_return
+	skt = sock.(TCP_Socket)
+
+	// NOTE(tetra): This is so that if we crash while the socket is open, we can
+	// bypass the cooldown period, and allow the next run of the program to
+	// use the same address immediately.
+	_ = set_option(skt, .Reuse_Address, true)
+
+	sockaddr := endpoint_to_sockaddr(endpoint)
+	res := os.connect(Platform_Socket(skt), (^os.SOCKADDR)(&sockaddr), i32(sockaddr.len))
+	if res != os.ERROR_NONE {
+		err = Dial_Error(res)
+		return
+	}
+
+	return
+}
+
+
+Bind_Error :: enum c.int {
+	// Another application is currently bound to this endpoint.
+	Address_In_Use = c.int(os.EADDRINUSE),
+	// The address is not a local address on this machine.
+	Given_Nonlocal_Address = c.int(os.EADDRNOTAVAIL),
+	// To bind a UDP socket to the broadcast address, the appropriate socket option must be set.
+	Broadcast_Disabled = c.int(os.EACCES),
+	// The address family of the address does not match that of the socket.
+	Address_Family_Mismatch = c.int(os.EFAULT),
+	// The socket is already bound to an address.
+	Already_Bound = c.int(os.EINVAL),
+	// There are not enough ephemeral ports available.
+	No_Ports_Available = c.int(os.ENOBUFS),
+}
+
+bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Network_Error) {
+	sockaddr := endpoint_to_sockaddr(ep)
+	s := any_socket_to_socket(skt)
+	res := os.bind(Platform_Socket(s), (^os.SOCKADDR)(&sockaddr), i32(sockaddr.len))
+	if res != os.ERROR_NONE {
+		err = Bind_Error(res)
+	}
+	return
+}
+
+
+// This type of socket becomes bound when you try to send data.
+// This is likely what you want if you want to send data unsolicited.
+//
+// This is like a client TCP socket, except that it can send data to any remote endpoint without needing to establish a connection first.
+make_unbound_udp_socket :: proc(family: Address_Family) -> (skt: UDP_Socket, err: Network_Error) {
+	sock := create_socket(family, .UDP) or_return
+	skt = sock.(UDP_Socket)
+	return
+}
+
+// This type of socket is bound immediately, which enables it to receive data on the port.
+// Since it's UDP, it's also able to send data without receiving any first.
+//
+// This is like a listening TCP socket, except that data packets can be sent and received without needing to establish a connection first.
+//
+// The bound_address is the address of the network interface that you want to use, or a loopback address if you don't care which to use.
+make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (skt: UDP_Socket, err: Network_Error) {
+	skt = make_unbound_udp_socket(family_from_address(bound_address)) or_return
+	bind(skt, {bound_address, port}) or_return
+	return
+}
+
+
+
+Listen_Error :: enum c.int {
+	Address_In_Use = c.int(os.EADDRINUSE),
+	Already_Connected = c.int(os.EISCONN),
+	No_Socket_Descriptors_Available = c.int(os.EMFILE),
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Nonlocal_Address = c.int(os.EADDRNOTAVAIL),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Listening_Not_Supported_For_This_Socket = c.int(os.EOPNOTSUPP),
+}
+
+listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_Socket, err: Network_Error) {
+	assert(backlog > 0 && i32(backlog) < max(i32))
+
+	family := family_from_endpoint(interface_endpoint)
+	sock := create_socket(family, .TCP) or_return
+	skt = sock.(TCP_Socket)
+
+	// NOTE(tetra): This is so that if we crash while the socket is open, we can
+	// bypass the cooldown period, and allow the next run of the program to
+	// use the same address immediately.
+	//
+	// TODO(tetra, 2022-02-15): Confirm that this doesn't mean other processes can hijack the address!
+	set_option(sock, .Reuse_Address, true) or_return
+
+	bind(sock, interface_endpoint) or_return
+
+	res := os.listen(Platform_Socket(skt), backlog)
+	if res != os.ERROR_NONE {
+		err = Listen_Error(res)
+		return
+	}
+
+	return
+}
+
+
+
+Accept_Error :: enum c.int {
+	Reset = c.int(os.ECONNRESET), // TODO(tetra): Is this error actually possible here? Or is like Linux, in which case we can remove it.
+	Not_Listening = c.int(os.EINVAL),
+	No_Socket_Descriptors_Available_For_Client_Socket = c.int(os.EMFILE),
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Not_Connection_Oriented_Socket = c.int(os.EOPNOTSUPP),
+	Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata?
+}
+
+accept_tcp :: proc(sock: TCP_Socket) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) {
+	sockaddr: os.SOCKADDR_STORAGE_LH
+	sockaddrlen := c.int(size_of(sockaddr))
+
+	client_sock, ok := os.accept(Platform_Socket(sock), cast(^os.SOCKADDR) &sockaddr, &sockaddrlen)
+	if ok != os.ERROR_NONE {
+		err = Accept_Error(ok)
+		return
+	}
+	client = TCP_Socket(client_sock)
+	source = sockaddr_to_endpoint(&sockaddr)
+	return
+}
+
+
+
+close :: proc(skt: Any_Socket) {
+	s := any_socket_to_socket(skt)
+	os.close(os.Handle(Platform_Socket(s)))
+}
+
+
+
+TCP_Recv_Error :: enum c.int {
+	Shutdown = c.int(os.ESHUTDOWN),
+	Not_Connected = c.int(os.ENOTCONN),
+	Connection_Broken = c.int(os.ENETRESET), // TODO(tetra): Is this error actually possible here?
+	Not_Socket = c.int(os.ENOTSOCK),
+	Aborted = c.int(os.ECONNABORTED),
+	Connection_Closed = c.int(os.ECONNRESET), // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them?
+	Offline = c.int(os.ENETDOWN),
+	Host_Unreachable = c.int(os.EHOSTUNREACH),
+	Interrupted = c.int(os.EINTR),
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+}
+
+recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) {
+	if len(buf) <= 0 {
+		return
+	}
+	res, ok := os.recv(Platform_Socket(skt), buf, 0)
+	if ok != os.ERROR_NONE {
+		err = TCP_Recv_Error(ok)
+		return
+	}
+	return int(res), nil
+}
+
+UDP_Recv_Error :: enum c.int {
+	// The buffer is too small to fit the entire message, and the message was truncated.
+	Truncated = c.int(os.EMSGSIZE),
+	// The so-called socket is not an open socket.
+	Not_Socket = c.int(os.ENOTSOCK),
+	// The so-called socket is, in fact, not even a valid descriptor.
+	Not_Descriptor = c.int(os.EBADF),
+	// The buffer did not point to a valid location in memory.
+	Bad_Buffer = c.int(os.EFAULT),
+	// A signal occurred before any data was transmitted.
+	// See signal(7).
+	Interrupted = c.int(os.EINTR),
+	// The send timeout duration passed before all data was sent.
+	// See Socket_Option.Send_Timeout.
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+	// The socket must be bound for this operation, but isn't.
+	Socket_Not_Bound = c.int(os.EINVAL),
+}
+
+recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) {
+	if len(buf) <= 0 {
+		return
+	}
+
+	from: os.SOCKADDR_STORAGE_LH
+	fromsize := c.int(size_of(from))
+	res, ok := os.recvfrom(Platform_Socket(skt), buf, 0, cast(^os.SOCKADDR) &from, &fromsize)
+	if ok != os.ERROR_NONE {
+		err = UDP_Recv_Error(ok)
+		return
+	}
+
+	bytes_read = int(res)
+	remote_endpoint = sockaddr_to_endpoint(&from)
+	return
+}
+
+recv :: proc{recv_tcp, recv_udp}
+
+
+
+// TODO
+TCP_Send_Error :: enum c.int {
+	Aborted = c.int(os.ECONNABORTED), // TODO: merge with other errors?
+	Connection_Closed = c.int(os.ECONNRESET),
+	Not_Connected = c.int(os.ENOTCONN),
+	Shutdown = c.int(os.ESHUTDOWN),
+	// The send queue was full.
+	// This is usually a transient issue.
+	//
+	// This also shouldn't normally happen on Linux, as data is dropped if it
+	// doesn't fit in the send queue.
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Offline = c.int(os.ENETDOWN),
+	Host_Unreachable = c.int(os.EHOSTUNREACH),
+	// A signal occurred before any data was transmitted.
+	// See signal(7).
+	Interrupted = c.int(os.EINTR),
+	// The send timeout duration passed before all data was sent.
+	// See Socket_Option.Send_Timeout.
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+	// The so-called socket is not an open socket.
+	Not_Socket = c.int(os.ENOTSOCK),
+}
+
+// Repeatedly sends data until the entire buffer is sent.
+// If a send fails before all data is sent, returns the amount
+// sent up to that point.
+send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) {
+	for bytes_written < len(buf) {
+		limit := min(int(max(i32)), len(buf) - bytes_written)
+		remaining := buf[bytes_written:][:limit]
+		res, ok := os.send(Platform_Socket(skt), remaining, 0)
+		if ok != os.ERROR_NONE {
+			err = TCP_Send_Error(ok)
+			return
+		}
+		bytes_written += int(res)
+	}
+	return
+}
+
+// TODO
+UDP_Send_Error :: enum c.int {
+	// The message is too big. No data was sent.
+	Truncated = c.int(os.EMSGSIZE),
+	// TODO: not sure what the exact circumstances for this is yet
+	Network_Unreachable = c.int(os.ENETUNREACH),
+	// There are no more emphemeral outbound ports available to bind the socket to, in order to send.
+	No_Outbound_Ports_Available = c.int(os.EAGAIN),
+	// The send timeout duration passed before all data was sent.
+	// See Socket_Option.Send_Timeout.
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+	// The so-called socket is not an open socket.
+	Not_Socket = c.int(os.ENOTSOCK),
+	// The so-called socket is, in fact, not even a valid descriptor.
+	Not_Descriptor = c.int(os.EBADF),
+	// The buffer did not point to a valid location in memory.
+	Bad_Buffer = c.int(os.EFAULT),
+	// A signal occurred before any data was transmitted.
+	// See signal(7).
+	Interrupted = c.int(os.EINTR),
+	// The send queue was full.
+	// This is usually a transient issue.
+	//
+	// This also shouldn't normally happen on Linux, as data is dropped if it
+	// doesn't fit in the send queue.
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	// No memory was available to properly manage the send queue.
+	No_Memory_Available = c.int(os.ENOMEM),
+}
+
+send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) {
+	toaddr := endpoint_to_sockaddr(to)
+	for bytes_written < len(buf) {
+		limit := min(1<<31, len(buf) - bytes_written)
+		remaining := buf[bytes_written:][:limit]
+		res, ok := os.sendto(Platform_Socket(skt), remaining, 0, cast(^os.SOCKADDR)&toaddr, i32(toaddr.len))
+		if ok != os.ERROR_NONE {
+			err = UDP_Send_Error(ok)
+			return
+		}
+		bytes_written += int(res)
+	}
+	return
+}
+
+send :: proc{send_tcp, send_udp}
+
+
+
+
+Shutdown_Manner :: enum c.int {
+	Receive = c.int(os.SHUT_RD),
+	Send = c.int(os.SHUT_WR),
+	Both = c.int(os.SHUT_RDWR),
+}
+
+Shutdown_Error :: enum c.int {
+	Aborted = c.int(os.ECONNABORTED),
+	Reset = c.int(os.ECONNRESET),
+	Offline = c.int(os.ENETDOWN),
+	Not_Connected = c.int(os.ENOTCONN),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Invalid_Manner = c.int(os.EINVAL),
+}
+
+shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) {
+	s := any_socket_to_socket(skt)
+	res := os.shutdown(Platform_Socket(s), int(manner))
+	if res != os.ERROR_NONE {
+		return Shutdown_Error(res)
+	}
+	return
+}
+
+
+
+
+Socket_Option :: enum c.int {
+	Reuse_Address = c.int(os.SO_REUSEADDR),
+	Keep_Alive = c.int(os.SO_KEEPALIVE),
+	Out_Of_Bounds_Data_Inline = c.int(os.SO_OOBINLINE),
+	TCP_Nodelay = c.int(os.TCP_NODELAY),
+
+	Linger = c.int(os.SO_LINGER),
+
+	Receive_Buffer_Size = c.int(os.SO_RCVBUF),
+	Send_Buffer_Size = c.int(os.SO_SNDBUF),
+	Receive_Timeout = c.int(os.SO_RCVTIMEO),
+	Send_Timeout = c.int(os.SO_SNDTIMEO),
+}
+
+Socket_Option_Error :: enum c.int {
+	Offline = c.int(os.ENETDOWN),
+	Timeout_When_Keepalive_Set = c.int(os.ENETRESET),
+	Invalid_Option_For_Socket = c.int(os.ENOPROTOOPT),
+	Reset_When_Keepalive_Set = c.int(os.ENOTCONN),
+	Not_Socket = c.int(os.ENOTSOCK),
+}
+
+set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error {
+	level := os.SOL_SOCKET if option != .TCP_Nodelay else os.IPPROTO_TCP
+
+	// NOTE(tetra, 2022-02-15): On Linux, you cannot merely give a single byte for a bool;
+	//  it _has_ to be a b32.
+	//  I haven't tested if you can give more than that.
+	bool_value: b32
+	int_value: i32
+	timeval_value: os.Timeval
+
+	ptr: rawptr
+	len: os.socklen_t
+
+	switch option {
+	case
+		.Reuse_Address,
+		.Keep_Alive,
+		.Out_Of_Bounds_Data_Inline,
+		.TCP_Nodelay:
+		// TODO: verify whether these are options or not on Linux
+		// .Broadcast,
+		// .Conditional_Accept,
+		// .Dont_Linger:
+			switch x in value {
+			case bool, b8:
+				x2 := x
+				bool_value = b32((^bool)(&x2)^)
+			case b16:
+				bool_value = b32(x)
+			case b32:
+				bool_value = b32(x)
+			case b64:
+				bool_value = b32(x)
+			case:
+				panic("set_option() value must be a boolean here", loc)
+			}
+			ptr = &bool_value
+			len = size_of(bool_value)
+	case
+		.Linger,
+		.Send_Timeout,
+		.Receive_Timeout:
+			t, ok := value.(time.Duration)
+			if !ok do panic("set_option() value must be a time.Duration here", loc)
+
+			nanos := time.duration_nanoseconds(t)
+			timeval_value.nanoseconds = int(nanos % 1e9)
+			timeval_value.seconds = (nanos - i64(timeval_value.nanoseconds)) / 1e9
+
+			ptr = &timeval_value
+			len = size_of(timeval_value)
+	case
+		.Receive_Buffer_Size,
+		.Send_Buffer_Size:
+			// TODO: check for out of range values and return .Value_Out_Of_Range?
+			switch i in value {
+			case i8, u8:   i2 := i; int_value = os.socklen_t((^u8)(&i2)^)
+			case i16, u16: i2 := i; int_value = os.socklen_t((^u16)(&i2)^)
+			case i32, u32: i2 := i; int_value = os.socklen_t((^u32)(&i2)^)
+			case i64, u64: i2 := i; int_value = os.socklen_t((^u64)(&i2)^)
+			case i128, u128: i2 := i; int_value = os.socklen_t((^u128)(&i2)^)
+			case int, uint: i2 := i; int_value = os.socklen_t((^uint)(&i2)^)
+			case:
+				panic("set_option() value must be an integer here", loc)
+			}
+			ptr = &int_value
+			len = size_of(int_value)
+	}
+
+	skt := any_socket_to_socket(s)
+	res := os.setsockopt(Platform_Socket(skt), int(level), int(option), ptr, len)
+	if res != os.ERROR_NONE {
+		return Socket_Option_Error(res)
+	}
+
+	return nil
+}

+ 532 - 0
core/net/socket_linux.odin

@@ -0,0 +1,532 @@
+// +build linux
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:c"
+import "core:os"
+import "core:time"
+
+Platform_Socket :: os.Socket
+
+Create_Socket_Error :: enum c.int {
+	Family_Not_Supported_For_This_Socket = c.int(os.EAFNOSUPPORT),
+	No_Socket_Descriptors_Available = c.int(os.EMFILE),
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	No_Memory_Available_Available = c.int(os.ENOMEM),
+	Protocol_Unsupported_By_System = c.int(os.EPROTONOSUPPORT),
+	Wrong_Protocol_For_Socket = c.int(os.EPROTONOSUPPORT),
+	Family_And_Socket_Type_Mismatch = c.int(os.EPROTONOSUPPORT),
+}
+
+create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) {
+	c_type, c_protocol, c_family: int
+
+	switch family {
+	case .IP4:  c_family = os.AF_INET
+	case .IP6:  c_family = os.AF_INET6
+	case:
+		unreachable()
+	}
+
+	switch protocol {
+	case .TCP:  c_type = os.SOCK_STREAM; c_protocol = os.IPPROTO_TCP
+	case .UDP:  c_type = os.SOCK_DGRAM;  c_protocol = os.IPPROTO_UDP
+	case:
+		unreachable()
+	}
+
+	sock, ok := os.socket(c_family, c_type, c_protocol)
+	if ok != os.ERROR_NONE {
+		err = Create_Socket_Error(ok)
+		return
+	}
+
+	switch protocol {
+	case .TCP:  return TCP_Socket(sock), nil
+	case .UDP:  return UDP_Socket(sock), nil
+	case:
+		unreachable()
+	}
+}
+
+
+Dial_Error :: enum c.int {
+	Port_Required = -1,
+
+	Address_In_Use = c.int(os.EADDRINUSE),
+	In_Progress = c.int(os.EINPROGRESS),
+	Cannot_Use_Any_Address = c.int(os.EADDRNOTAVAIL),
+	Wrong_Family_For_Socket = c.int(os.EAFNOSUPPORT),
+	Refused = c.int(os.ECONNREFUSED),
+	Is_Listening_Socket = c.int(os.EACCES),
+	Already_Connected = c.int(os.EISCONN),
+	Network_Unreachable = c.int(os.ENETUNREACH), // Device is offline
+	Host_Unreachable = c.int(os.EHOSTUNREACH), // Remote host cannot be reached
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Timeout = c.int(os.ETIMEDOUT),
+	Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata?
+}
+
+dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) {
+	if endpoint.port == 0 {
+		return 0, .Port_Required
+	}
+
+	family := family_from_endpoint(endpoint)
+	sock := create_socket(family, .TCP) or_return
+	skt = sock.(TCP_Socket)
+
+	// NOTE(tetra): This is so that if we crash while the socket is open, we can
+	// bypass the cooldown period, and allow the next run of the program to
+	// use the same address immediately.
+	_ = set_option(skt, .Reuse_Address, true)
+
+	sockaddr := endpoint_to_sockaddr(endpoint)
+	res := os.connect(Platform_Socket(skt), (^os.SOCKADDR)(&sockaddr), size_of(sockaddr))
+	if res != os.ERROR_NONE {
+		err = Dial_Error(res)
+		return
+	}
+
+	if options.no_delay {
+		_ = set_option(sock, .TCP_Nodelay, true) // NOTE(tetra): Not vital to succeed; error ignored
+	}
+
+	return
+}
+
+
+Bind_Error :: enum c.int {
+	// Another application is currently bound to this endpoint.
+	Address_In_Use = c.int(os.EADDRINUSE),
+	// The address is not a local address on this machine.
+	Given_Nonlocal_Address = c.int(os.EADDRNOTAVAIL),
+	// To bind a UDP socket to the broadcast address, the appropriate socket option must be set.
+	Broadcast_Disabled = c.int(os.EACCES),
+	// The address family of the address does not match that of the socket.
+	Address_Family_Mismatch = c.int(os.EFAULT),
+	// The socket is already bound to an address.
+	Already_Bound = c.int(os.EINVAL),
+	// There are not enough ephemeral ports available.
+	No_Ports_Available = c.int(os.ENOBUFS),
+}
+
+bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Network_Error) {
+	sockaddr := endpoint_to_sockaddr(ep)
+	s := any_socket_to_socket(skt)
+	res := os.bind(Platform_Socket(s), (^os.SOCKADDR)(&sockaddr), size_of(sockaddr))
+	if res != os.ERROR_NONE {
+		err = Bind_Error(res)
+	}
+	return
+}
+
+
+// This type of socket becomes bound when you try to send data.
+// This is likely what you want if you want to send data unsolicited.
+//
+// This is like a client TCP socket, except that it can send data to any remote endpoint without needing to establish a connection first.
+make_unbound_udp_socket :: proc(family: Address_Family) -> (skt: UDP_Socket, err: Network_Error) {
+	sock := create_socket(family, .UDP) or_return
+	skt = sock.(UDP_Socket)
+	return
+}
+
+// This type of socket is bound immediately, which enables it to receive data on the port.
+// Since it's UDP, it's also able to send data without receiving any first.
+//
+// This is like a listening TCP socket, except that data packets can be sent and received without needing to establish a connection first.
+//
+// The bound_address is the address of the network interface that you want to use, or a loopback address if you don't care which to use.
+make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (skt: UDP_Socket, err: Network_Error) {
+	skt = make_unbound_udp_socket(family_from_address(bound_address)) or_return
+	bind(skt, {bound_address, port}) or_return
+	return
+}
+
+
+
+Listen_Error :: enum c.int {
+	Address_In_Use = c.int(os.EADDRINUSE),
+	Already_Connected = c.int(os.EISCONN),
+	No_Socket_Descriptors_Available = c.int(os.EMFILE),
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Nonlocal_Address = c.int(os.EADDRNOTAVAIL),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Listening_Not_Supported_For_This_Socket = c.int(os.EOPNOTSUPP),
+}
+
+listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_Socket, err: Network_Error) {
+	assert(backlog > 0 && i32(backlog) < max(i32))
+
+	family := family_from_endpoint(interface_endpoint)
+	sock := create_socket(family, .TCP) or_return
+	skt = sock.(TCP_Socket)
+
+	// NOTE(tetra): This is so that if we crash while the socket is open, we can
+	// bypass the cooldown period, and allow the next run of the program to
+	// use the same address immediately.
+	//
+	// TODO(tetra, 2022-02-15): Confirm that this doesn't mean other processes can hijack the address!
+	set_option(sock, .Reuse_Address, true) or_return
+
+	bind(sock, interface_endpoint) or_return
+
+	res := os.listen(Platform_Socket(skt), backlog)
+	if res != os.ERROR_NONE {
+		err = Listen_Error(res)
+		return
+	}
+
+	return
+}
+
+
+
+Accept_Error :: enum c.int {
+	Not_Listening = c.int(os.EINVAL),
+	No_Socket_Descriptors_Available_For_Client_Socket = c.int(os.EMFILE),
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Not_Connection_Oriented_Socket = c.int(os.EOPNOTSUPP),
+	Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata?
+}
+
+accept_tcp :: proc(sock: TCP_Socket, options := default_tcp_options) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) {
+	sockaddr: os.SOCKADDR_STORAGE_LH
+	sockaddrlen := c.int(size_of(sockaddr))
+
+	client_sock, ok := os.accept(Platform_Socket(sock), cast(^os.SOCKADDR) &sockaddr, &sockaddrlen)
+	if ok != os.ERROR_NONE {
+		err = Accept_Error(ok)
+		return
+	}
+	client = TCP_Socket(client_sock)
+	source = sockaddr_to_endpoint(&sockaddr)
+	if options.no_delay {
+		_ = set_option(client, .TCP_Nodelay, true) // NOTE(tetra): Not vital to succeed; error ignored
+	}
+	return
+}
+
+
+
+close :: proc(skt: Any_Socket) {
+	s := any_socket_to_socket(skt)
+	os.close(os.Handle(Platform_Socket(s)))
+}
+
+
+
+TCP_Recv_Error :: enum c.int {
+	Shutdown = c.int(os.ESHUTDOWN),
+	Not_Connected = c.int(os.ENOTCONN),
+	Connection_Broken = c.int(os.ENETRESET),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Aborted = c.int(os.ECONNABORTED),
+	Connection_Closed = c.int(os.ECONNRESET), // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them?
+	Offline = c.int(os.ENETDOWN),
+	Host_Unreachable = c.int(os.EHOSTUNREACH),
+	Interrupted = c.int(os.EINTR),
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+}
+
+recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) {
+	if len(buf) <= 0 {
+		return
+	}
+	res, ok := os.recv(Platform_Socket(skt), buf, 0)
+	if ok != os.ERROR_NONE {
+		err = TCP_Recv_Error(ok)
+		return
+	}
+	return int(res), nil
+}
+
+UDP_Recv_Error :: enum c.int {
+	// The buffer is too small to fit the entire message, and the message was truncated.
+	// When this happens, the rest of message is lost.
+	Buffer_Too_Small = c.int(os.EMSGSIZE),
+	// The so-called socket is not an open socket.
+	Not_Socket = c.int(os.ENOTSOCK),
+	// The so-called socket is, in fact, not even a valid descriptor.
+	Not_Descriptor = c.int(os.EBADF),
+	// The buffer did not point to a valid location in memory.
+	Bad_Buffer = c.int(os.EFAULT),
+	// A signal occurred before any data was transmitted.
+	// See signal(7).
+	Interrupted = c.int(os.EINTR),
+	// The send timeout duration passed before all data was received.
+	// See Socket_Option.Receive_Timeout.
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+	// The socket must be bound for this operation, but isn't.
+	Socket_Not_Bound = c.int(os.EINVAL),
+}
+
+recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) {
+	if len(buf) <= 0 {
+		return
+	}
+
+	from: os.SOCKADDR_STORAGE_LH = ---
+	fromsize := c.int(size_of(from))
+
+	// NOTE(tetra): On Linux, if the buffer is too small to fit the entire datagram payload, the rest is silently discarded,
+	// and no error is returned.
+	// However, if you pass MSG_TRUNC here, 'res' will be the size of the incoming message, rather than how much was read.
+	// We can use this fact to detect this condition and return .Buffer_Too_Small.
+	res, ok := os.recvfrom(Platform_Socket(skt), buf, os.MSG_TRUNC, cast(^os.SOCKADDR) &from, &fromsize)
+	if ok != os.ERROR_NONE {
+		err = UDP_Recv_Error(ok)
+		return
+	}
+
+	bytes_read = int(res)
+	remote_endpoint = sockaddr_to_endpoint(&from)
+
+	if bytes_read > len(buf) {
+		// NOTE(tetra): The buffer has been filled, with a partial message.
+		bytes_read = len(buf)
+		err = .Buffer_Too_Small
+	}
+
+	return
+}
+
+recv :: proc{recv_tcp, recv_udp}
+
+
+
+// TODO
+TCP_Send_Error :: enum c.int {
+	Aborted = c.int(os.ECONNABORTED), // TODO(tetra): merge with other errors?
+	Connection_Closed = c.int(os.ECONNRESET),
+	Not_Connected = c.int(os.ENOTCONN),
+	Shutdown = c.int(os.ESHUTDOWN),
+	// The send queue was full.
+	// This is usually a transient issue.
+	//
+	// This also shouldn't normally happen on Linux, as data is dropped if it
+	// doesn't fit in the send queue.
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Offline = c.int(os.ENETDOWN),
+	Host_Unreachable = c.int(os.EHOSTUNREACH),
+	// A signal occurred before any data was transmitted.
+	// See signal(7).
+	Interrupted = c.int(os.EINTR),
+	// The send timeout duration passed before all data was sent.
+	// See Socket_Option.Send_Timeout.
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+	// The so-called socket is not an open socket.
+	Not_Socket = c.int(os.ENOTSOCK),
+}
+
+// Repeatedly sends data until the entire buffer is sent.
+// If a send fails before all data is sent, returns the amount
+// sent up to that point.
+send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) {
+	for bytes_written < len(buf) {
+		limit := min(int(max(i32)), len(buf) - bytes_written)
+		remaining := buf[bytes_written:][:limit]
+		res, ok := os.send(Platform_Socket(skt), remaining, 0)
+		if ok != os.ERROR_NONE {
+			err = TCP_Send_Error(ok)
+			return
+		}
+		bytes_written += int(res)
+	}
+	return
+}
+
+// TODO
+UDP_Send_Error :: enum c.int {
+	// The message is too big. No data was sent.
+	Message_Too_Long = c.int(os.EMSGSIZE),
+	// TODO: not sure what the exact circumstances for this is yet
+	Network_Unreachable = c.int(os.ENETUNREACH),
+	// There are no more emphemeral outbound ports available to bind the socket to, in order to send.
+	No_Outbound_Ports_Available = c.int(os.EAGAIN),
+	// The send timeout duration passed before all data was sent.
+	// See Socket_Option.Send_Timeout.
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+	// The so-called socket is not an open socket.
+	Not_Socket = c.int(os.ENOTSOCK),
+	// The so-called socket is, in fact, not even a valid descriptor.
+	Not_Descriptor = c.int(os.EBADF),
+	// The buffer did not point to a valid location in memory.
+	Bad_Buffer = c.int(os.EFAULT),
+	// A signal occurred before any data was transmitted.
+	// See signal(7).
+	Interrupted = c.int(os.EINTR),
+	// The send queue was full.
+	// This is usually a transient issue.
+	//
+	// This also shouldn't normally happen on Linux, as data is dropped if it
+	// doesn't fit in the send queue.
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	// No memory was available to properly manage the send queue.
+	No_Memory_Available = c.int(os.ENOMEM),
+}
+
+// Sends a single UDP datagram packet.
+//
+// Datagrams are limited in size; attempting to send more than this limit at once will result in a Message_Too_Long error.
+// UDP packets are not guarenteed to be received in order.
+send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) {
+	toaddr := endpoint_to_sockaddr(to)
+	res, os_err := os.sendto(Platform_Socket(skt), buf, 0, cast(^os.SOCKADDR) &toaddr, size_of(toaddr))
+	if os_err != os.ERROR_NONE {
+		err = UDP_Send_Error(os_err)
+		return
+	}
+	bytes_written = int(res)
+	return
+}
+
+send :: proc{send_tcp, send_udp}
+
+
+
+
+Shutdown_Manner :: enum c.int {
+	Receive = c.int(os.SHUT_RD),
+	Send = c.int(os.SHUT_WR),
+	Both = c.int(os.SHUT_RDWR),
+}
+
+Shutdown_Error :: enum c.int {
+	Aborted = c.int(os.ECONNABORTED),
+	Reset = c.int(os.ECONNRESET),
+	Offline = c.int(os.ENETDOWN),
+	Not_Connected = c.int(os.ENOTCONN),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Invalid_Manner = c.int(os.EINVAL),
+}
+
+shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) {
+	s := any_socket_to_socket(skt)
+	res := os.shutdown(Platform_Socket(s), int(manner))
+	if res != os.ERROR_NONE {
+		return Shutdown_Error(res)
+	}
+	return
+}
+
+
+
+
+Socket_Option :: enum c.int {
+	Reuse_Address = c.int(os.SO_REUSEADDR),
+	Keep_Alive = c.int(os.SO_KEEPALIVE),
+	Out_Of_Bounds_Data_Inline = c.int(os.SO_OOBINLINE),
+	TCP_Nodelay = c.int(os.TCP_NODELAY),
+
+	Linger = c.int(os.SO_LINGER),
+
+	Receive_Buffer_Size = c.int(os.SO_RCVBUF),
+	Send_Buffer_Size = c.int(os.SO_SNDBUF),
+	Receive_Timeout = c.int(os.SO_RCVTIMEO_NEW),
+	Send_Timeout = c.int(os.SO_SNDTIMEO_NEW),
+}
+
+Socket_Option_Error :: enum c.int {
+	Offline = c.int(os.ENETDOWN),
+	Timeout_When_Keepalive_Set = c.int(os.ENETRESET),
+	Invalid_Option_For_Socket = c.int(os.ENOPROTOOPT),
+	Reset_When_Keepalive_Set = c.int(os.ENOTCONN),
+	Not_Socket = c.int(os.ENOTSOCK),
+}
+
+set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error {
+	level := os.SOL_SOCKET if option != .TCP_Nodelay else os.IPPROTO_TCP
+
+	// NOTE(tetra, 2022-02-15): On Linux, you cannot merely give a single byte for a bool;
+	//  it _has_ to be a b32.
+	//  I haven't tested if you can give more than that.
+	bool_value: b32
+	int_value: i32
+	timeval_value: os.Timeval
+
+	ptr: rawptr
+	len: os.socklen_t
+
+	switch option {
+	case
+		.Reuse_Address,
+		.Keep_Alive,
+		.Out_Of_Bounds_Data_Inline,
+		.TCP_Nodelay:
+		// TODO: verify whether these are options or not on Linux
+		// .Broadcast,
+		// .Conditional_Accept,
+		// .Dont_Linger:
+			switch x in value {
+			case bool, b8:
+				x2 := x
+				bool_value = b32((^bool)(&x2)^)
+			case b16:
+				bool_value = b32(x)
+			case b32:
+				bool_value = b32(x)
+			case b64:
+				bool_value = b32(x)
+			case:
+				panic("set_option() value must be a boolean here", loc)
+			}
+			ptr = &bool_value
+			len = size_of(bool_value)
+	case
+		.Linger,
+		.Send_Timeout,
+		.Receive_Timeout:
+			t, ok := value.(time.Duration)
+			if !ok do panic("set_option() value must be a time.Duration here", loc)
+
+			nanos := time.duration_nanoseconds(t)
+			timeval_value.nanoseconds = int(nanos % 1e9)
+			timeval_value.seconds = (nanos - i64(timeval_value.nanoseconds)) / 1e9
+
+			ptr = &timeval_value
+			len = size_of(timeval_value)
+	case
+		.Receive_Buffer_Size,
+		.Send_Buffer_Size:
+			// TODO: check for out of range values and return .Value_Out_Of_Range?
+			switch i in value {
+			case i8, u8:   i2 := i; int_value = os.socklen_t((^u8)(&i2)^)
+			case i16, u16: i2 := i; int_value = os.socklen_t((^u16)(&i2)^)
+			case i32, u32: i2 := i; int_value = os.socklen_t((^u32)(&i2)^)
+			case i64, u64: i2 := i; int_value = os.socklen_t((^u64)(&i2)^)
+			case i128, u128: i2 := i; int_value = os.socklen_t((^u128)(&i2)^)
+			case int, uint: i2 := i; int_value = os.socklen_t((^uint)(&i2)^)
+			case:
+				panic("set_option() value must be an integer here", loc)
+			}
+			ptr = &int_value
+			len = size_of(int_value)
+	}
+
+	skt := any_socket_to_socket(s)
+	res := os.setsockopt(Platform_Socket(skt), int(level), int(option), ptr, len)
+	if res != os.ERROR_NONE {
+		return Socket_Option_Error(res)
+	}
+
+	return nil
+}

+ 515 - 0
core/net/socket_openbsd.odin

@@ -0,0 +1,515 @@
+// +build openbsd
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+
+
+	IMPORTANT/TODO: This is a carbon copy of `socket_darwin.odin`. Adjust if necessary.
+
+*/
+package net
+
+import "core:c"
+import "core:os"
+import "core:time"
+
+Platform_Socket :: os.Socket
+
+Create_Socket_Error :: enum c.int {
+	Family_Not_Supported_For_This_Socket = c.int(os.EAFNOSUPPORT),
+	No_Socket_Descriptors_Available      = c.int(os.EMFILE),
+	No_Buffer_Space_Available            = c.int(os.ENOBUFS),
+	No_Memory_Available_Available        = c.int(os.ENOMEM),
+	Protocol_Unsupported_By_System       = c.int(os.EPROTONOSUPPORT),
+	Wrong_Protocol_For_Socket            = c.int(os.EPROTONOSUPPORT),
+	Family_And_Socket_Type_Mismatch      = c.int(os.EPROTONOSUPPORT),
+}
+
+create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) {
+	c_type, c_protocol, c_family: int
+
+	switch family {
+	case .IP4:  c_family = os.AF_INET
+	case .IP6:  c_family = os.AF_INET6
+	case:
+		unreachable()
+	}
+
+	switch protocol {
+	case .TCP:  c_type = os.SOCK_STREAM; c_protocol = os.IPPROTO_TCP
+	case .UDP:  c_type = os.SOCK_DGRAM;  c_protocol = os.IPPROTO_UDP
+	case:
+		unreachable()
+	}
+
+	sock, ok := os.socket(c_family, c_type, c_protocol)
+	if ok != os.ERROR_NONE {
+		err = Create_Socket_Error(ok)
+		return
+	}
+
+	switch protocol {
+	case .TCP:  return TCP_Socket(sock), nil
+	case .UDP:  return UDP_Socket(sock), nil
+	case:
+		unreachable()
+	}
+}
+
+
+Dial_Error :: enum c.int {
+	Port_Required = -1,
+
+	Address_In_Use            = c.int(os.EADDRINUSE),
+	In_Progress               = c.int(os.EINPROGRESS),
+	Cannot_Use_Any_Address    = c.int(os.EADDRNOTAVAIL),
+	Wrong_Family_For_Socket   = c.int(os.EAFNOSUPPORT),
+	Refused                   = c.int(os.ECONNREFUSED),
+	Is_Listening_Socket       = c.int(os.EACCES),
+	Already_Connected         = c.int(os.EISCONN),
+	Network_Unreachable       = c.int(os.ENETUNREACH), // Device is offline
+	Host_Unreachable          = c.int(os.EHOSTUNREACH), // Remote host cannot be reached
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Not_Socket                = c.int(os.ENOTSOCK),
+	Timeout                   = c.int(os.ETIMEDOUT),
+	Would_Block               = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata?
+}
+
+dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) {
+	if endpoint.port == 0 {
+		return 0, .Port_Required
+	}
+
+	family := family_from_endpoint(endpoint)
+	sock := create_socket(family, .TCP) or_return
+	skt = sock.(TCP_Socket)
+
+	// NOTE(tetra): This is so that if we crash while the socket is open, we can
+	// bypass the cooldown period, and allow the next run of the program to
+	// use the same address immediately.
+	_ = set_option(skt, .Reuse_Address, true)
+
+	sockaddr := endpoint_to_sockaddr(endpoint)
+	res := os.connect(Platform_Socket(skt), (^os.SOCKADDR)(&sockaddr), size_of(sockaddr))
+	if res != os.ERROR_NONE {
+		err = Dial_Error(res)
+		return
+	}
+
+	return
+}
+
+
+Bind_Error :: enum c.int {
+	// Another application is currently bound to this endpoint.
+	Address_In_Use = c.int(os.EADDRINUSE),
+	// The address is not a local address on this machine.
+	Given_Nonlocal_Address = c.int(os.EADDRNOTAVAIL),
+	// To bind a UDP socket to the broadcast address, the appropriate socket option must be set.
+	Broadcast_Disabled = c.int(os.EACCES),
+	// The address family of the address does not match that of the socket.
+	Address_Family_Mismatch = c.int(os.EFAULT),
+	// The socket is already bound to an address.
+	Already_Bound = c.int(os.EINVAL),
+	// There are not enough ephemeral ports available.
+	No_Ports_Available = c.int(os.ENOBUFS),
+}
+
+bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Network_Error) {
+	sockaddr := endpoint_to_sockaddr(ep)
+	s := any_socket_to_socket(skt)
+	res := os.bind(Platform_Socket(s), (^os.SOCKADDR)(&sockaddr), size_of(sockaddr))
+	if res != os.ERROR_NONE {
+		err = Bind_Error(res)
+	}
+	return
+}
+
+
+// This type of socket becomes bound when you try to send data.
+// This is likely what you want if you want to send data unsolicited.
+//
+// This is like a client TCP socket, except that it can send data to any remote endpoint without needing to establish a connection first.
+make_unbound_udp_socket :: proc(family: Address_Family) -> (skt: UDP_Socket, err: Network_Error) {
+	sock := create_socket(family, .UDP) or_return
+	skt = sock.(UDP_Socket)
+	return
+}
+
+// This type of socket is bound immediately, which enables it to receive data on the port.
+// Since it's UDP, it's also able to send data without receiving any first.
+//
+// This is like a listening TCP socket, except that data packets can be sent and received without needing to establish a connection first.
+//
+// The bound_address is the address of the network interface that you want to use, or a loopback address if you don't care which to use.
+make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (skt: UDP_Socket, err: Network_Error) {
+	skt = make_unbound_udp_socket(family_from_address(bound_address)) or_return
+	bind(skt, {bound_address, port}) or_return
+	return
+}
+
+
+
+Listen_Error :: enum c.int {
+	Address_In_Use = c.int(os.EADDRINUSE),
+	Already_Connected = c.int(os.EISCONN),
+	No_Socket_Descriptors_Available = c.int(os.EMFILE),
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Nonlocal_Address = c.int(os.EADDRNOTAVAIL),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Listening_Not_Supported_For_This_Socket = c.int(os.EOPNOTSUPP),
+}
+
+listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_Socket, err: Network_Error) {
+	assert(backlog > 0 && i32(backlog) < max(i32))
+
+	family := family_from_endpoint(interface_endpoint)
+	sock := create_socket(family, .TCP) or_return
+	skt = sock.(TCP_Socket)
+
+	// NOTE(tetra): This is so that if we crash while the socket is open, we can
+	// bypass the cooldown period, and allow the next run of the program to
+	// use the same address immediately.
+	//
+	// TODO(tetra, 2022-02-15): Confirm that this doesn't mean other processes can hijack the address!
+	set_option(sock, .Reuse_Address, true) or_return
+
+	bind(sock, interface_endpoint) or_return
+
+	res := os.listen(Platform_Socket(skt), backlog)
+	if res != os.ERROR_NONE {
+		err = Listen_Error(res)
+		return
+	}
+
+	return
+}
+
+
+
+Accept_Error :: enum c.int {
+	Reset = c.int(os.ECONNRESET), // TODO(tetra): Is this error actually possible here? Or is like Linux, in which case we can remove it.
+	Not_Listening = c.int(os.EINVAL),
+	No_Socket_Descriptors_Available_For_Client_Socket = c.int(os.EMFILE),
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Not_Connection_Oriented_Socket = c.int(os.EOPNOTSUPP),
+	Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata?
+}
+
+accept_tcp :: proc(sock: TCP_Socket) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) {
+	sockaddr: os.SOCKADDR_STORAGE_LH
+	sockaddrlen := c.int(size_of(sockaddr))
+
+	client_sock, ok := os.accept(Platform_Socket(sock), cast(^os.SOCKADDR) &sockaddr, &sockaddrlen)
+	if ok != os.ERROR_NONE {
+		err = Accept_Error(ok)
+		return
+	}
+	client = TCP_Socket(client_sock)
+	source = sockaddr_to_endpoint(&sockaddr)
+	return
+}
+
+
+
+close :: proc(skt: Any_Socket) {
+	s := any_socket_to_socket(skt)
+	os.close(os.Handle(Platform_Socket(s)))
+}
+
+
+
+TCP_Recv_Error :: enum c.int {
+	Shutdown = c.int(os.ESHUTDOWN),
+	Not_Connected = c.int(os.ENOTCONN),
+	Connection_Broken = c.int(os.ENETRESET), // TODO(tetra): Is this error actually possible here?
+	Not_Socket = c.int(os.ENOTSOCK),
+	Aborted = c.int(os.ECONNABORTED),
+	Connection_Closed = c.int(os.ECONNRESET), // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them?
+	Offline = c.int(os.ENETDOWN),
+	Host_Unreachable = c.int(os.EHOSTUNREACH),
+	Interrupted = c.int(os.EINTR),
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+}
+
+recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) {
+	if len(buf) <= 0 {
+		return
+	}
+	res, ok := os.recv(Platform_Socket(skt), buf, 0)
+	if ok != os.ERROR_NONE {
+		err = TCP_Recv_Error(ok)
+		return
+	}
+	return int(res), nil
+}
+
+UDP_Recv_Error :: enum c.int {
+	// The buffer is too small to fit the entire message, and the message was truncated.
+	Truncated = c.int(os.EMSGSIZE),
+	// The so-called socket is not an open socket.
+	Not_Socket = c.int(os.ENOTSOCK),
+	// The so-called socket is, in fact, not even a valid descriptor.
+	Not_Descriptor = c.int(os.EBADF),
+	// The buffer did not point to a valid location in memory.
+	Bad_Buffer = c.int(os.EFAULT),
+	// A signal occurred before any data was transmitted.
+	// See signal(7).
+	Interrupted = c.int(os.EINTR),
+	// The send timeout duration passed before all data was sent.
+	// See Socket_Option.Send_Timeout.
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+	// The socket must be bound for this operation, but isn't.
+	Socket_Not_Bound = c.int(os.EINVAL),
+}
+
+recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) {
+	if len(buf) <= 0 {
+		return
+	}
+
+	from: os.SOCKADDR_STORAGE_LH
+	fromsize := c.int(size_of(from))
+	res, ok := os.recvfrom(Platform_Socket(skt), buf, 0, cast(^os.SOCKADDR) &from, &fromsize)
+	if ok != os.ERROR_NONE {
+		err = UDP_Recv_Error(ok)
+		return
+	}
+
+	bytes_read = int(res)
+	remote_endpoint = sockaddr_to_endpoint(&from)
+	return
+}
+
+recv :: proc{recv_tcp, recv_udp}
+
+
+
+// TODO
+TCP_Send_Error :: enum c.int {
+	Aborted = c.int(os.ECONNABORTED), // TODO: merge with other errors?
+	Connection_Closed = c.int(os.ECONNRESET),
+	Not_Connected = c.int(os.ENOTCONN),
+	Shutdown = c.int(os.ESHUTDOWN),
+	// The send queue was full.
+	// This is usually a transient issue.
+	//
+	// This also shouldn't normally happen on Linux, as data is dropped if it
+	// doesn't fit in the send queue.
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	Offline = c.int(os.ENETDOWN),
+	Host_Unreachable = c.int(os.EHOSTUNREACH),
+	// A signal occurred before any data was transmitted.
+	// See signal(7).
+	Interrupted = c.int(os.EINTR),
+	// The send timeout duration passed before all data was sent.
+	// See Socket_Option.Send_Timeout.
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+}
+
+// Repeatedly sends data until the entire buffer is sent.
+// If a send fails before all data is sent, returns the amount
+// sent up to that point.
+send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) {
+	for bytes_written < len(buf) {
+		limit := min(int(max(i32)), len(buf) - bytes_written)
+		remaining := buf[bytes_written:][:limit]
+		res, ok := os.send(Platform_Socket(skt), remaining, 0)
+		if ok != os.ERROR_NONE {
+			err = TCP_Send_Error(ok)
+			return
+		}
+		bytes_written += int(res)
+	}
+	return
+}
+
+// TODO
+UDP_Send_Error :: enum c.int {
+	// The message is too big. No data was sent.
+	Truncated = c.int(os.EMSGSIZE),
+	// TODO: not sure what the exact circumstances for this is yet
+	Network_Unreachable = c.int(os.ENETUNREACH),
+	// There are no more emphemeral outbound ports available to bind the socket to, in order to send.
+	No_Outbound_Ports_Available = c.int(os.EAGAIN),
+	// The send timeout duration passed before all data was sent.
+	// See Socket_Option.Send_Timeout.
+	Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets...
+	// The so-called socket is not an open socket.
+	Not_Socket = c.int(os.ENOTSOCK),
+	// The so-called socket is, in fact, not even a valid descriptor.
+	Not_Descriptor = c.int(os.EBADF),
+	// The buffer did not point to a valid location in memory.
+	Bad_Buffer = c.int(os.EFAULT),
+	// A signal occurred before any data was transmitted.
+	// See signal(7).
+	Interrupted = c.int(os.EINTR),
+	// The send queue was full.
+	// This is usually a transient issue.
+	//
+	// This also shouldn't normally happen on Linux, as data is dropped if it
+	// doesn't fit in the send queue.
+	No_Buffer_Space_Available = c.int(os.ENOBUFS),
+	// No memory was available to properly manage the send queue.
+	No_Memory_Available = c.int(os.ENOMEM),
+}
+
+send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) {
+	toaddr := endpoint_to_sockaddr(to)
+	for bytes_written < len(buf) {
+		limit := min(1<<31, len(buf) - bytes_written)
+		remaining := buf[bytes_written:][:limit]
+		res, ok := os.sendto(Platform_Socket(skt), remaining, 0, cast(^os.SOCKADDR)&toaddr, size_of(toaddr))
+		if ok != os.ERROR_NONE {
+			err = UDP_Send_Error(ok)
+			return
+		}
+		bytes_written += int(res)
+	}
+	return
+}
+
+send :: proc{send_tcp, send_udp}
+
+
+
+
+Shutdown_Manner :: enum c.int {
+	Receive = c.int(os.SHUT_RD),
+	Send = c.int(os.SHUT_WR),
+	Both = c.int(os.SHUT_RDWR),
+}
+
+Shutdown_Error :: enum c.int {
+	Aborted = c.int(os.ECONNABORTED),
+	Reset = c.int(os.ECONNRESET),
+	Offline = c.int(os.ENETDOWN),
+	Not_Connected = c.int(os.ENOTCONN),
+	Not_Socket = c.int(os.ENOTSOCK),
+	Invalid_Manner = c.int(os.EINVAL),
+}
+
+shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) {
+	s := any_socket_to_socket(skt)
+	res := os.shutdown(Platform_Socket(s), int(manner))
+	if res != os.ERROR_NONE {
+		return Shutdown_Error(res)
+	}
+	return
+}
+
+
+
+
+Socket_Option :: enum c.int {
+	Reuse_Address = c.int(os.SO_REUSEADDR),
+	Keep_Alive = c.int(os.SO_KEEPALIVE),
+	Out_Of_Bounds_Data_Inline = c.int(os.SO_OOBINLINE),
+	TCP_Nodelay = c.int(os.TCP_NODELAY),
+
+	Linger = c.int(os.SO_LINGER),
+
+	Receive_Buffer_Size = c.int(os.SO_RCVBUF),
+	Send_Buffer_Size = c.int(os.SO_SNDBUF),
+	Receive_Timeout = c.int(os.SO_RCVTIMEO),
+	Send_Timeout = c.int(os.SO_SNDTIMEO),
+}
+
+Socket_Option_Error :: enum c.int {
+	Offline = c.int(os.ENETDOWN),
+	Timeout_When_Keepalive_Set = c.int(os.ENETRESET),
+	Invalid_Option_For_Socket = c.int(os.ENOPROTOOPT),
+	Reset_When_Keepalive_Set = c.int(os.ENOTCONN),
+	Not_Socket = c.int(os.ENOTSOCK),
+}
+
+set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error {
+	level := os.SOL_SOCKET if option != .TCP_Nodelay else os.IPPROTO_TCP
+
+	// NOTE(tetra, 2022-02-15): On Linux, you cannot merely give a single byte for a bool;
+	//  it _has_ to be a b32.
+	//  I haven't tested if you can give more than that.
+	bool_value: b32
+	int_value: i32
+	timeval_value: os.Timeval
+
+	ptr: rawptr
+	len: os.socklen_t
+
+	switch option {
+	case
+		.Reuse_Address,
+		.Keep_Alive,
+		.Out_Of_Bounds_Data_Inline,
+		.TCP_Nodelay:
+		// TODO: verify whether these are options or not on Linux
+		// .Broadcast,
+		// .Conditional_Accept,
+		// .Dont_Linger:
+			switch x in value {
+			case bool, b8:
+				x2 := x
+				bool_value = b32((^bool)(&x2)^)
+			case b16:
+				bool_value = b32(x)
+			case b32:
+				bool_value = b32(x)
+			case b64:
+				bool_value = b32(x)
+			case:
+				panic("set_option() value must be a boolean here", loc)
+			}
+			ptr = &bool_value
+			len = size_of(bool_value)
+	case
+		.Linger,
+		.Send_Timeout,
+		.Receive_Timeout:
+			t, ok := value.(time.Duration)
+			if !ok do panic("set_option() value must be a time.Duration here", loc)
+
+			nanos := time.duration_nanoseconds(t)
+			timeval_value.nanoseconds = int(nanos % 1e9)
+			timeval_value.seconds = (nanos - i64(timeval_value.nanoseconds)) / 1e9
+
+			ptr = &timeval_value
+			len = size_of(timeval_value)
+	case
+		.Receive_Buffer_Size,
+		.Send_Buffer_Size:
+			// TODO: check for out of range values and return .Value_Out_Of_Range?
+			switch i in value {
+			case i8, u8:   i2 := i; int_value = os.socklen_t((^u8)(&i2)^)
+			case i16, u16: i2 := i; int_value = os.socklen_t((^u16)(&i2)^)
+			case i32, u32: i2 := i; int_value = os.socklen_t((^u32)(&i2)^)
+			case i64, u64: i2 := i; int_value = os.socklen_t((^u64)(&i2)^)
+			case i128, u128: i2 := i; int_value = os.socklen_t((^u128)(&i2)^)
+			case int, uint: i2 := i; int_value = os.socklen_t((^uint)(&i2)^)
+			case:
+				panic("set_option() value must be an integer here", loc)
+			}
+			ptr = &int_value
+			len = size_of(int_value)
+	}
+
+	skt := any_socket_to_socket(s)
+	res := os.setsockopt(Platform_Socket(skt), int(level), int(option), ptr, len)
+	if res != os.ERROR_NONE {
+		return Socket_Option_Error(res)
+	}
+
+	return nil
+}

+ 577 - 0
core/net/socket_windows.odin

@@ -0,0 +1,577 @@
+// +build windows
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:c"
+import win "core:sys/windows"
+import "core:time"
+
+Platform_Socket :: win.SOCKET
+
+Create_Socket_Error :: enum c.int {
+	Network_Subsystem_Failure = win.WSAENETDOWN,
+	Family_Not_Supported_For_This_Socket = win.WSAEAFNOSUPPORT,
+	No_Socket_Descriptors_Available = win.WSAEMFILE,
+	No_Buffer_Space_Available = win.WSAENOBUFS,
+	Protocol_Unsupported_By_System = win.WSAEPROTONOSUPPORT,
+	Wrong_Protocol_For_Socket = win.WSAEPROTOTYPE,
+	Family_And_Socket_Type_Mismatch = win.WSAESOCKTNOSUPPORT,
+}
+
+@(init, private)
+ensure_winsock_initialized :: proc() {
+	win.ensure_winsock_initialized()
+}
+
+create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) {
+	c_type, c_protocol, c_family: c.int
+
+	switch family {
+	case .IP4:  c_family = win.AF_INET
+	case .IP6:  c_family = win.AF_INET6
+	case:
+		unreachable()
+	}
+
+	switch protocol {
+	case .TCP:  c_type = win.SOCK_STREAM; c_protocol = win.IPPROTO_TCP
+	case .UDP:  c_type = win.SOCK_DGRAM;  c_protocol = win.IPPROTO_UDP
+	case:
+		unreachable()
+	}
+
+	sock := win.socket(c_family, c_type, c_protocol)
+	if sock == win.INVALID_SOCKET {
+		err = Create_Socket_Error(win.WSAGetLastError())
+		return
+	}
+
+	switch protocol {
+	case .TCP:  return TCP_Socket(sock), nil
+	case .UDP:  return UDP_Socket(sock), nil
+	case:
+		unreachable()
+	}
+}
+
+
+Dial_Error :: enum c.int {
+	Port_Required = -1,
+
+	Address_In_Use = win.WSAEADDRINUSE,
+	In_Progress = win.WSAEALREADY,
+	Cannot_Use_Any_Address = win.WSAEADDRNOTAVAIL,
+	Wrong_Family_For_Socket = win.WSAEAFNOSUPPORT,
+	Refused = win.WSAECONNREFUSED,
+	Is_Listening_Socket = win.WSAEINVAL,
+	Already_Connected = win.WSAEISCONN,
+	Network_Unreachable = win.WSAENETUNREACH, // Device is offline
+	Host_Unreachable = win.WSAEHOSTUNREACH, // Remote host cannot be reached
+	No_Buffer_Space_Available = win.WSAENOBUFS,
+	Not_Socket = win.WSAENOTSOCK,
+	Timeout = win.WSAETIMEDOUT,
+	Would_Block = win.WSAEWOULDBLOCK, // TODO: we may need special handling for this; maybe make a socket a struct with metadata?
+}
+
+dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) {
+	if endpoint.port == 0 {
+		err = .Port_Required
+		return
+	}
+
+	family := family_from_endpoint(endpoint)
+	sock := create_socket(family, .TCP) or_return
+	skt = sock.(TCP_Socket)
+
+	// NOTE(tetra): This is so that if we crash while the socket is open, we can
+	// bypass the cooldown period, and allow the next run of the program to
+	// use the same address immediately.
+	_ = set_option(skt, .Reuse_Address, true)
+
+	sockaddr := endpoint_to_sockaddr(endpoint)
+	res := win.connect(Platform_Socket(skt), &sockaddr, size_of(sockaddr))
+	if res < 0 {
+		err = Dial_Error(win.WSAGetLastError())
+		return
+	}
+
+	if options.no_delay {
+		_ = set_option(sock, .TCP_Nodelay, true) // NOTE(tetra): Not vital to succeed; error ignored
+	}
+
+	return
+}
+
+Bind_Error :: enum c.int {
+	// Another application is currently bound to this endpoint.
+	Address_In_Use = win.WSAEADDRINUSE,
+	// The address is not a local address on this machine.
+	Given_Nonlocal_Address = win.WSAEADDRNOTAVAIL,
+	// To bind a UDP socket to the broadcast address, the appropriate socket option must be set.
+	Broadcast_Disabled = win.WSAEACCES,
+	// The address family of the address does not match that of the socket.
+	Address_Family_Mismatch = win.WSAEFAULT,
+	// The socket is already bound to an address.
+	Already_Bound = win.WSAEINVAL,
+	// There are not enough ephemeral ports available.
+	No_Ports_Available = win.WSAENOBUFS,
+}
+
+bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Network_Error) {
+	sockaddr := endpoint_to_sockaddr(ep)
+	s := any_socket_to_socket(skt)
+	res := win.bind(Platform_Socket(s), &sockaddr, size_of(sockaddr))
+	if res < 0 {
+		err = Bind_Error(win.WSAGetLastError())
+	}
+	return
+}
+
+
+// This type of socket becomes bound when you try to send data.
+// This is likely what you want if you want to send data unsolicited.
+//
+// This is like a client TCP socket, except that it can send data to any remote endpoint without needing to establish a connection first.
+make_unbound_udp_socket :: proc(family: Address_Family) -> (skt: UDP_Socket, err: Network_Error) {
+	sock := create_socket(family, .UDP) or_return
+	skt = sock.(UDP_Socket)
+	return
+}
+
+// This type of socket is bound immediately, which enables it to receive data on the port.
+// Since it's UDP, it's also able to send data without receiving any first.
+//
+// This is like a listening TCP socket, except that data packets can be sent and received without needing to establish a connection first.
+//
+// The bound_address is the address of the network interface that you want to use, or a loopback address if you don't care which to use.
+make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (skt: UDP_Socket, err: Network_Error) {
+	skt = make_unbound_udp_socket(family_from_address(bound_address)) or_return
+	bind(skt, {bound_address, port}) or_return
+	return
+}
+
+
+
+Listen_Error :: enum c.int {
+	Address_In_Use = win.WSAEADDRINUSE,
+	Already_Connected = win.WSAEISCONN,
+	No_Socket_Descriptors_Available = win.WSAEMFILE,
+	No_Buffer_Space_Available = win.WSAENOBUFS,
+	Nonlocal_Address = win.WSAEADDRNOTAVAIL,
+	Not_Socket = win.WSAENOTSOCK,
+	Listening_Not_Supported_For_This_Socket = win.WSAEOPNOTSUPP,
+}
+
+listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_Socket, err: Network_Error) {
+	assert(backlog > 0 && i32(backlog) < max(i32))
+
+	family := family_from_endpoint(interface_endpoint)
+	sock := create_socket(family, .TCP) or_return
+	skt = sock.(TCP_Socket)
+
+	// NOTE(tetra): While I'm not 100% clear on it, my understanding is that this will
+	// prevent hijacking of the server's endpoint by other applications.
+	set_option(skt, .Exclusive_Addr_Use, true) or_return
+
+	bind(sock, interface_endpoint) or_return
+
+	res := win.listen(Platform_Socket(skt), i32(backlog))
+	if res == win.SOCKET_ERROR {
+		err = Listen_Error(win.WSAGetLastError())
+		return
+	}
+
+	return
+}
+
+
+
+Accept_Error :: enum c.int {
+	Not_Listening = win.WSAEINVAL,
+	No_Socket_Descriptors_Available_For_Client_Socket = win.WSAEMFILE,
+	No_Buffer_Space_Available = win.WSAENOBUFS,
+	Not_Socket = win.WSAENOTSOCK,
+	Not_Connection_Oriented_Socket = win.WSAEOPNOTSUPP,
+	Would_Block = win.WSAEWOULDBLOCK, // TODO: we may need special handling for this; maybe make a socket a struct with metadata?
+}
+
+accept_tcp :: proc(sock: TCP_Socket, options := default_tcp_options) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) {
+	for {
+		sockaddr: win.SOCKADDR_STORAGE_LH
+		sockaddrlen := c.int(size_of(sockaddr))
+		client_sock := win.accept(Platform_Socket(sock), &sockaddr, &sockaddrlen)
+		if int(client_sock) == win.SOCKET_ERROR {
+			e := win.WSAGetLastError()
+			if e == win.WSAECONNRESET {
+				// NOTE(tetra): Reset just means that a client that connection immediately lost the connection.
+				// There's no need to concern the user with this, so we handle it for them.
+				// On Linux, this error isn't possible in the first place according the man pages, so we also
+				// can do this to match the behaviour.
+				continue
+			}
+			err = Accept_Error(e)
+			return
+		}
+		client = TCP_Socket(client_sock)
+		source = sockaddr_to_endpoint(&sockaddr)
+		if options.no_delay {
+			_ = set_option(client, .TCP_Nodelay, true) // NOTE(tetra): Not vital to succeed; error ignored
+		}
+		return
+	}
+}
+
+
+
+close :: proc(skt: Any_Socket) {
+	if s := any_socket_to_socket(skt); s != {} {
+		win.closesocket(Platform_Socket(s))
+	}
+}
+
+
+
+TCP_Recv_Error :: enum c.int {
+	Network_Subsystem_Failure = win.WSAENETDOWN,
+	Not_Connected = win.WSAENOTCONN,
+	Bad_Buffer = win.WSAEFAULT,
+	Keepalive_Failure = win.WSAENETRESET,
+	Not_Socket = win.WSAENOTSOCK,
+	Shutdown = win.WSAESHUTDOWN,
+	Would_Block = win.WSAEWOULDBLOCK,
+	Aborted = win.WSAECONNABORTED, // TODO: not functionally different from Reset; merge?
+	Timeout = win.WSAETIMEDOUT,
+	Connection_Closed = win.WSAECONNRESET, // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them?
+	Host_Unreachable = win.WSAEHOSTUNREACH, // TODO: verify can actually happen
+}
+
+recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) {
+	if len(buf) <= 0 {
+		return
+	}
+	res := win.recv(Platform_Socket(skt), raw_data(buf), c.int(len(buf)), 0)
+	if res < 0 {
+		err = TCP_Recv_Error(win.WSAGetLastError())
+		return
+	}
+	return int(res), nil
+}
+
+UDP_Recv_Error :: enum c.int {
+	Network_Subsystem_Failure = win.WSAENETDOWN,
+	Aborted = win.WSAECONNABORTED, // TODO: not functionally different from Reset; merge?
+	// UDP packets are limited in size, and the length of the incoming message exceeded it.
+	Truncated = win.WSAEMSGSIZE,
+	// The machine at the remote endpoint doesn't have the given port open to receiving UDP data.
+	Remote_Not_Listening = win.WSAECONNRESET,
+	Shutdown = win.WSAESHUTDOWN,
+	// A broadcast address was specified, but the .Broadcast socket option isn't set.
+	Broadcast_Disabled = win.WSAEACCES,
+	Bad_Buffer = win.WSAEFAULT,
+	No_Buffer_Space_Available = win.WSAENOBUFS,
+	// The socket is not valid socket handle.
+	Not_Socket = win.WSAENOTSOCK,
+	Would_Block = win.WSAEWOULDBLOCK,
+	// The remote host cannot be reached from this host at this time.
+	Host_Unreachable = win.WSAEHOSTUNREACH,
+	// The network cannot be reached from this host at this time.
+	Offline = win.WSAENETUNREACH,
+	Timeout = win.WSAETIMEDOUT,
+	// The socket isn't bound; an unknown flag specified; or MSG_OOB specified with SO_OOBINLINE enabled.
+	Incorrectly_Configured = win.WSAEINVAL, // TODO: can this actually happen?
+	// The message took more hops than was allowed (the Time To Live) to reach the remote endpoint.
+	TTL_Expired = win.WSAENETRESET,
+}
+
+recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) {
+	if len(buf) <= 0 {
+		return
+	}
+
+	from: win.SOCKADDR_STORAGE_LH
+	fromsize := c.int(size_of(from))
+	res := win.recvfrom(Platform_Socket(skt), raw_data(buf), c.int(len(buf)), 0, &from, &fromsize)
+	if res < 0 {
+		err = UDP_Recv_Error(win.WSAGetLastError())
+		return
+	}
+
+	bytes_read = int(res)
+	remote_endpoint = sockaddr_to_endpoint(&from)
+	return
+}
+
+recv :: proc{recv_tcp, recv_udp}
+
+
+//
+// TODO: consider merging some errors to make handling them easier
+// TODO: verify once more what errors to actually expose
+//
+
+TCP_Send_Error :: enum c.int {
+	Aborted = win.WSAECONNABORTED, // TODO: not functionally different from Reset; merge?
+	Not_Connected = win.WSAENOTCONN,
+	Shutdown = win.WSAESHUTDOWN,
+	Connection_Closed = win.WSAECONNRESET,
+	No_Buffer_Space_Available = win.WSAENOBUFS,
+	Network_Subsystem_Failure = win.WSAENETDOWN,
+	Host_Unreachable = win.WSAEHOSTUNREACH,
+	Offline = win.WSAENETUNREACH, // TODO: verify possible, as not mentioned in docs
+	Timeout = win.WSAETIMEDOUT,
+	// A broadcast address was specified, but the .Broadcast socket option isn't set.
+	Broadcast_Disabled = win.WSAEACCES,
+	Bad_Buffer = win.WSAEFAULT,
+	// Connection is broken due to keepalive activity detecting a failure during the operation.
+	Keepalive_Failure = win.WSAENETRESET, // TODO: not functionally different from Reset; merge?
+	// The so-called socket is not an open socket.
+	Not_Socket = win.WSAENOTSOCK,
+}
+
+// Repeatedly sends data until the entire buffer is sent.
+// If a send fails before all data is sent, returns the amount
+// sent up to that point.
+send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) {
+	for bytes_written < len(buf) {
+		limit := min(int(max(i32)), len(buf) - bytes_written)
+		remaining := buf[bytes_written:]
+		res := win.send(Platform_Socket(skt), raw_data(remaining), c.int(limit), 0)
+		if res < 0 {
+			err = TCP_Send_Error(win.WSAGetLastError())
+			return
+		}
+		bytes_written += int(res)
+	}
+	return
+}
+
+UDP_Send_Error :: enum c.int {
+	Network_Subsystem_Failure = win.WSAENETDOWN,
+	Aborted = win.WSAECONNABORTED, // TODO: not functionally different from Reset; merge?
+	// UDP packets are limited in size, and len(buf) exceeded it.
+	Message_Too_Long = win.WSAEMSGSIZE,
+	// The machine at the remote endpoint doesn't have the given port open to receiving UDP data.
+	Remote_Not_Listening = win.WSAECONNRESET,
+	Shutdown = win.WSAESHUTDOWN,
+	// A broadcast address was specified, but the .Broadcast socket option isn't set.
+	Broadcast_Disabled = win.WSAEACCES,
+	Bad_Buffer = win.WSAEFAULT,
+	// Connection is broken due to keepalive activity detecting a failure during the operation.
+	Keepalive_Failure = win.WSAENETRESET, // TODO: not functionally different from Reset; merge?
+	No_Buffer_Space_Available = win.WSAENOBUFS,
+	// The socket is not valid socket handle.
+	Not_Socket = win.WSAENOTSOCK,
+	// This socket is unidirectional and cannot be used to send any data.
+	// TODO: verify possible; decide whether to keep if not
+	Receive_Only = win.WSAEOPNOTSUPP,
+	Would_Block = win.WSAEWOULDBLOCK,
+	// The remote host cannot be reached from this host at this time.
+	Host_Unreachable = win.WSAEHOSTUNREACH,
+	// Attempt to send to the Any address.
+	Cannot_Use_Any_Address = win.WSAEADDRNOTAVAIL,
+	// The address is of an incorrect address family for this socket.
+	Family_Not_Supported_For_This_Socket = win.WSAEAFNOSUPPORT,
+	// The network cannot be reached from this host at this time.
+	Offline = win.WSAENETUNREACH,
+	Timeout = win.WSAETIMEDOUT,
+}
+
+// Sends a single UDP datagram packet.
+//
+// Datagrams are limited in size; attempting to send more than this limit at once will result in a Message_Too_Long error.
+// UDP packets are not guarenteed to be received in order.
+send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) {
+	if len(buf) > int(max(c.int)) {
+		// NOTE(tetra): If we don't guard this, we'll return (0, nil) instead, which is misleading.
+		err = .Message_Too_Long
+		return
+	}
+	toaddr := endpoint_to_sockaddr(to)
+	res := win.sendto(Platform_Socket(skt), raw_data(buf), c.int(len(buf)), 0, &toaddr, size_of(toaddr))
+	if res < 0 {
+		err = UDP_Send_Error(win.WSAGetLastError())
+		return
+	}
+	bytes_written = int(res)
+	return
+}
+
+send :: proc{send_tcp, send_udp}
+
+
+
+
+Shutdown_Manner :: enum c.int {
+	Receive = win.SD_RECEIVE,
+	Send = win.SD_SEND,
+	Both = win.SD_BOTH,
+}
+
+Shutdown_Error :: enum c.int {
+	Aborted = win.WSAECONNABORTED,
+	Reset = win.WSAECONNRESET,
+	Offline = win.WSAENETDOWN,
+	Not_Connected = win.WSAENOTCONN,
+	Not_Socket = win.WSAENOTSOCK,
+	Invalid_Manner = win.WSAEINVAL,
+}
+
+shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) {
+	s := any_socket_to_socket(skt)
+	res := win.shutdown(Platform_Socket(s), c.int(manner))
+	if res < 0 {
+		return Shutdown_Error(win.WSAGetLastError())
+	}
+	return
+}
+
+
+
+
+Socket_Option :: enum c.int {
+	// bool: Whether the address that this socket is bound to can be reused by other sockets.
+	//       This allows you to bypass the cooldown period if a program dies while the socket is bound.
+	Reuse_Address = win.SO_REUSEADDR,
+	// bool: Whether other programs will be inhibited from binding the same endpoint as this socket.
+	Exclusive_Addr_Use = win.SO_EXCLUSIVEADDRUSE,
+	// bool: When true, keepalive packets will be automatically be sent for this connection.
+	// TODO: verify this understanding
+	Keep_Alive = win.SO_KEEPALIVE,
+	// bool: When true, client connections will immediately be sent a TCP/IP RST response, rather than
+	//       being accepted.
+	Conditional_Accept = win.SO_CONDITIONAL_ACCEPT,
+	// bool: If true, when the socket is closed, but data is still waiting to be sent, discard that data.
+	Dont_Linger = win.SO_DONTLINGER,
+	// bool: When true, 'out-of-band' data sent over the socket will be read by a normal net.recv() call,
+	//       the same as normal 'in-band' data.
+	Out_Of_Bounds_Data_Inline = win.SO_OOBINLINE,
+	// bool: When true, disables send-coalescing, therefore reducing latency.
+	TCP_Nodelay = win.TCP_NODELAY,
+	// win.LINGER: Customizes how long (if at all) the socket will remain open when there is some remaining data
+	//             waiting to be sent, and net.close() is called.
+	Linger = win.SO_LINGER,
+	// win.DWORD: The size, in bytes, of the OS-managed receive-buffer for this socket.
+	Receive_Buffer_Size = win.SO_RCVBUF,
+	// win.DWORD: The size, in bytes, of the OS-managed send-buffer for this socket.
+	Send_Buffer_Size = win.SO_SNDBUF,
+	// win.DWORD: For blocking sockets, the time in milliseconds to wait for incoming data to be received, before giving up and returning .Timeout.
+	//            For non-blocking sockets, ignored.
+	//            Use a value of zero to potentially wait forever.
+	Receive_Timeout = win.SO_RCVTIMEO,
+	// win.DWORD: For blocking sockets, the time in milliseconds to wait for outgoing data to be sent, before giving up and returning .Timeout.
+	//            For non-blocking sockets, ignored.
+	//            Use a value of zero to potentially wait forever.
+	Send_Timeout = win.SO_SNDTIMEO,
+	// bool: Allow sending to, receiving from, and binding to, a broadcast address.
+	Broadcast = win.SO_BROADCAST,
+}
+
+Socket_Option_Error :: enum c.int {
+	Linger_Only_Supports_Whole_Seconds = 1,
+	// The given value is too big or small to be given to the OS.
+	Value_Out_Of_Range,
+
+	Network_Subsystem_Failure = win.WSAENETDOWN,
+	Timeout_When_Keepalive_Set = win.WSAENETRESET,
+	Invalid_Option_For_Socket = win.WSAENOPROTOOPT,
+	Reset_When_Keepalive_Set = win.WSAENOTCONN,
+	Not_Socket = win.WSAENOTSOCK,
+}
+
+set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error {
+	level := win.SOL_SOCKET if option != .TCP_Nodelay else win.IPPROTO_TCP
+
+	bool_value: b32
+	int_value: i32
+	linger_value: win.LINGER
+
+	ptr: rawptr
+	len: c.int
+
+	switch option {
+	case
+		.Reuse_Address,
+		.Exclusive_Addr_Use,
+		.Keep_Alive,
+		.Out_Of_Bounds_Data_Inline,
+		.TCP_Nodelay,
+		.Broadcast,
+		.Conditional_Accept,
+		.Dont_Linger:
+			switch x in value {
+			case bool, b8:
+				x2 := x
+				bool_value = b32((^bool)(&x2)^)
+			case b16:
+				bool_value = b32(x)
+			case b32:
+				bool_value = b32(x)
+			case b64:
+				bool_value = b32(x)
+			case:
+				panic("set_option() value must be a boolean here", loc)
+			}
+			ptr = &bool_value
+			len = size_of(bool_value)
+	case .Linger:
+		t, ok := value.(time.Duration)
+		if !ok do panic("set_option() value must be a time.Duration here", loc)
+
+		num_secs := i64(time.duration_seconds(t))
+		if time.Duration(num_secs * 1e9) != t do return .Linger_Only_Supports_Whole_Seconds
+		if num_secs > i64(max(u16)) do return .Value_Out_Of_Range
+		linger_value.l_onoff = 1
+		linger_value.l_linger = c.ushort(num_secs)
+
+		ptr = &linger_value
+		len = size_of(linger_value)
+	case
+		.Receive_Timeout,
+		.Send_Timeout:
+			t, ok := value.(time.Duration)
+			if !ok do panic("set_option() value must be a time.Duration here", loc)
+
+			int_value = i32(time.duration_milliseconds(t))
+			ptr = &int_value
+			len = size_of(int_value)
+
+	case
+		.Receive_Buffer_Size,
+		.Send_Buffer_Size:
+			switch i in value {
+			case i8, u8:   i2 := i; int_value = c.int((^u8)(&i2)^)
+			case i16, u16: i2 := i; int_value = c.int((^u16)(&i2)^)
+			case i32, u32: i2 := i; int_value = c.int((^u32)(&i2)^)
+			case i64, u64: i2 := i; int_value = c.int((^u64)(&i2)^)
+			case i128, u128: i2 := i; int_value = c.int((^u128)(&i2)^)
+			case int, uint: i2 := i; int_value = c.int((^uint)(&i2)^)
+			case:
+				panic("set_option() value must be an integer here", loc)
+			}
+			ptr = &int_value
+			len = size_of(int_value)
+	}
+
+	skt := any_socket_to_socket(s)
+	res := win.setsockopt(Platform_Socket(skt), c.int(level), c.int(option), ptr, len)
+	if res < 0 {
+		return Socket_Option_Error(win.WSAGetLastError())
+	}
+
+	return nil
+}

+ 235 - 0
core/net/url.odin

@@ -0,0 +1,235 @@
+/*
+	Copyright 2022 Tetralux        <[email protected]>
+	Copyright 2022 Colin Davidson  <[email protected]>
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Tetralux:        Initial implementation
+		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
+		Jeroen van Rijn: Cross platform unification, code style, documentation
+*/
+
+/*
+	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
+	For other protocols and their features, see subdirectories of this package.
+*/
+package net
+
+import "core:strings"
+import "core:strconv"
+import "core:unicode/utf8"
+import "core:mem"
+
+split_url :: proc(url: string, allocator := context.allocator) -> (scheme, host, path: string, queries: map[string]string) {
+	s := url
+
+	i := strings.last_index(s, "://")
+	if i != -1 {
+		scheme = s[:i]
+		s = s[i+3:]
+	}
+
+	i = strings.index(s, "?")
+	if i != -1 {
+		query_str := s[i+1:]
+		s = s[:i]
+		if query_str != "" {
+			queries_parts := strings.split(query_str, "&")
+			queries = make(map[string]string, len(queries_parts), allocator)
+			for q in queries_parts {
+				parts := strings.split(q, "=")
+				switch len(parts) {
+				case 1:  queries[parts[0]] = ""        // NOTE(tetra): Query not set to anything, was but present.
+				case 2:  queries[parts[0]] = parts[1]  // NOTE(tetra): Query set to something.
+				case:    break
+				}
+			}
+		}
+	}
+
+	i = strings.index(s, "/")
+	if i == -1 {
+		host = s
+		path = "/"
+	} else {
+		host = s[:i]
+		path = s[i:]
+	}
+
+	return
+}
+
+join_url :: proc(scheme, host, path: string, queries: map[string]string, allocator := context.allocator) -> string {
+	using strings
+
+	b := make_builder(allocator)
+	grow_builder(&b, len(scheme) + 3 + len(host) + 1 + len(path))
+
+	write_string(&b, scheme)
+	write_string(&b, "://")
+	write_string(&b, trim_space(host))
+
+	if path != "" {
+		if path[0] != '/' do write_string(&b, "/")
+		write_string(&b, trim_space(path))
+	}
+
+
+	if len(queries) > 0 do write_string(&b, "?")
+	for query_name, query_value in queries {
+		write_string(&b, query_name)
+		if query_value != "" {
+			write_string(&b, "=")
+			write_string(&b, query_value)
+		}
+	}
+
+	return to_string(b)
+}
+
+percent_encode :: proc(s: string, allocator := context.allocator) -> string {
+	using strings
+
+	b := make_builder(allocator)
+	grow_builder(&b, len(s) + 16) // NOTE(tetra): A reasonable number to allow for the number of things we need to escape.
+
+	for ch in s {
+		switch ch {
+		case 'A'..='Z', 'a'..='z', '0'..='9', '-', '_', '.', '~':
+			write_rune_builder(&b, ch)
+		case:
+			bytes, n := utf8.encode_rune(ch)
+			for byte in bytes[:n] {
+				buf: [2]u8 = ---
+				t := strconv.append_int(buf[:], i64(byte), 16)
+				write_rune_builder(&b, '%')
+				write_string(&b, t)
+			}
+		}
+	}
+
+	return to_string(b)
+}
+
+percent_decode :: proc(encoded_string: string, allocator := context.allocator) -> (decoded_string: string, ok: bool) {
+	using strings
+
+	b := make_builder(allocator)
+	grow_builder(&b, len(encoded_string))
+	defer if !ok do destroy_builder(&b)
+
+	stack_buf: [4]u8
+	pending := mem.buffer_from_slice(stack_buf[:])
+	s := encoded_string
+
+	for len(s) > 0 {
+		i := index_rune(s, '%')
+		if i == -1 {
+			write_string(&b, s) // no '%'s; the string is already decoded
+			break
+		}
+
+		write_string(&b, s[:i])
+		s = s[i:]
+
+		if len(s) == 0 do return // percent without anything after it
+		s = s[1:]
+
+		if s[0] == '%' {
+			write_rune_builder(&b, '%')
+			s = s[1:]
+			continue
+		}
+
+		if len(s) < 2 do return // percent without encoded value
+
+		n: int
+		n, _ = strconv.parse_int(s[:2], 16)
+		switch n {
+		case 0x20:  write_rune_builder(&b, ' ')
+		case 0x21:  write_rune_builder(&b, '!')
+		case 0x23:  write_rune_builder(&b, '#')
+		case 0x24:  write_rune_builder(&b, '$')
+		case 0x25:  write_rune_builder(&b, '%')
+		case 0x26:  write_rune_builder(&b, '&')
+		case 0x27:  write_rune_builder(&b, '\'')
+		case 0x28:  write_rune_builder(&b, '(')
+		case 0x29:  write_rune_builder(&b, ')')
+		case 0x2A:  write_rune_builder(&b, '*')
+		case 0x2B:  write_rune_builder(&b, '+')
+		case 0x2C:  write_rune_builder(&b, ',')
+		case 0x2F:  write_rune_builder(&b, '/')
+		case 0x3A:  write_rune_builder(&b, ':')
+		case 0x3B:  write_rune_builder(&b, ';')
+		case 0x3D:  write_rune_builder(&b, '=')
+		case 0x3F:  write_rune_builder(&b, '?')
+		case 0x40:  write_rune_builder(&b, '@')
+		case 0x5B:  write_rune_builder(&b, '[')
+		case 0x5D:  write_rune_builder(&b, ']')
+		case:
+			// utf-8 bytes
+			// TODO(tetra): Audit this - 4 bytes???
+			append(&pending, s[0])
+			append(&pending, s[1])
+			if len(pending) == 4 {
+				r, _ := utf8.decode_rune(pending[:])
+				write_rune_builder(&b, r)
+				clear(&pending)
+			}
+		}
+		s = s[2:]
+	}
+
+	ok = true
+	decoded_string = to_string(b)
+	return
+}
+
+//
+// TODO: encoding/base64 is broken...
+//
+
+// // TODO(tetra): The whole "table" stuff in encoding/base64 is too impenetrable for me to
+// // make a table for this ... sigh - so this'll do for now.
+/*
+base64url_encode :: proc(data: []byte, allocator := context.allocator) -> string {
+	out := transmute([]byte) base64.encode(data, base64.ENC_TABLE, allocator);
+	for b, i in out {
+		switch b {
+		case '+': out[i] = '-';
+		case '/': out[i] = '_';
+		}
+	}
+	i := len(out)-1;
+	for ; i >= 0; i -= 1 {
+		if out[i] != '=' do break;
+	}
+	return string(out[:i+1]);
+}
+
+base64url_decode :: proc(s: string, allocator := context.allocator) -> []byte {
+	size := len(s);
+	padding := 0;
+	for size % 4 != 0 {
+		size += 1; // TODO: SPEED
+		padding += 1;
+	}
+
+	temp := make([]byte, size, context.temp_allocator);
+	copy(temp, transmute([]byte) s);
+
+	for b, i in temp {
+		switch b {
+		case '-': temp[i] = '+';
+		case '_': temp[i] = '/';
+		}
+	}
+
+	for in 0..padding-1 {
+		temp[len(temp)-1] = '=';
+	}
+
+	return base64.decode(string(temp), base64.DEC_TABLE, allocator);
+}
+*/

+ 239 - 0
core/os/os_darwin.odin

@@ -179,6 +179,93 @@ RTLD_NODELETE :: 0x80
 RTLD_NOLOAD   :: 0x10
 RTLD_NOLOAD   :: 0x10
 RTLD_FIRST    :: 0x100
 RTLD_FIRST    :: 0x100
 
 
+SOL_SOCKET :: 0xFFFF
+
+SOCK_STREAM    :: 1
+SOCK_DGRAM     :: 2
+SOCK_RAW       :: 3
+SOCK_RDM       :: 4
+SOCK_SEQPACKET :: 5
+
+SO_DEBUG       :: 0x0001
+SO_ACCEPTCONN  :: 0x0002
+SO_REUSEADDR   :: 0x0004
+SO_KEEPALIVE   :: 0x0008
+SO_DONTROUTE   :: 0x0010
+SO_BROADCAST   :: 0x0020
+SO_USELOOPBACK :: 0x0040
+SO_LINGER      :: 0x0080
+SO_OOBINLINE   :: 0x0100
+SO_REUSEPORT   :: 0x0200
+SO_TIMESTAMP   :: 0x0400
+
+SO_DONTTRUNC   :: 0x2000
+SO_WANTMORE    :: 0x4000
+SO_WANTOOBFLAG :: 0x8000
+SO_SNDBUF      :: 0x1001
+SO_RCVBUF      :: 0x1002
+SO_SNDLOWAT	   :: 0x1003
+SO_RCVLOWAT    :: 0x1004
+SO_SNDTIMEO    :: 0x1005
+SO_RCVTIMEO    :: 0x1006
+SO_ERROR       :: 0x1007
+SO_TYPE        :: 0x1008
+SO_PRIVSTATE   :: 0x1009
+SO_NREAD       :: 0x1020
+SO_NKE         :: 0x1021
+
+AF_UNSPEC     :: 0
+AF_LOCAL      :: 1
+AF_UNIX       :: AF_LOCAL
+AF_INET       :: 2
+AF_IMPLINK    :: 3
+AF_PUP        :: 4
+AF_CHAOS      :: 5
+AF_NS         :: 6
+AF_ISO        :: 7
+AF_OSI        :: AF_ISO
+AF_ECMA       :: 8
+AF_DATAKIT    :: 9
+AF_CCITT      :: 10
+AF_SNA        :: 11
+AF_DECnet     :: 12
+AF_DLI        :: 13
+AF_LAT        :: 14
+AF_HYLINK     :: 15
+AF_APPLETALK  :: 16
+AF_ROUTE	  :: 17
+AF_LINK		  :: 18
+pseudo_AF_XTP :: 19
+AF_COIP		  :: 20
+AF_CNT		  :: 21
+pseudo_AF_RTIP :: 22
+AF_IPX         :: 23
+AF_SIP         :: 24
+pseudo_AF_PIP  :: 25
+pseudo_AF_BLUE :: 26
+AF_NDRV        :: 27
+AF_ISDN        :: 28
+AF_E164        :: AF_ISDN
+pseudo_AF_KEY  :: 29
+AF_INET6       :: 30
+AF_NATM        :: 31
+AF_SYSTEM      :: 32
+AF_NETBIOS     :: 33
+AF_PPP         :: 34
+
+TCP_NODELAY	:: 0x01
+TCP_MAXSEG	:: 0x02
+TCP_NOPUSH	:: 0x04
+TCP_NOOPT	:: 0x08
+
+IPPROTO_ICMP :: 1
+IPPROTO_TCP  :: 6
+IPPROTO_UDP  :: 17
+
+SHUT_RD   :: 0
+SHUT_WR   :: 1
+SHUT_RDWR :: 2
+
 
 
 // "Argv" arguments converted to Odin strings
 // "Argv" arguments converted to Odin strings
 args := _alloc_command_line_arguments()
 args := _alloc_command_line_arguments()
@@ -224,6 +311,58 @@ Dirent :: struct {
 
 
 Dir :: distinct rawptr // DIR*
 Dir :: distinct rawptr // DIR*
 
 
+SOCKADDR :: struct #packed {
+	len: c.char,
+	family: c.char,
+	sa_data: [14]c.char,
+}
+
+SOCKADDR_STORAGE_LH :: struct #packed {
+	len: c.char,
+	family: c.char,
+	__ss_pad1: [6]c.char,
+	__ss_align: i64,
+	__ss_pad2: [112]c.char,
+}
+
+sockaddr_in :: struct #packed {
+	sin_len: c.char,
+	sin_family: c.char,
+	sin_port: u16be,
+	sin_addr: in_addr,
+	sin_zero: [8]c.char,
+}
+
+sockaddr_in6 :: struct #packed {
+	sin6_len: c.char,
+	sin6_family: c.char,
+	sin6_port: u16be,
+	sin6_flowinfo: c.uint,
+	sin6_addr: in6_addr,
+	sin6_scope_id: c.uint,
+}
+
+in_addr :: struct #packed {
+	s_addr: u32,
+}
+
+in6_addr :: struct #packed {
+	s6_addr: [16]u8,
+}
+
+Timeval :: struct {
+	seconds: i64,
+	nanoseconds: int,
+}
+
+Linger :: struct {
+	onoff: int,
+	linger: int,
+}
+
+Socket    :: distinct int
+socklen_t :: c.int
+
 // File type
 // File type
 S_IFMT   :: 0o170000 // Type of file mask
 S_IFMT   :: 0o170000 // Type of file mask
 S_IFIFO  :: 0o010000 // Named pipe (fifo)
 S_IFIFO  :: 0o010000 // Named pipe (fifo)
@@ -318,6 +457,18 @@ foreign libc {
 	@(link_name="strerror") _darwin_string_error :: proc(num : c.int) -> cstring ---
 	@(link_name="strerror") _darwin_string_error :: proc(num : c.int) -> cstring ---
 	@(link_name="sysctlbyname") _sysctlbyname    :: proc(path: cstring, oldp: rawptr, oldlenp: rawptr, newp: rawptr, newlen: int) -> c.int ---
 	@(link_name="sysctlbyname") _sysctlbyname    :: proc(path: cstring, oldp: rawptr, oldlenp: rawptr, newp: rawptr, newlen: int) -> c.int ---
 
 
+	@(link_name="socket")           _unix_socket        :: proc(domain: int, type: int, protocol: int) -> int ---
+	@(link_name="listen")           _unix_listen        :: proc(socket: int, backlog: int) -> int ---
+	@(link_name="accept")           _unix_accept        :: proc(socket: int, addr: rawptr, addr_len: rawptr) -> int ---
+	@(link_name="connect")          _unix_connect       :: proc(socket: int, addr: rawptr, addr_len: socklen_t) -> int ---
+	@(link_name="bind")             _unix_bind          :: proc(socket: int, addr: rawptr, addr_len: socklen_t) -> int ---
+	@(link_name="setsockopt")       _unix_setsockopt    :: proc(socket: int, level: int, opt_name: int, opt_val: rawptr, opt_len: socklen_t) -> int ---
+	@(link_name="recvfrom")         _unix_recvfrom      :: proc(socket: int, buffer: rawptr, buffer_len: c.size_t, flags: int, addr: rawptr, addr_len: ^socklen_t) -> c.ssize_t ---
+	@(link_name="recv")             _unix_recv          :: proc(socket: int, buffer: rawptr, buffer_len: c.size_t, flags: int) -> c.ssize_t ---
+	@(link_name="sendto")           _unix_sendto        :: proc(socket: int, buffer: rawptr, buffer_len: c.size_t, flags: int, addr: rawptr, addr_len: socklen_t) -> c.ssize_t ---
+	@(link_name="send")             _unix_send          :: proc(socket: int, buffer: rawptr, buffer_len: c.size_t, flags: int) -> c.ssize_t ---
+	@(link_name="shutdown")         _unix_shutdown      :: proc(socket: int, how: int) -> int ---
+
 	@(link_name="exit")    _unix_exit :: proc(status: c.int) -> ! ---
 	@(link_name="exit")    _unix_exit :: proc(status: c.int) -> ! ---
 }
 }
 
 
@@ -815,3 +966,91 @@ _alloc_command_line_arguments :: proc() -> []string {
 	}
 	}
 	return res
 	return res
 }
 }
+
+socket :: proc(domain: int, type: int, protocol: int) -> (Socket, Errno) {
+	result := _unix_socket(domain, type, protocol)
+	if result < 0 {
+		return 0, Errno(get_last_error())
+	}
+	return Socket(result), ERROR_NONE
+}
+
+connect :: proc(sd: Socket, addr: ^SOCKADDR, len: socklen_t) -> (Errno) {
+	result := _unix_connect(int(sd), addr, len)
+	if result < 0 {
+		return Errno(get_last_error())
+	}
+	return ERROR_NONE
+}
+
+bind :: proc(sd: Socket, addr: ^SOCKADDR, len: socklen_t) -> (Errno) {
+	result := _unix_bind(int(sd), addr, len)
+	if result < 0 {
+		return Errno(get_last_error())
+	}
+	return ERROR_NONE
+}
+
+accept :: proc(sd: Socket, addr: ^SOCKADDR, len: rawptr) -> (Socket, Errno) {
+	result := _unix_accept(int(sd), rawptr(addr), len)
+	if result < 0 {
+		return 0, Errno(get_last_error())
+	}
+	return Socket(result), ERROR_NONE
+}
+
+listen :: proc(sd: Socket, backlog: int) -> (Errno) {
+	result := _unix_listen(int(sd), backlog)
+	if result < 0 {
+		return Errno(get_last_error())
+	}
+	return ERROR_NONE
+}
+
+setsockopt :: proc(sd: Socket, level: int, optname: int, optval: rawptr, optlen: socklen_t) -> (Errno) {
+	result := _unix_setsockopt(int(sd), level, optname, optval, optlen)
+	if result < 0 {
+		return Errno(get_last_error())
+	}
+	return ERROR_NONE
+}
+
+recvfrom :: proc(sd: Socket, data: []byte, flags: int, addr: ^SOCKADDR, addr_size: ^socklen_t) -> (u32, Errno) {
+	result := _unix_recvfrom(int(sd), raw_data(data), len(data), flags, addr, addr_size)
+	if result < 0 {
+		return 0, Errno(get_last_error())
+	}
+	return u32(result), ERROR_NONE
+}
+
+recv :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Errno) {
+	result := _unix_recv(int(sd), raw_data(data), len(data), flags)
+	if result < 0 {
+		return 0, Errno(get_last_error())
+	}
+	return u32(result), ERROR_NONE
+}
+
+sendto :: proc(sd: Socket, data: []u8, flags: int, addr: ^SOCKADDR, addrlen: socklen_t) -> (u32, Errno) {
+	result := _unix_sendto(int(sd), raw_data(data), len(data), flags, addr, addrlen)
+	if result < 0 {
+		return 0, Errno(get_last_error())
+	}
+	return u32(result), ERROR_NONE
+}
+
+send :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Errno) {
+	result := _unix_send(int(sd), raw_data(data), len(data), 0)
+	if result < 0 {
+		return 0, Errno(get_last_error())
+	}
+	return u32(result), ERROR_NONE
+}
+
+shutdown :: proc(sd: Socket, how: int) -> (Errno) {
+	result := _unix_shutdown(int(sd), how)
+	if result < 0 {
+		return Errno(get_last_error())
+	}
+	return ERROR_NONE
+}

+ 243 - 0
core/os/os_linux.odin

@@ -171,6 +171,59 @@ SEEK_DATA  :: 3
 SEEK_HOLE  :: 4
 SEEK_HOLE  :: 4
 SEEK_MAX   :: SEEK_HOLE
 SEEK_MAX   :: SEEK_HOLE
 
 
+
+AF_UNSPEC:    int : 0
+AF_UNIX:      int : 1
+AF_LOCAL:     int : AF_UNIX
+AF_INET:      int : 2
+AF_INET6:     int : 10
+AF_PACKET:    int : 17
+AF_BLUETOOTH: int : 31
+
+SOCK_STREAM:    int : 1
+SOCK_DGRAM:     int : 2
+SOCK_RAW:       int : 3
+SOCK_RDM:       int : 4
+SOCK_SEQPACKET: int : 5
+SOCK_PACKET:    int : 10
+
+INADDR_ANY:       c.ulong : 0
+INADDR_BROADCAST: c.ulong : 0xffffffff
+INADDR_NONE:      c.ulong : 0xffffffff
+INADDR_DUMMY:     c.ulong : 0xc0000008
+
+IPPROTO_IP:       int : 0
+IPPROTO_ICMP:     int : 1
+IPPROTO_TCP:      int : 6
+IPPROTO_UDP:      int : 17
+IPPROTO_IPV6:     int : 41
+IPPROTO_ETHERNET: int : 143
+IPPROTO_RAW:      int : 255
+
+SHUT_RD:   int : 0
+SHUT_WR:   int : 1
+SHUT_RDWR: int : 2
+
+
+SOL_SOCKET:   int : 1
+SO_DEBUG:     int : 1
+SO_REUSEADDR: int : 2
+SO_DONTROUTE: int : 5
+SO_BROADCAST: int : 6
+SO_SNDBUF:    int : 7
+SO_RCVBUF:    int : 8
+SO_KEEPALIVE: int : 9
+SO_OOBINLINE: int : 10
+SO_LINGER:    int : 13
+SO_REUSEPORT: int : 15
+SO_RCVTIMEO_NEW: int : 66
+SO_SNDTIMEO_NEW: int : 67
+
+TCP_NODELAY: int : 1
+TCP_CORK:    int : 3
+
+MSG_TRUNC : int : 0x20
+
 // NOTE(zangent): These are OS specific!
 // NOTE(zangent): These are OS specific!
 // Do not mix these up!
 // Do not mix these up!
 RTLD_LAZY         :: 0x001
 RTLD_LAZY         :: 0x001
@@ -217,6 +270,102 @@ Dirent :: struct {
 	name:   [256]byte,
 	name:   [256]byte,
 }
 }
 
 
+ADDRESS_FAMILY :: u16
+SOCKADDR :: struct #packed {
+	sa_family: ADDRESS_FAMILY,
+	sa_data: [14]c.char,
+}
+
+SOCKADDR_STORAGE_LH :: struct #packed {
+	ss_family: ADDRESS_FAMILY,
+	__ss_pad1: [6]c.char,
+	__ss_align: i64,
+	__ss_pad2: [112]c.char,
+}
+
+sockaddr_in :: struct #packed {
+	sin_family: ADDRESS_FAMILY,
+	sin_port: u16be,
+	sin_addr: in_addr,
+	sin_zero: [8]c.char,
+}
+
+sockaddr_in6 :: struct #packed {
+	sin6_family: ADDRESS_FAMILY,
+	sin6_port: u16be,
+	sin6_flowinfo: c.ulong,
+	sin6_addr: in6_addr,
+	sin6_scope_id: c.ulong,
+}
+
+in_addr :: struct #packed {
+	s_addr: u32,
+}
+
+in6_addr :: struct #packed {
+	s6_addr: [16]u8,
+}
+
+rtnl_link_stats :: struct #packed {
+	rx_packets:          u32,
+	tx_packets:          u32,
+	rx_bytes:            u32,
+	tx_bytes:            u32,
+	rx_errors:           u32,
+	tx_errors:           u32,
+	rx_dropped:          u32,
+	tx_dropped:          u32,
+	multicast:           u32,
+	collisions:          u32,
+	rx_length_errors:    u32,
+	rx_over_errors:      u32,
+	rx_crc_errors:       u32,
+	rx_frame_errors:     u32,
+	rx_fifo_errors:      u32,
+	rx_missed_errors:    u32,
+	tx_aborted_errors:   u32,
+	tx_carrier_errors:   u32,
+	tx_fifo_errors:      u32,
+	tx_heartbeat_errors: u32,
+	tx_window_errors:    u32,
+	rx_compressed:       u32,
+	tx_compressed:       u32,
+	rx_nohandler:        u32,
+}
+
+SIOCGIFFLAG :: enum c.int {
+	UP             = 0,  /* Interface is up.  */
+	BROADCAST      = 1,  /* Broadcast address valid.  */
+	DEBUG          = 2,  /* Turn on debugging.  */
+	LOOPBACK       = 3,  /* Is a loopback net.  */
+	POINT_TO_POINT = 4,  /* Interface is point-to-point link.  */
+	NO_TRAILERS    = 5,  /* Avoid use of trailers.  */
+	RUNNING        = 6,  /* Resources allocated.  */
+	NOARP          = 7,  /* No address resolution protocol.  */
+	PROMISC        = 8,  /* Receive all packets.  */
+	ALL_MULTI      = 9,  /* Receive all multicast packets. Unimplemented. */
+	MASTER         = 10, /* Master of a load balancer.  */
+	SLAVE          = 11, /* Slave of a load balancer.  */
+	MULTICAST      = 12, /* Supports multicast.  */
+	PORTSEL        = 13, /* Can set media type.  */
+	AUTOMEDIA      = 14, /* Auto media select active.  */
+	DYNAMIC        = 15, /* Dialup device with changing addresses.  */
+        LOWER_UP       = 16,
+        DORMANT        = 17,
+        ECHO           = 18,
+}
+SIOCGIFFLAGS :: bit_set[SIOCGIFFLAG; c.int]
+
+ifaddrs :: struct {
+	next:              ^ifaddrs,
+	name:              cstring,
+	flags:             SIOCGIFFLAGS,
+	address:           ^SOCKADDR,
+	netmask:           ^SOCKADDR,
+	broadcast_or_dest: ^SOCKADDR,  // Broadcast or Point-to-Point address
+	data:              rawptr,     // Address-specific data.
+}
+
 Dir :: distinct rawptr // DIR*
 Dir :: distinct rawptr // DIR*
 
 
 // File type
 // File type
@@ -297,6 +446,9 @@ foreign dl {
 	@(link_name="dlsym")            _unix_dlsym         :: proc(handle: rawptr, symbol: cstring) -> rawptr ---
 	@(link_name="dlsym")            _unix_dlsym         :: proc(handle: rawptr, symbol: cstring) -> rawptr ---
 	@(link_name="dlclose")          _unix_dlclose       :: proc(handle: rawptr) -> c.int ---
 	@(link_name="dlclose")          _unix_dlclose       :: proc(handle: rawptr) -> c.int ---
 	@(link_name="dlerror")          _unix_dlerror       :: proc() -> cstring ---
 	@(link_name="dlerror")          _unix_dlerror       :: proc() -> cstring ---
+
+	@(link_name="getifaddrs")       _getifaddrs         :: proc(ifap: ^^ifaddrs) -> (c.int) ---
+	@(link_name="freeifaddrs")      _freeifaddrs        :: proc(ifa: ^ifaddrs) ---
 }
 }
 
 
 is_path_separator :: proc(r: rune) -> bool {
 is_path_separator :: proc(r: rune) -> bool {
@@ -823,3 +975,94 @@ _alloc_command_line_arguments :: proc() -> []string {
 	}
 	}
 	return res
 	return res
 }
 }
+
+socket :: proc(domain: int, type: int, protocol: int) -> (Socket, Errno) {
+	result := unix.sys_socket(domain, type, protocol)
+	if result < 0 {
+		return 0, _get_errno(result)
+	}
+	return Socket(result), ERROR_NONE
+}
+
+bind :: proc(sd: Socket, addr: ^SOCKADDR, len: socklen_t) -> (Errno) {
+	result := unix.sys_bind(int(sd), addr, len)
+	if result < 0 {
+		return _get_errno(result)
+	}
+	return ERROR_NONE
+}
+
+
+connect :: proc(sd: Socket, addr: ^SOCKADDR, len: socklen_t) -> (Errno) {
+	result := unix.sys_connect(int(sd), addr, len)
+	if result < 0 {
+		return _get_errno(result)
+	}
+	return ERROR_NONE
+}
+
+accept :: proc(sd: Socket, addr: ^SOCKADDR, len: rawptr) -> (Socket, Errno) {
+	result := unix.sys_accept(int(sd), rawptr(addr), len)
+	if result < 0 {
+		return 0, _get_errno(result)
+	}
+	return Socket(result), ERROR_NONE
+}
+
+listen :: proc(sd: Socket, backlog: int) -> (Errno) {
+	result := unix.sys_listen(int(sd), backlog)
+	if result < 0 {
+		return _get_errno(result)
+	}
+	return ERROR_NONE
+}
+
+setsockopt :: proc(sd: Socket, level: int, optname: int, optval: rawptr, optlen: socklen_t) -> (Errno) {
+	result := unix.sys_setsockopt(int(sd), level, optname, optval, optlen)
+	if result < 0 {
+		return _get_errno(result)
+	}
+	return ERROR_NONE
+}
+
+
+recvfrom :: proc(sd: Socket, data: []byte, flags: int, addr: ^SOCKADDR, addr_size: ^socklen_t) -> (u32, Errno) {
+	result := unix.sys_recvfrom(int(sd), raw_data(data), len(data), flags, addr, uintptr(addr_size))
+	if result < 0 {
+		return 0, _get_errno(int(result))
+	}
+	return u32(result), ERROR_NONE
+}
+
+recv :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Errno) {
+	result := unix.sys_recvfrom(int(sd), raw_data(data), len(data), flags, nil, 0)
+	if result < 0 {
+		return 0, _get_errno(int(result))
+	}
+	return u32(result), ERROR_NONE
+}
+
+
+sendto :: proc(sd: Socket, data: []u8, flags: int, addr: ^SOCKADDR, addrlen: socklen_t) -> (u32, Errno) {
+	result := unix.sys_sendto(int(sd), raw_data(data), len(data), flags, addr, addrlen)
+	if result < 0 {
+		return 0, _get_errno(int(result))
+	}
+	return u32(result), ERROR_NONE
+}
+
+send :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Errno) {
+	result := unix.sys_sendto(int(sd), raw_data(data), len(data), 0, nil, 0)
+	if result < 0 {
+		return 0, _get_errno(int(result))
+	}
+	return u32(result), ERROR_NONE
+}
+
+shutdown :: proc(sd: Socket, how: int) -> (Errno) {
+	result := unix.sys_shutdown(int(sd), how)
+	if result < 0 {
+		return _get_errno(result)
+	}
+	return ERROR_NONE
+}

+ 36 - 0
core/sys/unix/syscalls_linux.odin

@@ -2008,6 +2008,42 @@ sys_utimensat :: proc "contextless" (dfd: int, path: cstring, times: rawptr, fla
 	return int(intrinsics.syscall(SYS_utimensat, uintptr(dfd), uintptr(rawptr(path)), uintptr(times), uintptr(flags)))
 	return int(intrinsics.syscall(SYS_utimensat, uintptr(dfd), uintptr(rawptr(path)), uintptr(times), uintptr(flags)))
 }
 }
 
 
+sys_socket :: proc "contextless" (domain: int, type: int, protocol: int) -> int {
+	return int(intrinsics.syscall(unix.SYS_socket, uintptr(domain), uintptr(type), uintptr(protocol)))
+}
+
+sys_connect :: proc "contextless" (sd: int, addr: rawptr, len: socklen_t) -> int {
+	return int(intrinsics.syscall(unix.SYS_connect, uintptr(sd), uintptr(addr), uintptr(len)))
+}
+
+sys_accept :: proc "contextless" (sd: int, addr: rawptr, len: rawptr) -> int {
+	return int(intrinsics.syscall(unix.SYS_accept4, uintptr(sd), uintptr(addr), uintptr(len), uintptr(0)))
+}
+
+sys_listen :: proc "contextless" (sd: int, backlog: int) -> int {
+	return int(intrinsics.syscall(unix.SYS_listen, uintptr(sd), uintptr(backlog)))
+}
+
+sys_bind :: proc "contextless" (sd: int, addr: rawptr, len: socklen_t) -> int {
+	return int(intrinsics.syscall(unix.SYS_bind, uintptr(sd), uintptr(addr), uintptr(len)))
+}
+
+sys_setsockopt :: proc "contextless" (sd: int, level: int, optname: int, optval: rawptr, optlen: socklen_t) -> int {
+	return int(intrinsics.syscall(unix.SYS_setsockopt, uintptr(sd), uintptr(level), uintptr(optname), uintptr(optval), uintptr(optlen)))
+}
+
+sys_recvfrom :: proc "contextless" (sd: int, buf: rawptr, len: uint, flags: int, addr: rawptr, alen: uintptr) -> i64 {
+	return i64(intrinsics.syscall(unix.SYS_recvfrom, uintptr(sd), uintptr(buf), uintptr(len), uintptr(flags), uintptr(addr), uintptr(alen)))
+}
+
+sys_sendto :: proc "contextless" (sd: int, buf: rawptr, len: uint, flags: int, addr: rawptr, alen: socklen_t) -> i64 {
+	return i64(intrinsics.syscall(unix.SYS_sendto, uintptr(sd), uintptr(buf), uintptr(len), uintptr(flags), uintptr(addr), uintptr(alen)))
+}
+
+sys_shutdown :: proc "contextless" (sd: int, how: int) -> int {
+	return int(intrinsics.syscall(unix.SYS_shutdown, uintptr(sd), uintptr(how)))
+}
+
 sys_perf_event_open :: proc "contextless" (event_attr: rawptr, pid: i32, cpu: i32, group_fd: i32, flags: u32) -> int {
 sys_perf_event_open :: proc "contextless" (event_attr: rawptr, pid: i32, cpu: i32, group_fd: i32, flags: u32) -> int {
 	return int(intrinsics.syscall(SYS_perf_event_open, uintptr(event_attr), uintptr(pid), uintptr(cpu), uintptr(group_fd), uintptr(flags)))
 	return int(intrinsics.syscall(SYS_perf_event_open, uintptr(event_attr), uintptr(pid), uintptr(cpu), uintptr(group_fd), uintptr(flags)))
 }
 }

+ 10 - 0
core/sys/windows/dnsapi.odin

@@ -0,0 +1,10 @@
+// +build windows
+package sys_windows
+
+foreign import "system:Dnsapi.lib"
+
+@(default_calling_convention="std")
+foreign Dnsapi {
+    DnsQuery_UTF8 :: proc(name: cstring, type: u16, options: DWORD, extra: PVOID, results: ^^DNS_RECORD, reserved: PVOID) -> DNS_STATUS ---
+    DnsRecordListFree :: proc(list: ^DNS_RECORD, options: DWORD) ---
+}

+ 234 - 0
core/sys/windows/ip_helper.odin

@@ -0,0 +1,234 @@
+// +build windows
+package sys_windows
+
+foreign import "system:iphlpapi.lib"
+
+Address_Family :: enum u32 {
+	Unspecified = 0,   // Return both IPv4 and IPv6 addresses associated with adapters with them enabled.
+	IPv4        = 2,   // Return only IPv4 addresses associated with adapters with it enabled.
+	IPv6        = 23,  // Return only IPv6 addresses associated with adapters with it enabled.
+}
+
+GAA_Flag :: enum u32 {
+	Skip_Unicast                 = 0,  // Do not return unicast addresses.
+	Skip_Anycast                 = 1,  // Do not return IPv6 anycast addresses.
+	Skip_Multicast               = 2,  // Do not return multicast addresses.
+	Skip_DNS_Server              = 3,  // Do not return addresses of DNS servers.
+	Include_Prefix               = 4,  // (XP SP1+) Return a list of IP address prefixes on this adapter. When this flag is set, IP address prefixes are returned for both IPv6 and IPv4 addresses.
+	Skip_Friendly_Name           = 5,  // Do not return the adapter friendly name.
+	Include_WINS_info            = 6,  // (Vista+) Return addresses of Windows Internet Name Service (WINS) servers.
+	Include_Gateways             = 7,  // (Vista+) Return the addresses of default gateways.
+	Include_All_Interfaces       = 8,  // (Vista+) Return addresses for all NDIS interfaces.
+	Include_All_Compartments     = 9,  // (Reserved, Unsupported) Return addresses in all routing compartments.
+	Include_Tunnel_Binding_Order = 10, // (Vista+) Return the adapter addresses sorted in tunnel binding order.
+}
+GAA_Flags :: bit_set[GAA_Flag; u32]
+
+IP_Adapter_Addresses :: struct {
+	Raw: struct #raw_union {
+		Alignment: u64,
+		Anonymous: struct {
+			Length:  u32,
+			IfIndex: u32,
+		},
+	},
+	Next:                   ^IP_Adapter_Addresses,
+	AdapterName:            cstring,
+	FirstUnicastAddress:    ^IP_ADAPTER_UNICAST_ADDRESS_LH,
+	FirstAnycastAddress:    ^IP_ADAPTER_ANYCAST_ADDRESS_XP,
+	FirstMulticastAddress:  ^IP_ADAPTER_MULTICAST_ADDRESS_XP,
+	FirstDnsServerAddress:  ^IP_ADAPTER_DNS_SERVER_ADDRESS_XP,
+	DnsSuffix:              ^u16,
+	Description:            ^u16,
+	FriendlyName:           ^u16,
+	PhysicalAddress:        [8]u8,
+	PhysicalAddressLength:  u32,
+	Anonymous2:             struct #raw_union {
+		Flags:     u32,
+		Anonymous: struct {
+			_bitfield: u32,
+		},
+	},
+	MTU:                    u32,
+	IfType:                 u32,
+	OperStatus:             IF_OPER_STATUS,
+	Ipv6IfIndex:            u32,
+	ZoneIndices:            [16]u32,
+	FirstPrefix:            rawptr, // ^IP_ADAPTER_PREFIX_XP,
+	TransmitLinkSpeed:      u64,
+	ReceiveLinkSpeed:       u64,
+	FirstWinsServerAddress: rawptr, // ^IP_ADAPTER_WINS_SERVER_ADDRESS_LH,
+	FirstGatewayAddress:    ^IP_ADAPTER_GATEWAY_ADDRESS_LH,
+	Ipv4Metric:             u32,
+	Ipv6Metric:             u32,
+	Luid:                   NET_LUID_LH,
+	Dhcpv4Server:           SOCKET_ADDRESS,
+	CompartmentId:          u32,
+	NetworkGuid:            GUID,
+	ConnectionType:         NET_IF_CONNECTION_TYPE,
+	TunnelType:             TUNNEL_TYPE,
+	Dhcpv6Server:           SOCKET_ADDRESS,
+	Dhcpv6ClientDuid:       [130]u8,
+	Dhcpv6ClientDuidLength: u32,
+	Dhcpv6Iaid:             u32,
+	FirstDnsSuffix:         rawptr, // ^IP_ADAPTER_DNS_SUFFIX,
+}
+
+IP_ADAPTER_UNICAST_ADDRESS_LH :: struct {
+	Anonymous:          struct #raw_union {
+		Alignment: u64,
+		Anonymous: struct {
+			Length: u32,
+			Flags:  u32,
+		},
+	},
+	Next:               ^IP_ADAPTER_UNICAST_ADDRESS_LH,
+	Address:            SOCKET_ADDRESS,
+	PrefixOrigin:       NL_PREFIX_ORIGIN,
+	SuffixOrigin:       NL_SUFFIX_ORIGIN,
+	DadState:           NL_DAD_STATE,
+	ValidLifetime:      u32,
+	PreferredLifetime:  u32,
+	LeaseLifetime:      u32,
+	OnLinkPrefixLength: u8,
+}
+
+IP_ADAPTER_ANYCAST_ADDRESS_XP :: struct {
+	Anonymous: struct #raw_union {
+		Alignment: u64,
+		Anonymous: struct {
+			Length: u32,
+			Flags:  u32,
+		},
+	},
+	Next:      ^IP_ADAPTER_ANYCAST_ADDRESS_XP,
+	Address:   SOCKET_ADDRESS,
+}
+
+IP_ADAPTER_MULTICAST_ADDRESS_XP :: struct {
+	Anonymous: struct #raw_union {
+		Alignment: u64,
+		Anonymous: struct {
+			Length: u32,
+			Flags:  u32,
+		},
+	},
+	Next:      ^IP_ADAPTER_MULTICAST_ADDRESS_XP,
+	Address:   SOCKET_ADDRESS,
+}
+
+IP_ADAPTER_GATEWAY_ADDRESS_LH :: struct {
+	Anonymous: struct #raw_union {
+		Alignment: u64,
+		Anonymous: struct {
+			Length:   u32,
+			Reserved: u32,
+		},
+	},
+	Next:      ^IP_ADAPTER_GATEWAY_ADDRESS_LH,
+	Address:   SOCKET_ADDRESS,
+}
+
+IP_ADAPTER_DNS_SERVER_ADDRESS_XP :: struct {
+	Anonymous: struct #raw_union {
+		Alignment: u64,
+		Anonymous: struct {
+			Length:   u32,
+			Reserved: u32,
+		},
+	},
+	Next:      ^IP_ADAPTER_DNS_SERVER_ADDRESS_XP,
+	Address:   SOCKET_ADDRESS,
+}
+
+IF_OPER_STATUS :: enum i32 {
+	Up             = 1,
+	Down           = 2,
+	Testing        = 3,
+	Unknown        = 4,
+	Dormant        = 5,
+	NotPresent     = 6,
+	LowerLayerDown = 7,
+}
+
+NET_LUID_LH :: struct #raw_union {
+	Value: u64,
+	Info:  struct {
+		_bitfield: u64,
+	},
+}
+
+SOCKET_ADDRESS :: struct {
+	lpSockaddr:      ^SOCKADDR,
+	iSockaddrLength: i32,
+}
+
+NET_IF_CONNECTION_TYPE :: enum i32 {
+	NET_IF_CONNECTION_DEDICATED = 1,
+	NET_IF_CONNECTION_PASSIVE   = 2,
+	NET_IF_CONNECTION_DEMAND    = 3,
+	NET_IF_CONNECTION_MAXIMUM   = 4,
+}
+
+TUNNEL_TYPE :: enum i32 {
+	TUNNEL_TYPE_NONE    = 0,
+	TUNNEL_TYPE_OTHER   = 1,
+	TUNNEL_TYPE_DIRECT  = 2,
+	TUNNEL_TYPE_6TO4    = 11,
+	TUNNEL_TYPE_ISATAP  = 13,
+	TUNNEL_TYPE_TEREDO  = 14,
+	TUNNEL_TYPE_IPHTTPS = 15,
+}
+NL_PREFIX_ORIGIN :: enum i32 {
+	IpPrefixOriginOther               = 0,
+	IpPrefixOriginManual              = 1,
+	IpPrefixOriginWellKnown           = 2,
+	IpPrefixOriginDhcp                = 3,
+	IpPrefixOriginRouterAdvertisement = 4,
+	IpPrefixOriginUnchanged           = 16,
+}
+
+NL_SUFFIX_ORIGIN :: enum i32 {
+	NlsoOther                      = 0,
+	NlsoManual                     = 1,
+	NlsoWellKnown                  = 2,
+	NlsoDhcp                       = 3,
+	NlsoLinkLayerAddress           = 4,
+	NlsoRandom                     = 5,
+	IpSuffixOriginOther            = 0,
+	IpSuffixOriginManual           = 1,
+	IpSuffixOriginWellKnown        = 2,
+	IpSuffixOriginDhcp             = 3,
+	IpSuffixOriginLinkLayerAddress = 4,
+	IpSuffixOriginRandom           = 5,
+	IpSuffixOriginUnchanged        = 16,
+}
+
+NL_DAD_STATE :: enum i32 {
+	NldsInvalid          = 0,
+	NldsTentative        = 1,
+	NldsDuplicate        = 2,
+	NldsDeprecated       = 3,
+	NldsPreferred        = 4,
+	IpDadStateInvalid    = 0,
+	IpDadStateTentative  = 1,
+	IpDadStateDuplicate  = 2,
+	IpDadStateDeprecated = 3,
+	IpDadStatePreferred  = 4,
+}
+
+@(default_calling_convention = "std")
+foreign iphlpapi {
+	/*
+		The GetAdaptersAddresses function retrieves the addresses associated with the adapters on the local computer.
+		See: https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses
+	*/
+	@(link_name="GetAdaptersAddresses") get_adapters_addresses :: proc(
+		family:            Address_Family,
+		flags:             GAA_Flags,
+		_reserved:         rawptr,
+		adapter_addresses: [^]IP_Adapter_Addresses,
+		size:              ^u32,
+	) -> ULONG ---
+
+}

+ 230 - 139
core/sys/windows/types.odin

@@ -154,10 +154,6 @@ TIMER_QUERY_STATE  :: 0x0001
 TIMER_MODIFY_STATE :: 0x0002
 TIMER_MODIFY_STATE :: 0x0002
 TIMER_ALL_ACCESS   :: STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | TIMER_QUERY_STATE | TIMER_MODIFY_STATE
 TIMER_ALL_ACCESS   :: STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | TIMER_QUERY_STATE | TIMER_MODIFY_STATE
 
 
-SOCKET :: distinct uintptr // TODO
-socklen_t :: c_int
-ADDRESS_FAMILY :: USHORT
-
 TRUE  :: BOOL(true)
 TRUE  :: BOOL(true)
 FALSE :: BOOL(false)
 FALSE :: BOOL(false)
 
 
@@ -1868,30 +1864,6 @@ BI_BITFIELDS :: 3
 BI_JPEG      :: 4
 BI_JPEG      :: 4
 BI_PNG       :: 5
 BI_PNG       :: 5
 
 
-WSA_FLAG_OVERLAPPED: DWORD : 0x01
-WSA_FLAG_NO_HANDLE_INHERIT: DWORD : 0x80
-
-WSADESCRIPTION_LEN :: 256
-WSASYS_STATUS_LEN :: 128
-WSAPROTOCOL_LEN: DWORD : 255
-INVALID_SOCKET :: ~SOCKET(0)
-
-WSAEACCES: c_int : 10013
-WSAEINVAL: c_int : 10022
-WSAEWOULDBLOCK: c_int : 10035
-WSAEPROTOTYPE: c_int : 10041
-WSAEADDRINUSE: c_int : 10048
-WSAEADDRNOTAVAIL: c_int : 10049
-WSAECONNABORTED: c_int : 10053
-WSAECONNRESET: c_int : 10054
-WSAENOTCONN: c_int : 10057
-WSAESHUTDOWN: c_int : 10058
-WSAETIMEDOUT: c_int : 10060
-WSAECONNREFUSED: c_int : 10061
-WSATRY_AGAIN: c_int : 11002
-
-MAX_PROTOCOL_CHAIN: DWORD : 7
-
 MAXIMUM_REPARSE_DATA_BUFFER_SIZE :: 16 * 1024
 MAXIMUM_REPARSE_DATA_BUFFER_SIZE :: 16 * 1024
 FSCTL_GET_REPARSE_POINT: DWORD : 0x900a8
 FSCTL_GET_REPARSE_POINT: DWORD : 0x900a8
 IO_REPARSE_TAG_SYMLINK: DWORD : 0xa000000c
 IO_REPARSE_TAG_SYMLINK: DWORD : 0xa000000c
@@ -1949,44 +1921,6 @@ CREATE_NEW_PROCESS_GROUP: DWORD : 0x00000200
 CREATE_UNICODE_ENVIRONMENT: DWORD : 0x00000400
 CREATE_UNICODE_ENVIRONMENT: DWORD : 0x00000400
 STARTF_USESTDHANDLES: DWORD : 0x00000100
 STARTF_USESTDHANDLES: DWORD : 0x00000100
 
 
-AF_INET: c_int : 2
-AF_INET6: c_int : 23
-SD_BOTH: c_int : 2
-SD_RECEIVE: c_int : 0
-SD_SEND: c_int : 1
-SOCK_DGRAM: c_int : 2
-SOCK_STREAM: c_int : 1
-SOL_SOCKET: c_int : 0xffff
-SO_RCVTIMEO: c_int : 0x1006
-SO_SNDTIMEO: c_int : 0x1005
-SO_REUSEADDR: c_int : 0x0004
-IPPROTO_IP: c_int : 0
-IPPROTO_TCP: c_int : 6
-IPPROTO_IPV6: c_int : 41
-TCP_NODELAY: c_int : 0x0001
-IP_TTL: c_int : 4
-IPV6_V6ONLY: c_int : 27
-SO_ERROR: c_int : 0x1007
-SO_BROADCAST: c_int : 0x0020
-IP_MULTICAST_LOOP: c_int : 11
-IPV6_MULTICAST_LOOP: c_int : 11
-IP_MULTICAST_TTL: c_int : 10
-IP_ADD_MEMBERSHIP: c_int : 12
-IP_DROP_MEMBERSHIP: c_int : 13
-IPV6_ADD_MEMBERSHIP: c_int : 12
-IPV6_DROP_MEMBERSHIP: c_int : 13
-MSG_PEEK: c_int : 0x2
-
-ip_mreq :: struct {
-	imr_multiaddr: in_addr,
-	imr_interface: in_addr,
-}
-
-ipv6_mreq :: struct {
-	ipv6mr_multiaddr: in6_addr,
-	ipv6mr_interface: c_uint,
-}
-
 VOLUME_NAME_DOS: DWORD : 0x0
 VOLUME_NAME_DOS: DWORD : 0x0
 MOVEFILE_REPLACE_EXISTING: DWORD : 1
 MOVEFILE_REPLACE_EXISTING: DWORD : 1
 
 
@@ -2369,11 +2303,6 @@ STARTUPINFO :: struct {
 	hStdError: HANDLE,
 	hStdError: HANDLE,
 }
 }
 
 
-SOCKADDR :: struct {
-	sa_family: ADDRESS_FAMILY,
-	sa_data: [14]CHAR,
-}
-
 FILETIME :: struct {
 FILETIME :: struct {
 	dwLowDateTime: DWORD,
 	dwLowDateTime: DWORD,
 	dwHighDateTime: DWORD,
 	dwHighDateTime: DWORD,
@@ -2406,74 +2335,6 @@ ADDRESS_MODE :: enum c_int {
 	AddrModeFlat,
 	AddrModeFlat,
 }
 }
 
 
-SOCKADDR_STORAGE_LH :: struct {
-	ss_family: ADDRESS_FAMILY,
-	__ss_pad1: [6]CHAR,
-	__ss_align: i64,
-	__ss_pad2: [112]CHAR,
-}
-
-ADDRINFOA :: struct {
-	ai_flags: c_int,
-	ai_family: c_int,
-	ai_socktype: c_int,
-	ai_protocol: c_int,
-	ai_addrlen: size_t,
-	ai_canonname: ^c_char,
-	ai_addr: ^SOCKADDR,
-	ai_next: ^ADDRINFOA,
-}
-
-PADDRINFOEXW  :: ^ADDRINFOEXW
-LPADDRINFOEXW :: ^ADDRINFOEXW
-ADDRINFOEXW :: struct {
-	ai_flags:     c_int,
-	ai_family:    c_int,
-	ai_socktype:  c_int,
-	ai_protocol:  c_int,
-	ai_addrlen:   size_t,
-	ai_canonname: wstring,
-	ai_addr:      ^sockaddr,
-	ai_blob:      rawptr,
-	ai_bloblen:   size_t,
-	ai_provider:  LPGUID,
-	ai_next:      ^ADDRINFOEXW,
-}
-
-LPLOOKUPSERVICE_COMPLETION_ROUTINE :: #type proc "stdcall" (
-	dwErrorCode: DWORD,
-	dwNumberOfBytesTransfered: DWORD,
-	lpOverlapped: LPOVERLAPPED,
-)
-
-sockaddr  :: struct {
-	sa_family: USHORT,
-	sa_data:   [14]byte,
-}
-
-sockaddr_in :: struct {
-	sin_family: ADDRESS_FAMILY,
-	sin_port: USHORT,
-	sin_addr: in_addr,
-	sin_zero: [8]CHAR,
-}
-
-sockaddr_in6 :: struct {
-	sin6_family: ADDRESS_FAMILY,
-	sin6_port: USHORT,
-	sin6_flowinfo: c_ulong,
-	sin6_addr: in6_addr,
-	sin6_scope_id: c_ulong,
-}
-
-in_addr :: struct {
-	s_addr: u32,
-}
-
-in6_addr :: struct {
-	s6_addr: [16]u8,
-}
-
 EXCEPTION_DISPOSITION :: enum c_int {
 EXCEPTION_DISPOSITION :: enum c_int {
 	ExceptionContinueExecution,
 	ExceptionContinueExecution,
 	ExceptionContinueSearch,
 	ExceptionContinueSearch,
@@ -3884,3 +3745,233 @@ COORD :: struct {
 	X: SHORT,
 	X: SHORT,
 	Y: SHORT,
 	Y: SHORT,
 }
 }
+
+//
+// Networking
+//
+WSA_FLAG_OVERLAPPED             :: 1
+WSA_FLAG_MULTIPOINT_C_ROOT      :: 2
+WSA_FLAG_MULTIPOINT_C_LEAF      :: 4
+WSA_FLAG_MULTIPOINT_D_ROOT      :: 8
+WSA_FLAG_MULTIPOINT_D_LEAF      :: 16
+WSA_FLAG_ACCESS_SYSTEM_SECURITY :: 32
+WSA_FLAG_NO_HANDLE_INHERIT      :: 128
+WSADESCRIPTION_LEN :: 256
+WSASYS_STATUS_LEN  :: 128
+WSAPROTOCOL_LEN    :: 255
+INVALID_SOCKET :: ~SOCKET(0)
+SOMAXCONN    :: 128 // The number of messages that can be queued in memory after being received; use 2-4 for Bluetooth.
+                    // This is for the 'backlog' parameter to listen().
+SOCKET_ERROR :: -1
+
+// Networking errors
+WSAEINTR               :: 10004 // Call interrupted. CancelBlockingCall was called. (This is different on Linux.)
+WSAEACCES              :: 10013 // If you try to bind a Udp socket to the broadcast address without the socket option set.
+WSAEFAULT              :: 10014 // A pointer that was passed to a WSA function is invalid, such as a buffer size is smaller than you said it was
+WSAEINVAL              :: 10022 // Invalid argument supplied
+WSAEMFILE              :: 10024 // SOCKET handles exhausted
+WSAEWOULDBLOCK         :: 10035 // No data is ready yet
+WSAENOTSOCK            :: 10038 // Not a socket.
+WSAEINPROGRESS         :: 10036 // WS1.1 call is in progress or callback function is still being processed
+WSAEALREADY            :: 10037 // Already connecting in parallel.
+WSAEMSGSIZE            :: 10040 // Message was truncated because it exceeded max datagram size.
+WSAEPROTOTYPE          :: 10041 // Wrong protocol for the provided socket
+WSAENOPROTOOPT         :: 10042 // TODO
+WSAEPROTONOSUPPORT     :: 10043 // Protocol not supported
+WSAESOCKTNOSUPPORT     :: 10044 // Socket type not supported in the given address family
+WSAEAFNOSUPPORT        :: 10047 // Address family not supported
+WSAEOPNOTSUPP          :: 10045 // Attempt to accept on non-stream socket, etc.
+WSAEADDRINUSE          :: 10048 // Endpoint being bound is in use by another socket.
+WSAEADDRNOTAVAIL       :: 10049 // Not a valid local IP address on this computer.
+WSAENETDOWN            :: 10050 // Network subsystem failure on the local machine.
+WSAENETUNREACH         :: 10051 // The local machine is not connected to the network.
+WSAENETRESET           :: 10052 // Keepalive failure detected, or TTL exceeded when receiving UDP packets.
+WSAECONNABORTED        :: 10053 // Connection has been aborted by software in the host machine.
+WSAECONNRESET          :: 10054 // The connection was reset while trying to accept, read or write.
+WSAENOBUFS             :: 10055 // No buffer space is available. The outgoing queue may be full in which case you should probably try again after a pause.
+WSAEISCONN             :: 10056 // The socket is already connected.
+WSAENOTCONN            :: 10057 // The socket is not connected yet, or no address was supplied to sendto.
+WSAESHUTDOWN           :: 10058 // The socket has been shutdown in the direction required.
+WSAETIMEDOUT           :: 10060 // The timeout duration was reached before any data was received / before all data was sent.
+WSAECONNREFUSED        :: 10061 // The remote machine is not listening on that endpoint.
+WSAEHOSTDOWN           :: 10064 // Destination host was down.
+WSAEHOSTUNREACH        :: 10065 // The remote machine is not connected to the network.
+WSAENOTINITIALISED     :: 10093 // Needs WSAStartup call
+WSAEINVALIDPROCTABLE   :: 10104 // Invalid or incomplete procedure table was returned
+WSAEINVALIDPROVIDER    :: 10105 // Service provider version is not 2.2
+WSAEPROVIDERFAILEDINIT :: 10106 // Service provider failed to initialize
+
+// Address families
+AF_UNSPEC : c_int : 0  // Unspecified
+AF_INET   : c_int : 2  // IPv4
+AF_INET6  : c_int : 23 // IPv6
+AF_IRDA   : c_int : 26 // Infrared
+AF_BTH    : c_int : 32 // Bluetooth
+
+// Socket types
+SOCK_STREAM    : c_int : 1 // TCP
+SOCK_DGRAM     : c_int : 2 // UDP
+SOCK_RAW       : c_int : 3 // Requires options IP_HDRINCL for v4, IPV6_HDRINCL for v6, on the socket
+SOCK_RDM       : c_int : 4 // Requires "Reliable Multicast Protocol" to be installed - see WSAEnumProtocols
+SOCK_SEQPACKET : c_int : 5 // Provides psuedo-stream packet based on DGRAMs.
+
+// Protocols
+IPPROTO_IP      : c_int : 0
+IPPROTO_ICMP    : c_int : 1   // (AF_UNSPEC, AF_INET, AF_INET6) + SOCK_RAW | not specified
+IPPROTO_IGMP    : c_int : 2   // (AF_UNSPEC, AF_INET, AF_INET6) + SOCK_RAW | not specified
+BTHPROTO_RFCOMM : c_int : 3   // Bluetooth: AF_BTH + SOCK_STREAM
+IPPROTO_TCP     : c_int : 6   // (AF_INET, AF_INET6) + SOCK_STREAM
+IPPROTO_UDP     : c_int : 17  // (AF_INET, AF_INET6) + SOCK_DGRAM
+IPPROTO_ICMPV6  : c_int : 58  // (AF_UNSPEC, AF_INET, AF_INET6) + SOCK_RAW
+IPPROTO_RM      : c_int : 113 // AF_INET + SOCK_RDM [requires "Reliable Multicast Protocol" to be installed - see WSAEnumProtocols]
+
+// Shutdown manners
+SD_RECEIVE : c_int : 0
+SD_SEND    : c_int : 1
+SD_BOTH    : c_int : 2
+
+// Socket 'levels'
+SOL_SOCKET   : c_int : 0xffff // Socket options for any socket.
+IPPROTO_IPV6 : c_int : 41     // Socket options for IPV6.
+
+// Options for any sockets
+SO_ACCEPTCONN         : c_int : 0x0002
+SO_REUSEADDR          : c_int : 0x0004
+SO_KEEPALIVE          : c_int : 0x0008
+SO_SNDTIMEO           : c_int : 0x1005
+SO_RCVTIMEO           : c_int : 0x1006
+SO_EXCLUSIVEADDRUSE   : c_int : ~SO_REUSEADDR
+SO_CONDITIONAL_ACCEPT : c_int : 0x3002
+SO_DONTLINGER         : c_int : ~SO_LINGER
+SO_OOBINLINE          : c_int : 0x0100
+SO_LINGER             : c_int : 0x0080
+SO_RCVBUF             : c_int : 0x1002
+SO_SNDBUF             : c_int : 0x1001
+SO_ERROR              : c_int : 0x1007
+SO_BROADCAST          : c_int : 0x0020
+
+TCP_NODELAY: c_int : 0x0001
+IP_TTL: c_int : 4
+IPV6_V6ONLY: c_int : 27
+IP_MULTICAST_LOOP: c_int : 11
+IPV6_MULTICAST_LOOP: c_int : 11
+IP_MULTICAST_TTL: c_int : 10
+IP_ADD_MEMBERSHIP: c_int : 12
+
+IPV6_ADD_MEMBERSHIP: c_int : 12
+IPV6_DROP_MEMBERSHIP: c_int : 13
+
+MAX_PROTOCOL_CHAIN: DWORD : 7
+
+// Used with the SO_LINGER socket option to setsockopt().
+LINGER :: struct {
+	l_onoff: c.ushort,
+	l_linger: c.ushort,
+}
+// Send/Receive flags.
+MSG_OOB  : c_int : 1 // `send`/`recv` should process out-of-band data.
+MSG_PEEK : c_int : 2 // `recv` should not remove the data from the buffer. Only valid for non-overlapped operations.
+
+
+SOCKET :: distinct uintptr // TODO
+socklen_t :: c_int
+ADDRESS_FAMILY :: USHORT
+
+ip_mreq :: struct {
+	imr_multiaddr: in_addr,
+	imr_interface: in_addr,
+}
+
+ipv6_mreq :: struct {
+	ipv6mr_multiaddr: in6_addr,
+	ipv6mr_interface: c_uint,
+}
+
+SOCKADDR_STORAGE_LH :: struct {
+	ss_family: ADDRESS_FAMILY,
+	__ss_pad1: [6]CHAR,
+	__ss_align: i64,
+	__ss_pad2: [112]CHAR,
+}
+
+ADDRINFOA :: struct {
+	ai_flags: c_int,
+	ai_family: c_int,
+	ai_socktype: c_int,
+	ai_protocol: c_int,
+	ai_addrlen: size_t,
+	ai_canonname: ^c_char,
+	ai_addr: ^SOCKADDR,
+	ai_next: ^ADDRINFOA,
+}
+
+sockaddr_in :: struct {
+	sin_family: ADDRESS_FAMILY,
+	sin_port: u16be,
+	sin_addr: in_addr,
+	sin_zero: [8]CHAR,
+}
+sockaddr_in6 :: struct {
+	sin6_family: ADDRESS_FAMILY,
+	sin6_port: u16be,
+	sin6_flowinfo: c_ulong,
+	sin6_addr: in6_addr,
+	sin6_scope_id: c_ulong,
+}
+
+in_addr :: struct {
+	s_addr: u32,
+}
+
+in6_addr :: struct {
+	s6_addr: [16]u8,
+}
+
+
+DNS_STATUS :: distinct DWORD // zero is success
+DNS_INFO_NO_RECORDS :: 9501
+DNS_QUERY_NO_RECURSION :: 0x00000004
+
+DNS_RECORD :: struct {
+    pNext: ^DNS_RECORD,
+    pName: cstring,
+    wType: WORD,
+    wDataLength: USHORT,
+    Flags: DWORD,
+    dwTtl: DWORD,
+    _: DWORD,
+    Data: struct #raw_union {
+        CNAME: DNS_PTR_DATAA,
+        A: u32be, // Ipv4 Address
+        AAAA: u128be, // Ipv6 Address
+        TXT: DNS_TXT_DATAA,
+        NS: DNS_PTR_DATAA,
+        MX: DNS_MX_DATAA,
+        SRV: DNS_SRV_DATAA,
+    },
+}
+
+DNS_TXT_DATAA :: struct {
+    dwStringCount: DWORD,
+    pStringArray: cstring,
+}
+
+DNS_PTR_DATAA :: cstring
+
+DNS_MX_DATAA :: struct {
+    pNameExchange: cstring, // the hostname
+    wPreference: WORD, // lower values preferred
+    _: WORD, // padding.
+}
+DNS_SRV_DATAA :: struct {
+	pNameTarget: cstring,
+	wPriority: u16,
+	wWeight: u16,
+	wPort: u16,
+	_: WORD, // padding
+}
+
+SOCKADDR :: struct {
+	sa_family: ADDRESS_FAMILY,
+	sa_data: [14]CHAR,
+}

+ 21 - 0
core/sys/windows/util.odin

@@ -485,3 +485,24 @@ run_as_user :: proc(username, password, application, commandline: string, pi: ^P
 		return false
 		return false
 	}
 	}
 }
 }
+
+ensure_winsock_initialized :: proc() {
+	@static gate := false
+	@static initted := false
+
+	if initted {
+		return
+	}
+
+	for intrinsics.atomic_compare_exchange_strong(&gate, false, true) {
+		intrinsics.cpu_relax()
+	}
+	defer intrinsics.atomic_store(&gate, false)
+
+	unused_info: WSADATA
+	version_requested := WORD(2) << 8 | 2
+	res := WSAStartup(version_requested, &unused_info)
+	assert(res == 0, "unable to initialized Winsock2")
+
+	initted = true
+}

+ 7 - 7
core/sys/windows/ws2_32.odin

@@ -54,7 +54,7 @@ foreign ws2_32 {
 		buf: rawptr,
 		buf: rawptr,
 		len: c_int,
 		len: c_int,
 		flags: c_int,
 		flags: c_int,
-		addr: ^SOCKADDR,
+		addr: ^SOCKADDR_STORAGE_LH,
 		addrlen: ^c_int,
 		addrlen: ^c_int,
 	) -> c_int ---
 	) -> c_int ---
 	sendto :: proc(
 	sendto :: proc(
@@ -62,11 +62,11 @@ foreign ws2_32 {
 		buf: rawptr,
 		buf: rawptr,
 		len: c_int,
 		len: c_int,
 		flags: c_int,
 		flags: c_int,
-		addr: ^SOCKADDR,
+		addr: ^SOCKADDR_STORAGE_LH,
 		addrlen: c_int,
 		addrlen: c_int,
 	) -> c_int ---
 	) -> c_int ---
 	shutdown :: proc(socket: SOCKET, how: c_int) -> c_int ---
 	shutdown :: proc(socket: SOCKET, how: c_int) -> c_int ---
-	accept :: proc(socket: SOCKET, address: ^SOCKADDR, address_len: ^c_int) -> SOCKET ---
+	accept :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, address_len: ^c_int) -> SOCKET ---
 
 
 	setsockopt :: proc(
 	setsockopt :: proc(
 		s: SOCKET,
 		s: SOCKET,
@@ -75,11 +75,11 @@ foreign ws2_32 {
 		optval: rawptr,
 		optval: rawptr,
 		optlen: c_int,
 		optlen: c_int,
 	) -> c_int ---
 	) -> c_int ---
-	getsockname :: proc(socket: SOCKET, address: ^SOCKADDR, address_len: ^c_int) -> c_int ---
-	getpeername :: proc(socket: SOCKET, address: ^SOCKADDR, address_len: ^c_int) -> c_int ---
-	bind :: proc(socket: SOCKET, address: ^SOCKADDR, address_len: socklen_t) -> c_int ---
+	getsockname :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, address_len: ^c_int) -> c_int ---
+	getpeername :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, address_len: ^c_int) -> c_int ---
+	bind :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, address_len: socklen_t) -> c_int ---
 	listen :: proc(socket: SOCKET, backlog: c_int) -> c_int ---
 	listen :: proc(socket: SOCKET, backlog: c_int) -> c_int ---
-	connect :: proc(socket: SOCKET, address: ^SOCKADDR, len: c_int) -> c_int ---
+	connect :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, len: c_int) -> c_int ---
 	getaddrinfo :: proc(
 	getaddrinfo :: proc(
 		node: cstring,
 		node: cstring,
 		service: cstring,
 		service: cstring,

+ 4 - 1
tests/core/Makefile

@@ -2,7 +2,7 @@ ODIN=../../odin
 PYTHON=$(shell which python3)
 PYTHON=$(shell which python3)
 
 
 all: download_test_assets image_test compress_test strings_test hash_test crypto_test noise_test encoding_test \
 all: download_test_assets image_test compress_test strings_test hash_test crypto_test noise_test encoding_test \
-	 math_test linalg_glsl_math_test filepath_test reflect_test os_exit_test i18n_test match_test c_libc_test
+	 math_test linalg_glsl_math_test filepath_test reflect_test os_exit_test i18n_test match_test c_libc_test net_test
 
 
 download_test_assets:
 download_test_assets:
 	$(PYTHON) download_assets.py
 	$(PYTHON) download_assets.py
@@ -54,3 +54,6 @@ match_test:
 
 
 c_libc_test:
 c_libc_test:
 	$(ODIN) run c/libc -out:test_core_libc
 	$(ODIN) run c/libc -out:test_core_libc
+
+net_test:
+	$(ODIN) run net -out:test_core_net

+ 5 - 0
tests/core/build.bat

@@ -71,6 +71,11 @@ echo Running core:text/i18n tests
 echo ---
 echo ---
 %PATH_TO_ODIN% run text\i18n %COMMON% -out:test_core_i18n.exe
 %PATH_TO_ODIN% run text\i18n %COMMON% -out:test_core_i18n.exe
 
 
+echo ---
+echo Running core:net
+echo ---
+%PATH_TO_ODIN% run net %COMMON% -out:test_core_net.exe
+
 echo ---
 echo ---
 echo Running core:text/lua tests
 echo Running core:text/lua tests
 echo ---
 echo ---

+ 508 - 0
tests/core/net/test_core_net.odin

@@ -0,0 +1,508 @@
+/*
+	Copyright 2021 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Jeroen van Rijn: Initial implementation.
+		graphitemaster:  pton/ntop IANA test vectors
+
+	A test suite for `core:net`
+*/
+package test_core_net
+
+import "core:testing"
+import "core:mem"
+import "core:fmt"
+import "core:net"
+import "core:strconv"
+import "core:time"
+import "core:thread"
+
+_, _ :: time, thread
+
+TEST_count := 0
+TEST_fail  := 0
+
+t := &testing.T{}
+
+when ODIN_TEST {
+    expect  :: testing.expect
+    log     :: testing.log
+} else {
+    expect  :: proc(t: ^testing.T, condition: bool, message: string, loc := #caller_location) {
+        TEST_count += 1
+        if !condition {
+            TEST_fail += 1
+            fmt.printf("[%v] %v\n", loc, message)
+            return
+        }
+    }
+    log     :: proc(t: ^testing.T, v: any, loc := #caller_location) {
+        fmt.printf("[%v] ", loc)
+        fmt.printf("log: %v\n", v)
+    }
+}
+
+_tracking_allocator := mem.Tracking_Allocator{}
+
+print_tracking_allocator_report :: proc() {
+	for _, leak in _tracking_allocator.allocation_map {
+		fmt.printf("%v leaked %v bytes\n", leak.location, leak.size)
+	}
+
+	for bf in _tracking_allocator.bad_free_array {
+		fmt.printf("%v allocation %p was freed badly\n", bf.location, bf.memory)
+	}
+}
+
+main :: proc() {
+	mem.tracking_allocator_init(&_tracking_allocator, context.allocator)
+	context.allocator = mem.tracking_allocator(&_tracking_allocator)
+
+	address_parsing_test(t)
+
+	when ODIN_OS != .Windows {
+		fmt.printf("IMPORTANT: `core:thread` seems to still be a bit wonky on Linux and MacOS, so we can't run tests relying on them.\n", ODIN_OS)
+	} else {
+		tcp_tests(t)
+	}
+
+	fmt.printf("%v/%v tests successful.\n", TEST_count - TEST_fail, TEST_count)
+
+	print_tracking_allocator_report()
+}
+
+@test
+address_parsing_test :: proc(t: ^testing.T) {
+	for vector in IP_Address_Parsing_Test_Vectors {
+		kind := ""
+		switch vector.family {
+		case .IP4:     kind = "[IPv4]"
+		case .IP4_Alt: kind = "[IPv4 Non-Decimal]"
+		case .IP6:     kind = "[IPv6]"
+		case: panic("Add support to the test for this type.")
+		}
+
+		valid := len(vector.binstr) > 0
+
+		fmt.printf("%v %v\n", kind, vector.input)
+
+		msg := "-set a proper message-"
+		switch vector.family {
+		case .IP4, .IP4_Alt:
+			/*
+				Does `net.parse_ip4_address` think we parsed the address properly?
+			*/
+			non_decimal := vector.family == .IP4_Alt
+
+			any_addr  := net.parse_address(vector.input, non_decimal)
+			parsed_ok := any_addr != nil
+			parsed:   net.IP4_Address
+
+			/*
+				Ensure that `parse_address` doesn't parse IPv4 addresses into IPv6 addreses by mistake.
+			*/
+			switch addr in any_addr {
+			case net.IP4_Address:
+				parsed = addr
+			case net.IP6_Address:
+				parsed_ok = false
+				msg = fmt.tprintf("parse_address mistook %v as IPv6 address %04x", vector.input, addr)
+				expect(t, false, msg)
+			}
+
+			if !parsed_ok && valid {
+				msg = fmt.tprintf("parse_ip4_address failed to parse %v, expected %v", vector.input, binstr_to_address(vector.binstr))
+
+			} else if parsed_ok && !valid {
+				msg = fmt.tprintf("parse_ip4_address parsed %v into %v, expected failure", vector.input, parsed)
+			}
+			expect(t, parsed_ok == valid, msg)
+
+			if valid && parsed_ok {
+				actual_binary := address_to_binstr(parsed)
+				msg = fmt.tprintf("parse_ip4_address parsed %v into %v, expected %v", vector.input, actual_binary, vector.binstr)
+				expect(t, actual_binary == vector.binstr, msg)
+
+				/*
+					Do we turn an address back into the same string properly?
+					No point in testing the roundtrip if the first part failed.
+				*/
+				if len(vector.output) > 0 && actual_binary == vector.binstr {
+					stringified := net.address_to_string(parsed)
+					msg = fmt.tprintf("address_to_string turned %v into %v, expected %v", parsed, stringified, vector.output)
+					expect(t, stringified == vector.output, msg)
+				}
+			}
+
+		case .IP6:
+			/*
+				Do we parse the address properly?
+			*/
+			parsed, parsed_ok := net.parse_ip6_address(vector.input)
+
+			if !parsed_ok && valid {
+				msg = fmt.tprintf("parse_ip6_address failed to parse %v, expected %04x", vector.input, binstr_to_address(vector.binstr))
+
+			} else if parsed_ok && !valid {
+				msg = fmt.tprintf("parse_ip6_address parsed %v into %04x, expected failure", vector.input, parsed)
+			}
+			expect(t, parsed_ok == valid, msg)
+
+			if valid && parsed_ok {
+				actual_binary := address_to_binstr(parsed)
+				msg = fmt.tprintf("parse_ip6_address parsed %v into %v, expected %v", vector.input, actual_binary, vector.binstr)
+				expect(t, actual_binary == vector.binstr, msg)
+
+				/*
+					Do we turn an address back into the same string properly?
+					No point in testing the roundtrip if the first part failed.
+				*/
+				if len(vector.output) > 0 && actual_binary == vector.binstr {
+					stringified := net.address_to_string(parsed)
+					msg = fmt.tprintf("address_to_string turned %v into %v, expected %v", parsed, stringified, vector.output)
+					expect(t, stringified == vector.output, msg)
+				}
+			}
+		}
+	}
+}
+
+address_to_binstr :: proc(address: net.Address) -> (binstr: string) {
+	switch t in address {
+	case net.IP4_Address:
+		b := transmute(u32be)t
+		return fmt.tprintf("%08x", b)
+	case net.IP6_Address:
+		b := transmute(u128be)t
+		return fmt.tprintf("%32x", b)
+	case:
+		return ""
+	}
+	unreachable()
+}
+
+binstr_to_address :: proc(binstr: string) -> (address: net.Address) {
+	switch len(binstr) {
+	case 8:  // IPv4
+		a, ok := strconv.parse_u64_of_base(binstr, 16)
+		expect(t, ok, "failed to parse test case bin string")
+
+		ipv4 := u32be(a)
+		return net.IP4_Address(transmute([4]u8)ipv4)
+
+
+	case 32: // IPv6
+		a, ok := strconv.parse_u128_of_base(binstr, 16)
+		expect(t, ok, "failed to parse test case bin string")
+
+		ipv4 := u128be(a)
+		return net.IP6_Address(transmute([8]u16be)ipv4)
+
+	case 0:
+		return nil
+	}
+	panic("Invalid test case")
+}
+
+Kind :: enum {
+	IP4,     // Decimal IPv4
+	IP4_Alt, // Non-decimal address
+	IP6,     // Hex IPv6 or mixed IPv4/IPv6.
+}
+
+IP_Address_Parsing_Test_Vector :: struct {
+	// Give it to the IPv4 or IPv6 parser?
+	family:           Kind,
+
+	// Input address to try and parse.
+	input:            string,
+
+	/*
+		Hexadecimal representation of the expected numeric value of the address.
+		Zero length means input is invalid and the parser should report failure.
+	*/
+	binstr:           string,
+
+	// Expected `address_to_string` output, if a valid input and this string is non-empty.
+	output:           string,
+}
+
+IP_Address_Parsing_Test_Vectors :: []IP_Address_Parsing_Test_Vector{
+	// dotted-decimal notation
+	{ .IP4, "0.0.0.0",                 "00000000", "0.0.0.0"        },
+	{ .IP4, "127.0.0.1",               "7f000001", "127.0.0.1"      },
+	{ .IP4, "10.0.128.31",             "0a00801f", "10.0.128.31"    },
+	{ .IP4, "255.255.255.255",         "ffffffff", "255.255.255.255"},
+
+	// Odin custom: Address + port, valid
+	{ .IP4, "0.0.0.0:80",              "00000000", "0.0.0.0"        },
+	{ .IP4, "127.0.0.1:80",            "7f000001", "127.0.0.1"      },
+	{ .IP4, "10.0.128.31:80",          "0a00801f", "10.0.128.31"    },
+	{ .IP4, "255.255.255.255:80",      "ffffffff", "255.255.255.255"},
+
+	{ .IP4, "[0.0.0.0]:80",            "00000000", "0.0.0.0"        },
+	{ .IP4, "[127.0.0.1]:80",          "7f000001", "127.0.0.1"      },
+	{ .IP4, "[10.0.128.31]:80",        "0a00801f", "10.0.128.31"    },
+	{ .IP4, "[255.255.255.255]:80",    "ffffffff", "255.255.255.255"},
+
+	// Odin custom: Address + port, invalid
+	{ .IP4, "[]:80",                   "", ""},
+	{ .IP4, "[0.0.0.0]",               "", ""},
+	{ .IP4, "[127.0.0.1]:",            "", ""},
+	{ .IP4, "[10.0.128.31] :80",       "", ""},
+	{ .IP4, "[255.255.255.255]:65536", "", ""},
+
+
+	// numbers-and-dots notation, but not dotted-decimal
+	{ .IP4_Alt, "1.2.03.4",                "01020304", ""},
+	{ .IP4_Alt, "1.2.0x33.4",              "01023304", ""},
+	{ .IP4_Alt, "1.2.0XAB.4",              "0102ab04", ""},
+	{ .IP4_Alt, "1.2.0xabcd",              "0102abcd", ""},
+	{ .IP4_Alt, "1.0xabcdef",              "01abcdef", ""},
+	{ .IP4_Alt, "0x01abcdef",              "01abcdef", ""},
+	{ .IP4_Alt, "00377.0x0ff.65534",       "fffffffe", ""},
+
+	// invalid as decimal address
+	{ .IP4, "",                        "", ""},
+	{ .IP4, ".1.2.3",                  "", ""},
+	{ .IP4, "1..2.3",                  "", ""},
+	{ .IP4, "1.2.3.",                  "", ""},
+	{ .IP4, "1.2.3.4.5",               "", ""},
+	{ .IP4, "1.2.3.a",                 "", ""},
+	{ .IP4, "1.256.2.3",               "", ""},
+	{ .IP4, "1.2.4294967296.3",        "", ""},
+	{ .IP4, "1.2.-4294967295.3",       "", ""},
+	{ .IP4, "1.2. 3.4",                "", ""},
+
+	// invalid as non-decimal address
+	{ .IP4_Alt, "",                        "", ""},
+	{ .IP4_Alt, ".1.2.3",                  "", ""},
+	{ .IP4_Alt, "1..2.3",                  "", ""},
+	{ .IP4_Alt, "1.2.3.",                  "", ""},
+	{ .IP4_Alt, "1.2.3.4.5",               "", ""},
+	{ .IP4_Alt, "1.2.3.a",                 "", ""},
+	{ .IP4_Alt, "1.256.2.3",               "", ""},
+	{ .IP4_Alt, "1.2.4294967296.3",        "", ""},
+	{ .IP4_Alt, "1.2.-4294967295.3",       "", ""},
+	{ .IP4_Alt, "1.2. 3.4",                "", ""},
+
+	// Valid IPv6 addresses
+	{ .IP6, "::",                                            "00000000000000000000000000000000", "::"},
+	{ .IP6, "::1",                                           "00000000000000000000000000000001", "::1"},
+	{ .IP6, "::192.168.1.1",                                 "000000000000000000000000c0a80101", "::c0a8:101"},
+	{ .IP6, "0000:0000:0000:0000:0000:ffff:255.255.255.255", "00000000000000000000ffffffffffff", "::ffff:ffff:ffff"},
+
+	{ .IP6, "0:0:0:0:0:0:192.168.1.1", "000000000000000000000000c0a80101", "::c0a8:101"},
+	{ .IP6, "0:0::0:0:0:192.168.1.1",  "000000000000000000000000c0a80101", "::c0a8:101"},
+	{ .IP6, "::ffff:192.168.1.1",      "00000000000000000000ffffc0a80101", "::ffff:c0a8:101"},
+	{ .IP6, "a:0b:00c:000d:E:F::",     "000a000b000c000d000e000f00000000", "a:b:c:d:e:f::"},
+	{ .IP6, "1:2:3:4:5:6::",           "00010002000300040005000600000000", "1:2:3:4:5:6::"},
+	{ .IP6, "1:2:3:4:5:6:7::",         "00010002000300040005000600070000", "1:2:3:4:5:6:7:0"},
+	{ .IP6, "::1:2:3:4:5:6",           "00000000000100020003000400050006", "::1:2:3:4:5:6"},
+	{ .IP6, "::1:2:3:4:5:6:7",         "00000001000200030004000500060007", "0:1:2:3:4:5:6:7"},
+	{ .IP6, "a:b::c:d:e:f",            "000a000b00000000000c000d000e000f", "a:b::c:d:e:f"},
+	{ .IP6, "0:0:0:0:0:ffff:c0a8:5e4", "00000000000000000000ffffc0a805e4", "::ffff:c0a8:5e4"},
+	{ .IP6, "0::ffff:c0a8:5e4",        "00000000000000000000ffffc0a805e4", "::ffff:c0a8:5e4"},
+
+
+	// If multiple zero runs are present, shorten the longest one.
+	{ .IP6, "1:0:0:2:0:0:0:3",         "00010000000000020000000000000003", "1:0:0:2::3"},
+
+	// Invalid IPv6 addresses
+	{ .IP6, "",                        "", ""},
+	{ .IP6, ":",                       "", ""},
+	{ .IP6, ":::",                     "", ""},
+	{ .IP6, "192.168.1.1",             "", ""},
+	{ .IP6, ":192.168.1.1",            "", ""},
+	{ .IP6, "::012.34.56.78",          "", ""},
+	{ .IP6, ":ffff:192.168.1.1",       "", ""},
+	{ .IP6, ".192.168.1.1",            "", ""},
+	{ .IP6, ":.192.168.1.1",           "", ""},
+	{ .IP6, "a:0b:00c:000d:0000e:f::", "", ""},
+	{ .IP6, "1:2:3:4:5:6:7:8::",       "", ""},
+	{ .IP6, "1:2:3:4:5:6:7::9",        "", ""},
+	{ .IP6, "::1:2:3:4:5:6:7:8",       "", ""},
+	{ .IP6, "ffff:c0a8:5e4",           "", ""},
+	{ .IP6, ":ffff:c0a8:5e4",          "", ""},
+	{ .IP6, "0:0:0:0:ffff:c0a8:5e4",   "", ""},
+	{ .IP6, "::0::ffff:c0a8:5e4",      "", ""},
+	{ .IP6, "c0a8",                    "", ""},
+}
+
+
+ENDPOINT := net.Endpoint{
+	net.IP4_Address{127, 0, 0, 1},
+	9999,
+}
+
+CONTENT := "Hellope!"
+
+SEND_TIMEOUT :: time.Duration(2 * time.Second)
+RECV_TIMEOUT :: time.Duration(2 * time.Second)
+
+Thread_Data :: struct {
+	skt: net.Any_Socket,
+	err: net.Network_Error,
+	tid: ^thread.Thread,
+
+	no_accept: bool,  // Tell the server proc not to accept.
+
+	data:   [1024]u8, // Received data and its length
+	length: int,
+}
+
+thread_data := [3]Thread_Data{}
+
+/*
+	This runs a bunch of socket tests using threads:
+	- two servers trying to bind the same endpoint
+	- client trying to connect to closed port
+	- client trying to connect to an open port with a non-accepting server
+	- client sending server data and server sending client data
+	- etc.
+*/
+tcp_tests :: proc(t: ^testing.T) {
+	fmt.println("Testing two servers trying to bind to the same endpoint...")
+	two_servers_binding_same_endpoint(t)
+	fmt.println("Testing client connecting to a closed port...")
+	client_connects_to_closed_port(t)
+	fmt.println("Testing client connecting to port that doesn't accept...")
+	client_connects_to_open_but_non_accepting_port(t)
+	fmt.println("Testing client sending server data...")
+	client_sends_server_data(t)
+}
+
+tcp_client :: proc(retval: rawptr) {
+	r := transmute(^Thread_Data)retval
+
+	if r.skt, r.err = net.dial_tcp(ENDPOINT); r.err != nil {
+		return
+	}
+	defer net.close(r.skt)
+
+	net.set_option(r.skt, .Send_Timeout,    SEND_TIMEOUT)
+	net.set_option(r.skt, .Receive_Timeout, RECV_TIMEOUT)
+
+	_, r.err = net.send(r.skt.(net.TCP_Socket), transmute([]u8)CONTENT)
+	return
+}
+
+tcp_server :: proc(retval: rawptr) {
+	r := transmute(^Thread_Data)retval
+
+	if r.skt, r.err = net.listen_tcp(ENDPOINT); r.err != nil {
+		return
+	}
+	defer net.close(r.skt)
+
+	if r.no_accept {
+		// Don't accept any connections, just listen.
+		return
+	}
+
+	client: net.TCP_Socket
+	if client, _, r.err = net.accept_tcp(r.skt.(net.TCP_Socket)); r.err != nil {
+		return
+	}
+	defer net.close(client)
+
+	r.length, r.err = net.recv_tcp(client, r.data[:])
+	return
+}
+
+cleanup_thread :: proc(data: Thread_Data) {
+	net.close(data.skt)
+
+	thread.terminate(data.tid, 1)
+	thread.destroy(data.tid)
+}
+
+two_servers_binding_same_endpoint :: proc(t: ^testing.T) {
+	thread_data = {}
+
+	thread_data[0].tid = thread.create_and_start_with_data(&thread_data[0], tcp_server, context)
+	thread_data[1].tid = thread.create_and_start_with_data(&thread_data[1], tcp_server, context)
+
+	defer {
+		cleanup_thread(thread_data[0])
+		cleanup_thread(thread_data[1])
+	}
+
+	// Give the two servers enough time to try and bind the same endpoint
+	time.sleep(1 * time.Second)
+
+	first_won  := thread_data[0].err == nil && thread_data[1].err == net.Bind_Error.Address_In_Use
+	second_won := thread_data[1].err == nil && thread_data[0].err == net.Bind_Error.Address_In_Use
+
+	okay := first_won || second_won
+	msg  := fmt.tprintf("Expected servers to return `nil` and `Address_In_Use`, got %v and %v", thread_data[0].err, thread_data[1].err)
+	expect(t, okay, msg)
+}
+
+client_connects_to_closed_port :: proc(t: ^testing.T) {
+	thread_data = {}
+
+	thread_data[0].tid = thread.create_and_start_with_data(&thread_data[0], tcp_client, context)
+
+	defer {
+		cleanup_thread(thread_data[0])
+	}
+
+	// Give the socket enough time to return `Refused`
+	time.sleep(4 * time.Second)
+
+	okay := thread_data[0].err == net.Dial_Error.Refused
+	msg  := fmt.tprintf("Expected client to return `Refused` connecting to closed port, got %v", thread_data[0].err)
+	expect(t, okay, msg)
+}
+
+client_connects_to_open_but_non_accepting_port :: proc(t: ^testing.T) {
+	thread_data = {}
+
+	// Tell server proc not to accept
+	thread_data[0].no_accept = true
+
+	thread_data[0].tid = thread.create_and_start_with_data(&thread_data[0], tcp_server, context)
+	thread_data[1].tid = thread.create_and_start_with_data(&thread_data[1], tcp_client, context)
+
+	defer {
+		cleanup_thread(thread_data[0])
+		cleanup_thread(thread_data[1])
+	}
+
+	// Give the two servers enough time to try and bind the same endpoint
+	time.sleep(4 * time.Second)
+
+	okay := thread_data[0].err == nil && thread_data[1].err == net.Dial_Error.Refused
+	msg  := fmt.tprintf("Expected server and client to return `nil` and `Refused`, got %v and %v", thread_data[0].err, thread_data[1].err)
+	expect(t, okay, msg)
+}
+
+client_sends_server_data :: proc(t: ^testing.T) {
+	thread_data = {}
+
+	// Tell server proc not to accept
+	// thread_data[0].no_accept = true
+
+	thread_data[0].tid = thread.create_and_start_with_data(&thread_data[0], tcp_server, context)
+	thread_data[1].tid = thread.create_and_start_with_data(&thread_data[1], tcp_client, context)
+
+	defer {
+		cleanup_thread(thread_data[0])
+		cleanup_thread(thread_data[1])
+	}
+
+	// Give the two servers enough time to try and bind the same endpoint
+	time.sleep(1 * time.Second)
+
+	okay := thread_data[0].err == nil && thread_data[1].err == nil
+	msg  := fmt.tprintf("Expected client and server to return `nil`, got %v and %v", thread_data[0].err, thread_data[1].err)
+	expect(t, okay, msg)
+
+	received := string(thread_data[0].data[:thread_data[0].length])
+
+	okay  = received == CONTENT
+	msg   = fmt.tprintf("Expected client to send \"{}\", got \"{}\"", CONTENT, received)
+	expect(t, okay, msg)
+}