From 28f7f572473c4e97ccd6133bb4f5fa6f45505530 Mon Sep 17 00:00:00 2001 From: Colin Davidson Date: Wed, 1 Mar 2023 07:58:30 -0800 Subject: [PATCH] manually start merging core_net --- core/net/addr.odin | 818 ++++++++++++++++++++++++++++ core/net/addr_darwin.odin | 71 +++ core/net/addr_linux.odin | 93 ++++ core/net/addr_openbsd.odin | 69 +++ core/net/addr_windows.odin | 69 +++ core/net/common.odin | 437 +++++++++++++++ core/net/dns.odin | 873 ++++++++++++++++++++++++++++++ core/net/dns_unix.odin | 83 +++ core/net/dns_windows.odin | 166 ++++++ core/net/doc.odin | 47 ++ core/net/interface.odin | 68 +++ core/net/interface_darwin.odin | 23 + core/net/interface_linux.odin | 147 +++++ core/net/interface_windows.odin | 182 +++++++ core/net/socket.odin | 87 +++ core/net/socket_darwin.odin | 513 ++++++++++++++++++ core/net/socket_linux.odin | 532 ++++++++++++++++++ core/net/socket_openbsd.odin | 515 ++++++++++++++++++ core/net/socket_windows.odin | 577 ++++++++++++++++++++ core/net/url.odin | 235 ++++++++ core/os/os_darwin.odin | 239 ++++++++ core/os/os_linux.odin | 243 +++++++++ core/sys/unix/syscalls_linux.odin | 36 ++ core/sys/windows/dnsapi.odin | 10 + core/sys/windows/ip_helper.odin | 234 ++++++++ core/sys/windows/types.odin | 369 ++++++++----- core/sys/windows/util.odin | 21 + core/sys/windows/ws2_32.odin | 14 +- tests/core/Makefile | 5 +- tests/core/build.bat | 5 + tests/core/net/test_core_net.odin | 508 +++++++++++++++++ 31 files changed, 7142 insertions(+), 147 deletions(-) create mode 100644 core/net/addr.odin create mode 100644 core/net/addr_darwin.odin create mode 100644 core/net/addr_linux.odin create mode 100644 core/net/addr_openbsd.odin create mode 100644 core/net/addr_windows.odin create mode 100644 core/net/common.odin create mode 100644 core/net/dns.odin create mode 100644 core/net/dns_unix.odin create mode 100644 core/net/dns_windows.odin create mode 100644 core/net/doc.odin create mode 100644 core/net/interface.odin create mode 100644 core/net/interface_darwin.odin create mode 100644 core/net/interface_linux.odin create mode 100644 core/net/interface_windows.odin create mode 100644 core/net/socket.odin create mode 100644 core/net/socket_darwin.odin create mode 100644 core/net/socket_linux.odin create mode 100644 core/net/socket_openbsd.odin create mode 100644 core/net/socket_windows.odin create mode 100644 core/net/url.odin create mode 100644 core/sys/windows/dnsapi.odin create mode 100644 core/sys/windows/ip_helper.odin create mode 100644 tests/core/net/test_core_net.odin 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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.. 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.. 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.. 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 + Copyright 2022 Colin Davidson + Copyright 2022 Jeroen van Rijn . + 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 diff --git a/core/os/os_darwin.odin b/core/os/os_darwin.odin index c16cf737e..e1327c3b6 100644 --- a/core/os/os_darwin.odin +++ b/core/os/os_darwin.odin @@ -179,6 +179,93 @@ RTLD_NODELETE :: 0x80 RTLD_NOLOAD :: 0x10 RTLD_FIRST :: 0x100 +SOL_SOCKET :: 0xFFFF + +SOCK_STREAM :: 1 +SOCK_DGRAM :: 2 +SOCK_RAW :: 3 +SOCK_RDM :: 4 +SOCK_SEQPACKET :: 5 + +SO_DEBUG :: 0x0001 +SO_ACCEPTCONN :: 0x0002 +SO_REUSEADDR :: 0x0004 +SO_KEEPALIVE :: 0x0008 +SO_DONTROUTE :: 0x0010 +SO_BROADCAST :: 0x0020 +SO_USELOOPBACK :: 0x0040 +SO_LINGER :: 0x0080 +SO_OOBINLINE :: 0x0100 +SO_REUSEPORT :: 0x0200 +SO_TIMESTAMP :: 0x0400 + +SO_DONTTRUNC :: 0x2000 +SO_WANTMORE :: 0x4000 +SO_WANTOOBFLAG :: 0x8000 +SO_SNDBUF :: 0x1001 +SO_RCVBUF :: 0x1002 +SO_SNDLOWAT :: 0x1003 +SO_RCVLOWAT :: 0x1004 +SO_SNDTIMEO :: 0x1005 +SO_RCVTIMEO :: 0x1006 +SO_ERROR :: 0x1007 +SO_TYPE :: 0x1008 +SO_PRIVSTATE :: 0x1009 +SO_NREAD :: 0x1020 +SO_NKE :: 0x1021 + +AF_UNSPEC :: 0 +AF_LOCAL :: 1 +AF_UNIX :: AF_LOCAL +AF_INET :: 2 +AF_IMPLINK :: 3 +AF_PUP :: 4 +AF_CHAOS :: 5 +AF_NS :: 6 +AF_ISO :: 7 +AF_OSI :: AF_ISO +AF_ECMA :: 8 +AF_DATAKIT :: 9 +AF_CCITT :: 10 +AF_SNA :: 11 +AF_DECnet :: 12 +AF_DLI :: 13 +AF_LAT :: 14 +AF_HYLINK :: 15 +AF_APPLETALK :: 16 +AF_ROUTE :: 17 +AF_LINK :: 18 +pseudo_AF_XTP :: 19 +AF_COIP :: 20 +AF_CNT :: 21 +pseudo_AF_RTIP :: 22 +AF_IPX :: 23 +AF_SIP :: 24 +pseudo_AF_PIP :: 25 +pseudo_AF_BLUE :: 26 +AF_NDRV :: 27 +AF_ISDN :: 28 +AF_E164 :: AF_ISDN +pseudo_AF_KEY :: 29 +AF_INET6 :: 30 +AF_NATM :: 31 +AF_SYSTEM :: 32 +AF_NETBIOS :: 33 +AF_PPP :: 34 + +TCP_NODELAY :: 0x01 +TCP_MAXSEG :: 0x02 +TCP_NOPUSH :: 0x04 +TCP_NOOPT :: 0x08 + +IPPROTO_ICMP :: 1 +IPPROTO_TCP :: 6 +IPPROTO_UDP :: 17 + +SHUT_RD :: 0 +SHUT_WR :: 1 +SHUT_RDWR :: 2 + // "Argv" arguments converted to Odin strings args := _alloc_command_line_arguments() @@ -224,6 +311,58 @@ Dirent :: struct { Dir :: distinct rawptr // DIR* +SOCKADDR :: struct #packed { + len: c.char, + family: c.char, + sa_data: [14]c.char, +} + +SOCKADDR_STORAGE_LH :: struct #packed { + len: c.char, + family: c.char, + __ss_pad1: [6]c.char, + __ss_align: i64, + __ss_pad2: [112]c.char, +} + +sockaddr_in :: struct #packed { + sin_len: c.char, + sin_family: c.char, + sin_port: u16be, + sin_addr: in_addr, + sin_zero: [8]c.char, +} + +sockaddr_in6 :: struct #packed { + sin6_len: c.char, + sin6_family: c.char, + sin6_port: u16be, + sin6_flowinfo: c.uint, + sin6_addr: in6_addr, + sin6_scope_id: c.uint, +} + +in_addr :: struct #packed { + s_addr: u32, +} + +in6_addr :: struct #packed { + s6_addr: [16]u8, +} + +Timeval :: struct { + seconds: i64, + nanoseconds: int, +} + +Linger :: struct { + onoff: int, + linger: int, +} + +Socket :: distinct int +socklen_t :: c.int + // File type S_IFMT :: 0o170000 // Type of file mask S_IFIFO :: 0o010000 // Named pipe (fifo) @@ -318,6 +457,18 @@ foreign libc { @(link_name="strerror") _darwin_string_error :: proc(num : c.int) -> cstring --- @(link_name="sysctlbyname") _sysctlbyname :: proc(path: cstring, oldp: rawptr, oldlenp: rawptr, newp: rawptr, newlen: int) -> c.int --- + @(link_name="socket") _unix_socket :: proc(domain: int, type: int, protocol: int) -> int --- + @(link_name="listen") _unix_listen :: proc(socket: int, backlog: int) -> int --- + @(link_name="accept") _unix_accept :: proc(socket: int, addr: rawptr, addr_len: rawptr) -> int --- + @(link_name="connect") _unix_connect :: proc(socket: int, addr: rawptr, addr_len: socklen_t) -> int --- + @(link_name="bind") _unix_bind :: proc(socket: int, addr: rawptr, addr_len: socklen_t) -> int --- + @(link_name="setsockopt") _unix_setsockopt :: proc(socket: int, level: int, opt_name: int, opt_val: rawptr, opt_len: socklen_t) -> int --- + @(link_name="recvfrom") _unix_recvfrom :: proc(socket: int, buffer: rawptr, buffer_len: c.size_t, flags: int, addr: rawptr, addr_len: ^socklen_t) -> c.ssize_t --- + @(link_name="recv") _unix_recv :: proc(socket: int, buffer: rawptr, buffer_len: c.size_t, flags: int) -> c.ssize_t --- + @(link_name="sendto") _unix_sendto :: proc(socket: int, buffer: rawptr, buffer_len: c.size_t, flags: int, addr: rawptr, addr_len: socklen_t) -> c.ssize_t --- + @(link_name="send") _unix_send :: proc(socket: int, buffer: rawptr, buffer_len: c.size_t, flags: int) -> c.ssize_t --- + @(link_name="shutdown") _unix_shutdown :: proc(socket: int, how: int) -> int --- + @(link_name="exit") _unix_exit :: proc(status: c.int) -> ! --- } @@ -815,3 +966,91 @@ _alloc_command_line_arguments :: proc() -> []string { } return res } + +socket :: proc(domain: int, type: int, protocol: int) -> (Socket, Errno) { + result := _unix_socket(domain, type, protocol) + if result < 0 { + return 0, Errno(get_last_error()) + } + return Socket(result), ERROR_NONE +} + +connect :: proc(sd: Socket, addr: ^SOCKADDR, len: socklen_t) -> (Errno) { + result := _unix_connect(int(sd), addr, len) + if result < 0 { + return Errno(get_last_error()) + } + return ERROR_NONE +} + +bind :: proc(sd: Socket, addr: ^SOCKADDR, len: socklen_t) -> (Errno) { + result := _unix_bind(int(sd), addr, len) + if result < 0 { + return Errno(get_last_error()) + } + return ERROR_NONE +} + +accept :: proc(sd: Socket, addr: ^SOCKADDR, len: rawptr) -> (Socket, Errno) { + result := _unix_accept(int(sd), rawptr(addr), len) + if result < 0 { + return 0, Errno(get_last_error()) + } + return Socket(result), ERROR_NONE +} + +listen :: proc(sd: Socket, backlog: int) -> (Errno) { + result := _unix_listen(int(sd), backlog) + if result < 0 { + return Errno(get_last_error()) + } + return ERROR_NONE +} + +setsockopt :: proc(sd: Socket, level: int, optname: int, optval: rawptr, optlen: socklen_t) -> (Errno) { + result := _unix_setsockopt(int(sd), level, optname, optval, optlen) + if result < 0 { + return Errno(get_last_error()) + } + return ERROR_NONE +} + +recvfrom :: proc(sd: Socket, data: []byte, flags: int, addr: ^SOCKADDR, addr_size: ^socklen_t) -> (u32, Errno) { + result := _unix_recvfrom(int(sd), raw_data(data), len(data), flags, addr, addr_size) + if result < 0 { + return 0, Errno(get_last_error()) + } + return u32(result), ERROR_NONE +} + +recv :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Errno) { + result := _unix_recv(int(sd), raw_data(data), len(data), flags) + if result < 0 { + return 0, Errno(get_last_error()) + } + return u32(result), ERROR_NONE +} + +sendto :: proc(sd: Socket, data: []u8, flags: int, addr: ^SOCKADDR, addrlen: socklen_t) -> (u32, Errno) { + result := _unix_sendto(int(sd), raw_data(data), len(data), flags, addr, addrlen) + if result < 0 { + return 0, Errno(get_last_error()) + } + return u32(result), ERROR_NONE +} + +send :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Errno) { + result := _unix_send(int(sd), raw_data(data), len(data), 0) + if result < 0 { + return 0, Errno(get_last_error()) + } + return u32(result), ERROR_NONE +} + +shutdown :: proc(sd: Socket, how: int) -> (Errno) { + result := _unix_shutdown(int(sd), how) + if result < 0 { + return Errno(get_last_error()) + } + return ERROR_NONE +} diff --git a/core/os/os_linux.odin b/core/os/os_linux.odin index 09b7f1a0f..22858d6ac 100644 --- a/core/os/os_linux.odin +++ b/core/os/os_linux.odin @@ -171,6 +171,59 @@ SEEK_DATA :: 3 SEEK_HOLE :: 4 SEEK_MAX :: SEEK_HOLE + +AF_UNSPEC: int : 0 +AF_UNIX: int : 1 +AF_LOCAL: int : AF_UNIX +AF_INET: int : 2 +AF_INET6: int : 10 +AF_PACKET: int : 17 +AF_BLUETOOTH: int : 31 + +SOCK_STREAM: int : 1 +SOCK_DGRAM: int : 2 +SOCK_RAW: int : 3 +SOCK_RDM: int : 4 +SOCK_SEQPACKET: int : 5 +SOCK_PACKET: int : 10 + +INADDR_ANY: c.ulong : 0 +INADDR_BROADCAST: c.ulong : 0xffffffff +INADDR_NONE: c.ulong : 0xffffffff +INADDR_DUMMY: c.ulong : 0xc0000008 + +IPPROTO_IP: int : 0 +IPPROTO_ICMP: int : 1 +IPPROTO_TCP: int : 6 +IPPROTO_UDP: int : 17 +IPPROTO_IPV6: int : 41 +IPPROTO_ETHERNET: int : 143 +IPPROTO_RAW: int : 255 + +SHUT_RD: int : 0 +SHUT_WR: int : 1 +SHUT_RDWR: int : 2 + + +SOL_SOCKET: int : 1 +SO_DEBUG: int : 1 +SO_REUSEADDR: int : 2 +SO_DONTROUTE: int : 5 +SO_BROADCAST: int : 6 +SO_SNDBUF: int : 7 +SO_RCVBUF: int : 8 +SO_KEEPALIVE: int : 9 +SO_OOBINLINE: int : 10 +SO_LINGER: int : 13 +SO_REUSEPORT: int : 15 +SO_RCVTIMEO_NEW: int : 66 +SO_SNDTIMEO_NEW: int : 67 + +TCP_NODELAY: int : 1 +TCP_CORK: int : 3 + +MSG_TRUNC : int : 0x20 + // NOTE(zangent): These are OS specific! // Do not mix these up! RTLD_LAZY :: 0x001 @@ -217,6 +270,102 @@ Dirent :: struct { name: [256]byte, } +ADDRESS_FAMILY :: u16 +SOCKADDR :: struct #packed { + sa_family: ADDRESS_FAMILY, + sa_data: [14]c.char, +} + +SOCKADDR_STORAGE_LH :: struct #packed { + ss_family: ADDRESS_FAMILY, + __ss_pad1: [6]c.char, + __ss_align: i64, + __ss_pad2: [112]c.char, +} + +sockaddr_in :: struct #packed { + sin_family: ADDRESS_FAMILY, + sin_port: u16be, + sin_addr: in_addr, + sin_zero: [8]c.char, +} + +sockaddr_in6 :: struct #packed { + sin6_family: ADDRESS_FAMILY, + sin6_port: u16be, + sin6_flowinfo: c.ulong, + sin6_addr: in6_addr, + sin6_scope_id: c.ulong, +} + +in_addr :: struct #packed { + s_addr: u32, +} + +in6_addr :: struct #packed { + s6_addr: [16]u8, +} + +rtnl_link_stats :: struct #packed { + rx_packets: u32, + tx_packets: u32, + rx_bytes: u32, + tx_bytes: u32, + rx_errors: u32, + tx_errors: u32, + rx_dropped: u32, + tx_dropped: u32, + multicast: u32, + collisions: u32, + rx_length_errors: u32, + rx_over_errors: u32, + rx_crc_errors: u32, + rx_frame_errors: u32, + rx_fifo_errors: u32, + rx_missed_errors: u32, + tx_aborted_errors: u32, + tx_carrier_errors: u32, + tx_fifo_errors: u32, + tx_heartbeat_errors: u32, + tx_window_errors: u32, + rx_compressed: u32, + tx_compressed: u32, + rx_nohandler: u32, +} + +SIOCGIFFLAG :: enum c.int { + UP = 0, /* Interface is up. */ + BROADCAST = 1, /* Broadcast address valid. */ + DEBUG = 2, /* Turn on debugging. */ + LOOPBACK = 3, /* Is a loopback net. */ + POINT_TO_POINT = 4, /* Interface is point-to-point link. */ + NO_TRAILERS = 5, /* Avoid use of trailers. */ + RUNNING = 6, /* Resources allocated. */ + NOARP = 7, /* No address resolution protocol. */ + PROMISC = 8, /* Receive all packets. */ + ALL_MULTI = 9, /* Receive all multicast packets. Unimplemented. */ + MASTER = 10, /* Master of a load balancer. */ + SLAVE = 11, /* Slave of a load balancer. */ + MULTICAST = 12, /* Supports multicast. */ + PORTSEL = 13, /* Can set media type. */ + AUTOMEDIA = 14, /* Auto media select active. */ + DYNAMIC = 15, /* Dialup device with changing addresses. */ + LOWER_UP = 16, + DORMANT = 17, + ECHO = 18, +} +SIOCGIFFLAGS :: bit_set[SIOCGIFFLAG; c.int] + +ifaddrs :: struct { + next: ^ifaddrs, + name: cstring, + flags: SIOCGIFFLAGS, + address: ^SOCKADDR, + netmask: ^SOCKADDR, + broadcast_or_dest: ^SOCKADDR, // Broadcast or Point-to-Point address + data: rawptr, // Address-specific data. +} + Dir :: distinct rawptr // DIR* // File type @@ -297,6 +446,9 @@ foreign dl { @(link_name="dlsym") _unix_dlsym :: proc(handle: rawptr, symbol: cstring) -> rawptr --- @(link_name="dlclose") _unix_dlclose :: proc(handle: rawptr) -> c.int --- @(link_name="dlerror") _unix_dlerror :: proc() -> cstring --- + + @(link_name="getifaddrs") _getifaddrs :: proc(ifap: ^^ifaddrs) -> (c.int) --- + @(link_name="freeifaddrs") _freeifaddrs :: proc(ifa: ^ifaddrs) --- } is_path_separator :: proc(r: rune) -> bool { @@ -823,3 +975,94 @@ _alloc_command_line_arguments :: proc() -> []string { } return res } + +socket :: proc(domain: int, type: int, protocol: int) -> (Socket, Errno) { + result := unix.sys_socket(domain, type, protocol) + if result < 0 { + return 0, _get_errno(result) + } + return Socket(result), ERROR_NONE +} + +bind :: proc(sd: Socket, addr: ^SOCKADDR, len: socklen_t) -> (Errno) { + result := unix.sys_bind(int(sd), addr, len) + if result < 0 { + return _get_errno(result) + } + return ERROR_NONE +} + + +connect :: proc(sd: Socket, addr: ^SOCKADDR, len: socklen_t) -> (Errno) { + result := unix.sys_connect(int(sd), addr, len) + if result < 0 { + return _get_errno(result) + } + return ERROR_NONE +} + +accept :: proc(sd: Socket, addr: ^SOCKADDR, len: rawptr) -> (Socket, Errno) { + result := unix.sys_accept(int(sd), rawptr(addr), len) + if result < 0 { + return 0, _get_errno(result) + } + return Socket(result), ERROR_NONE +} + +listen :: proc(sd: Socket, backlog: int) -> (Errno) { + result := unix.sys_listen(int(sd), backlog) + if result < 0 { + return _get_errno(result) + } + return ERROR_NONE +} + +setsockopt :: proc(sd: Socket, level: int, optname: int, optval: rawptr, optlen: socklen_t) -> (Errno) { + result := unix.sys_setsockopt(int(sd), level, optname, optval, optlen) + if result < 0 { + return _get_errno(result) + } + return ERROR_NONE +} + + +recvfrom :: proc(sd: Socket, data: []byte, flags: int, addr: ^SOCKADDR, addr_size: ^socklen_t) -> (u32, Errno) { + result := unix.sys_recvfrom(int(sd), raw_data(data), len(data), flags, addr, uintptr(addr_size)) + if result < 0 { + return 0, _get_errno(int(result)) + } + return u32(result), ERROR_NONE +} + +recv :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Errno) { + result := unix.sys_recvfrom(int(sd), raw_data(data), len(data), flags, nil, 0) + if result < 0 { + return 0, _get_errno(int(result)) + } + return u32(result), ERROR_NONE +} + + +sendto :: proc(sd: Socket, data: []u8, flags: int, addr: ^SOCKADDR, addrlen: socklen_t) -> (u32, Errno) { + result := unix.sys_sendto(int(sd), raw_data(data), len(data), flags, addr, addrlen) + if result < 0 { + return 0, _get_errno(int(result)) + } + return u32(result), ERROR_NONE +} + +send :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Errno) { + result := unix.sys_sendto(int(sd), raw_data(data), len(data), 0, nil, 0) + if result < 0 { + return 0, _get_errno(int(result)) + } + return u32(result), ERROR_NONE +} + +shutdown :: proc(sd: Socket, how: int) -> (Errno) { + result := unix.sys_shutdown(int(sd), how) + if result < 0 { + return _get_errno(result) + } + return ERROR_NONE +} diff --git a/core/sys/unix/syscalls_linux.odin b/core/sys/unix/syscalls_linux.odin index 91c0accfa..3659d8437 100644 --- a/core/sys/unix/syscalls_linux.odin +++ b/core/sys/unix/syscalls_linux.odin @@ -2008,6 +2008,42 @@ sys_utimensat :: proc "contextless" (dfd: int, path: cstring, times: rawptr, fla return int(intrinsics.syscall(SYS_utimensat, uintptr(dfd), uintptr(rawptr(path)), uintptr(times), uintptr(flags))) } +sys_socket :: proc "contextless" (domain: int, type: int, protocol: int) -> int { + return int(intrinsics.syscall(unix.SYS_socket, uintptr(domain), uintptr(type), uintptr(protocol))) +} + +sys_connect :: proc "contextless" (sd: int, addr: rawptr, len: socklen_t) -> int { + return int(intrinsics.syscall(unix.SYS_connect, uintptr(sd), uintptr(addr), uintptr(len))) +} + +sys_accept :: proc "contextless" (sd: int, addr: rawptr, len: rawptr) -> int { + return int(intrinsics.syscall(unix.SYS_accept4, uintptr(sd), uintptr(addr), uintptr(len), uintptr(0))) +} + +sys_listen :: proc "contextless" (sd: int, backlog: int) -> int { + return int(intrinsics.syscall(unix.SYS_listen, uintptr(sd), uintptr(backlog))) +} + +sys_bind :: proc "contextless" (sd: int, addr: rawptr, len: socklen_t) -> int { + return int(intrinsics.syscall(unix.SYS_bind, uintptr(sd), uintptr(addr), uintptr(len))) +} + +sys_setsockopt :: proc "contextless" (sd: int, level: int, optname: int, optval: rawptr, optlen: socklen_t) -> int { + return int(intrinsics.syscall(unix.SYS_setsockopt, uintptr(sd), uintptr(level), uintptr(optname), uintptr(optval), uintptr(optlen))) +} + +sys_recvfrom :: proc "contextless" (sd: int, buf: rawptr, len: uint, flags: int, addr: rawptr, alen: uintptr) -> i64 { + return i64(intrinsics.syscall(unix.SYS_recvfrom, uintptr(sd), uintptr(buf), uintptr(len), uintptr(flags), uintptr(addr), uintptr(alen))) +} + +sys_sendto :: proc "contextless" (sd: int, buf: rawptr, len: uint, flags: int, addr: rawptr, alen: socklen_t) -> i64 { + return i64(intrinsics.syscall(unix.SYS_sendto, uintptr(sd), uintptr(buf), uintptr(len), uintptr(flags), uintptr(addr), uintptr(alen))) +} + +sys_shutdown :: proc "contextless" (sd: int, how: int) -> int { + return int(intrinsics.syscall(unix.SYS_shutdown, uintptr(sd), uintptr(how))) +} + sys_perf_event_open :: proc "contextless" (event_attr: rawptr, pid: i32, cpu: i32, group_fd: i32, flags: u32) -> int { return int(intrinsics.syscall(SYS_perf_event_open, uintptr(event_attr), uintptr(pid), uintptr(cpu), uintptr(group_fd), uintptr(flags))) } diff --git a/core/sys/windows/dnsapi.odin b/core/sys/windows/dnsapi.odin new file mode 100644 index 000000000..623fa2889 --- /dev/null +++ b/core/sys/windows/dnsapi.odin @@ -0,0 +1,10 @@ +// +build windows +package sys_windows + +foreign import "system:Dnsapi.lib" + +@(default_calling_convention="std") +foreign Dnsapi { + DnsQuery_UTF8 :: proc(name: cstring, type: u16, options: DWORD, extra: PVOID, results: ^^DNS_RECORD, reserved: PVOID) -> DNS_STATUS --- + DnsRecordListFree :: proc(list: ^DNS_RECORD, options: DWORD) --- +} diff --git a/core/sys/windows/ip_helper.odin b/core/sys/windows/ip_helper.odin new file mode 100644 index 000000000..d8f93f533 --- /dev/null +++ b/core/sys/windows/ip_helper.odin @@ -0,0 +1,234 @@ +// +build windows +package sys_windows + +foreign import "system:iphlpapi.lib" + +Address_Family :: enum u32 { + Unspecified = 0, // Return both IPv4 and IPv6 addresses associated with adapters with them enabled. + IPv4 = 2, // Return only IPv4 addresses associated with adapters with it enabled. + IPv6 = 23, // Return only IPv6 addresses associated with adapters with it enabled. +} + +GAA_Flag :: enum u32 { + Skip_Unicast = 0, // Do not return unicast addresses. + Skip_Anycast = 1, // Do not return IPv6 anycast addresses. + Skip_Multicast = 2, // Do not return multicast addresses. + Skip_DNS_Server = 3, // Do not return addresses of DNS servers. + Include_Prefix = 4, // (XP SP1+) Return a list of IP address prefixes on this adapter. When this flag is set, IP address prefixes are returned for both IPv6 and IPv4 addresses. + Skip_Friendly_Name = 5, // Do not return the adapter friendly name. + Include_WINS_info = 6, // (Vista+) Return addresses of Windows Internet Name Service (WINS) servers. + Include_Gateways = 7, // (Vista+) Return the addresses of default gateways. + Include_All_Interfaces = 8, // (Vista+) Return addresses for all NDIS interfaces. + Include_All_Compartments = 9, // (Reserved, Unsupported) Return addresses in all routing compartments. + Include_Tunnel_Binding_Order = 10, // (Vista+) Return the adapter addresses sorted in tunnel binding order. +} +GAA_Flags :: bit_set[GAA_Flag; u32] + +IP_Adapter_Addresses :: struct { + Raw: struct #raw_union { + Alignment: u64, + Anonymous: struct { + Length: u32, + IfIndex: u32, + }, + }, + Next: ^IP_Adapter_Addresses, + AdapterName: cstring, + FirstUnicastAddress: ^IP_ADAPTER_UNICAST_ADDRESS_LH, + FirstAnycastAddress: ^IP_ADAPTER_ANYCAST_ADDRESS_XP, + FirstMulticastAddress: ^IP_ADAPTER_MULTICAST_ADDRESS_XP, + FirstDnsServerAddress: ^IP_ADAPTER_DNS_SERVER_ADDRESS_XP, + DnsSuffix: ^u16, + Description: ^u16, + FriendlyName: ^u16, + PhysicalAddress: [8]u8, + PhysicalAddressLength: u32, + Anonymous2: struct #raw_union { + Flags: u32, + Anonymous: struct { + _bitfield: u32, + }, + }, + MTU: u32, + IfType: u32, + OperStatus: IF_OPER_STATUS, + Ipv6IfIndex: u32, + ZoneIndices: [16]u32, + FirstPrefix: rawptr, // ^IP_ADAPTER_PREFIX_XP, + TransmitLinkSpeed: u64, + ReceiveLinkSpeed: u64, + FirstWinsServerAddress: rawptr, // ^IP_ADAPTER_WINS_SERVER_ADDRESS_LH, + FirstGatewayAddress: ^IP_ADAPTER_GATEWAY_ADDRESS_LH, + Ipv4Metric: u32, + Ipv6Metric: u32, + Luid: NET_LUID_LH, + Dhcpv4Server: SOCKET_ADDRESS, + CompartmentId: u32, + NetworkGuid: GUID, + ConnectionType: NET_IF_CONNECTION_TYPE, + TunnelType: TUNNEL_TYPE, + Dhcpv6Server: SOCKET_ADDRESS, + Dhcpv6ClientDuid: [130]u8, + Dhcpv6ClientDuidLength: u32, + Dhcpv6Iaid: u32, + FirstDnsSuffix: rawptr, // ^IP_ADAPTER_DNS_SUFFIX, +} + +IP_ADAPTER_UNICAST_ADDRESS_LH :: struct { + Anonymous: struct #raw_union { + Alignment: u64, + Anonymous: struct { + Length: u32, + Flags: u32, + }, + }, + Next: ^IP_ADAPTER_UNICAST_ADDRESS_LH, + Address: SOCKET_ADDRESS, + PrefixOrigin: NL_PREFIX_ORIGIN, + SuffixOrigin: NL_SUFFIX_ORIGIN, + DadState: NL_DAD_STATE, + ValidLifetime: u32, + PreferredLifetime: u32, + LeaseLifetime: u32, + OnLinkPrefixLength: u8, +} + +IP_ADAPTER_ANYCAST_ADDRESS_XP :: struct { + Anonymous: struct #raw_union { + Alignment: u64, + Anonymous: struct { + Length: u32, + Flags: u32, + }, + }, + Next: ^IP_ADAPTER_ANYCAST_ADDRESS_XP, + Address: SOCKET_ADDRESS, +} + +IP_ADAPTER_MULTICAST_ADDRESS_XP :: struct { + Anonymous: struct #raw_union { + Alignment: u64, + Anonymous: struct { + Length: u32, + Flags: u32, + }, + }, + Next: ^IP_ADAPTER_MULTICAST_ADDRESS_XP, + Address: SOCKET_ADDRESS, +} + +IP_ADAPTER_GATEWAY_ADDRESS_LH :: struct { + Anonymous: struct #raw_union { + Alignment: u64, + Anonymous: struct { + Length: u32, + Reserved: u32, + }, + }, + Next: ^IP_ADAPTER_GATEWAY_ADDRESS_LH, + Address: SOCKET_ADDRESS, +} + +IP_ADAPTER_DNS_SERVER_ADDRESS_XP :: struct { + Anonymous: struct #raw_union { + Alignment: u64, + Anonymous: struct { + Length: u32, + Reserved: u32, + }, + }, + Next: ^IP_ADAPTER_DNS_SERVER_ADDRESS_XP, + Address: SOCKET_ADDRESS, +} + +IF_OPER_STATUS :: enum i32 { + Up = 1, + Down = 2, + Testing = 3, + Unknown = 4, + Dormant = 5, + NotPresent = 6, + LowerLayerDown = 7, +} + +NET_LUID_LH :: struct #raw_union { + Value: u64, + Info: struct { + _bitfield: u64, + }, +} + +SOCKET_ADDRESS :: struct { + lpSockaddr: ^SOCKADDR, + iSockaddrLength: i32, +} + +NET_IF_CONNECTION_TYPE :: enum i32 { + NET_IF_CONNECTION_DEDICATED = 1, + NET_IF_CONNECTION_PASSIVE = 2, + NET_IF_CONNECTION_DEMAND = 3, + NET_IF_CONNECTION_MAXIMUM = 4, +} + +TUNNEL_TYPE :: enum i32 { + TUNNEL_TYPE_NONE = 0, + TUNNEL_TYPE_OTHER = 1, + TUNNEL_TYPE_DIRECT = 2, + TUNNEL_TYPE_6TO4 = 11, + TUNNEL_TYPE_ISATAP = 13, + TUNNEL_TYPE_TEREDO = 14, + TUNNEL_TYPE_IPHTTPS = 15, +} +NL_PREFIX_ORIGIN :: enum i32 { + IpPrefixOriginOther = 0, + IpPrefixOriginManual = 1, + IpPrefixOriginWellKnown = 2, + IpPrefixOriginDhcp = 3, + IpPrefixOriginRouterAdvertisement = 4, + IpPrefixOriginUnchanged = 16, +} + +NL_SUFFIX_ORIGIN :: enum i32 { + NlsoOther = 0, + NlsoManual = 1, + NlsoWellKnown = 2, + NlsoDhcp = 3, + NlsoLinkLayerAddress = 4, + NlsoRandom = 5, + IpSuffixOriginOther = 0, + IpSuffixOriginManual = 1, + IpSuffixOriginWellKnown = 2, + IpSuffixOriginDhcp = 3, + IpSuffixOriginLinkLayerAddress = 4, + IpSuffixOriginRandom = 5, + IpSuffixOriginUnchanged = 16, +} + +NL_DAD_STATE :: enum i32 { + NldsInvalid = 0, + NldsTentative = 1, + NldsDuplicate = 2, + NldsDeprecated = 3, + NldsPreferred = 4, + IpDadStateInvalid = 0, + IpDadStateTentative = 1, + IpDadStateDuplicate = 2, + IpDadStateDeprecated = 3, + IpDadStatePreferred = 4, +} + +@(default_calling_convention = "std") +foreign iphlpapi { + /* + The GetAdaptersAddresses function retrieves the addresses associated with the adapters on the local computer. + See: https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses + */ + @(link_name="GetAdaptersAddresses") get_adapters_addresses :: proc( + family: Address_Family, + flags: GAA_Flags, + _reserved: rawptr, + adapter_addresses: [^]IP_Adapter_Addresses, + size: ^u32, + ) -> ULONG --- + +} diff --git a/core/sys/windows/types.odin b/core/sys/windows/types.odin index 1d5bdaf04..cb2c55cd3 100644 --- a/core/sys/windows/types.odin +++ b/core/sys/windows/types.odin @@ -154,10 +154,6 @@ TIMER_QUERY_STATE :: 0x0001 TIMER_MODIFY_STATE :: 0x0002 TIMER_ALL_ACCESS :: STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | TIMER_QUERY_STATE | TIMER_MODIFY_STATE -SOCKET :: distinct uintptr // TODO -socklen_t :: c_int -ADDRESS_FAMILY :: USHORT - TRUE :: BOOL(true) FALSE :: BOOL(false) @@ -1868,30 +1864,6 @@ BI_BITFIELDS :: 3 BI_JPEG :: 4 BI_PNG :: 5 -WSA_FLAG_OVERLAPPED: DWORD : 0x01 -WSA_FLAG_NO_HANDLE_INHERIT: DWORD : 0x80 - -WSADESCRIPTION_LEN :: 256 -WSASYS_STATUS_LEN :: 128 -WSAPROTOCOL_LEN: DWORD : 255 -INVALID_SOCKET :: ~SOCKET(0) - -WSAEACCES: c_int : 10013 -WSAEINVAL: c_int : 10022 -WSAEWOULDBLOCK: c_int : 10035 -WSAEPROTOTYPE: c_int : 10041 -WSAEADDRINUSE: c_int : 10048 -WSAEADDRNOTAVAIL: c_int : 10049 -WSAECONNABORTED: c_int : 10053 -WSAECONNRESET: c_int : 10054 -WSAENOTCONN: c_int : 10057 -WSAESHUTDOWN: c_int : 10058 -WSAETIMEDOUT: c_int : 10060 -WSAECONNREFUSED: c_int : 10061 -WSATRY_AGAIN: c_int : 11002 - -MAX_PROTOCOL_CHAIN: DWORD : 7 - MAXIMUM_REPARSE_DATA_BUFFER_SIZE :: 16 * 1024 FSCTL_GET_REPARSE_POINT: DWORD : 0x900a8 IO_REPARSE_TAG_SYMLINK: DWORD : 0xa000000c @@ -1949,44 +1921,6 @@ CREATE_NEW_PROCESS_GROUP: DWORD : 0x00000200 CREATE_UNICODE_ENVIRONMENT: DWORD : 0x00000400 STARTF_USESTDHANDLES: DWORD : 0x00000100 -AF_INET: c_int : 2 -AF_INET6: c_int : 23 -SD_BOTH: c_int : 2 -SD_RECEIVE: c_int : 0 -SD_SEND: c_int : 1 -SOCK_DGRAM: c_int : 2 -SOCK_STREAM: c_int : 1 -SOL_SOCKET: c_int : 0xffff -SO_RCVTIMEO: c_int : 0x1006 -SO_SNDTIMEO: c_int : 0x1005 -SO_REUSEADDR: c_int : 0x0004 -IPPROTO_IP: c_int : 0 -IPPROTO_TCP: c_int : 6 -IPPROTO_IPV6: c_int : 41 -TCP_NODELAY: c_int : 0x0001 -IP_TTL: c_int : 4 -IPV6_V6ONLY: c_int : 27 -SO_ERROR: c_int : 0x1007 -SO_BROADCAST: c_int : 0x0020 -IP_MULTICAST_LOOP: c_int : 11 -IPV6_MULTICAST_LOOP: c_int : 11 -IP_MULTICAST_TTL: c_int : 10 -IP_ADD_MEMBERSHIP: c_int : 12 -IP_DROP_MEMBERSHIP: c_int : 13 -IPV6_ADD_MEMBERSHIP: c_int : 12 -IPV6_DROP_MEMBERSHIP: c_int : 13 -MSG_PEEK: c_int : 0x2 - -ip_mreq :: struct { - imr_multiaddr: in_addr, - imr_interface: in_addr, -} - -ipv6_mreq :: struct { - ipv6mr_multiaddr: in6_addr, - ipv6mr_interface: c_uint, -} - VOLUME_NAME_DOS: DWORD : 0x0 MOVEFILE_REPLACE_EXISTING: DWORD : 1 @@ -2369,11 +2303,6 @@ STARTUPINFO :: struct { hStdError: HANDLE, } -SOCKADDR :: struct { - sa_family: ADDRESS_FAMILY, - sa_data: [14]CHAR, -} - FILETIME :: struct { dwLowDateTime: DWORD, dwHighDateTime: DWORD, @@ -2406,74 +2335,6 @@ ADDRESS_MODE :: enum c_int { AddrModeFlat, } -SOCKADDR_STORAGE_LH :: struct { - ss_family: ADDRESS_FAMILY, - __ss_pad1: [6]CHAR, - __ss_align: i64, - __ss_pad2: [112]CHAR, -} - -ADDRINFOA :: struct { - ai_flags: c_int, - ai_family: c_int, - ai_socktype: c_int, - ai_protocol: c_int, - ai_addrlen: size_t, - ai_canonname: ^c_char, - ai_addr: ^SOCKADDR, - ai_next: ^ADDRINFOA, -} - -PADDRINFOEXW :: ^ADDRINFOEXW -LPADDRINFOEXW :: ^ADDRINFOEXW -ADDRINFOEXW :: struct { - ai_flags: c_int, - ai_family: c_int, - ai_socktype: c_int, - ai_protocol: c_int, - ai_addrlen: size_t, - ai_canonname: wstring, - ai_addr: ^sockaddr, - ai_blob: rawptr, - ai_bloblen: size_t, - ai_provider: LPGUID, - ai_next: ^ADDRINFOEXW, -} - -LPLOOKUPSERVICE_COMPLETION_ROUTINE :: #type proc "stdcall" ( - dwErrorCode: DWORD, - dwNumberOfBytesTransfered: DWORD, - lpOverlapped: LPOVERLAPPED, -) - -sockaddr :: struct { - sa_family: USHORT, - sa_data: [14]byte, -} - -sockaddr_in :: struct { - sin_family: ADDRESS_FAMILY, - sin_port: USHORT, - sin_addr: in_addr, - sin_zero: [8]CHAR, -} - -sockaddr_in6 :: struct { - sin6_family: ADDRESS_FAMILY, - sin6_port: USHORT, - sin6_flowinfo: c_ulong, - sin6_addr: in6_addr, - sin6_scope_id: c_ulong, -} - -in_addr :: struct { - s_addr: u32, -} - -in6_addr :: struct { - s6_addr: [16]u8, -} - EXCEPTION_DISPOSITION :: enum c_int { ExceptionContinueExecution, ExceptionContinueSearch, @@ -3884,3 +3745,233 @@ COORD :: struct { X: SHORT, Y: SHORT, } + +// +// Networking +// +WSA_FLAG_OVERLAPPED :: 1 +WSA_FLAG_MULTIPOINT_C_ROOT :: 2 +WSA_FLAG_MULTIPOINT_C_LEAF :: 4 +WSA_FLAG_MULTIPOINT_D_ROOT :: 8 +WSA_FLAG_MULTIPOINT_D_LEAF :: 16 +WSA_FLAG_ACCESS_SYSTEM_SECURITY :: 32 +WSA_FLAG_NO_HANDLE_INHERIT :: 128 +WSADESCRIPTION_LEN :: 256 +WSASYS_STATUS_LEN :: 128 +WSAPROTOCOL_LEN :: 255 +INVALID_SOCKET :: ~SOCKET(0) +SOMAXCONN :: 128 // The number of messages that can be queued in memory after being received; use 2-4 for Bluetooth. + // This is for the 'backlog' parameter to listen(). +SOCKET_ERROR :: -1 + +// Networking errors +WSAEINTR :: 10004 // Call interrupted. CancelBlockingCall was called. (This is different on Linux.) +WSAEACCES :: 10013 // If you try to bind a Udp socket to the broadcast address without the socket option set. +WSAEFAULT :: 10014 // A pointer that was passed to a WSA function is invalid, such as a buffer size is smaller than you said it was +WSAEINVAL :: 10022 // Invalid argument supplied +WSAEMFILE :: 10024 // SOCKET handles exhausted +WSAEWOULDBLOCK :: 10035 // No data is ready yet +WSAENOTSOCK :: 10038 // Not a socket. +WSAEINPROGRESS :: 10036 // WS1.1 call is in progress or callback function is still being processed +WSAEALREADY :: 10037 // Already connecting in parallel. +WSAEMSGSIZE :: 10040 // Message was truncated because it exceeded max datagram size. +WSAEPROTOTYPE :: 10041 // Wrong protocol for the provided socket +WSAENOPROTOOPT :: 10042 // TODO +WSAEPROTONOSUPPORT :: 10043 // Protocol not supported +WSAESOCKTNOSUPPORT :: 10044 // Socket type not supported in the given address family +WSAEAFNOSUPPORT :: 10047 // Address family not supported +WSAEOPNOTSUPP :: 10045 // Attempt to accept on non-stream socket, etc. +WSAEADDRINUSE :: 10048 // Endpoint being bound is in use by another socket. +WSAEADDRNOTAVAIL :: 10049 // Not a valid local IP address on this computer. +WSAENETDOWN :: 10050 // Network subsystem failure on the local machine. +WSAENETUNREACH :: 10051 // The local machine is not connected to the network. +WSAENETRESET :: 10052 // Keepalive failure detected, or TTL exceeded when receiving UDP packets. +WSAECONNABORTED :: 10053 // Connection has been aborted by software in the host machine. +WSAECONNRESET :: 10054 // The connection was reset while trying to accept, read or write. +WSAENOBUFS :: 10055 // No buffer space is available. The outgoing queue may be full in which case you should probably try again after a pause. +WSAEISCONN :: 10056 // The socket is already connected. +WSAENOTCONN :: 10057 // The socket is not connected yet, or no address was supplied to sendto. +WSAESHUTDOWN :: 10058 // The socket has been shutdown in the direction required. +WSAETIMEDOUT :: 10060 // The timeout duration was reached before any data was received / before all data was sent. +WSAECONNREFUSED :: 10061 // The remote machine is not listening on that endpoint. +WSAEHOSTDOWN :: 10064 // Destination host was down. +WSAEHOSTUNREACH :: 10065 // The remote machine is not connected to the network. +WSAENOTINITIALISED :: 10093 // Needs WSAStartup call +WSAEINVALIDPROCTABLE :: 10104 // Invalid or incomplete procedure table was returned +WSAEINVALIDPROVIDER :: 10105 // Service provider version is not 2.2 +WSAEPROVIDERFAILEDINIT :: 10106 // Service provider failed to initialize + +// Address families +AF_UNSPEC : c_int : 0 // Unspecified +AF_INET : c_int : 2 // IPv4 +AF_INET6 : c_int : 23 // IPv6 +AF_IRDA : c_int : 26 // Infrared +AF_BTH : c_int : 32 // Bluetooth + +// Socket types +SOCK_STREAM : c_int : 1 // TCP +SOCK_DGRAM : c_int : 2 // UDP +SOCK_RAW : c_int : 3 // Requires options IP_HDRINCL for v4, IPV6_HDRINCL for v6, on the socket +SOCK_RDM : c_int : 4 // Requires "Reliable Multicast Protocol" to be installed - see WSAEnumProtocols +SOCK_SEQPACKET : c_int : 5 // Provides psuedo-stream packet based on DGRAMs. + +// Protocols +IPPROTO_IP : c_int : 0 +IPPROTO_ICMP : c_int : 1 // (AF_UNSPEC, AF_INET, AF_INET6) + SOCK_RAW | not specified +IPPROTO_IGMP : c_int : 2 // (AF_UNSPEC, AF_INET, AF_INET6) + SOCK_RAW | not specified +BTHPROTO_RFCOMM : c_int : 3 // Bluetooth: AF_BTH + SOCK_STREAM +IPPROTO_TCP : c_int : 6 // (AF_INET, AF_INET6) + SOCK_STREAM +IPPROTO_UDP : c_int : 17 // (AF_INET, AF_INET6) + SOCK_DGRAM +IPPROTO_ICMPV6 : c_int : 58 // (AF_UNSPEC, AF_INET, AF_INET6) + SOCK_RAW +IPPROTO_RM : c_int : 113 // AF_INET + SOCK_RDM [requires "Reliable Multicast Protocol" to be installed - see WSAEnumProtocols] + +// Shutdown manners +SD_RECEIVE : c_int : 0 +SD_SEND : c_int : 1 +SD_BOTH : c_int : 2 + +// Socket 'levels' +SOL_SOCKET : c_int : 0xffff // Socket options for any socket. +IPPROTO_IPV6 : c_int : 41 // Socket options for IPV6. + +// Options for any sockets +SO_ACCEPTCONN : c_int : 0x0002 +SO_REUSEADDR : c_int : 0x0004 +SO_KEEPALIVE : c_int : 0x0008 +SO_SNDTIMEO : c_int : 0x1005 +SO_RCVTIMEO : c_int : 0x1006 +SO_EXCLUSIVEADDRUSE : c_int : ~SO_REUSEADDR +SO_CONDITIONAL_ACCEPT : c_int : 0x3002 +SO_DONTLINGER : c_int : ~SO_LINGER +SO_OOBINLINE : c_int : 0x0100 +SO_LINGER : c_int : 0x0080 +SO_RCVBUF : c_int : 0x1002 +SO_SNDBUF : c_int : 0x1001 +SO_ERROR : c_int : 0x1007 +SO_BROADCAST : c_int : 0x0020 + +TCP_NODELAY: c_int : 0x0001 +IP_TTL: c_int : 4 +IPV6_V6ONLY: c_int : 27 +IP_MULTICAST_LOOP: c_int : 11 +IPV6_MULTICAST_LOOP: c_int : 11 +IP_MULTICAST_TTL: c_int : 10 +IP_ADD_MEMBERSHIP: c_int : 12 + +IPV6_ADD_MEMBERSHIP: c_int : 12 +IPV6_DROP_MEMBERSHIP: c_int : 13 + +MAX_PROTOCOL_CHAIN: DWORD : 7 + +// Used with the SO_LINGER socket option to setsockopt(). +LINGER :: struct { + l_onoff: c.ushort, + l_linger: c.ushort, +} +// Send/Receive flags. +MSG_OOB : c_int : 1 // `send`/`recv` should process out-of-band data. +MSG_PEEK : c_int : 2 // `recv` should not remove the data from the buffer. Only valid for non-overlapped operations. + + +SOCKET :: distinct uintptr // TODO +socklen_t :: c_int +ADDRESS_FAMILY :: USHORT + +ip_mreq :: struct { + imr_multiaddr: in_addr, + imr_interface: in_addr, +} + +ipv6_mreq :: struct { + ipv6mr_multiaddr: in6_addr, + ipv6mr_interface: c_uint, +} + +SOCKADDR_STORAGE_LH :: struct { + ss_family: ADDRESS_FAMILY, + __ss_pad1: [6]CHAR, + __ss_align: i64, + __ss_pad2: [112]CHAR, +} + +ADDRINFOA :: struct { + ai_flags: c_int, + ai_family: c_int, + ai_socktype: c_int, + ai_protocol: c_int, + ai_addrlen: size_t, + ai_canonname: ^c_char, + ai_addr: ^SOCKADDR, + ai_next: ^ADDRINFOA, +} + +sockaddr_in :: struct { + sin_family: ADDRESS_FAMILY, + sin_port: u16be, + sin_addr: in_addr, + sin_zero: [8]CHAR, +} +sockaddr_in6 :: struct { + sin6_family: ADDRESS_FAMILY, + sin6_port: u16be, + sin6_flowinfo: c_ulong, + sin6_addr: in6_addr, + sin6_scope_id: c_ulong, +} + +in_addr :: struct { + s_addr: u32, +} + +in6_addr :: struct { + s6_addr: [16]u8, +} + + +DNS_STATUS :: distinct DWORD // zero is success +DNS_INFO_NO_RECORDS :: 9501 +DNS_QUERY_NO_RECURSION :: 0x00000004 + +DNS_RECORD :: struct { + pNext: ^DNS_RECORD, + pName: cstring, + wType: WORD, + wDataLength: USHORT, + Flags: DWORD, + dwTtl: DWORD, + _: DWORD, + Data: struct #raw_union { + CNAME: DNS_PTR_DATAA, + A: u32be, // Ipv4 Address + AAAA: u128be, // Ipv6 Address + TXT: DNS_TXT_DATAA, + NS: DNS_PTR_DATAA, + MX: DNS_MX_DATAA, + SRV: DNS_SRV_DATAA, + }, +} + +DNS_TXT_DATAA :: struct { + dwStringCount: DWORD, + pStringArray: cstring, +} + +DNS_PTR_DATAA :: cstring + +DNS_MX_DATAA :: struct { + pNameExchange: cstring, // the hostname + wPreference: WORD, // lower values preferred + _: WORD, // padding. +} +DNS_SRV_DATAA :: struct { + pNameTarget: cstring, + wPriority: u16, + wWeight: u16, + wPort: u16, + _: WORD, // padding +} + +SOCKADDR :: struct { + sa_family: ADDRESS_FAMILY, + sa_data: [14]CHAR, +} diff --git a/core/sys/windows/util.odin b/core/sys/windows/util.odin index 298588cb6..7f8e51d38 100644 --- a/core/sys/windows/util.odin +++ b/core/sys/windows/util.odin @@ -485,3 +485,24 @@ run_as_user :: proc(username, password, application, commandline: string, pi: ^P return false } } + +ensure_winsock_initialized :: proc() { + @static gate := false + @static initted := false + + if initted { + return + } + + for intrinsics.atomic_compare_exchange_strong(&gate, false, true) { + intrinsics.cpu_relax() + } + defer intrinsics.atomic_store(&gate, false) + + unused_info: WSADATA + version_requested := WORD(2) << 8 | 2 + res := WSAStartup(version_requested, &unused_info) + assert(res == 0, "unable to initialized Winsock2") + + initted = true +} diff --git a/core/sys/windows/ws2_32.odin b/core/sys/windows/ws2_32.odin index 09af86bce..30515d430 100644 --- a/core/sys/windows/ws2_32.odin +++ b/core/sys/windows/ws2_32.odin @@ -54,7 +54,7 @@ foreign ws2_32 { buf: rawptr, len: c_int, flags: c_int, - addr: ^SOCKADDR, + addr: ^SOCKADDR_STORAGE_LH, addrlen: ^c_int, ) -> c_int --- sendto :: proc( @@ -62,11 +62,11 @@ foreign ws2_32 { buf: rawptr, len: c_int, flags: c_int, - addr: ^SOCKADDR, + addr: ^SOCKADDR_STORAGE_LH, addrlen: c_int, ) -> c_int --- shutdown :: proc(socket: SOCKET, how: c_int) -> c_int --- - accept :: proc(socket: SOCKET, address: ^SOCKADDR, address_len: ^c_int) -> SOCKET --- + accept :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, address_len: ^c_int) -> SOCKET --- setsockopt :: proc( s: SOCKET, @@ -75,11 +75,11 @@ foreign ws2_32 { optval: rawptr, optlen: c_int, ) -> c_int --- - getsockname :: proc(socket: SOCKET, address: ^SOCKADDR, address_len: ^c_int) -> c_int --- - getpeername :: proc(socket: SOCKET, address: ^SOCKADDR, address_len: ^c_int) -> c_int --- - bind :: proc(socket: SOCKET, address: ^SOCKADDR, address_len: socklen_t) -> c_int --- + getsockname :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, address_len: ^c_int) -> c_int --- + getpeername :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, address_len: ^c_int) -> c_int --- + bind :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, address_len: socklen_t) -> c_int --- listen :: proc(socket: SOCKET, backlog: c_int) -> c_int --- - connect :: proc(socket: SOCKET, address: ^SOCKADDR, len: c_int) -> c_int --- + connect :: proc(socket: SOCKET, address: ^SOCKADDR_STORAGE_LH, len: c_int) -> c_int --- getaddrinfo :: proc( node: cstring, service: cstring, diff --git a/tests/core/Makefile b/tests/core/Makefile index 478d6ae2c..77d4b85a0 100644 --- a/tests/core/Makefile +++ b/tests/core/Makefile @@ -2,7 +2,7 @@ ODIN=../../odin PYTHON=$(shell which python3) all: download_test_assets image_test compress_test strings_test hash_test crypto_test noise_test encoding_test \ - math_test linalg_glsl_math_test filepath_test reflect_test os_exit_test i18n_test match_test c_libc_test + math_test linalg_glsl_math_test filepath_test reflect_test os_exit_test i18n_test match_test c_libc_test net_test download_test_assets: $(PYTHON) download_assets.py @@ -54,3 +54,6 @@ match_test: c_libc_test: $(ODIN) run c/libc -out:test_core_libc + +net_test: + $(ODIN) run net -out:test_core_net diff --git a/tests/core/build.bat b/tests/core/build.bat index e4e146588..5a86e5b41 100644 --- a/tests/core/build.bat +++ b/tests/core/build.bat @@ -71,6 +71,11 @@ echo Running core:text/i18n tests echo --- %PATH_TO_ODIN% run text\i18n %COMMON% -out:test_core_i18n.exe +echo --- +echo Running core:net +echo --- +%PATH_TO_ODIN% run net %COMMON% -out:test_core_net.exe + echo --- echo Running core:text/lua tests echo --- diff --git a/tests/core/net/test_core_net.odin b/tests/core/net/test_core_net.odin new file mode 100644 index 000000000..ad764c24d --- /dev/null +++ b/tests/core/net/test_core_net.odin @@ -0,0 +1,508 @@ +/* + Copyright 2021 Jeroen van Rijn . + Made available under Odin's BSD-3 license. + + List of contributors: + Jeroen van Rijn: Initial implementation. + graphitemaster: pton/ntop IANA test vectors + + A test suite for `core:net` +*/ +package test_core_net + +import "core:testing" +import "core:mem" +import "core:fmt" +import "core:net" +import "core:strconv" +import "core:time" +import "core:thread" + +_, _ :: time, thread + +TEST_count := 0 +TEST_fail := 0 + +t := &testing.T{} + +when ODIN_TEST { + expect :: testing.expect + log :: testing.log +} else { + expect :: proc(t: ^testing.T, condition: bool, message: string, loc := #caller_location) { + TEST_count += 1 + if !condition { + TEST_fail += 1 + fmt.printf("[%v] %v\n", loc, message) + return + } + } + log :: proc(t: ^testing.T, v: any, loc := #caller_location) { + fmt.printf("[%v] ", loc) + fmt.printf("log: %v\n", v) + } +} + +_tracking_allocator := mem.Tracking_Allocator{} + +print_tracking_allocator_report :: proc() { + for _, leak in _tracking_allocator.allocation_map { + fmt.printf("%v leaked %v bytes\n", leak.location, leak.size) + } + + for bf in _tracking_allocator.bad_free_array { + fmt.printf("%v allocation %p was freed badly\n", bf.location, bf.memory) + } +} + +main :: proc() { + mem.tracking_allocator_init(&_tracking_allocator, context.allocator) + context.allocator = mem.tracking_allocator(&_tracking_allocator) + + address_parsing_test(t) + + when ODIN_OS != .Windows { + fmt.printf("IMPORTANT: `core:thread` seems to still be a bit wonky on Linux and MacOS, so we can't run tests relying on them.\n", ODIN_OS) + } else { + tcp_tests(t) + } + + fmt.printf("%v/%v tests successful.\n", TEST_count - TEST_fail, TEST_count) + + print_tracking_allocator_report() +} + +@test +address_parsing_test :: proc(t: ^testing.T) { + for vector in IP_Address_Parsing_Test_Vectors { + kind := "" + switch vector.family { + case .IP4: kind = "[IPv4]" + case .IP4_Alt: kind = "[IPv4 Non-Decimal]" + case .IP6: kind = "[IPv6]" + case: panic("Add support to the test for this type.") + } + + valid := len(vector.binstr) > 0 + + fmt.printf("%v %v\n", kind, vector.input) + + msg := "-set a proper message-" + switch vector.family { + case .IP4, .IP4_Alt: + /* + Does `net.parse_ip4_address` think we parsed the address properly? + */ + non_decimal := vector.family == .IP4_Alt + + any_addr := net.parse_address(vector.input, non_decimal) + parsed_ok := any_addr != nil + parsed: net.IP4_Address + + /* + Ensure that `parse_address` doesn't parse IPv4 addresses into IPv6 addreses by mistake. + */ + switch addr in any_addr { + case net.IP4_Address: + parsed = addr + case net.IP6_Address: + parsed_ok = false + msg = fmt.tprintf("parse_address mistook %v as IPv6 address %04x", vector.input, addr) + expect(t, false, msg) + } + + if !parsed_ok && valid { + msg = fmt.tprintf("parse_ip4_address failed to parse %v, expected %v", vector.input, binstr_to_address(vector.binstr)) + + } else if parsed_ok && !valid { + msg = fmt.tprintf("parse_ip4_address parsed %v into %v, expected failure", vector.input, parsed) + } + expect(t, parsed_ok == valid, msg) + + if valid && parsed_ok { + actual_binary := address_to_binstr(parsed) + msg = fmt.tprintf("parse_ip4_address parsed %v into %v, expected %v", vector.input, actual_binary, vector.binstr) + expect(t, actual_binary == vector.binstr, msg) + + /* + Do we turn an address back into the same string properly? + No point in testing the roundtrip if the first part failed. + */ + if len(vector.output) > 0 && actual_binary == vector.binstr { + stringified := net.address_to_string(parsed) + msg = fmt.tprintf("address_to_string turned %v into %v, expected %v", parsed, stringified, vector.output) + expect(t, stringified == vector.output, msg) + } + } + + case .IP6: + /* + Do we parse the address properly? + */ + parsed, parsed_ok := net.parse_ip6_address(vector.input) + + if !parsed_ok && valid { + msg = fmt.tprintf("parse_ip6_address failed to parse %v, expected %04x", vector.input, binstr_to_address(vector.binstr)) + + } else if parsed_ok && !valid { + msg = fmt.tprintf("parse_ip6_address parsed %v into %04x, expected failure", vector.input, parsed) + } + expect(t, parsed_ok == valid, msg) + + if valid && parsed_ok { + actual_binary := address_to_binstr(parsed) + msg = fmt.tprintf("parse_ip6_address parsed %v into %v, expected %v", vector.input, actual_binary, vector.binstr) + expect(t, actual_binary == vector.binstr, msg) + + /* + Do we turn an address back into the same string properly? + No point in testing the roundtrip if the first part failed. + */ + if len(vector.output) > 0 && actual_binary == vector.binstr { + stringified := net.address_to_string(parsed) + msg = fmt.tprintf("address_to_string turned %v into %v, expected %v", parsed, stringified, vector.output) + expect(t, stringified == vector.output, msg) + } + } + } + } +} + +address_to_binstr :: proc(address: net.Address) -> (binstr: string) { + switch t in address { + case net.IP4_Address: + b := transmute(u32be)t + return fmt.tprintf("%08x", b) + case net.IP6_Address: + b := transmute(u128be)t + return fmt.tprintf("%32x", b) + case: + return "" + } + unreachable() +} + +binstr_to_address :: proc(binstr: string) -> (address: net.Address) { + switch len(binstr) { + case 8: // IPv4 + a, ok := strconv.parse_u64_of_base(binstr, 16) + expect(t, ok, "failed to parse test case bin string") + + ipv4 := u32be(a) + return net.IP4_Address(transmute([4]u8)ipv4) + + + case 32: // IPv6 + a, ok := strconv.parse_u128_of_base(binstr, 16) + expect(t, ok, "failed to parse test case bin string") + + ipv4 := u128be(a) + return net.IP6_Address(transmute([8]u16be)ipv4) + + case 0: + return nil + } + panic("Invalid test case") +} + +Kind :: enum { + IP4, // Decimal IPv4 + IP4_Alt, // Non-decimal address + IP6, // Hex IPv6 or mixed IPv4/IPv6. +} + +IP_Address_Parsing_Test_Vector :: struct { + // Give it to the IPv4 or IPv6 parser? + family: Kind, + + // Input address to try and parse. + input: string, + + /* + Hexadecimal representation of the expected numeric value of the address. + Zero length means input is invalid and the parser should report failure. + */ + binstr: string, + + // Expected `address_to_string` output, if a valid input and this string is non-empty. + output: string, +} + +IP_Address_Parsing_Test_Vectors :: []IP_Address_Parsing_Test_Vector{ + // dotted-decimal notation + { .IP4, "0.0.0.0", "00000000", "0.0.0.0" }, + { .IP4, "127.0.0.1", "7f000001", "127.0.0.1" }, + { .IP4, "10.0.128.31", "0a00801f", "10.0.128.31" }, + { .IP4, "255.255.255.255", "ffffffff", "255.255.255.255"}, + + // Odin custom: Address + port, valid + { .IP4, "0.0.0.0:80", "00000000", "0.0.0.0" }, + { .IP4, "127.0.0.1:80", "7f000001", "127.0.0.1" }, + { .IP4, "10.0.128.31:80", "0a00801f", "10.0.128.31" }, + { .IP4, "255.255.255.255:80", "ffffffff", "255.255.255.255"}, + + { .IP4, "[0.0.0.0]:80", "00000000", "0.0.0.0" }, + { .IP4, "[127.0.0.1]:80", "7f000001", "127.0.0.1" }, + { .IP4, "[10.0.128.31]:80", "0a00801f", "10.0.128.31" }, + { .IP4, "[255.255.255.255]:80", "ffffffff", "255.255.255.255"}, + + // Odin custom: Address + port, invalid + { .IP4, "[]:80", "", ""}, + { .IP4, "[0.0.0.0]", "", ""}, + { .IP4, "[127.0.0.1]:", "", ""}, + { .IP4, "[10.0.128.31] :80", "", ""}, + { .IP4, "[255.255.255.255]:65536", "", ""}, + + + // numbers-and-dots notation, but not dotted-decimal + { .IP4_Alt, "1.2.03.4", "01020304", ""}, + { .IP4_Alt, "1.2.0x33.4", "01023304", ""}, + { .IP4_Alt, "1.2.0XAB.4", "0102ab04", ""}, + { .IP4_Alt, "1.2.0xabcd", "0102abcd", ""}, + { .IP4_Alt, "1.0xabcdef", "01abcdef", ""}, + { .IP4_Alt, "0x01abcdef", "01abcdef", ""}, + { .IP4_Alt, "00377.0x0ff.65534", "fffffffe", ""}, + + // invalid as decimal address + { .IP4, "", "", ""}, + { .IP4, ".1.2.3", "", ""}, + { .IP4, "1..2.3", "", ""}, + { .IP4, "1.2.3.", "", ""}, + { .IP4, "1.2.3.4.5", "", ""}, + { .IP4, "1.2.3.a", "", ""}, + { .IP4, "1.256.2.3", "", ""}, + { .IP4, "1.2.4294967296.3", "", ""}, + { .IP4, "1.2.-4294967295.3", "", ""}, + { .IP4, "1.2. 3.4", "", ""}, + + // invalid as non-decimal address + { .IP4_Alt, "", "", ""}, + { .IP4_Alt, ".1.2.3", "", ""}, + { .IP4_Alt, "1..2.3", "", ""}, + { .IP4_Alt, "1.2.3.", "", ""}, + { .IP4_Alt, "1.2.3.4.5", "", ""}, + { .IP4_Alt, "1.2.3.a", "", ""}, + { .IP4_Alt, "1.256.2.3", "", ""}, + { .IP4_Alt, "1.2.4294967296.3", "", ""}, + { .IP4_Alt, "1.2.-4294967295.3", "", ""}, + { .IP4_Alt, "1.2. 3.4", "", ""}, + + // Valid IPv6 addresses + { .IP6, "::", "00000000000000000000000000000000", "::"}, + { .IP6, "::1", "00000000000000000000000000000001", "::1"}, + { .IP6, "::192.168.1.1", "000000000000000000000000c0a80101", "::c0a8:101"}, + { .IP6, "0000:0000:0000:0000:0000:ffff:255.255.255.255", "00000000000000000000ffffffffffff", "::ffff:ffff:ffff"}, + + { .IP6, "0:0:0:0:0:0:192.168.1.1", "000000000000000000000000c0a80101", "::c0a8:101"}, + { .IP6, "0:0::0:0:0:192.168.1.1", "000000000000000000000000c0a80101", "::c0a8:101"}, + { .IP6, "::ffff:192.168.1.1", "00000000000000000000ffffc0a80101", "::ffff:c0a8:101"}, + { .IP6, "a:0b:00c:000d:E:F::", "000a000b000c000d000e000f00000000", "a:b:c:d:e:f::"}, + { .IP6, "1:2:3:4:5:6::", "00010002000300040005000600000000", "1:2:3:4:5:6::"}, + { .IP6, "1:2:3:4:5:6:7::", "00010002000300040005000600070000", "1:2:3:4:5:6:7:0"}, + { .IP6, "::1:2:3:4:5:6", "00000000000100020003000400050006", "::1:2:3:4:5:6"}, + { .IP6, "::1:2:3:4:5:6:7", "00000001000200030004000500060007", "0:1:2:3:4:5:6:7"}, + { .IP6, "a:b::c:d:e:f", "000a000b00000000000c000d000e000f", "a:b::c:d:e:f"}, + { .IP6, "0:0:0:0:0:ffff:c0a8:5e4", "00000000000000000000ffffc0a805e4", "::ffff:c0a8:5e4"}, + { .IP6, "0::ffff:c0a8:5e4", "00000000000000000000ffffc0a805e4", "::ffff:c0a8:5e4"}, + + + // If multiple zero runs are present, shorten the longest one. + { .IP6, "1:0:0:2:0:0:0:3", "00010000000000020000000000000003", "1:0:0:2::3"}, + + // Invalid IPv6 addresses + { .IP6, "", "", ""}, + { .IP6, ":", "", ""}, + { .IP6, ":::", "", ""}, + { .IP6, "192.168.1.1", "", ""}, + { .IP6, ":192.168.1.1", "", ""}, + { .IP6, "::012.34.56.78", "", ""}, + { .IP6, ":ffff:192.168.1.1", "", ""}, + { .IP6, ".192.168.1.1", "", ""}, + { .IP6, ":.192.168.1.1", "", ""}, + { .IP6, "a:0b:00c:000d:0000e:f::", "", ""}, + { .IP6, "1:2:3:4:5:6:7:8::", "", ""}, + { .IP6, "1:2:3:4:5:6:7::9", "", ""}, + { .IP6, "::1:2:3:4:5:6:7:8", "", ""}, + { .IP6, "ffff:c0a8:5e4", "", ""}, + { .IP6, ":ffff:c0a8:5e4", "", ""}, + { .IP6, "0:0:0:0:ffff:c0a8:5e4", "", ""}, + { .IP6, "::0::ffff:c0a8:5e4", "", ""}, + { .IP6, "c0a8", "", ""}, +} + + +ENDPOINT := net.Endpoint{ + net.IP4_Address{127, 0, 0, 1}, + 9999, +} + +CONTENT := "Hellope!" + +SEND_TIMEOUT :: time.Duration(2 * time.Second) +RECV_TIMEOUT :: time.Duration(2 * time.Second) + +Thread_Data :: struct { + skt: net.Any_Socket, + err: net.Network_Error, + tid: ^thread.Thread, + + no_accept: bool, // Tell the server proc not to accept. + + data: [1024]u8, // Received data and its length + length: int, +} + +thread_data := [3]Thread_Data{} + +/* + This runs a bunch of socket tests using threads: + - two servers trying to bind the same endpoint + - client trying to connect to closed port + - client trying to connect to an open port with a non-accepting server + - client sending server data and server sending client data + - etc. +*/ +tcp_tests :: proc(t: ^testing.T) { + fmt.println("Testing two servers trying to bind to the same endpoint...") + two_servers_binding_same_endpoint(t) + fmt.println("Testing client connecting to a closed port...") + client_connects_to_closed_port(t) + fmt.println("Testing client connecting to port that doesn't accept...") + client_connects_to_open_but_non_accepting_port(t) + fmt.println("Testing client sending server data...") + client_sends_server_data(t) +} + +tcp_client :: proc(retval: rawptr) { + r := transmute(^Thread_Data)retval + + if r.skt, r.err = net.dial_tcp(ENDPOINT); r.err != nil { + return + } + defer net.close(r.skt) + + net.set_option(r.skt, .Send_Timeout, SEND_TIMEOUT) + net.set_option(r.skt, .Receive_Timeout, RECV_TIMEOUT) + + _, r.err = net.send(r.skt.(net.TCP_Socket), transmute([]u8)CONTENT) + return +} + +tcp_server :: proc(retval: rawptr) { + r := transmute(^Thread_Data)retval + + if r.skt, r.err = net.listen_tcp(ENDPOINT); r.err != nil { + return + } + defer net.close(r.skt) + + if r.no_accept { + // Don't accept any connections, just listen. + return + } + + client: net.TCP_Socket + if client, _, r.err = net.accept_tcp(r.skt.(net.TCP_Socket)); r.err != nil { + return + } + defer net.close(client) + + r.length, r.err = net.recv_tcp(client, r.data[:]) + return +} + +cleanup_thread :: proc(data: Thread_Data) { + net.close(data.skt) + + thread.terminate(data.tid, 1) + thread.destroy(data.tid) +} + +two_servers_binding_same_endpoint :: proc(t: ^testing.T) { + thread_data = {} + + thread_data[0].tid = thread.create_and_start_with_data(&thread_data[0], tcp_server, context) + thread_data[1].tid = thread.create_and_start_with_data(&thread_data[1], tcp_server, context) + + defer { + cleanup_thread(thread_data[0]) + cleanup_thread(thread_data[1]) + } + + // Give the two servers enough time to try and bind the same endpoint + time.sleep(1 * time.Second) + + first_won := thread_data[0].err == nil && thread_data[1].err == net.Bind_Error.Address_In_Use + second_won := thread_data[1].err == nil && thread_data[0].err == net.Bind_Error.Address_In_Use + + okay := first_won || second_won + msg := fmt.tprintf("Expected servers to return `nil` and `Address_In_Use`, got %v and %v", thread_data[0].err, thread_data[1].err) + expect(t, okay, msg) +} + +client_connects_to_closed_port :: proc(t: ^testing.T) { + thread_data = {} + + thread_data[0].tid = thread.create_and_start_with_data(&thread_data[0], tcp_client, context) + + defer { + cleanup_thread(thread_data[0]) + } + + // Give the socket enough time to return `Refused` + time.sleep(4 * time.Second) + + okay := thread_data[0].err == net.Dial_Error.Refused + msg := fmt.tprintf("Expected client to return `Refused` connecting to closed port, got %v", thread_data[0].err) + expect(t, okay, msg) +} + +client_connects_to_open_but_non_accepting_port :: proc(t: ^testing.T) { + thread_data = {} + + // Tell server proc not to accept + thread_data[0].no_accept = true + + thread_data[0].tid = thread.create_and_start_with_data(&thread_data[0], tcp_server, context) + thread_data[1].tid = thread.create_and_start_with_data(&thread_data[1], tcp_client, context) + + defer { + cleanup_thread(thread_data[0]) + cleanup_thread(thread_data[1]) + } + + // Give the two servers enough time to try and bind the same endpoint + time.sleep(4 * time.Second) + + okay := thread_data[0].err == nil && thread_data[1].err == net.Dial_Error.Refused + msg := fmt.tprintf("Expected server and client to return `nil` and `Refused`, got %v and %v", thread_data[0].err, thread_data[1].err) + expect(t, okay, msg) +} + +client_sends_server_data :: proc(t: ^testing.T) { + thread_data = {} + + // Tell server proc not to accept + // thread_data[0].no_accept = true + + thread_data[0].tid = thread.create_and_start_with_data(&thread_data[0], tcp_server, context) + thread_data[1].tid = thread.create_and_start_with_data(&thread_data[1], tcp_client, context) + + defer { + cleanup_thread(thread_data[0]) + cleanup_thread(thread_data[1]) + } + + // Give the two servers enough time to try and bind the same endpoint + time.sleep(1 * time.Second) + + okay := thread_data[0].err == nil && thread_data[1].err == nil + msg := fmt.tprintf("Expected client and server to return `nil`, got %v and %v", thread_data[0].err, thread_data[1].err) + expect(t, okay, msg) + + received := string(thread_data[0].data[:thread_data[0].length]) + + okay = received == CONTENT + msg = fmt.tprintf("Expected client to send \"{}\", got \"{}\"", CONTENT, received) + expect(t, okay, msg) +} \ No newline at end of file