aboutsummaryrefslogtreecommitdiff
path: root/core/net
diff options
context:
space:
mode:
authorColin Davidson <colrdavidson@gmail.com>2023-03-01 07:58:30 -0800
committerColin Davidson <colrdavidson@gmail.com>2023-03-01 07:58:30 -0800
commit28f7f572473c4e97ccd6133bb4f5fa6f45505530 (patch)
tree0bd159c24c617df409f72b0ec75daa086372e94d /core/net
parent3567c006e6683d989805c078db48a95a901d9e72 (diff)
manually start merging core_net
Diffstat (limited to 'core/net')
-rw-r--r--core/net/addr.odin818
-rw-r--r--core/net/addr_darwin.odin71
-rw-r--r--core/net/addr_linux.odin93
-rw-r--r--core/net/addr_openbsd.odin69
-rw-r--r--core/net/addr_windows.odin69
-rw-r--r--core/net/common.odin437
-rw-r--r--core/net/dns.odin873
-rw-r--r--core/net/dns_unix.odin83
-rw-r--r--core/net/dns_windows.odin166
-rw-r--r--core/net/doc.odin47
-rw-r--r--core/net/interface.odin68
-rw-r--r--core/net/interface_darwin.odin23
-rw-r--r--core/net/interface_linux.odin147
-rw-r--r--core/net/interface_windows.odin182
-rw-r--r--core/net/socket.odin87
-rw-r--r--core/net/socket_darwin.odin513
-rw-r--r--core/net/socket_linux.odin532
-rw-r--r--core/net/socket_openbsd.odin515
-rw-r--r--core/net/socket_windows.odin577
-rw-r--r--core/net/url.odin235
20 files changed, 5605 insertions, 0 deletions
diff --git a/core/net/addr.odin b/core/net/addr.odin
new file mode 100644
index 000000000..80ba91110
--- /dev/null
+++ b/core/net/addr.odin
@@ -0,0 +1,818 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+} \ No newline at end of file
diff --git a/core/net/addr_darwin.odin b/core/net/addr_darwin.odin
new file mode 100644
index 000000000..801b89a96
--- /dev/null
+++ b/core/net/addr_darwin.odin
@@ -0,0 +1,71 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+}
diff --git a/core/net/addr_linux.odin b/core/net/addr_linux.odin
new file mode 100644
index 000000000..90ce0c9ef
--- /dev/null
+++ b/core/net/addr_linux.odin
@@ -0,0 +1,93 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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 } \ No newline at end of file
diff --git a/core/net/addr_openbsd.odin b/core/net/addr_openbsd.odin
new file mode 100644
index 000000000..34a49b526
--- /dev/null
+++ b/core/net/addr_openbsd.odin
@@ -0,0 +1,69 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+} \ No newline at end of file
diff --git a/core/net/addr_windows.odin b/core/net/addr_windows.odin
new file mode 100644
index 000000000..4225216ca
--- /dev/null
+++ b/core/net/addr_windows.odin
@@ -0,0 +1,69 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+} \ No newline at end of file
diff --git a/core/net/common.odin b/core/net/common.odin
new file mode 100644
index 000000000..9e980e88e
--- /dev/null
+++ b/core/net/common.odin
@@ -0,0 +1,437 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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,
+} \ No newline at end of file
diff --git a/core/net/dns.odin b/core/net/dns.odin
new file mode 100644
index 000000000..32bc1be6b
--- /dev/null
+++ b/core/net/dns.odin
@@ -0,0 +1,873 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+}
diff --git a/core/net/dns_unix.odin b/core/net/dns_unix.odin
new file mode 100644
index 000000000..7f57927fd
--- /dev/null
+++ b/core/net/dns_unix.odin
@@ -0,0 +1,83 @@
+//+build linux, darwin, freebsd, openbsd, !windows
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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[:])
+}
diff --git a/core/net/dns_windows.odin b/core/net/dns_windows.odin
new file mode 100644
index 000000000..ae38ca05f
--- /dev/null
+++ b/core/net/dns_windows.odin
@@ -0,0 +1,166 @@
+//+build windows
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+}
diff --git a/core/net/doc.odin b/core/net/doc.odin
new file mode 100644
index 000000000..0c6c08daa
--- /dev/null
+++ b/core/net/doc.odin
@@ -0,0 +1,47 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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 \ No newline at end of file
diff --git a/core/net/interface.odin b/core/net/interface.odin
new file mode 100644
index 000000000..354cba53f
--- /dev/null
+++ b/core/net/interface.odin
@@ -0,0 +1,68 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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)
+} \ No newline at end of file
diff --git a/core/net/interface_darwin.odin b/core/net/interface_darwin.odin
new file mode 100644
index 000000000..9a9bda5d3
--- /dev/null
+++ b/core/net/interface_darwin.odin
@@ -0,0 +1,23 @@
+//+build darwin
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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.
+*/
diff --git a/core/net/interface_linux.odin b/core/net/interface_linux.odin
new file mode 100644
index 000000000..c5973fa2d
--- /dev/null
+++ b/core/net/interface_linux.odin
@@ -0,0 +1,147 @@
+//+build linux, darwin, openbsd, !windows
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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[:], {}
+} \ No newline at end of file
diff --git a/core/net/interface_windows.odin b/core/net/interface_windows.odin
new file mode 100644
index 000000000..57cc0e8ef
--- /dev/null
+++ b/core/net/interface_windows.odin
@@ -0,0 +1,182 @@
+//+build windows
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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()
+} \ No newline at end of file
diff --git a/core/net/socket.odin b/core/net/socket.odin
new file mode 100644
index 000000000..1fa57aac0
--- /dev/null
+++ b/core/net/socket.odin
@@ -0,0 +1,87 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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,
+} \ No newline at end of file
diff --git a/core/net/socket_darwin.odin b/core/net/socket_darwin.odin
new file mode 100644
index 000000000..89ad7b31c
--- /dev/null
+++ b/core/net/socket_darwin.odin
@@ -0,0 +1,513 @@
+// +build darwin
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+}
diff --git a/core/net/socket_linux.odin b/core/net/socket_linux.odin
new file mode 100644
index 000000000..dcc48d3fa
--- /dev/null
+++ b/core/net/socket_linux.odin
@@ -0,0 +1,532 @@
+// +build linux
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+} \ No newline at end of file
diff --git a/core/net/socket_openbsd.odin b/core/net/socket_openbsd.odin
new file mode 100644
index 000000000..746b886ef
--- /dev/null
+++ b/core/net/socket_openbsd.odin
@@ -0,0 +1,515 @@
+// +build openbsd
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+} \ No newline at end of file
diff --git a/core/net/socket_windows.odin b/core/net/socket_windows.odin
new file mode 100644
index 000000000..a08248d91
--- /dev/null
+++ b/core/net/socket_windows.odin
@@ -0,0 +1,577 @@
+// +build windows
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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
+}
diff --git a/core/net/url.odin b/core/net/url.odin
new file mode 100644
index 000000000..ffd1bbf83
--- /dev/null
+++ b/core/net/url.odin
@@ -0,0 +1,235 @@
+/*
+ Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
+ Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
+ Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
+ 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);
+}
+*/ \ No newline at end of file