diff options
| author | Colin Davidson <colrdavidson@gmail.com> | 2023-03-01 07:58:30 -0800 |
|---|---|---|
| committer | Colin Davidson <colrdavidson@gmail.com> | 2023-03-01 07:58:30 -0800 |
| commit | 28f7f572473c4e97ccd6133bb4f5fa6f45505530 (patch) | |
| tree | 0bd159c24c617df409f72b0ec75daa086372e94d /core/net | |
| parent | 3567c006e6683d989805c078db48a95a901d9e72 (diff) | |
manually start merging core_net
Diffstat (limited to 'core/net')
| -rw-r--r-- | core/net/addr.odin | 818 | ||||
| -rw-r--r-- | core/net/addr_darwin.odin | 71 | ||||
| -rw-r--r-- | core/net/addr_linux.odin | 93 | ||||
| -rw-r--r-- | core/net/addr_openbsd.odin | 69 | ||||
| -rw-r--r-- | core/net/addr_windows.odin | 69 | ||||
| -rw-r--r-- | core/net/common.odin | 437 | ||||
| -rw-r--r-- | core/net/dns.odin | 873 | ||||
| -rw-r--r-- | core/net/dns_unix.odin | 83 | ||||
| -rw-r--r-- | core/net/dns_windows.odin | 166 | ||||
| -rw-r--r-- | core/net/doc.odin | 47 | ||||
| -rw-r--r-- | core/net/interface.odin | 68 | ||||
| -rw-r--r-- | core/net/interface_darwin.odin | 23 | ||||
| -rw-r--r-- | core/net/interface_linux.odin | 147 | ||||
| -rw-r--r-- | core/net/interface_windows.odin | 182 | ||||
| -rw-r--r-- | core/net/socket.odin | 87 | ||||
| -rw-r--r-- | core/net/socket_darwin.odin | 513 | ||||
| -rw-r--r-- | core/net/socket_linux.odin | 532 | ||||
| -rw-r--r-- | core/net/socket_openbsd.odin | 515 | ||||
| -rw-r--r-- | core/net/socket_windows.odin | 577 | ||||
| -rw-r--r-- | core/net/url.odin | 235 |
20 files changed, 5605 insertions, 0 deletions
diff --git a/core/net/addr.odin b/core/net/addr.odin new file mode 100644 index 000000000..80ba91110 --- /dev/null +++ b/core/net/addr.odin @@ -0,0 +1,818 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, IPv4 + IPv6 parsers, documentation. +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:strconv" +import "core:strings" +import "core:fmt" + +/* + Expects an IPv4 address with no leading or trailing whitespace: + - a.b.c.d + - a.b.c.d:port + - [a.b.c.d]:port + + If the IP address is bracketed, the port must be present and valid (though it will be ignored): + - [a.b.c.d] will be treated as a parsing failure. + + The port, if present, is required to be a base 10 number in the range 0-65535, inclusive. + + If `non_decimal_address` is true, `aton` is told each component must be decimal and max 255. +*/ +parse_ip4_address :: proc(address_and_maybe_port: string, non_decimal_address := false) -> (addr: IP4_Address, ok: bool) { + res, res_ok := aton(address_and_maybe_port, .IP4, !non_decimal_address) + if ip4, ip4_ok := res.(IP4_Address); ip4_ok { + return ip4, res_ok + } + return {}, false +} + +/* + Parses an IP address in "non-decimal" `inet_aton` form. + + e.g."00377.0x0ff.65534" = 255.255.255.254 + 00377 = 255 in octal + 0x0ff = 255 in hexadecimal + This leaves 16 bits worth of address + .65534 then accounts for the last two digits + + For the address part the allowed forms are: + a.b.c.d - where each part represents a byte + a.b.c - where `a` & `b` represent a byte and `c` a u16 + a.b - where `a` represents a byte and `b` supplies the trailing 24 bits + a - where `a` gives the entire 32-bit value + + The port, if present, is required to be a base 10 number in the range 0-65535, inclusive. +*/ +aton :: proc(address_and_maybe_port: string, family: Address_Family, is_decimal_only := false) -> (addr: Address, ok: bool) { + switch family { + case .IP4: + /* + There is no valid address shorter than `0.0.0.0`. + */ + if len(address_and_maybe_port) < 7 { + return {}, false + } + + address, _ := split_port(address_and_maybe_port) or_return // This call doesn't allocate + + buf: [4]u64 = {} + i := 0 + + max_value := u64(max(u32)) + bases := DEFAULT_DIGIT_BASES + + if is_decimal_only { + max_value = 255 + bases = {.Dec} + } + + for len(address) > 0 { + if i == 4 { + return {}, false + } + + /* + Decimal-only addresses may not have a leading zero. + */ + if is_decimal_only && len(address) > 1 && address[0] == '0' && address[1] != '.' { + return + } + + number, consumed, number_ok := parse_ip_component(address, max_value, bases) + if !number_ok || consumed == 0 { + return {}, false + } + + buf[i] = number + + address = address[consumed:] + + if len(address) > 0 && address[0] == '.' { + address = address[1:] + } + i += 1 + } + + /* + Distribute parts. + */ + switch i { + case 1: + buf[1] = buf[0] & 0xffffff + buf[0] >>= 24 + fallthrough + case 2: + buf[2] = buf[1] & 0xffff + buf[1] >>= 16 + fallthrough + case 3: + buf[3] = buf[2] & 0xff + buf[2] >>= 8 + } + + a: [4]u8 = --- + for v, i in buf { + if v > 255 { return {}, false } + a[i] = u8(v) + } + return IP4_Address(a), true + + case .IP6: + return parse_ip6_address(address_and_maybe_port) + + case: + return nil, false + } +} + +/* + The minimum length of a valid IPv6 address string is 2, e.g. `::` + + The maximum length of a valid IPv6 address string is 45, when it embeds an IPv4, + e.g. `0000:0000:0000:0000:0000:ffff:255.255.255.255` + + An IPv6 address must contain at least 3 pieces, e.g. `::`, + and at most 9 (using `::` for a trailing or leading 0) +*/ +IPv6_MIN_STRING_LENGTH :: 2 +IPv6_MAX_STRING_LENGTH :: 45 +IPv6_MIN_COLONS :: 2 +IPv6_PIECE_COUNT :: 8 + +parse_ip6_address :: proc(address_and_maybe_port: string) -> (addr: IP6_Address, ok: bool) { + /* + If we have an IPv6 address of the form [IP]:Port, first get us just the IP. + */ + address, _ := split_port(address_and_maybe_port) or_return + + /* + Early bailouts based on length and number of pieces. + */ + if len(address) < IPv6_MIN_STRING_LENGTH || len(address) > IPv6_MAX_STRING_LENGTH { return } + + /* + Do a pre-pass on the string that checks how many `:` and `.` we have, + if they're in the right order, and if the things between them are digits as expected. + + It's not strictly necessary considering we could use `strings.split`, + but this way we can avoid using an allocator and return earlier on bogus input. Win-win. + */ + colon_count := 0 + dot_count := 0 + + pieces_temp: [IPv6_PIECE_COUNT + 1]string + + piece_start := 0 + piece_end := 0 + + for ch, i in address { + switch ch { + case '0'..'9', 'a'..'f', 'A'..'F': + piece_end += 1 + + case ':': + /* + If we see a `:` after a `.`, it means an IPv4 part was sandwiched between IPv6, + instead of it being the tail: invalid. + */ + if dot_count > 0 { return } + + pieces_temp[colon_count] = address[piece_start:piece_end] + + colon_count += 1 + if colon_count > IPv6_PIECE_COUNT { return } + + /* + If there's anything left, put it in the next piece. + */ + piece_start = i + 1 + piece_end = piece_start + + case '.': + /* + IPv4 address is treated as one piece. No need to update `piece_*`. + */ + dot_count += 1 + + + case: // Invalid character, return early + return + } + } + + if colon_count < IPv6_MIN_COLONS { return } + + /* + Assign the last piece string. + */ + pieces_temp[colon_count] = address[piece_start:] + + /* + `pieces` now holds the same output as it would if had used `strings.split`. + */ + pieces := pieces_temp[:colon_count + 1] + + /* + Check if we have what looks like an embedded IPv4 address. + */ + ipv4: IP4_Address + have_ipv4: bool + + if dot_count > 0 { + /* + If we have an IPv4 address accounting for the last 32 bits, + this means we can have at most 6 IPv6 pieces, like so: `x:x:X:x:x:x:d.d.d.d` + + Or, put differently: 6 pieces IPv6 (5 colons), a colon, 1 piece IPv4 (3 dots), + for a total of 6 colons and 3 dots. + */ + if dot_count != 3 || colon_count > 6 { return } + + /* + Try to parse IPv4 address. + If successful, we have our least significant 32 bits. + If not, it invalidates the whole address and we can bail. + */ + ipv4, have_ipv4 = parse_ip4_address(pieces_temp[colon_count]) + if !have_ipv4 { return } + } + + /* + Check for `::` being used more than once, and save the skip. + */ + zero_skip := -1 + for i in 1..<len(pieces) - 1 { + if pieces[i] == "" { + /* + Return if skip has already been set. + */ + if zero_skip != -1 { + return + } + + zero_skip = i + } + } + + /* + Now check if we have the necessary number pieces, accounting for any `::`, + and how many were skipped by it if applicable. + */ + before_skip := 0 + after_skip := 0 + num_skipped := 0 + + if zero_skip != -1 { + before_skip = zero_skip + after_skip = len(pieces) - zero_skip - 1 + + /* + An IPv4 "piece" accounts for 2 IPv6 pieces we haven't added to the pieces slice, so add 1. + */ + if have_ipv4 { + after_skip += 1 + } + + /* + Adjust for leading `::`. + */ + if pieces[0] == "" { + before_skip -= 1 + // Leading `:` can only be part of `::`. + if before_skip > 0 { return } + } + + /* + Adjust for trailing `::`. + */ + if pieces[len(pieces) - 1] == "" { + after_skip -= 1 + // Trailing `:` can only be part of `::`. + if after_skip > 0 { return } + } + + /* + Calculate how many zero pieces we skipped. + It should be at least one, considering we encountered a `::`. + */ + num_skipped = IPv6_PIECE_COUNT - before_skip - after_skip + if num_skipped < 1 { return } + + } else { + /* + No zero skip means everything is part of "before the skip". + An IPv4 "piece" accounts for 2 IPv6 pieces we haven't added to the pieces slice, so add 1. + */ + piece_count := len(pieces) + if have_ipv4 { + piece_count += 1 + } + + /* + Do we have the complete set? + */ + if piece_count != IPv6_PIECE_COUNT { return } + + /* + Validate leading and trailing empty parts, as they can only be part of a `::`. + */ + if pieces[0] == "" || pieces[len(pieces) - 1] == "" { return } + + + before_skip = piece_count + after_skip = 0 + num_skipped = 0 + } + + /* + Now try to parse the pieces into a 8 16-bit pieces. + */ + piece_values: [IPv6_PIECE_COUNT]u16be + + idx := 0 + val_idx := 0 + + for _ in 0..<before_skip { + /* + An empty piece is the default zero. Otherwise, try to parse as an IPv6 hex piece. + If we have an IPv4 address, stop on the penultimate index. + */ + if have_ipv4 && val_idx == 6 { + break + } + + piece := pieces[idx] + + /* + An IPv6 piece can at most contain 4 hex digits. + */ + if len(piece) > 4 { return } + + if piece != "" { + val, _ := parse_ip_component(piece, 65535, {.IPv6}) or_return + piece_values[val_idx] = u16be(val) + } + + idx += 1 + val_idx += 1 + } + + if before_skip == 0 { + idx += 1 + } + + if num_skipped > 0 { + idx += 1 + val_idx += num_skipped + } + + if after_skip > 0 { + for _ in 0..<after_skip { + /* + An empty piece is the default zero. Otherwise, try to parse as an IPv6 hex piece. + If we have an IPv4 address, stop on the penultimate index. + */ + if have_ipv4 && val_idx == 6 { + break + } + + piece := pieces[idx] + + /* + An IPv6 piece can at most contain 4 hex digits. + */ + if len(piece) > 4 { return } + + if piece != "" { + val, _ := parse_ip_component(piece, 65535, {.IPv6}) or_return + piece_values[val_idx] = u16be(val) + } + + idx += 1 + val_idx += 1 + } + } + + /* + Distribute IPv4 address into last two pieces, if applicable. + */ + if have_ipv4 { + val := u16(ipv4[0]) << 8 + val |= u16(ipv4[1]) + piece_values[6] = u16be(val) + + + val = u16(ipv4[2]) << 8 + val |= u16(ipv4[3]) + piece_values[7] = u16be(val) + } + + return transmute(IP6_Address)piece_values, true +} + +/* + Try parsing as an IPv6 address. + If it's determined not to be, try as an IPv4 address, optionally in non-decimal format. +*/ +parse_address :: proc(address_and_maybe_port: string, non_decimal_address := false) -> Address { + addr6, ok6 := parse_ip6_address(address_and_maybe_port) + if ok6 do return addr6 + addr4, ok4 := parse_ip4_address(address_and_maybe_port, non_decimal_address) + if ok4 do return addr4 + return nil +} + +parse_endpoint :: proc(endpoint_str: string) -> (ep: Endpoint, ok: bool) { + addr_str, port, split_ok := split_port(endpoint_str) + if !split_ok do return + + addr := parse_address(addr_str) + if addr == nil do return + + ep = Endpoint { address = addr, port = port } + ok = true + return +} + +Host :: struct { + hostname: string, + port: int, +} +Host_Or_Endpoint :: union { + Host, + Endpoint, +} +Parse_Endpoint_Error :: enum { + Bad_Port = 1, + Bad_Address, + Bad_Hostname, +} + +// Takes a string consisting of a hostname or IP address, and an optional port, +// and return the component parts in a useful form. +parse_hostname_or_endpoint :: proc(endpoint_str: string) -> (target: Host_Or_Endpoint, err: Network_Error) { + host, port, port_ok := split_port(endpoint_str) + if !port_ok { + return nil, .Bad_Port + } + if addr := parse_address(host); addr != nil { + return Endpoint{addr, port}, nil + } + if !validate_hostname(host) { + return nil, .Bad_Hostname + } + return Host{host, port}, nil +} + + +// Takes an endpoint string and returns its parts. +// Returns ok=false if port is not a number. +split_port :: proc(endpoint_str: string) -> (addr_or_host: string, port: int, ok: bool) { + // IP6 [addr_or_host]:port + if i := strings.last_index(endpoint_str, "]:"); i != -1 { + addr_or_host = endpoint_str[1:i] + port, ok = strconv.parse_int(endpoint_str[i+2:], 10) + + if port > 65535 { + ok = false + } + return + } + + if n := strings.count(endpoint_str, ":"); n == 1 { + // IP4 addr_or_host:port + i := strings.last_index(endpoint_str, ":") + assert(i != -1) + + addr_or_host = endpoint_str[:i] + port, ok = strconv.parse_int(endpoint_str[i+1:], 10) + + if port > 65535 { + ok = false + } + return + } else if n > 1 { + // IP6 address without port + } + + // No port + addr_or_host = endpoint_str + port = 0 + ok = true + return +} + +// Joins an address or hostname with a port. +join_port :: proc(address_or_host: string, port: int, allocator := context.allocator) -> string { + addr_or_host, _, ok := split_port(address_or_host) + if !ok do return addr_or_host + + b := strings.make_builder(allocator) + + addr := parse_address(addr_or_host) + if addr == nil { + // hostname + fmt.sbprintf(&b, "%v:%v", addr_or_host, port) + } else { + switch in addr { + case IP4_Address: + fmt.sbprintf(&b, "%v:%v", address_to_string(addr), port) + case IP6_Address: + fmt.sbprintf(&b, "[%v]:%v", address_to_string(addr), port) + } + } + return strings.to_string(b) +} + + + +// TODO(tetra): Do we need this? +map_to_ip6 :: proc(addr: Address) -> Address { + if addr6, ok := addr.(IP6_Address); ok { + return addr6 + } + addr4 := addr.(IP4_Address) + addr4_u16 := transmute([2]u16be) addr4 + addr6: IP6_Address + addr6[4] = 0xffff + copy(addr6[5:], addr4_u16[:]) + return addr6 +} + +/* + Returns a temporarily-allocated string representation of the address. + + See RFC 5952 section 4 for IPv6 representation recommendations. +*/ +address_to_string :: proc(addr: Address, allocator := context.temp_allocator) -> string { + b := strings.make_builder(allocator) + switch v in addr { + case IP4_Address: + fmt.sbprintf(&b, "%v.%v.%v.%v", v[0], v[1], v[2], v[3]) + case IP6_Address: + /* + First find the longest run of zeroes. + */ + Zero_Run :: struct { + start: int, + end: int, + } + + /* + We're dealing with 0-based indices, appropriately enough for runs of zeroes. + Still, it means we need to initialize runs with some value outside of the possible range. + */ + run := Zero_Run{-1, -1} + best := Zero_Run{-1, -1} + + addr := transmute([8]u16be)v + + last := u16be(1) + for val, i in addr { + /* + If we encounter adjacent zeroes, then start a new run if not already in one. + Also remember the rightmost index regardless, because it'll be the new + frontier of both new and existing runs. + */ + if last == 0 && val == 0 { + run.end = i + if run.start == -1 { + run.start = i - 1 + } + } + + /* + If we're in a run check if its length is better than the best recorded so far. + If so, update the best run's start and end. + */ + if run.start != -1 { + length_to_beat := best.end - best.start + length := run.end - run.start + + if length > length_to_beat { + best = run + } + } + + /* + If we were in a run, this is where we reset it. + */ + if val != 0 { + run = {-1, -1} + } + + last = val + } + + for val, i in addr { + if best.start == i || best.end == i { + /* + For the left and right side of the best zero run, print a `:`. + */ + fmt.sbprint(&b, ":") + } else if i < best.start { + /* + If we haven't made it to the best run yet, print the digit. + Make sure we only print a `:` after the digit if it's not + immediately followed by the run's own leftmost `:`. + */ + fmt.sbprintf(&b, "%x", val) + if i < best.start - 1 { + fmt.sbprintf(&b, ":") + } + } else if i > best.end { + /* + If there are any digits after the zero run, print them. + But don't print the `:` at the end of the IP number. + */ + fmt.sbprintf(&b, "%x", val) + if i != 7 { + fmt.sbprintf(&b, ":") + } + } + } + } + return strings.to_string(b) +} + +// Returns a temporarily-allocated string representation of the endpoint. +// If there's a port, uses the `[address]:port` format. +endpoint_to_string :: proc(ep: Endpoint, allocator := context.temp_allocator) -> string { + if ep.port == 0 { + return address_to_string(ep.address, allocator) + } else { + s := address_to_string(ep.address, context.temp_allocator) + b := strings.make_builder(allocator) + switch a in ep.address { + case IP4_Address: fmt.sbprintf(&b, "%v:%v", s, ep.port) + case IP6_Address: fmt.sbprintf(&b, "[%v]:%v", s, ep.port) + } + return strings.to_string(b) + } +} + +to_string :: proc{address_to_string, endpoint_to_string} + + +family_from_address :: proc(addr: Address) -> Address_Family { + switch in addr { + case IP4_Address: return .IP4 + case IP6_Address: return .IP6 + case: + unreachable() + } +} +family_from_endpoint :: proc(ep: Endpoint) -> Address_Family { + return family_from_address(ep.address) +} + + +Digit_Parse_Base :: enum u8 { + Dec = 0, // No prefix + Oct = 1, // Leading zero + Hex = 2, // 0x prefix + IPv6 = 3, // Unprefixed IPv6 piece hex. Can't be used with other bases. +} +Digit_Parse_Bases :: bit_set[Digit_Parse_Base; u8] +DEFAULT_DIGIT_BASES :: Digit_Parse_Bases{.Dec, .Oct, .Hex} + +/* + Parses a single unsigned number in requested `bases` from `input`. + `max_value` represents the maximum allowed value for this number. + + Returns the `value`, the `bytes_consumed` so far, and `ok` to signal success or failure. + + An out-of-range or invalid number will return the accumulated value so far (which can be out of range), + the number of bytes consumed leading up the error, and `ok = false`. + + When `.` or `:` are encountered, they'll be considered valid separators and will stop parsing, + returning the valid number leading up to it. + + Other non-digit characters are treated as an error. + + Octal numbers are expected to have a leading zero, with no 'o' format specifier. + Hexadecimal numbers are expected to be preceded by '0x' or '0X'. + Numbers will otherwise be considered to be in base 10. +*/ +parse_ip_component :: proc(input: string, max_value := u64(max(u32)), bases := DEFAULT_DIGIT_BASES) -> (value: u64, bytes_consumed: int, ok: bool) { + /* + Default to base 10 + */ + base := u64(10) + input := input + + /* + We keep track of the number of prefix bytes and digit bytes separately. + This way if a prefix is consumed and we encounter a separator or the end of the string, + the number is only considered valid if at least 1 digit byte has been consumed and the value is within range. + */ + prefix_bytes := 0 + digit_bytes := 0 + + /* + IPv6 hex bytes are unprefixed and can't be disambiguated from octal or hex unless the digit is out of range. + If we got the `.IPv6` option, skip prefix scanning and other flags aren't also used. + */ + if .IPv6 in bases { + if bases != {.IPv6} { return } // Must be used on its own. + base = 16 + } else { + /* + Scan for and consume prefix, if applicable. + */ + if len(input) >= 2 && input[0] == '0' { + if .Hex in bases && (input[1] == 'x' || input[1] == 'X') { + base = 16 + input = input[2:] + prefix_bytes = 2 + } + if prefix_bytes == 0 && .Oct in bases { + base = 8 + input = input[1:] + prefix_bytes = 1 + } + } + } + + parse_loop: for ch in input { + switch ch { + case '0'..'7': + digit_bytes += 1 + value = value * base + u64(ch - '0') + + case '8'..'9': + digit_bytes += 1 + + if base == 8 { + /* + Out of range for octal numbers. + */ + return value, digit_bytes + prefix_bytes, false + } + value = value * base + u64(ch - '0') + + case 'a'..'f': + digit_bytes += 1 + + if base == 8 || base == 10 { + /* + Out of range for octal and decimal numbers. + */ + return value, digit_bytes + prefix_bytes, false + } + value = value * base + (u64(ch - 'a') + 10) + + case 'A'..'F': + digit_bytes += 1 + + if base == 8 || base == 10 { + /* + Out of range for octal and decimal numbers. + */ + return value, digit_bytes + prefix_bytes, false + } + value = value * base + (u64(ch - 'A') + 10) + + case '.', ':': + /* + Number separator. Return early. + We don't need to check if the number is in range. + We do that each time through the loop. + */ + break parse_loop + + case: + /* + Invalid character encountered. + */ + return value, digit_bytes + prefix_bytes, false + } + + if value > max_value { + /* + Out-of-range number. + */ + return value, digit_bytes + prefix_bytes, false + } + } + + /* + If we consumed at least 1 digit byte, `value` *should* continue a valid number in an appropriate base in the allowable range. + */ + return value, digit_bytes + prefix_bytes, digit_bytes >= 1 +}
\ No newline at end of file diff --git a/core/net/addr_darwin.odin b/core/net/addr_darwin.odin new file mode 100644 index 000000000..801b89a96 --- /dev/null +++ b/core/net/addr_darwin.odin @@ -0,0 +1,71 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:os" + +// Returns an address for each interface that can be bound to. +get_network_interfaces :: proc() -> []Address { + // TODO + return nil +} + +@private +endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: os.SOCKADDR_STORAGE_LH) { + switch a in ep.address { + case IP4_Address: + (^os.sockaddr_in)(&sockaddr)^ = os.sockaddr_in { + sin_port = u16be(ep.port), + sin_addr = transmute(os.in_addr) a, + sin_family = u8(os.AF_INET), + sin_len = size_of(os.sockaddr_in), + } + return + case IP6_Address: + (^os.sockaddr_in6)(&sockaddr)^ = os.sockaddr_in6 { + sin6_port = u16be(ep.port), + sin6_addr = transmute(os.in6_addr) a, + sin6_family = u8(os.AF_INET6), + sin6_len = size_of(os.sockaddr_in6), + } + return + } + unreachable() +} + +@private +sockaddr_to_endpoint :: proc(native_addr: ^os.SOCKADDR_STORAGE_LH) -> (ep: Endpoint) { + switch native_addr.family { + case u8(os.AF_INET): + addr := cast(^os.sockaddr_in) native_addr + port := int(addr.sin_port) + ep = Endpoint { + address = IP4_Address(transmute([4]byte) addr.sin_addr), + port = port, + } + case u8(os.AF_INET6): + addr := cast(^os.sockaddr_in6) native_addr + port := int(addr.sin6_port) + ep = Endpoint { + address = IP6_Address(transmute([8]u16be) addr.sin6_addr), + port = port, + } + case: + panic("native_addr is neither IP4 or IP6 address") + } + return +} diff --git a/core/net/addr_linux.odin b/core/net/addr_linux.odin new file mode 100644 index 000000000..90ce0c9ef --- /dev/null +++ b/core/net/addr_linux.odin @@ -0,0 +1,93 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:os" + +// Returns an address for each interface that can be bound to. +get_network_interfaces :: proc() -> []Address { + // TODO + return nil +} + +@private +endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: os.SOCKADDR_STORAGE_LH) { + switch a in ep.address { + case IP4_Address: + (^os.sockaddr_in)(&sockaddr)^ = os.sockaddr_in { + sin_port = u16be(ep.port), + sin_addr = transmute(os.in_addr) a, + sin_family = u16(os.AF_INET), + } + return + case IP6_Address: + (^os.sockaddr_in6)(&sockaddr)^ = os.sockaddr_in6 { + sin6_port = u16be(ep.port), + sin6_addr = transmute(os.in6_addr) a, + sin6_family = u16(os.AF_INET6), + } + return + } + unreachable() +} + +sockaddr_storage_to_endpoint :: proc(native_addr: ^os.SOCKADDR_STORAGE_LH) -> (ep: Endpoint) { + switch native_addr.ss_family { + case u16(os.AF_INET): + addr := cast(^os.sockaddr_in) native_addr + port := int(addr.sin_port) + ep = Endpoint { + address = IP4_Address(transmute([4]byte) addr.sin_addr), + port = port, + } + case u16(os.AF_INET6): + addr := cast(^os.sockaddr_in6) native_addr + port := int(addr.sin6_port) + ep = Endpoint { + address = IP6_Address(transmute([8]u16be) addr.sin6_addr), + port = port, + } + case: + panic("native_addr is neither IP4 or IP6 address") + } + return +} + +sockaddr_basic_to_endpoint :: proc(native_addr: ^os.SOCKADDR) -> (ep: Endpoint) { + switch native_addr.sa_family { + case u16(os.AF_INET): + addr := cast(^os.sockaddr_in) native_addr + port := int(addr.sin_port) + ep = Endpoint { + address = IP4_Address(transmute([4]byte) addr.sin_addr), + port = port, + } + case u16(os.AF_INET6): + addr := cast(^os.sockaddr_in6) native_addr + port := int(addr.sin6_port) + ep = Endpoint { + address = IP6_Address(transmute([8]u16be) addr.sin6_addr), + port = port, + } + case: + //panic("native_addr is neither IP4 or IP6 address") + return {} + } + return +} + +sockaddr_to_endpoint :: proc { sockaddr_basic_to_endpoint, sockaddr_storage_to_endpoint }
\ No newline at end of file diff --git a/core/net/addr_openbsd.odin b/core/net/addr_openbsd.odin new file mode 100644 index 000000000..34a49b526 --- /dev/null +++ b/core/net/addr_openbsd.odin @@ -0,0 +1,69 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:os" + +// Returns an address for each interface that can be bound to. +get_network_interfaces :: proc() -> []Address { + // TODO + return nil +} + +@private +endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: os.SOCKADDR_STORAGE_LH) { + switch a in ep.address { + case IP4_Address: + (^os.sockaddr_in)(&sockaddr)^ = os.sockaddr_in { + sin_port = u16be(ep.port), + sin_addr = transmute(os.in_addr) a, + sin_family = u8(os.AF_INET), + } + return + case IP6_Address: + (^os.sockaddr_in6)(&sockaddr)^ = os.sockaddr_in6 { + sin6_port = u16be(ep.port), + sin6_addr = transmute(os.in6_addr) a, + sin6_family = u8(os.AF_INET6), + } + return + } + unreachable() +} + +@private +sockaddr_to_endpoint :: proc(native_addr: ^os.SOCKADDR_STORAGE_LH) -> (ep: Endpoint) { + switch native_addr.ss_family { + case u8(os.AF_INET): + addr := cast(^os.sockaddr_in)native_addr + port := int(addr.sin_port) + ep = Endpoint { + address = IP4_Address(transmute([4]byte) addr.sin_addr), + port = port, + } + case u8(os.AF_INET6): + addr := cast(^os.sockaddr_in6)native_addr + port := int(addr.sin6_port) + ep = Endpoint { + address = IP6_Address(transmute([8]u16be) addr.sin6_addr), + port = port, + } + case: + panic("native_addr is neither IP4 or IP6 address") + } + return +}
\ No newline at end of file diff --git a/core/net/addr_windows.odin b/core/net/addr_windows.odin new file mode 100644 index 000000000..4225216ca --- /dev/null +++ b/core/net/addr_windows.odin @@ -0,0 +1,69 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import win "core:sys/windows" + +// Returns an address for each interface that can be bound to. +get_network_interfaces :: proc() -> []Address { + // TODO + return nil +} + +@private +endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: win.SOCKADDR_STORAGE_LH) { + switch a in ep.address { + case IP4_Address: + (^win.sockaddr_in)(&sockaddr)^ = win.sockaddr_in { + sin_port = u16be(win.USHORT(ep.port)), + sin_addr = transmute(win.in_addr) a, + sin_family = u16(win.AF_INET), + } + return + case IP6_Address: + (^win.sockaddr_in6)(&sockaddr)^ = win.sockaddr_in6 { + sin6_port = u16be(win.USHORT(ep.port)), + sin6_addr = transmute(win.in6_addr) a, + sin6_family = u16(win.AF_INET6), + } + return + } + unreachable() +} + +@private +sockaddr_to_endpoint :: proc(native_addr: ^win.SOCKADDR_STORAGE_LH) -> (ep: Endpoint) { + switch native_addr.ss_family { + case u16(win.AF_INET): + addr := cast(^win.sockaddr_in) native_addr + port := int(addr.sin_port) + ep = Endpoint { + address = IP4_Address(transmute([4]byte) addr.sin_addr), + port = port, + } + case u16(win.AF_INET6): + addr := cast(^win.sockaddr_in6) native_addr + port := int(addr.sin6_port) + ep = Endpoint { + address = IP6_Address(transmute([8]u16be) addr.sin6_addr), + port = port, + } + case: + panic("native_addr is neither IP4 or IP6 address") + } + return +}
\ No newline at end of file diff --git a/core/net/common.odin b/core/net/common.odin new file mode 100644 index 000000000..9e980e88e --- /dev/null +++ b/core/net/common.odin @@ -0,0 +1,437 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. + + This file collects structs, enums and settings applicable to the entire package in one handy place. + Platform-specific ones can be found in their respective `*_windows.odin` and similar files. +*/ +package net + +import "core:runtime" + +/* + TUNEABLES +*/ + +/* + Determines the default value for whether dial_tcp() and accept_tcp() will set TCP_NODELAY on the new + socket, and the client socket, respectively. + This can also be set on a per-socket basis using the 'options' optional parameter to those procedures. + + When TCP_NODELAY is set, data will be sent out to the peer as quickly as possible, rather than being + coalesced into fewer network packets. + + This makes the networking layer more eagerly send data when you ask it to, + which can reduce latency by up to 200ms. + + This does mean that a lot of small writes will negatively effect throughput however, + since the Nagle algorithm will be disabled, and each write becomes one + IP packet. This will increase traffic by a factor of 40, with IP and TCP + headers for each payload. + + However, you can avoid this by buffering things up yourself if you wish to send a lot of + short data chunks, when TCP_NODELAY is enabled on that socket. +*/ +ODIN_NET_TCP_NODELAY_DEFAULT :: #config(ODIN_NET_TCP_NODELAY_DEFAULT, true) + +/* + See also top of `dns.odin` for DNS configuration. +*/ + +/* + COMMON DEFINITIONS +*/ + +Maybe :: runtime.Maybe + +General_Error :: enum { + Unable_To_Enumerate_Network_Interfaces = 1, +} + +/* + `Platform_Error` is used to wrap errors returned by the different platforms that defy translation into a common error. +*/ +Platform_Error :: enum u32 {} + +/* + NOTE(tetra): Enums in Network_Error should not have a named zero value. + If you have a proc that returns an enum with an Ok=0 value, using or_return from the callsite, when the caller returns a union, works as expected. + However, if that proc returns the union directly, returning the Ok value will NOT work with the caller's or_return, as it will treat Error{.Ok} as != nil, and early-return with it. + + The approach currently taken to avoid this is: + - Remove the named zero values for the enums + - Use the union everywhere +*/ +Network_Error :: union { + General_Error, + Platform_Error, + Create_Socket_Error, + Dial_Error, + Listen_Error, + Accept_Error, + Bind_Error, + TCP_Send_Error, + UDP_Send_Error, + TCP_Recv_Error, + UDP_Recv_Error, + Shutdown_Error, + Socket_Option_Error, + Parse_Endpoint_Error, + Resolve_Error, + DNS_Error, +} + + +Resolve_Error :: enum { + Unable_To_Resolve = 1, +} + +DNS_Error :: enum { + Invalid_Hostname_Error = 1, + Invalid_Hosts_Config_Error, + Invalid_Resolv_Config_Error, + Connection_Error, + Server_Error, + System_Error, +} + +/* + SOCKET OPTIONS & DEFINITIONS +*/ + +TCP_Options :: struct { + no_delay: bool, +} + +default_tcp_options := TCP_Options { + no_delay = ODIN_NET_TCP_NODELAY_DEFAULT, +} + +/* + To allow freely using `Socket` in your own data structures in a cross-platform manner, + we treat it as a handle large enough to accomodate OS-specific notions of socket handles. + + The platform code will perform the cast so you don't have to. +*/ +Socket :: distinct i64 + +TCP_Socket :: distinct Socket +UDP_Socket :: distinct Socket + +Socket_Protocol :: enum { + TCP, + UDP, +} + +Any_Socket :: union { + TCP_Socket, + UDP_Socket, +} + +/* + ADDRESS DEFINITIONS +*/ + +IP4_Address :: distinct [4]u8 +IP6_Address :: distinct [8]u16be +Address :: union {IP4_Address, IP6_Address} + +IP4_Loopback := IP4_Address{127, 0, 0, 1} +IP6_Loopback := IP6_Address{0, 0, 0, 0, 0, 0, 0, 1} + +IP4_Any := IP4_Address{} +IP6_Any := IP6_Address{} + +Endpoint :: struct { + address: Address, + port: int, +} + +Address_Family :: enum { + IP4, + IP6, +} + +Netmask :: distinct Address + +/* + INTERFACE / LINK STATE +*/ +Network_Interface :: struct { + adapter_name: string, // On Windows this is a GUID that we could parse back into its u128 for more compact storage. + friendly_name: string, + description: string, + dns_suffix: string, + + physical_address: string, // MAC address, etc. + mtu: u32, + + unicast: [dynamic]Lease, + multicast: [dynamic]Address, + anycast: [dynamic]Address, + + gateways: [dynamic]Address, + dhcp_v4: Address, + dhcp_v6: Address, + + tunnel_type: Tunnel_Type, + + link: struct { + state: Link_State, + transmit_speed: u64, + receive_speed: u64, + }, +} + +/* + Empty bit set is unknown state. +*/ +Link_States :: enum u32 { + Up = 1, + Down = 2, + Testing = 3, + Dormant = 4, + Not_Present = 5, + Lower_Layer_Down = 6, + Loopback = 7, +} +Link_State :: bit_set[Link_States; u32] + +Lease :: struct { + address: Address, + netmask: Netmask, + lifetime: struct { + valid: u32, + preferred: u32, + lease: u32, + }, + origin: struct { + prefix: Prefix_Origin, + suffix: Suffix_Origin, + }, + address_duplication: Address_Duplication, +} + +Tunnel_Type :: enum i32 { + None = 0, + Other = 1, + Direct = 2, + IPv4_To_IPv6 = 11, + ISA_TAP = 13, + Teredo = 14, + IP_HTTPS = 15, +} + +Prefix_Origin :: enum i32 { + Other = 0, + Manual = 1, + Well_Known = 2, + DHCP = 3, + Router_Advertisement = 4, + Unchanged = 16, +} + +Suffix_Origin :: enum i32 { + Other = 0, + Manual = 1, + Well_Known = 2, + DHCP = 3, + Link_Layer_Address = 4, + Random = 5, + Unchanged = 16, +} + +Address_Duplication :: enum i32 { + Invalid = 0, + Tentative = 1, + Duplicate = 2, + Deprecated = 3, + Preferred = 4, +} + +/* + DNS DEFINITIONS +*/ + +DNS_Configuration :: struct { + /* + Configuration files. + */ + resolv_conf: string, + hosts_file: string, + + // TODO: Allow loading these up with `reload_configuration()` call or the like so we don't have to do it each call. + name_servers: []Endpoint, + hosts_file_entries: []DNS_Record, +} + +DNS_Record_Type :: enum u16 { + DNS_TYPE_A = 0x1, // IP4 address. + DNS_TYPE_NS = 0x2, // IP6 address. + DNS_TYPE_CNAME = 0x5, // Another host name. + DNS_TYPE_MX = 0xf, // Arbitrary binary data or text. + DNS_TYPE_AAAA = 0x1c, // Address of a name (DNS) server. + DNS_TYPE_TEXT = 0x10, // Address and preference priority of a mail exchange server. + DNS_TYPE_SRV = 0x21, // Address, port, priority, and weight of a host that provides a particular service. + + IP4 = DNS_TYPE_A, + IP6 = DNS_TYPE_AAAA, + CNAME = DNS_TYPE_CNAME, + TXT = DNS_TYPE_TEXT, + NS = DNS_TYPE_NS, + MX = DNS_TYPE_MX, + SRV = DNS_TYPE_SRV, +} + +/* + Base DNS Record. All DNS responses will carry a hostname and TTL (time to live) field. +*/ +DNS_Record_Base :: struct { + record_name: string, + ttl_seconds: u32, // The time in seconds that this service will take to update, after the record is updated. +} + +/* + An IP4 address that the domain name maps to. There can be any number of these. +*/ +DNS_Record_IP4 :: struct { + using base: DNS_Record_Base, + address: IP4_Address, +} + +/* + An IPv6 address that the domain name maps to. There can be any number of these. +*/ +DNS_Record_IP6 :: struct { + using base: DNS_Record_Base, + address: IP6_Address, +} + +/* + Another domain name that the domain name maps to. + Domains can be pointed to another domain instead of directly to an IP address. + `get_dns_records` will recursively follow these if you request this type of record. +*/ +DNS_Record_CNAME :: struct { + using base: DNS_Record_Base, + host_name: string, +} + +/* + Arbitrary string data that is associated with the domain name. + Commonly of the form `key=value` to be parsed, though there is no specific format for them. + These can be used for any purpose. +*/ +DNS_Record_TXT :: struct { + using base: DNS_Record_Base, + value: string, +} + +/* + Domain names of other DNS servers that are associated with the domain name. + TODO(tetra): Expand on what these records are used for, and when you should use pay attention to these. +*/ +DNS_Record_NS :: struct { + using base: DNS_Record_Base, + host_name: string, +} + +// Domain names for email servers that are associated with the domain name. +// These records also have values which ranks them in the order they should be preferred. Lower is more-preferred. +DNS_Record_MX :: struct { + using base: DNS_Record_Base, + host_name: string, + preference: int, +} + +// An endpoint for a service that is available through the domain name. +// This is the way to discover the services that a domain name provides. +// +// Clients MUST attempt to contact the host with the lowest priority that they can reach. +// If two hosts have the same priority, they should be contacted in the order according to their weight. +// Hosts with larger weights should have a proportionally higher chance of being contacted by clients. +// A weight of zero indicates a very low weight, or, when there is no choice (to reduce visual noise). +// +// The host may be "." to indicate that it is "decidedly not available" on this domain. +DNS_Record_SRV :: struct { + // base contains the full name of this record. + // e.g: _sip._tls.example.com + using base: DNS_Record_Base, + + // The hostname or address where this service can be found. + target: string, + // The port on which this service can be found. + port: int, + + service_name: string, // NOTE(tetra): These are substrings of 'record_name' + protocol_name: string, // NOTE(tetra): These are substrings of 'record_name' + + // Lower is higher priority + priority: int, + // Relative weight of this host compared to other of same priority; the chance of using this host should be proporitional to this weight. + // The number of seconds that it will take to update the record. + weight: int, +} + +DNS_Record :: union { + DNS_Record_IP4, + DNS_Record_IP6, + DNS_Record_CNAME, + DNS_Record_TXT, + DNS_Record_NS, + DNS_Record_MX, + DNS_Record_SRV, +} + +DNS_Response_Code :: enum u16be { + No_Error, + Format_Error, + Server_Failure, + Name_Error, + Not_Implemented, + Refused, +} + +DNS_Query :: enum u16be { + Host_Address = 1, + Authoritative_Name_Server = 2, + Mail_Destination = 3, + Mail_Forwarder = 4, + CNAME = 5, + All = 255, +} + +DNS_Header :: struct { + id: u16be, + is_response: bool, + opcode: u16be, + is_authoritative: bool, + is_truncated: bool, + is_recursion_desired: bool, + is_recursion_available: bool, + response_code: DNS_Response_Code, +} + +DNS_Record_Header :: struct #packed { + type: u16be, + class: u16be, + ttl: u32be, + length: u16be, +} + +DNS_Host_Entry :: struct { + name: string, + addr: Address, +}
\ No newline at end of file diff --git a/core/net/dns.odin b/core/net/dns.odin new file mode 100644 index 000000000..32bc1be6b --- /dev/null +++ b/core/net/dns.odin @@ -0,0 +1,873 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:mem" +import "core:strings" +import "core:time" +import "core:os" + +/* + Default configuration for DNS resolution. +*/ +when ODIN_OS == .Windows { + getenv :: proc(key: string) -> (val: string) { + return os.get_env(key) + } + + DEFAULT_DNS_CONFIGURATION :: DNS_Configuration{ + resolv_conf = "", + hosts_file = "%WINDIR%\\system32\\drivers\\etc\\hosts", + } +} else when ODIN_OS == .Linux || ODIN_OS == .Darwin || ODIN_OS == .OpenBSD { + getenv :: proc(key: string) -> (val: string) { + val, _ = os.getenv(key) + return + } + + DEFAULT_DNS_CONFIGURATION :: DNS_Configuration{ + resolv_conf = "/etc/resolv.conf", + hosts_file = "/etc/hosts", + } +} else { + #panic("Please add a configuration for this OS.") +} + +@(init) +init_dns_configuration :: proc() { + /* + Resolve %ENVIRONMENT% placeholders in their paths. + */ + dns_configuration.resolv_conf, _ = replace_environment_path(dns_configuration.resolv_conf) + dns_configuration.hosts_file, _ = replace_environment_path(dns_configuration.hosts_file) +} + +destroy_dns_configuration :: proc() { + delete(dns_configuration.resolv_conf) + delete(dns_configuration.hosts_file) +} + +dns_configuration := DEFAULT_DNS_CONFIGURATION + +/* + Always allocates for consistency. +*/ +replace_environment_path :: proc(path: string, allocator := context.allocator) -> (res: string, ok: bool) { + /* + Nothing to replace. Return a clone of the original. + */ + if strings.count(path, "%") != 2 { + return strings.clone(path), true + } + + left := strings.index(path, "%") + 1 + assert(left > 0 && left <= len(path)) // should be covered by there being two % + + right := strings.index(path[left:], "%") + 1 + assert(right > 0 && right <= len(path)) // should be covered by there being two % + + env_key := path[left: right] + env_val := getenv(env_key) + defer delete(env_val) + + res, _ = strings.replace(path, path[left - 1: right + 1], env_val, 1) + + return res, true +} + + +/* + Resolves a hostname to exactly one IP4 and IP6 endpoint. + It's then up to you which one you use. + Note that which address you use to open a socket, determines the type of the socket you get. + + Returns `ok=false` if the host name could not be resolved to any endpoints. + + Returned endpoints have the same port as provided in the string, or 0 if absent. + If you want to use a specific port, just modify the field after the call to this procedure. + + If the hostname part of the endpoint is actually a string representation of an IP address, DNS resolution will be skipped. + This allows you to pass both strings like "example.com:9000" and "1.2.3.4:9000" to this function end reliably get + back an endpoint in both cases. +*/ +resolve :: proc(hostname_and_maybe_port: string) -> (ep4, ep6: Endpoint, err: Network_Error) { + target := parse_hostname_or_endpoint(hostname_and_maybe_port) or_return + switch t in target { + case Endpoint: + // NOTE(tetra): The hostname was actually an IP address; nothing to resolve, so just return it. + switch in t.address { + case IP4_Address: ep4 = t + case IP6_Address: ep6 = t + case: unreachable() + } + return + + case Host: + err4, err6: Network_Error = ---, --- + ep4, err4 = resolve_ip4(t.hostname) + ep6, err6 = resolve_ip6(t.hostname) + if err4 != nil && err6 != nil { + err = err4 + } + return + } + unreachable() +} +resolve_ip4 :: proc(hostname_and_maybe_port: string) -> (ep4: Endpoint, err: Network_Error) { + target := parse_hostname_or_endpoint(hostname_and_maybe_port) or_return + switch t in target { + case Endpoint: + // NOTE(tetra): The hostname was actually an IP address; nothing to resolve, so just return it. + switch in t.address { + case IP4_Address: + return t, nil + case IP6_Address: + err = .Unable_To_Resolve + return + } + case Host: + recs, _ := get_dns_records_from_os(t.hostname, .IP4, context.temp_allocator) + if len(recs) == 0 { + err = .Unable_To_Resolve + return + } + ep4 = { + address = recs[0].(DNS_Record_IP4).address, + port = t.port, + } + return + } + unreachable() +} +resolve_ip6 :: proc(hostname_and_maybe_port: string) -> (ep6: Endpoint, err: Network_Error) { + target := parse_hostname_or_endpoint(hostname_and_maybe_port) or_return + switch t in target { + case Endpoint: + // NOTE(tetra): The hostname was actually an IP address; nothing to resolve, so just return it. + switch in t.address { + case IP4_Address: + err = .Unable_To_Resolve + return + case IP6_Address: + return t, nil + } + case Host: + recs, _ := get_dns_records_from_os(t.hostname, .IP6, context.temp_allocator) + if len(recs) == 0 { + err = .Unable_To_Resolve + return + } + ep6 = { + address = recs[0].(DNS_Record_IP6).address, + port = t.port, + } + return + } + unreachable() +} + +/* + `get_dns_records` uses OS-specific methods to query DNS records. +*/ +when ODIN_OS == .Windows { + get_dns_records_from_os :: get_dns_records_windows +} else when ODIN_OS == .Linux || ODIN_OS == .Darwin || ODIN_OS == .OpenBSD { + get_dns_records_from_os :: get_dns_records_unix +} else { + #panic("get_dns_records_from_os not implemented on this OS") +} + +/* + A generic DNS client usable on any platform. + Performs a recursive DNS query for records of a particular type for the hostname. + + NOTE: This procedure instructs the DNS resolver to recursively perform CNAME requests on our behalf, + meaning that DNS queries for a hostname will resolve through CNAME records until an + IP address is reached. +*/ +get_dns_records_from_nameservers :: proc(hostname: string, type: DNS_Record_Type, name_servers: []Endpoint, host_overrides: []DNS_Record, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) { + context.allocator = allocator + + if type != .SRV { + // NOTE(tetra): 'hostname' can contain underscores when querying SRV records + ok := validate_hostname(hostname) + if !ok { + return nil, .Invalid_Hostname_Error + } + } + + hdr := DNS_Header{ + id = 0, + is_response = false, + opcode = 0, + is_authoritative = false, + is_truncated = false, + is_recursion_desired = true, + is_recursion_available = false, + response_code = DNS_Response_Code.No_Error, + } + + id, bits := pack_dns_header(hdr) + dns_hdr := [6]u16be{} + dns_hdr[0] = id + dns_hdr[1] = bits + dns_hdr[2] = 1 + + dns_query := [2]u16be{ u16be(type), 1 } + + output := [(size_of(u16be) * 6) + NAME_MAX + (size_of(u16be) * 2)]u8{} + b := strings.builder_from_slice(output[:]) + + strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_hdr[:])) + ok := encode_hostname(&b, hostname) + if !ok { + return nil, .Invalid_Hostname_Error + } + strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_query[:])) + + dns_packet := output[:strings.builder_len(b)] + + dns_response_buf := [4096]u8{} + dns_response: []u8 + for name_server in name_servers { + conn, sock_err := make_unbound_udp_socket(family_from_endpoint(name_server)) + if sock_err != nil { + return nil, .Connection_Error + } + defer close(conn) + + _, send_err := send(conn, dns_packet[:], name_server) + if send_err != nil { + continue + } + + set_err := set_option(conn, .Receive_Timeout, time.Second * 1) + if set_err != nil { + return nil, .Connection_Error + } + + recv_sz, _, recv_err := recv_udp(conn, dns_response_buf[:]) + if recv_err == UDP_Recv_Error.Timeout { + continue + } else if recv_err != nil { + continue + } + + if recv_sz == 0 { + continue + } + + dns_response = dns_response_buf[:recv_sz] + + rsp, _ok := parse_response(dns_response, type) + if !_ok { + return nil, .Server_Error + } + + if len(rsp) == 0 { + continue + } + + return rsp[:], nil + } + + return +} + +// `records` slice is also destroyed. +destroy_dns_records :: proc(records: []DNS_Record, allocator := context.allocator) { + context.allocator = allocator + + for rec in records { + switch r in rec { + case DNS_Record_IP4: + delete(r.base.record_name) + + case DNS_Record_IP6: + delete(r.base.record_name) + + case DNS_Record_CNAME: + delete(r.base.record_name) + delete(r.host_name) + + case DNS_Record_TXT: + delete(r.base.record_name) + delete(r.value) + + case DNS_Record_NS: + delete(r.base.record_name) + delete(r.host_name) + + case DNS_Record_MX: + delete(r.base.record_name) + delete(r.host_name) + + case DNS_Record_SRV: + delete(r.record_name) + delete(r.target) + } + } + + delete(records, allocator) +} + +/* + TODO(cloin): Does the DNS Resolver need to recursively hop through CNAMEs to get the IP + or is that what recursion desired does? Do we need to handle recursion unavailable? + How do we deal with is_authoritative / is_truncated? +*/ + +NAME_MAX :: 255 +LABEL_MAX :: 63 + +pack_dns_header :: proc(hdr: DNS_Header) -> (id: u16be, bits: u16be) { + id = hdr.id + bits = hdr.opcode << 1 | u16be(hdr.response_code) + if hdr.is_response { + bits |= 1 << 15 + } + if hdr.is_authoritative { + bits |= 1 << 10 + } + if hdr.is_truncated { + bits |= 1 << 9 + } + if hdr.is_recursion_desired { + bits |= 1 << 8 + } + if hdr.is_recursion_available { + bits |= 1 << 7 + } + + return id, bits +} + +unpack_dns_header :: proc(id: u16be, bits: u16be) -> (hdr: DNS_Header) { + hdr.id = id + hdr.is_response = (bits & (1 << 15)) != 0 + hdr.opcode = (bits >> 11) & 0xF + hdr.is_authoritative = (bits & (1 << 10)) != 0 + hdr.is_truncated = (bits & (1 << 9)) != 0 + hdr.is_recursion_desired = (bits & (1 << 8)) != 0 + hdr.is_recursion_available = (bits & (1 << 7)) != 0 + hdr.response_code = DNS_Response_Code(bits & 0xF) + + return hdr +} + +load_resolv_conf :: proc(resolv_conf_path: string, allocator := context.allocator) -> (name_servers: []Endpoint, ok: bool) { + context.allocator = allocator + + res, success := os.read_entire_file_from_filename(resolv_conf_path) + if !success { + return + } + defer delete(res) + resolv_str := string(res) + + _name_servers := make([dynamic]Endpoint, 0, allocator) + for line in strings.split_lines_iterator(&resolv_str) { + if len(line) == 0 || line[0] == '#' { + continue + } + + id_str := "nameserver" + if strings.compare(line[:len(id_str)], id_str) != 0 { + continue + } + + server_ip_str := strings.trim_left_space(line[len(id_str):]) + if len(server_ip_str) == 0 { + continue + } + + addr := parse_address(server_ip_str) + endpoint := Endpoint{ + addr, + 53, + } + append(&_name_servers, endpoint) + } + + return _name_servers[:], true +} + +load_hosts :: proc(hosts_file_path: string, allocator := context.allocator) -> (hosts: []DNS_Host_Entry, ok: bool) { + context.allocator = allocator + + res, success := os.read_entire_file_from_filename(hosts_file_path, allocator) + if !success { + return + } + defer delete(res) + + _hosts := make([dynamic]DNS_Host_Entry, 0, allocator) + hosts_str := string(res) + for line in strings.split_lines_iterator(&hosts_str) { + if len(line) == 0 || line[0] == '#' { + continue + } + + splits := strings.fields(line) + defer delete(splits) + + ip_str := splits[0] + addr := parse_address(ip_str) + if addr == nil { + continue + } + + for hostname in splits[1:] { + if len(hostname) == 0 { + continue + } + + append(&_hosts, DNS_Host_Entry{hostname, addr}) + } + } + + return _hosts[:], true +} + +/* + www.google.com -> 3www6google3com0 +*/ +encode_hostname :: proc(b: ^strings.Builder, hostname: string, allocator := context.allocator) -> (ok: bool) { + _hostname := hostname + for section in strings.split_iterator(&_hostname, ".") { + if len(section) > LABEL_MAX { + return + } + + strings.write_byte(b, u8(len(section))) + strings.write_string(b, section) + } + strings.write_byte(b, 0) + + return true +} + +skip_hostname :: proc(packet: []u8, start_idx: int, allocator := context.allocator) -> (encode_size: int, ok: bool) { + out_size := 0 + + cur_idx := start_idx + iteration_max := 0 + top: for cur_idx < len(packet) { + if packet[cur_idx] == 0 { + out_size += 1 + break + } + + if iteration_max > 255 { + return + } + + if packet[cur_idx] > 63 && packet[cur_idx] != 0xC0 { + return + } + + switch packet[cur_idx] { + case 0xC0: + out_size += 2 + break top + case: + label_size := int(packet[cur_idx]) + 1 + idx2 := cur_idx + label_size + + if idx2 < cur_idx + 1 || idx2 > len(packet) { + return + } + + out_size += label_size + cur_idx = idx2 + } + + iteration_max += 1 + } + + if start_idx + out_size > len(packet) { + return + } + + return out_size, true +} + +decode_hostname :: proc(packet: []u8, start_idx: int, allocator := context.allocator) -> (hostname: string, encode_size: int, ok: bool) { + output := [NAME_MAX]u8{} + b := strings.builder_from_slice(output[:]) + + // If you're on level 0, update out_bytes, everything through a pointer + // doesn't count towards this hostname's packet length + + // Evaluate tokens to generate the hostname + out_size := 0 + level := 0 + print_size := 0 + cur_idx := start_idx + iteration_max := 0 + labels_added := 0 + for cur_idx < len(packet) { + if packet[cur_idx] == 0 { + + if (level == 0) { + out_size += 1 + } + + break + } + + if iteration_max > 255 { + return + } + + if packet[cur_idx] > 63 && packet[cur_idx] != 0xC0 { + return + } + + switch packet[cur_idx] { + + // This is a offset to more data in the packet, jump to it + case 0xC0: + pkt := packet[cur_idx:cur_idx+2] + val := (^u16be)(raw_data(pkt))^ + offset := int(val & 0x3FFF) + if offset > len(packet) { + return + } + + cur_idx = offset + + if (level == 0) { + out_size += 2 + level += 1 + } + + // This is a label, insert it into the hostname + case: + label_size := int(packet[cur_idx]) + idx2 := cur_idx + label_size + 1 + if idx2 < cur_idx + 1 || idx2 > len(packet) { + return + } + + if print_size + label_size + 1 > NAME_MAX { + return + } + + if labels_added > 0 { + strings.write_byte(&b, '.') + } + strings.write_bytes(&b, packet[cur_idx+1:idx2]) + print_size += label_size + 1 + labels_added += 1 + + cur_idx = idx2 + + if (level == 0) { + out_size += label_size + 1 + } + } + + iteration_max += 1 + } + + if start_idx + out_size > len(packet) { + return + } + + return strings.clone(strings.to_string(b), allocator), out_size, true +} + +// Uses RFC 952 & RFC 1123 +validate_hostname :: proc(hostname: string) -> (ok: bool) { + if len(hostname) > 255 || len(hostname) == 0 { + return + } + + if hostname[0] == '-' { + return + } + + _hostname := hostname + for label in strings.split_iterator(&_hostname, ".") { + if len(label) > 63 || len(label) == 0 { + return + } + + for ch in label { + switch ch { + case: + return + case 'a'..'z', 'A'..'Z', '0'..'9', '-': + continue + } + } + } + + return true +} + +parse_record :: proc(packet: []u8, cur_off: ^int, filter: DNS_Record_Type = nil) -> (record: DNS_Record, ok: bool) { + record_buf := packet[cur_off^:] + + srv_record_name, hn_sz := decode_hostname(packet, cur_off^, context.temp_allocator) or_return + // TODO(tetra): Not sure what we should call this. + // Is it really only used in SRVs? + // Maybe some refactoring is required? + + ahdr_sz := size_of(DNS_Record_Header) + if len(record_buf) - hn_sz < ahdr_sz { + return + } + + record_hdr_bytes := record_buf[hn_sz:hn_sz+ahdr_sz] + record_hdr := cast(^DNS_Record_Header)raw_data(record_hdr_bytes) + + data_sz := record_hdr.length + data_off := cur_off^ + int(hn_sz) + int(ahdr_sz) + data := packet[data_off:data_off+int(data_sz)] + cur_off^ += int(hn_sz) + int(ahdr_sz) + int(data_sz) + + if u16be(filter) != record_hdr.type { + return nil, true + } + + _record: DNS_Record + #partial switch DNS_Record_Type(record_hdr.type) { + case .IP4: + if len(data) != 4 { + return + } + + addr := (^IP4_Address)(raw_data(data))^ + + _record = DNS_Record_IP4{ + base = DNS_Record_Base{ + record_name = strings.clone(srv_record_name), + ttl_seconds = u32(record_hdr.ttl), + }, + address = addr, + } + + case .IP6: + if len(data) != 16 { + return + } + + addr := (^IP6_Address)(raw_data(data))^ + + _record = DNS_Record_IP6{ + base = DNS_Record_Base{ + record_name = strings.clone(srv_record_name), + ttl_seconds = u32(record_hdr.ttl), + }, + address = addr, + } + + case .CNAME: + hostname, _ := decode_hostname(packet, data_off) or_return + + _record = DNS_Record_CNAME{ + base = DNS_Record_Base{ + record_name = strings.clone(srv_record_name), + ttl_seconds = u32(record_hdr.ttl), + }, + host_name = hostname, + } + + case .TXT: + _record = DNS_Record_TXT{ + base = DNS_Record_Base{ + record_name = strings.clone(srv_record_name), + ttl_seconds = u32(record_hdr.ttl), + }, + value = strings.clone(string(data)), + } + + case .NS: + name, _ := decode_hostname(packet, data_off) or_return + + _record = DNS_Record_NS{ + base = DNS_Record_Base{ + record_name = strings.clone(srv_record_name), + ttl_seconds = u32(record_hdr.ttl), + }, + host_name = name, + } + + case .SRV: + if len(data) <= 6 { + return + } + + priority: u16be = mem.slice_data_cast([]u16be, data)[0] + weight: u16be = mem.slice_data_cast([]u16be, data)[1] + port: u16be = mem.slice_data_cast([]u16be, data)[2] + target, _ := decode_hostname(packet, data_off + (size_of(u16be) * 3)) or_return + + // NOTE(tetra): Srv record name should be of the form '_servicename._protocol.hostname' + // The record name is the name of the record. + // Not to be confused with the _target_ of the record, which is--in combination with the port--what we're looking up + // by making this request in the first place. + + // NOTE(Jeroen): Service Name and Protocol Name can probably just be string slices into the record name. + // It's already cloned, after all. I wouldn't put them on the temp allocator like this. + + parts := strings.split_n(srv_record_name, ".", 3, context.temp_allocator) + if len(parts) != 3 { + return + } + service_name, protocol_name := parts[0], parts[1] + + _record = DNS_Record_SRV{ + base = DNS_Record_Base{ + record_name = strings.clone(srv_record_name), + ttl_seconds = u32(record_hdr.ttl), + }, + target = target, + service_name = service_name, + protocol_name = protocol_name, + priority = int(priority), + weight = int(weight), + port = int(port), + } + + case .MX: + if len(data) <= 2 { + return + } + + preference: u16be = mem.slice_data_cast([]u16be, data)[0] + hostname, _ := decode_hostname(packet, data_off + size_of(u16be)) or_return + + _record = DNS_Record_MX{ + base = DNS_Record_Base{ + record_name = strings.clone(srv_record_name), + ttl_seconds = u32(record_hdr.ttl), + }, + host_name = hostname, + preference = int(preference), + } + + case: + return + + } + + return _record, true +} + +/* + DNS Query Response Format: + - DNS_Header (packed) + - Query Count + - Answer Count + - Authority Count + - Additional Count + - Query[] + - Hostname -- encoded + - Type + - Class + - Answer[] + - DNS Record Data + - Authority[] + - DNS Record Data + - Additional[] + - DNS Record Data + + DNS Record Data: + - DNS_Record_Header + - Data[] +*/ + +parse_response :: proc(response: []u8, filter: DNS_Record_Type = nil, allocator := context.allocator) -> (records: []DNS_Record, ok: bool) { + header_size_bytes :: 12 + if len(response) < header_size_bytes { + return + } + + _records := make([dynamic]DNS_Record, 0) + + dns_hdr_chunks := mem.slice_data_cast([]u16be, response[:header_size_bytes]) + hdr := unpack_dns_header(dns_hdr_chunks[0], dns_hdr_chunks[1]) + if !hdr.is_response { + return + } + + question_count := int(dns_hdr_chunks[2]) + if question_count != 1 { + return + } + answer_count := int(dns_hdr_chunks[3]) + authority_count := int(dns_hdr_chunks[4]) + additional_count := int(dns_hdr_chunks[5]) + + cur_idx := header_size_bytes + + for i := 0; i < question_count; i += 1 { + if cur_idx == len(response) { + continue + } + + dq_sz :: 4 + hn_sz := skip_hostname(response, cur_idx, context.temp_allocator) or_return + dns_query := mem.slice_data_cast([]u16be, response[cur_idx+hn_sz:cur_idx+hn_sz+dq_sz]) + + cur_idx += hn_sz + dq_sz + } + + for i := 0; i < answer_count; i += 1 { + if cur_idx == len(response) { + continue + } + + rec := parse_record(response, &cur_idx, filter) or_return + if rec == nil { + continue + } + + append(&_records, rec) + } + + for i := 0; i < authority_count; i += 1 { + if cur_idx == len(response) { + continue + } + + rec := parse_record(response, &cur_idx, filter) or_return + if rec == nil { + continue + } + + append(&_records, rec) + } + + for i := 0; i < additional_count; i += 1 { + if cur_idx == len(response) { + continue + } + + rec := parse_record(response, &cur_idx, filter) or_return + if rec == nil { + continue + } + + append(&_records, rec) + } + + return _records[:], true +} diff --git a/core/net/dns_unix.odin b/core/net/dns_unix.odin new file mode 100644 index 000000000..7f57927fd --- /dev/null +++ b/core/net/dns_unix.odin @@ -0,0 +1,83 @@ +//+build linux, darwin, freebsd, openbsd, !windows +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:strings" + +get_dns_records_unix :: proc(hostname: string, type: DNS_Record_Type, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) { + context.allocator = allocator + + if type != .SRV { + // NOTE(tetra): 'hostname' can contain underscores when querying SRV records + ok := validate_hostname(hostname) + if !ok { + return nil, .Invalid_Hostname_Error + } + } + + name_servers, resolve_ok := load_resolv_conf(dns_configuration.resolv_conf) + defer delete(name_servers) + if !resolve_ok { + return nil, .Invalid_Resolv_Config_Error + } + if len(name_servers) == 0 { + return + } + + hosts, hosts_ok := load_hosts(dns_configuration.hosts_file) + defer delete(hosts) + if !hosts_ok { + return nil, .Invalid_Hosts_Config_Error + } + if len(hosts) == 0 { + return + } + + host_overrides := make([dynamic]DNS_Record) + for host in hosts { + if strings.compare(host.name, hostname) != 0 { + continue + } + + if type == .IP4 && family_from_address(host.addr) == .IP4 { + record := DNS_Record_IP4{ + base = { + record_name = strings.clone(hostname), + ttl_seconds = 0, + }, + address = host.addr.(IP4_Address), + } + append(&host_overrides, record) + } else if type == .IP6 && family_from_address(host.addr) == .IP6 { + record := DNS_Record_IP6{ + base = { + record_name = strings.clone(hostname), + ttl_seconds = 0, + }, + address = host.addr.(IP6_Address), + } + append(&host_overrides, record) + } + } + + if len(host_overrides) > 0 { + return host_overrides[:], nil + } + + return get_dns_records_from_nameservers(hostname, type, name_servers, host_overrides[:]) +} diff --git a/core/net/dns_windows.odin b/core/net/dns_windows.odin new file mode 100644 index 000000000..ae38ca05f --- /dev/null +++ b/core/net/dns_windows.odin @@ -0,0 +1,166 @@ +//+build windows +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:strings" +import "core:mem" + +import win "core:sys/windows" + +// Performs a recursive DNS query for records of a particular type for the hostname. +// +// NOTE: This procedure instructs the DNS resolver to recursively perform CNAME requests on our behalf, +// meaning that DNS queries for a hostname will resolve through CNAME records until an +// IP address is reached. +// +// WARNING: This procedure allocates memory for each record returned; deleting just the returned slice is not enough! +// See `destroy_records`. +get_dns_records_windows :: proc(hostname: string, type: DNS_Record_Type, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) { + context.allocator = allocator + + host_cstr := strings.clone_to_cstring(hostname, context.temp_allocator) + rec: ^win.DNS_RECORD + res := win.DnsQuery_UTF8(host_cstr, u16(type), 0, nil, &rec, nil) + + switch u32(res) { + case 0: + // okay + case win.ERROR_INVALID_NAME: + return nil, .Invalid_Hostname_Error + case win.DNS_INFO_NO_RECORDS: + return + case: + return nil, .System_Error + } + defer win.DnsRecordListFree(rec, 1) // 1 means that we're freeing a list... because the proc name isn't enough. + + count := 0 + for r := rec; r != nil; r = r.pNext { + if r.wType != u16(type) do continue // NOTE(tetra): Should never happen, but... + count += 1 + } + + + recs := make([dynamic]DNS_Record, 0, count) + if recs == nil do return nil, .System_Error // return no results if OOM. + + for r := rec; r != nil; r = r.pNext { + if r.wType != u16(type) do continue // NOTE(tetra): Should never happen, but... + + base_record := DNS_Record_Base{ + record_name = strings.clone(string(r.pName)), + ttl_seconds = r.dwTtl, + } + + switch DNS_Record_Type(r.wType) { + case .IP4: + addr := IP4_Address(transmute([4]u8)r.Data.A) + record := DNS_Record_IP4{ + base = base_record, + address = addr, + } + append(&recs, record) + + case .IP6: + addr := IP6_Address(transmute([8]u16be) r.Data.AAAA) + record := DNS_Record_IP6{ + base = base_record, + address = addr, + } + append(&recs, record) + + case .CNAME: + + hostname := strings.clone(string(r.Data.CNAME)) + record := DNS_Record_CNAME{ + base = base_record, + host_name = hostname, + } + append(&recs, record) + + case .TXT: + n := r.Data.TXT.dwStringCount + ptr := &r.Data.TXT.pStringArray + c_strs := mem.slice_ptr(ptr, int(n)) + + for cstr in c_strs { + record := DNS_Record_TXT{ + base = base_record, + value = strings.clone(string(cstr)), + } + append(&recs, record) + } + + case .NS: + hostname := strings.clone(string(r.Data.NS)) + record := DNS_Record_NS{ + base = base_record, + host_name = hostname, + } + append(&recs, record) + + case .MX: + /* + TODO(tetra): Order by preference priority? (Prefer hosts with lower preference values.) + Or maybe not because you're supposed to just use the first one that works + and which order they're in changes every few calls. + */ + + record := DNS_Record_MX{ + base = base_record, + host_name = strings.clone(string(r.Data.MX.pNameExchange)), + preference = int(r.Data.MX.wPreference), + } + append(&recs, record) + + case .SRV: + target := strings.clone(string(r.Data.SRV.pNameTarget)) // The target hostname/address that the service can be found on + priority := int(r.Data.SRV.wPriority) + weight := int(r.Data.SRV.wWeight) + port := int(r.Data.SRV.wPort) + + // NOTE(tetra): Srv record name should be of the form '_servicename._protocol.hostname' + // The record name is the name of the record. + // Not to be confused with the _target_ of the record, which is--in combination with the port--what we're looking up + // by making this request in the first place. + + // NOTE(Jeroen): Service Name and Protocol Name can probably just be string slices into the record name. + // It's already cloned, after all. I wouldn't put them on the temp allocator like this. + + parts := strings.split_n(base_record.record_name, ".", 3, context.temp_allocator) + if len(parts) != 3 { + continue + } + service_name, protocol_name := parts[0], parts[1] + + append(&recs, DNS_Record_SRV { + base = base_record, + target = target, + port = port, + service_name = service_name, + protocol_name = protocol_name, + priority = priority, + weight = weight, + + }) + } + } + + records = recs[:] + return +} diff --git a/core/net/doc.odin b/core/net/doc.odin new file mode 100644 index 000000000..0c6c08daa --- /dev/null +++ b/core/net/doc.odin @@ -0,0 +1,47 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. + + Features: + - Supports Windows, Linux and OSX. + - Opening and closing of TCP and UDP sockets. + - Sending to and receiving from these sockets. + - DNS name lookup, using either the OS or our own resolver. + + Planned: + - Nonblocking IO + - `Connection` struct + A "fat socket" struct that remembers how you opened it, etc, instead of just being a handle. + - IP Range structs, CIDR/class ranges, netmask calculator and associated helper procedures. + - Use `context.temp_allocator` instead of stack-based arenas? + And check it's the default temp allocator or can give us 4 MiB worth of memory + without punting to the main allocator by comparing their addresses in an @(init) procedure. + Panic if this assumption is not met. + + - Document assumptions about libc usage (or avoidance thereof) for each platform. + + Assumptions: + - For performance reasons this package relies on the `context.temp_allocator` in some places. + + You can replace the default `context.temp_allocator` with your own as long as it meets + this requirement: A minimum of 4 MiB of scratch space that's expected not to be freed. + + If this expectation is not met, the package's @(init) procedure will attempt to detect + this and panic to avoid temp allocations prematurely overwriting data and garbling results, + or worse. This means that should you replace the temp allocator with an insufficient one, + we'll do our best to loudly complain the first time you try it. + +*/ +package net
\ No newline at end of file diff --git a/core/net/interface.odin b/core/net/interface.odin new file mode 100644 index 000000000..354cba53f --- /dev/null +++ b/core/net/interface.odin @@ -0,0 +1,68 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:strings" + +/* + `destroy_interfaces` cleans up a list of network interfaces retrieved by e.g. `enumerate_interfaces`. +*/ +destroy_interfaces :: proc(interfaces: []Network_Interface, allocator := context.allocator) { + context.allocator = allocator + + for i in interfaces { + delete(i.adapter_name) + delete(i.friendly_name) + delete(i.description) + delete(i.dns_suffix) + + delete(i.physical_address) + + delete(i.unicast) + delete(i.multicast) + delete(i.anycast) + delete(i.gateways) + } + delete(interfaces, allocator) +} + +/* + Turns a slice of bytes (from e.g. `get_adapters_addresses`) into a "XX:XX:XX:..." string. +*/ +physical_address_to_string :: proc(phy_addr: []u8, allocator := context.allocator) -> (phy_string: string) { + context.allocator = allocator + + MAC_HEX := "0123456789ABCDEF" + + if len(phy_addr) == 0 { + return "" + } + + buf: strings.Builder + + for b, i in phy_addr { + if i > 0 { + strings.write_rune_builder(&buf, ':') + } + + hi := rune(MAC_HEX[b >> 4]) + lo := rune(MAC_HEX[b & 15]) + strings.write_rune_builder(&buf, hi) + strings.write_rune_builder(&buf, lo) + } + return strings.to_string(buf) +}
\ No newline at end of file diff --git a/core/net/interface_darwin.odin b/core/net/interface_darwin.odin new file mode 100644 index 000000000..9a9bda5d3 --- /dev/null +++ b/core/net/interface_darwin.odin @@ -0,0 +1,23 @@ +//+build darwin +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +/* + TODO: Implement. Can probably use the (current) Linux implementation, + which will itself be switched over to talking to the kernel via NETLINK protocol once we have raw sockets. +*/ diff --git a/core/net/interface_linux.odin b/core/net/interface_linux.odin new file mode 100644 index 000000000..c5973fa2d --- /dev/null +++ b/core/net/interface_linux.odin @@ -0,0 +1,147 @@ +//+build linux, darwin, openbsd, !windows +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +/* + This file uses `getifaddrs` libc call to enumerate interfaces. + + TODO: When we have raw sockets, split off into its own file for Linux so we can use the NETLINK protocol and bypass libc. +*/ + +import "core:os" +import "core:strings" + +/* + `enumerate_interfaces` retrieves a list of network interfaces with their associated properties. +*/ +enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Network_Error) { + context.allocator = allocator + + head: ^os.ifaddrs + + if res := os._getifaddrs(&head); res < 0 { + return {}, .Unable_To_Enumerate_Network_Interfaces + } + + /* + Unlike Windows, *nix regrettably doesn't return all it knows about an interface in one big struct. + We're going to have to iterate over a list and coalesce information as we go. + */ + + ifaces: map[string]^Network_Interface + defer delete(ifaces) + + for ifaddr := head; ifaddr != nil; ifaddr = ifaddr.next { + adapter_name := string(ifaddr.name) + + /* + Check if we have seen this interface name before so we can reuse the `Network_Interface`. + Else, create a new one. + */ + if adapter_name not_in ifaces { + ifaces[adapter_name] = new(Network_Interface) + ifaces[adapter_name].adapter_name = strings.clone(adapter_name) + } + iface := ifaces[adapter_name] + + address: Address + netmask: Netmask + + if ifaddr.address != nil { + switch int(ifaddr.address.sa_family) { + case os.AF_INET, os.AF_INET6: + address = sockaddr_to_endpoint(ifaddr.address).address + + case os.AF_PACKET: + /* + For some obscure reason the 64-bit `getifaddrs` calls returns a pointer to a + 32-bit `RTNL_LINK_STATS` structure, which of course means that tx/rx byte count + is truncated beyond usefulness. + + We're not going to retrieve stats now. Instead this serves as a reminder to use + the NETLINK protocol for this purpose. + + But in case you were curious: + stats := transmute(^os.rtnl_link_stats)ifaddr.data + fmt.println(stats) + */ + case: + } + } + + if ifaddr.netmask != nil { + switch int(ifaddr.netmask.sa_family) { + case os.AF_INET, os.AF_INET6: + netmask = Netmask(sockaddr_to_endpoint(ifaddr.netmask).address) + case: + } + } + + if ifaddr.broadcast_or_dest != nil && .BROADCAST in ifaddr.flags { + switch int(ifaddr.broadcast_or_dest.sa_family) { + case os.AF_INET, os.AF_INET6: + broadcast := sockaddr_to_endpoint(ifaddr.broadcast_or_dest).address + append(&iface.multicast, broadcast) + case: + } + } + + if address != nil { + lease := Lease{ + address = address, + netmask = netmask, + } + append(&iface.unicast, lease) + } + + /* + TODO: Refine this based on the type of adapter. + */ + state := Link_State{} + + if .UP in ifaddr.flags { + state |= {.Up} + } + + if .DORMANT in ifaddr.flags { + state |= {.Dormant} + } + + if .LOOPBACK in ifaddr.flags { + state |= {.Loopback} + } + + iface.link.state = state + } + + /* + Free the OS structures. + */ + os._freeifaddrs(head) + + /* + Turn the map into a slice to return. + */ + _interfaces := make([dynamic]Network_Interface, 0, allocator) + for _, iface in ifaces { + append(&_interfaces, iface^) + free(iface) + } + + return _interfaces[:], {} +}
\ No newline at end of file diff --git a/core/net/interface_windows.odin b/core/net/interface_windows.odin new file mode 100644 index 000000000..57cc0e8ef --- /dev/null +++ b/core/net/interface_windows.odin @@ -0,0 +1,182 @@ +//+build windows +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import sys "core:sys/windows" +import win32 "core:sys/win32" +import strings "core:strings" + +MAX_INTERFACE_ENUMERATION_TRIES :: 3 + +/* + `enumerate_interfaces` retrieves a list of network interfaces with their associated properties. +*/ +enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Network_Error) { + context.allocator = allocator + + buf: []u8 + defer delete(buf) + buf_size: u32 + + res: u32 + + gaa: for _ in 1..=MAX_INTERFACE_ENUMERATION_TRIES { + res = sys.get_adapters_addresses( + .Unspecified, // Return both IPv4 and IPv6 adapters. + sys.GAA_Flags{ + .Include_Prefix, // (XP SP1+) Return a list of IP address prefixes on this adapter. When this flag is set, IP address prefixes are returned for both IPv6 and IPv4 addresses. + .Include_Gateways, // (Vista+) Return the addresses of default gateways. + .Include_Tunnel_Binding_Order, // (Vista+) Return the adapter addresses sorted in tunnel binding order. + }, + nil, // Reserved + (^sys.IP_Adapter_Addresses)(raw_data(buf)), + &buf_size, + ) + + switch res { + case 111: // ERROR_BUFFER_OVERFLOW: + delete(buf) + buf = make([]u8, buf_size) + case 0: + break gaa + case: + return {}, Platform_Error(res) + } + } + + if res != 0 { + return {}, .Unable_To_Enumerate_Network_Interfaces + } + + _interfaces := make([dynamic]Network_Interface, 0, allocator) + + for adapter := (^sys.IP_Adapter_Addresses)(raw_data(buf)); adapter != nil; adapter = adapter.Next { + interface := Network_Interface{ + adapter_name = strings.clone(string(adapter.AdapterName)), + friendly_name = wstring_to_string(adapter.FriendlyName), + description = wstring_to_string(adapter.Description), + dns_suffix = wstring_to_string(adapter.DnsSuffix), + + mtu = adapter.MTU, + + link = { + transmit_speed = adapter.TransmitLinkSpeed, + receive_speed = adapter.ReceiveLinkSpeed, + }, + } + + if adapter.PhysicalAddressLength > 0 && adapter.PhysicalAddressLength <= len(adapter.PhysicalAddress) { + interface.physical_address = physical_address_to_string(adapter.PhysicalAddress[:adapter.PhysicalAddressLength]) + } + + for u_addr := (^sys.IP_ADAPTER_UNICAST_ADDRESS_LH)(adapter.FirstUnicastAddress); u_addr != nil; u_addr = u_addr.Next { + win_addr := parse_socket_address(u_addr.Address) + + lease := Lease{ + address = win_addr.address, + origin = { + prefix = Prefix_Origin(u_addr.PrefixOrigin), + suffix = Suffix_Origin(u_addr.SuffixOrigin), + }, + lifetime = { + valid = u_addr.ValidLifetime, + preferred = u_addr.PreferredLifetime, + lease = u_addr.LeaseLifetime, + }, + address_duplication = Address_Duplication(u_addr.DadState), + } + append(&interface.unicast, lease) + } + + for a_addr := (^sys.IP_ADAPTER_ANYCAST_ADDRESS_XP)(adapter.FirstAnycastAddress); a_addr != nil; a_addr = a_addr.Next { + addr := parse_socket_address(a_addr.Address) + append(&interface.anycast, addr.address) + } + + for m_addr := (^sys.IP_ADAPTER_MULTICAST_ADDRESS_XP)(adapter.FirstMulticastAddress); m_addr != nil; m_addr = m_addr.Next { + addr := parse_socket_address(m_addr.Address) + append(&interface.multicast, addr.address) + } + + for g_addr := (^sys.IP_ADAPTER_GATEWAY_ADDRESS_LH)(adapter.FirstGatewayAddress); g_addr != nil; g_addr = g_addr.Next { + addr := parse_socket_address(g_addr.Address) + append(&interface.gateways, addr.address) + } + + interface.dhcp_v4 = parse_socket_address(adapter.Dhcpv4Server).address + interface.dhcp_v6 = parse_socket_address(adapter.Dhcpv6Server).address + + switch adapter.OperStatus { + case .Up: interface.link.state = {.Up} + case .Down: interface.link.state = {.Down} + case .Testing: interface.link.state = {.Testing} + case .Dormant: interface.link.state = {.Dormant} + case .NotPresent: interface.link.state = {.Not_Present} + case .LowerLayerDown: interface.link.state = {.Lower_Layer_Down} + case .Unknown: fallthrough + case: interface.link.state = {} + } + + interface.tunnel_type = Tunnel_Type(adapter.TunnelType) + + append(&_interfaces, interface) + } + + return _interfaces[:], {} +} + +/* + Takes a UTF-16 Wstring and clones it. +*/ +wstring_to_string :: proc(s: ^u16, max_size := 256, allocator := context.allocator) -> (res: string) { + temp := win32.wstring_to_utf8((win32.Wstring)(s), max_size, context.temp_allocator) + return strings.clone(temp[:len(temp)], allocator) +} + +/* + Interpret SOCKET_ADDRESS as an Address +*/ +parse_socket_address :: proc(addr_in: sys.SOCKET_ADDRESS) -> (addr: Endpoint) { + if addr_in.lpSockaddr == nil { + return // Empty or invalid address type + } + + sock := addr_in.lpSockaddr^ + + switch sock.sa_family { + case u16(sys.AF_INET): + win_addr := cast(^sys.sockaddr_in)addr_in.lpSockaddr + port := int(win_addr.sin_port) + return Endpoint { + address = IP4_Address(transmute([4]byte)win_addr.sin_addr), + port = port, + } + + case u16(sys.AF_INET6): + win_addr := cast(^sys.sockaddr_in6)addr_in.lpSockaddr + port := int(win_addr.sin6_port) + return Endpoint { + address = IP6_Address(transmute([8]u16be)win_addr.sin6_addr), + port = port, + } + + + case: return // Empty or invalid address type + } + unreachable() +}
\ No newline at end of file diff --git a/core/net/socket.odin b/core/net/socket.odin new file mode 100644 index 000000000..1fa57aac0 --- /dev/null +++ b/core/net/socket.odin @@ -0,0 +1,87 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +// +// TODO(tetra): Bluetooth, Raw +// + +any_socket_to_socket :: proc(any_socket: Any_Socket) -> Socket { + switch s in any_socket { + case TCP_Socket: return Socket(s) + case UDP_Socket: return Socket(s) + case: + return Socket({}) + } +} + +/* + Expects both hostname and port to be present in the `hostname_and_port` parameter, either as: + `a.host.name:9999`, or as `1.2.3.4:9999`, or IP6 equivalent. + + Calls `parse_hostname_or_endpoint` and `resolve`, then `dial_tcp_from_endpoint`. +*/ +dial_tcp_from_hostname_and_port_string :: proc(hostname_and_port: string, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) { + target := parse_hostname_or_endpoint(hostname_and_port) or_return + switch t in target { + case Endpoint: + return dial_tcp_from_endpoint(t, options) + case Host: + if t.port == 0 { + return 0, .Port_Required + } + ep4, ep6 := resolve(t.hostname) or_return + ep := ep4 if ep4.address != nil else ep6 // NOTE(tetra): We don't know what family the server uses, so we just default to IP4. + ep.port = t.port + return dial_tcp_from_endpoint(ep, options) + } + unreachable() +} + +/* + Expects the `hostname` as a string and `port` as a `int`. + `parse_hostname_or_endpoint` is called and the `hostname` will be resolved into an IP. + + If a `hostname` of form `a.host.name:9999` is given, the port will be ignored in favor of the explicit `port` param. +*/ +dial_tcp_from_hostname_string_and_explicit_port :: proc(hostname: string, port: int, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) { + target := parse_hostname_or_endpoint(hostname) or_return + switch t in target { + case Endpoint: + return dial_tcp_from_endpoint({t.address, port}, options) + case Host: + if port == 0 { + return 0, .Port_Required + } + ep4, ep6 := resolve(t.hostname) or_return + ep := ep4 if ep4.address != nil else ep6 // NOTE(tetra): We don't know what family the server uses, so we just default to IP4. + ep.port = port + return dial_tcp_from_endpoint(ep, options) + } + unreachable() +} + +dial_tcp_from_address_and_port :: proc(address: Address, port: int, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) { + return dial_tcp_from_endpoint({address, port}, options) +} + +dial_tcp :: proc{ + dial_tcp_from_endpoint, + dial_tcp_from_address_and_port, + dial_tcp_from_hostname_and_port_string, + dial_tcp_from_hostname_string_and_explicit_port, +}
\ No newline at end of file diff --git a/core/net/socket_darwin.odin b/core/net/socket_darwin.odin new file mode 100644 index 000000000..89ad7b31c --- /dev/null +++ b/core/net/socket_darwin.odin @@ -0,0 +1,513 @@ +// +build darwin +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:c" +import "core:os" +import "core:time" + +Platform_Socket :: os.Socket + +Create_Socket_Error :: enum c.int { + Family_Not_Supported_For_This_Socket = c.int(os.EAFNOSUPPORT), + No_Socket_Descriptors_Available = c.int(os.EMFILE), + No_Buffer_Space_Available = c.int(os.ENOBUFS), + No_Memory_Available_Available = c.int(os.ENOMEM), + Protocol_Unsupported_By_System = c.int(os.EPROTONOSUPPORT), + Wrong_Protocol_For_Socket = c.int(os.EPROTONOSUPPORT), + Family_And_Socket_Type_Mismatch = c.int(os.EPROTONOSUPPORT), +} + +create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) { + c_type, c_protocol, c_family: int + + switch family { + case .IP4: c_family = os.AF_INET + case .IP6: c_family = os.AF_INET6 + case: + unreachable() + } + + switch protocol { + case .TCP: c_type = os.SOCK_STREAM; c_protocol = os.IPPROTO_TCP + case .UDP: c_type = os.SOCK_DGRAM; c_protocol = os.IPPROTO_UDP + case: + unreachable() + } + + sock, ok := os.socket(c_family, c_type, c_protocol) + if ok != os.ERROR_NONE { + err = Create_Socket_Error(ok) + return + } + + switch protocol { + case .TCP: return TCP_Socket(sock), nil + case .UDP: return UDP_Socket(sock), nil + case: + unreachable() + } +} + + +Dial_Error :: enum c.int { + Port_Required = -1, + + Address_In_Use = c.int(os.EADDRINUSE), + In_Progress = c.int(os.EINPROGRESS), + Cannot_Use_Any_Address = c.int(os.EADDRNOTAVAIL), + Wrong_Family_For_Socket = c.int(os.EAFNOSUPPORT), + Refused = c.int(os.ECONNREFUSED), + Is_Listening_Socket = c.int(os.EACCES), + Already_Connected = c.int(os.EISCONN), + Network_Unreachable = c.int(os.ENETUNREACH), // Device is offline + Host_Unreachable = c.int(os.EHOSTUNREACH), // Remote host cannot be reached + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Not_Socket = c.int(os.ENOTSOCK), + Timeout = c.int(os.ETIMEDOUT), + Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata? +} + +dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) { + if endpoint.port == 0 { + return 0, .Port_Required + } + + family := family_from_endpoint(endpoint) + sock := create_socket(family, .TCP) or_return + skt = sock.(TCP_Socket) + + // NOTE(tetra): This is so that if we crash while the socket is open, we can + // bypass the cooldown period, and allow the next run of the program to + // use the same address immediately. + _ = set_option(skt, .Reuse_Address, true) + + sockaddr := endpoint_to_sockaddr(endpoint) + res := os.connect(Platform_Socket(skt), (^os.SOCKADDR)(&sockaddr), i32(sockaddr.len)) + if res != os.ERROR_NONE { + err = Dial_Error(res) + return + } + + return +} + + +Bind_Error :: enum c.int { + // Another application is currently bound to this endpoint. + Address_In_Use = c.int(os.EADDRINUSE), + // The address is not a local address on this machine. + Given_Nonlocal_Address = c.int(os.EADDRNOTAVAIL), + // To bind a UDP socket to the broadcast address, the appropriate socket option must be set. + Broadcast_Disabled = c.int(os.EACCES), + // The address family of the address does not match that of the socket. + Address_Family_Mismatch = c.int(os.EFAULT), + // The socket is already bound to an address. + Already_Bound = c.int(os.EINVAL), + // There are not enough ephemeral ports available. + No_Ports_Available = c.int(os.ENOBUFS), +} + +bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Network_Error) { + sockaddr := endpoint_to_sockaddr(ep) + s := any_socket_to_socket(skt) + res := os.bind(Platform_Socket(s), (^os.SOCKADDR)(&sockaddr), i32(sockaddr.len)) + if res != os.ERROR_NONE { + err = Bind_Error(res) + } + return +} + + +// This type of socket becomes bound when you try to send data. +// This is likely what you want if you want to send data unsolicited. +// +// This is like a client TCP socket, except that it can send data to any remote endpoint without needing to establish a connection first. +make_unbound_udp_socket :: proc(family: Address_Family) -> (skt: UDP_Socket, err: Network_Error) { + sock := create_socket(family, .UDP) or_return + skt = sock.(UDP_Socket) + return +} + +// This type of socket is bound immediately, which enables it to receive data on the port. +// Since it's UDP, it's also able to send data without receiving any first. +// +// This is like a listening TCP socket, except that data packets can be sent and received without needing to establish a connection first. +// +// The bound_address is the address of the network interface that you want to use, or a loopback address if you don't care which to use. +make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (skt: UDP_Socket, err: Network_Error) { + skt = make_unbound_udp_socket(family_from_address(bound_address)) or_return + bind(skt, {bound_address, port}) or_return + return +} + + + +Listen_Error :: enum c.int { + Address_In_Use = c.int(os.EADDRINUSE), + Already_Connected = c.int(os.EISCONN), + No_Socket_Descriptors_Available = c.int(os.EMFILE), + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Nonlocal_Address = c.int(os.EADDRNOTAVAIL), + Not_Socket = c.int(os.ENOTSOCK), + Listening_Not_Supported_For_This_Socket = c.int(os.EOPNOTSUPP), +} + +listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_Socket, err: Network_Error) { + assert(backlog > 0 && i32(backlog) < max(i32)) + + family := family_from_endpoint(interface_endpoint) + sock := create_socket(family, .TCP) or_return + skt = sock.(TCP_Socket) + + // NOTE(tetra): This is so that if we crash while the socket is open, we can + // bypass the cooldown period, and allow the next run of the program to + // use the same address immediately. + // + // TODO(tetra, 2022-02-15): Confirm that this doesn't mean other processes can hijack the address! + set_option(sock, .Reuse_Address, true) or_return + + bind(sock, interface_endpoint) or_return + + res := os.listen(Platform_Socket(skt), backlog) + if res != os.ERROR_NONE { + err = Listen_Error(res) + return + } + + return +} + + + +Accept_Error :: enum c.int { + Reset = c.int(os.ECONNRESET), // TODO(tetra): Is this error actually possible here? Or is like Linux, in which case we can remove it. + Not_Listening = c.int(os.EINVAL), + No_Socket_Descriptors_Available_For_Client_Socket = c.int(os.EMFILE), + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Not_Socket = c.int(os.ENOTSOCK), + Not_Connection_Oriented_Socket = c.int(os.EOPNOTSUPP), + Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata? +} + +accept_tcp :: proc(sock: TCP_Socket) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) { + sockaddr: os.SOCKADDR_STORAGE_LH + sockaddrlen := c.int(size_of(sockaddr)) + + client_sock, ok := os.accept(Platform_Socket(sock), cast(^os.SOCKADDR) &sockaddr, &sockaddrlen) + if ok != os.ERROR_NONE { + err = Accept_Error(ok) + return + } + client = TCP_Socket(client_sock) + source = sockaddr_to_endpoint(&sockaddr) + return +} + + + +close :: proc(skt: Any_Socket) { + s := any_socket_to_socket(skt) + os.close(os.Handle(Platform_Socket(s))) +} + + + +TCP_Recv_Error :: enum c.int { + Shutdown = c.int(os.ESHUTDOWN), + Not_Connected = c.int(os.ENOTCONN), + Connection_Broken = c.int(os.ENETRESET), // TODO(tetra): Is this error actually possible here? + Not_Socket = c.int(os.ENOTSOCK), + Aborted = c.int(os.ECONNABORTED), + Connection_Closed = c.int(os.ECONNRESET), // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them? + Offline = c.int(os.ENETDOWN), + Host_Unreachable = c.int(os.EHOSTUNREACH), + Interrupted = c.int(os.EINTR), + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... +} + +recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) { + if len(buf) <= 0 { + return + } + res, ok := os.recv(Platform_Socket(skt), buf, 0) + if ok != os.ERROR_NONE { + err = TCP_Recv_Error(ok) + return + } + return int(res), nil +} + +UDP_Recv_Error :: enum c.int { + // The buffer is too small to fit the entire message, and the message was truncated. + Truncated = c.int(os.EMSGSIZE), + // The so-called socket is not an open socket. + Not_Socket = c.int(os.ENOTSOCK), + // The so-called socket is, in fact, not even a valid descriptor. + Not_Descriptor = c.int(os.EBADF), + // The buffer did not point to a valid location in memory. + Bad_Buffer = c.int(os.EFAULT), + // A signal occurred before any data was transmitted. + // See signal(7). + Interrupted = c.int(os.EINTR), + // The send timeout duration passed before all data was sent. + // See Socket_Option.Send_Timeout. + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... + // The socket must be bound for this operation, but isn't. + Socket_Not_Bound = c.int(os.EINVAL), +} + +recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) { + if len(buf) <= 0 { + return + } + + from: os.SOCKADDR_STORAGE_LH + fromsize := c.int(size_of(from)) + res, ok := os.recvfrom(Platform_Socket(skt), buf, 0, cast(^os.SOCKADDR) &from, &fromsize) + if ok != os.ERROR_NONE { + err = UDP_Recv_Error(ok) + return + } + + bytes_read = int(res) + remote_endpoint = sockaddr_to_endpoint(&from) + return +} + +recv :: proc{recv_tcp, recv_udp} + + + +// TODO +TCP_Send_Error :: enum c.int { + Aborted = c.int(os.ECONNABORTED), // TODO: merge with other errors? + Connection_Closed = c.int(os.ECONNRESET), + Not_Connected = c.int(os.ENOTCONN), + Shutdown = c.int(os.ESHUTDOWN), + // The send queue was full. + // This is usually a transient issue. + // + // This also shouldn't normally happen on Linux, as data is dropped if it + // doesn't fit in the send queue. + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Offline = c.int(os.ENETDOWN), + Host_Unreachable = c.int(os.EHOSTUNREACH), + // A signal occurred before any data was transmitted. + // See signal(7). + Interrupted = c.int(os.EINTR), + // The send timeout duration passed before all data was sent. + // See Socket_Option.Send_Timeout. + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... + // The so-called socket is not an open socket. + Not_Socket = c.int(os.ENOTSOCK), +} + +// Repeatedly sends data until the entire buffer is sent. +// If a send fails before all data is sent, returns the amount +// sent up to that point. +send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) { + for bytes_written < len(buf) { + limit := min(int(max(i32)), len(buf) - bytes_written) + remaining := buf[bytes_written:][:limit] + res, ok := os.send(Platform_Socket(skt), remaining, 0) + if ok != os.ERROR_NONE { + err = TCP_Send_Error(ok) + return + } + bytes_written += int(res) + } + return +} + +// TODO +UDP_Send_Error :: enum c.int { + // The message is too big. No data was sent. + Truncated = c.int(os.EMSGSIZE), + // TODO: not sure what the exact circumstances for this is yet + Network_Unreachable = c.int(os.ENETUNREACH), + // There are no more emphemeral outbound ports available to bind the socket to, in order to send. + No_Outbound_Ports_Available = c.int(os.EAGAIN), + // The send timeout duration passed before all data was sent. + // See Socket_Option.Send_Timeout. + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... + // The so-called socket is not an open socket. + Not_Socket = c.int(os.ENOTSOCK), + // The so-called socket is, in fact, not even a valid descriptor. + Not_Descriptor = c.int(os.EBADF), + // The buffer did not point to a valid location in memory. + Bad_Buffer = c.int(os.EFAULT), + // A signal occurred before any data was transmitted. + // See signal(7). + Interrupted = c.int(os.EINTR), + // The send queue was full. + // This is usually a transient issue. + // + // This also shouldn't normally happen on Linux, as data is dropped if it + // doesn't fit in the send queue. + No_Buffer_Space_Available = c.int(os.ENOBUFS), + // No memory was available to properly manage the send queue. + No_Memory_Available = c.int(os.ENOMEM), +} + +send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) { + toaddr := endpoint_to_sockaddr(to) + for bytes_written < len(buf) { + limit := min(1<<31, len(buf) - bytes_written) + remaining := buf[bytes_written:][:limit] + res, ok := os.sendto(Platform_Socket(skt), remaining, 0, cast(^os.SOCKADDR)&toaddr, i32(toaddr.len)) + if ok != os.ERROR_NONE { + err = UDP_Send_Error(ok) + return + } + bytes_written += int(res) + } + return +} + +send :: proc{send_tcp, send_udp} + + + + +Shutdown_Manner :: enum c.int { + Receive = c.int(os.SHUT_RD), + Send = c.int(os.SHUT_WR), + Both = c.int(os.SHUT_RDWR), +} + +Shutdown_Error :: enum c.int { + Aborted = c.int(os.ECONNABORTED), + Reset = c.int(os.ECONNRESET), + Offline = c.int(os.ENETDOWN), + Not_Connected = c.int(os.ENOTCONN), + Not_Socket = c.int(os.ENOTSOCK), + Invalid_Manner = c.int(os.EINVAL), +} + +shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) { + s := any_socket_to_socket(skt) + res := os.shutdown(Platform_Socket(s), int(manner)) + if res != os.ERROR_NONE { + return Shutdown_Error(res) + } + return +} + + + + +Socket_Option :: enum c.int { + Reuse_Address = c.int(os.SO_REUSEADDR), + Keep_Alive = c.int(os.SO_KEEPALIVE), + Out_Of_Bounds_Data_Inline = c.int(os.SO_OOBINLINE), + TCP_Nodelay = c.int(os.TCP_NODELAY), + + Linger = c.int(os.SO_LINGER), + + Receive_Buffer_Size = c.int(os.SO_RCVBUF), + Send_Buffer_Size = c.int(os.SO_SNDBUF), + Receive_Timeout = c.int(os.SO_RCVTIMEO), + Send_Timeout = c.int(os.SO_SNDTIMEO), +} + +Socket_Option_Error :: enum c.int { + Offline = c.int(os.ENETDOWN), + Timeout_When_Keepalive_Set = c.int(os.ENETRESET), + Invalid_Option_For_Socket = c.int(os.ENOPROTOOPT), + Reset_When_Keepalive_Set = c.int(os.ENOTCONN), + Not_Socket = c.int(os.ENOTSOCK), +} + +set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error { + level := os.SOL_SOCKET if option != .TCP_Nodelay else os.IPPROTO_TCP + + // NOTE(tetra, 2022-02-15): On Linux, you cannot merely give a single byte for a bool; + // it _has_ to be a b32. + // I haven't tested if you can give more than that. + bool_value: b32 + int_value: i32 + timeval_value: os.Timeval + + ptr: rawptr + len: os.socklen_t + + switch option { + case + .Reuse_Address, + .Keep_Alive, + .Out_Of_Bounds_Data_Inline, + .TCP_Nodelay: + // TODO: verify whether these are options or not on Linux + // .Broadcast, + // .Conditional_Accept, + // .Dont_Linger: + switch x in value { + case bool, b8: + x2 := x + bool_value = b32((^bool)(&x2)^) + case b16: + bool_value = b32(x) + case b32: + bool_value = b32(x) + case b64: + bool_value = b32(x) + case: + panic("set_option() value must be a boolean here", loc) + } + ptr = &bool_value + len = size_of(bool_value) + case + .Linger, + .Send_Timeout, + .Receive_Timeout: + t, ok := value.(time.Duration) + if !ok do panic("set_option() value must be a time.Duration here", loc) + + nanos := time.duration_nanoseconds(t) + timeval_value.nanoseconds = int(nanos % 1e9) + timeval_value.seconds = (nanos - i64(timeval_value.nanoseconds)) / 1e9 + + ptr = &timeval_value + len = size_of(timeval_value) + case + .Receive_Buffer_Size, + .Send_Buffer_Size: + // TODO: check for out of range values and return .Value_Out_Of_Range? + switch i in value { + case i8, u8: i2 := i; int_value = os.socklen_t((^u8)(&i2)^) + case i16, u16: i2 := i; int_value = os.socklen_t((^u16)(&i2)^) + case i32, u32: i2 := i; int_value = os.socklen_t((^u32)(&i2)^) + case i64, u64: i2 := i; int_value = os.socklen_t((^u64)(&i2)^) + case i128, u128: i2 := i; int_value = os.socklen_t((^u128)(&i2)^) + case int, uint: i2 := i; int_value = os.socklen_t((^uint)(&i2)^) + case: + panic("set_option() value must be an integer here", loc) + } + ptr = &int_value + len = size_of(int_value) + } + + skt := any_socket_to_socket(s) + res := os.setsockopt(Platform_Socket(skt), int(level), int(option), ptr, len) + if res != os.ERROR_NONE { + return Socket_Option_Error(res) + } + + return nil +} diff --git a/core/net/socket_linux.odin b/core/net/socket_linux.odin new file mode 100644 index 000000000..dcc48d3fa --- /dev/null +++ b/core/net/socket_linux.odin @@ -0,0 +1,532 @@ +// +build linux +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:c" +import "core:os" +import "core:time" + +Platform_Socket :: os.Socket + +Create_Socket_Error :: enum c.int { + Family_Not_Supported_For_This_Socket = c.int(os.EAFNOSUPPORT), + No_Socket_Descriptors_Available = c.int(os.EMFILE), + No_Buffer_Space_Available = c.int(os.ENOBUFS), + No_Memory_Available_Available = c.int(os.ENOMEM), + Protocol_Unsupported_By_System = c.int(os.EPROTONOSUPPORT), + Wrong_Protocol_For_Socket = c.int(os.EPROTONOSUPPORT), + Family_And_Socket_Type_Mismatch = c.int(os.EPROTONOSUPPORT), +} + +create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) { + c_type, c_protocol, c_family: int + + switch family { + case .IP4: c_family = os.AF_INET + case .IP6: c_family = os.AF_INET6 + case: + unreachable() + } + + switch protocol { + case .TCP: c_type = os.SOCK_STREAM; c_protocol = os.IPPROTO_TCP + case .UDP: c_type = os.SOCK_DGRAM; c_protocol = os.IPPROTO_UDP + case: + unreachable() + } + + sock, ok := os.socket(c_family, c_type, c_protocol) + if ok != os.ERROR_NONE { + err = Create_Socket_Error(ok) + return + } + + switch protocol { + case .TCP: return TCP_Socket(sock), nil + case .UDP: return UDP_Socket(sock), nil + case: + unreachable() + } +} + + +Dial_Error :: enum c.int { + Port_Required = -1, + + Address_In_Use = c.int(os.EADDRINUSE), + In_Progress = c.int(os.EINPROGRESS), + Cannot_Use_Any_Address = c.int(os.EADDRNOTAVAIL), + Wrong_Family_For_Socket = c.int(os.EAFNOSUPPORT), + Refused = c.int(os.ECONNREFUSED), + Is_Listening_Socket = c.int(os.EACCES), + Already_Connected = c.int(os.EISCONN), + Network_Unreachable = c.int(os.ENETUNREACH), // Device is offline + Host_Unreachable = c.int(os.EHOSTUNREACH), // Remote host cannot be reached + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Not_Socket = c.int(os.ENOTSOCK), + Timeout = c.int(os.ETIMEDOUT), + Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata? +} + +dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) { + if endpoint.port == 0 { + return 0, .Port_Required + } + + family := family_from_endpoint(endpoint) + sock := create_socket(family, .TCP) or_return + skt = sock.(TCP_Socket) + + // NOTE(tetra): This is so that if we crash while the socket is open, we can + // bypass the cooldown period, and allow the next run of the program to + // use the same address immediately. + _ = set_option(skt, .Reuse_Address, true) + + sockaddr := endpoint_to_sockaddr(endpoint) + res := os.connect(Platform_Socket(skt), (^os.SOCKADDR)(&sockaddr), size_of(sockaddr)) + if res != os.ERROR_NONE { + err = Dial_Error(res) + return + } + + if options.no_delay { + _ = set_option(sock, .TCP_Nodelay, true) // NOTE(tetra): Not vital to succeed; error ignored + } + + return +} + + +Bind_Error :: enum c.int { + // Another application is currently bound to this endpoint. + Address_In_Use = c.int(os.EADDRINUSE), + // The address is not a local address on this machine. + Given_Nonlocal_Address = c.int(os.EADDRNOTAVAIL), + // To bind a UDP socket to the broadcast address, the appropriate socket option must be set. + Broadcast_Disabled = c.int(os.EACCES), + // The address family of the address does not match that of the socket. + Address_Family_Mismatch = c.int(os.EFAULT), + // The socket is already bound to an address. + Already_Bound = c.int(os.EINVAL), + // There are not enough ephemeral ports available. + No_Ports_Available = c.int(os.ENOBUFS), +} + +bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Network_Error) { + sockaddr := endpoint_to_sockaddr(ep) + s := any_socket_to_socket(skt) + res := os.bind(Platform_Socket(s), (^os.SOCKADDR)(&sockaddr), size_of(sockaddr)) + if res != os.ERROR_NONE { + err = Bind_Error(res) + } + return +} + + +// This type of socket becomes bound when you try to send data. +// This is likely what you want if you want to send data unsolicited. +// +// This is like a client TCP socket, except that it can send data to any remote endpoint without needing to establish a connection first. +make_unbound_udp_socket :: proc(family: Address_Family) -> (skt: UDP_Socket, err: Network_Error) { + sock := create_socket(family, .UDP) or_return + skt = sock.(UDP_Socket) + return +} + +// This type of socket is bound immediately, which enables it to receive data on the port. +// Since it's UDP, it's also able to send data without receiving any first. +// +// This is like a listening TCP socket, except that data packets can be sent and received without needing to establish a connection first. +// +// The bound_address is the address of the network interface that you want to use, or a loopback address if you don't care which to use. +make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (skt: UDP_Socket, err: Network_Error) { + skt = make_unbound_udp_socket(family_from_address(bound_address)) or_return + bind(skt, {bound_address, port}) or_return + return +} + + + +Listen_Error :: enum c.int { + Address_In_Use = c.int(os.EADDRINUSE), + Already_Connected = c.int(os.EISCONN), + No_Socket_Descriptors_Available = c.int(os.EMFILE), + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Nonlocal_Address = c.int(os.EADDRNOTAVAIL), + Not_Socket = c.int(os.ENOTSOCK), + Listening_Not_Supported_For_This_Socket = c.int(os.EOPNOTSUPP), +} + +listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_Socket, err: Network_Error) { + assert(backlog > 0 && i32(backlog) < max(i32)) + + family := family_from_endpoint(interface_endpoint) + sock := create_socket(family, .TCP) or_return + skt = sock.(TCP_Socket) + + // NOTE(tetra): This is so that if we crash while the socket is open, we can + // bypass the cooldown period, and allow the next run of the program to + // use the same address immediately. + // + // TODO(tetra, 2022-02-15): Confirm that this doesn't mean other processes can hijack the address! + set_option(sock, .Reuse_Address, true) or_return + + bind(sock, interface_endpoint) or_return + + res := os.listen(Platform_Socket(skt), backlog) + if res != os.ERROR_NONE { + err = Listen_Error(res) + return + } + + return +} + + + +Accept_Error :: enum c.int { + Not_Listening = c.int(os.EINVAL), + No_Socket_Descriptors_Available_For_Client_Socket = c.int(os.EMFILE), + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Not_Socket = c.int(os.ENOTSOCK), + Not_Connection_Oriented_Socket = c.int(os.EOPNOTSUPP), + Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata? +} + +accept_tcp :: proc(sock: TCP_Socket, options := default_tcp_options) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) { + sockaddr: os.SOCKADDR_STORAGE_LH + sockaddrlen := c.int(size_of(sockaddr)) + + client_sock, ok := os.accept(Platform_Socket(sock), cast(^os.SOCKADDR) &sockaddr, &sockaddrlen) + if ok != os.ERROR_NONE { + err = Accept_Error(ok) + return + } + client = TCP_Socket(client_sock) + source = sockaddr_to_endpoint(&sockaddr) + if options.no_delay { + _ = set_option(client, .TCP_Nodelay, true) // NOTE(tetra): Not vital to succeed; error ignored + } + return +} + + + +close :: proc(skt: Any_Socket) { + s := any_socket_to_socket(skt) + os.close(os.Handle(Platform_Socket(s))) +} + + + +TCP_Recv_Error :: enum c.int { + Shutdown = c.int(os.ESHUTDOWN), + Not_Connected = c.int(os.ENOTCONN), + Connection_Broken = c.int(os.ENETRESET), + Not_Socket = c.int(os.ENOTSOCK), + Aborted = c.int(os.ECONNABORTED), + Connection_Closed = c.int(os.ECONNRESET), // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them? + Offline = c.int(os.ENETDOWN), + Host_Unreachable = c.int(os.EHOSTUNREACH), + Interrupted = c.int(os.EINTR), + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... +} + +recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) { + if len(buf) <= 0 { + return + } + res, ok := os.recv(Platform_Socket(skt), buf, 0) + if ok != os.ERROR_NONE { + err = TCP_Recv_Error(ok) + return + } + return int(res), nil +} + +UDP_Recv_Error :: enum c.int { + // The buffer is too small to fit the entire message, and the message was truncated. + // When this happens, the rest of message is lost. + Buffer_Too_Small = c.int(os.EMSGSIZE), + // The so-called socket is not an open socket. + Not_Socket = c.int(os.ENOTSOCK), + // The so-called socket is, in fact, not even a valid descriptor. + Not_Descriptor = c.int(os.EBADF), + // The buffer did not point to a valid location in memory. + Bad_Buffer = c.int(os.EFAULT), + // A signal occurred before any data was transmitted. + // See signal(7). + Interrupted = c.int(os.EINTR), + // The send timeout duration passed before all data was received. + // See Socket_Option.Receive_Timeout. + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... + // The socket must be bound for this operation, but isn't. + Socket_Not_Bound = c.int(os.EINVAL), +} + +recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) { + if len(buf) <= 0 { + return + } + + from: os.SOCKADDR_STORAGE_LH = --- + fromsize := c.int(size_of(from)) + + // NOTE(tetra): On Linux, if the buffer is too small to fit the entire datagram payload, the rest is silently discarded, + // and no error is returned. + // However, if you pass MSG_TRUNC here, 'res' will be the size of the incoming message, rather than how much was read. + // We can use this fact to detect this condition and return .Buffer_Too_Small. + res, ok := os.recvfrom(Platform_Socket(skt), buf, os.MSG_TRUNC, cast(^os.SOCKADDR) &from, &fromsize) + if ok != os.ERROR_NONE { + err = UDP_Recv_Error(ok) + return + } + + bytes_read = int(res) + remote_endpoint = sockaddr_to_endpoint(&from) + + if bytes_read > len(buf) { + // NOTE(tetra): The buffer has been filled, with a partial message. + bytes_read = len(buf) + err = .Buffer_Too_Small + } + + return +} + +recv :: proc{recv_tcp, recv_udp} + + + +// TODO +TCP_Send_Error :: enum c.int { + Aborted = c.int(os.ECONNABORTED), // TODO(tetra): merge with other errors? + Connection_Closed = c.int(os.ECONNRESET), + Not_Connected = c.int(os.ENOTCONN), + Shutdown = c.int(os.ESHUTDOWN), + // The send queue was full. + // This is usually a transient issue. + // + // This also shouldn't normally happen on Linux, as data is dropped if it + // doesn't fit in the send queue. + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Offline = c.int(os.ENETDOWN), + Host_Unreachable = c.int(os.EHOSTUNREACH), + // A signal occurred before any data was transmitted. + // See signal(7). + Interrupted = c.int(os.EINTR), + // The send timeout duration passed before all data was sent. + // See Socket_Option.Send_Timeout. + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... + // The so-called socket is not an open socket. + Not_Socket = c.int(os.ENOTSOCK), +} + +// Repeatedly sends data until the entire buffer is sent. +// If a send fails before all data is sent, returns the amount +// sent up to that point. +send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) { + for bytes_written < len(buf) { + limit := min(int(max(i32)), len(buf) - bytes_written) + remaining := buf[bytes_written:][:limit] + res, ok := os.send(Platform_Socket(skt), remaining, 0) + if ok != os.ERROR_NONE { + err = TCP_Send_Error(ok) + return + } + bytes_written += int(res) + } + return +} + +// TODO +UDP_Send_Error :: enum c.int { + // The message is too big. No data was sent. + Message_Too_Long = c.int(os.EMSGSIZE), + // TODO: not sure what the exact circumstances for this is yet + Network_Unreachable = c.int(os.ENETUNREACH), + // There are no more emphemeral outbound ports available to bind the socket to, in order to send. + No_Outbound_Ports_Available = c.int(os.EAGAIN), + // The send timeout duration passed before all data was sent. + // See Socket_Option.Send_Timeout. + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... + // The so-called socket is not an open socket. + Not_Socket = c.int(os.ENOTSOCK), + // The so-called socket is, in fact, not even a valid descriptor. + Not_Descriptor = c.int(os.EBADF), + // The buffer did not point to a valid location in memory. + Bad_Buffer = c.int(os.EFAULT), + // A signal occurred before any data was transmitted. + // See signal(7). + Interrupted = c.int(os.EINTR), + // The send queue was full. + // This is usually a transient issue. + // + // This also shouldn't normally happen on Linux, as data is dropped if it + // doesn't fit in the send queue. + No_Buffer_Space_Available = c.int(os.ENOBUFS), + // No memory was available to properly manage the send queue. + No_Memory_Available = c.int(os.ENOMEM), +} + +// Sends a single UDP datagram packet. +// +// Datagrams are limited in size; attempting to send more than this limit at once will result in a Message_Too_Long error. +// UDP packets are not guarenteed to be received in order. +send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) { + toaddr := endpoint_to_sockaddr(to) + res, os_err := os.sendto(Platform_Socket(skt), buf, 0, cast(^os.SOCKADDR) &toaddr, size_of(toaddr)) + if os_err != os.ERROR_NONE { + err = UDP_Send_Error(os_err) + return + } + bytes_written = int(res) + return +} + +send :: proc{send_tcp, send_udp} + + + + +Shutdown_Manner :: enum c.int { + Receive = c.int(os.SHUT_RD), + Send = c.int(os.SHUT_WR), + Both = c.int(os.SHUT_RDWR), +} + +Shutdown_Error :: enum c.int { + Aborted = c.int(os.ECONNABORTED), + Reset = c.int(os.ECONNRESET), + Offline = c.int(os.ENETDOWN), + Not_Connected = c.int(os.ENOTCONN), + Not_Socket = c.int(os.ENOTSOCK), + Invalid_Manner = c.int(os.EINVAL), +} + +shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) { + s := any_socket_to_socket(skt) + res := os.shutdown(Platform_Socket(s), int(manner)) + if res != os.ERROR_NONE { + return Shutdown_Error(res) + } + return +} + + + + +Socket_Option :: enum c.int { + Reuse_Address = c.int(os.SO_REUSEADDR), + Keep_Alive = c.int(os.SO_KEEPALIVE), + Out_Of_Bounds_Data_Inline = c.int(os.SO_OOBINLINE), + TCP_Nodelay = c.int(os.TCP_NODELAY), + + Linger = c.int(os.SO_LINGER), + + Receive_Buffer_Size = c.int(os.SO_RCVBUF), + Send_Buffer_Size = c.int(os.SO_SNDBUF), + Receive_Timeout = c.int(os.SO_RCVTIMEO_NEW), + Send_Timeout = c.int(os.SO_SNDTIMEO_NEW), +} + +Socket_Option_Error :: enum c.int { + Offline = c.int(os.ENETDOWN), + Timeout_When_Keepalive_Set = c.int(os.ENETRESET), + Invalid_Option_For_Socket = c.int(os.ENOPROTOOPT), + Reset_When_Keepalive_Set = c.int(os.ENOTCONN), + Not_Socket = c.int(os.ENOTSOCK), +} + +set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error { + level := os.SOL_SOCKET if option != .TCP_Nodelay else os.IPPROTO_TCP + + // NOTE(tetra, 2022-02-15): On Linux, you cannot merely give a single byte for a bool; + // it _has_ to be a b32. + // I haven't tested if you can give more than that. + bool_value: b32 + int_value: i32 + timeval_value: os.Timeval + + ptr: rawptr + len: os.socklen_t + + switch option { + case + .Reuse_Address, + .Keep_Alive, + .Out_Of_Bounds_Data_Inline, + .TCP_Nodelay: + // TODO: verify whether these are options or not on Linux + // .Broadcast, + // .Conditional_Accept, + // .Dont_Linger: + switch x in value { + case bool, b8: + x2 := x + bool_value = b32((^bool)(&x2)^) + case b16: + bool_value = b32(x) + case b32: + bool_value = b32(x) + case b64: + bool_value = b32(x) + case: + panic("set_option() value must be a boolean here", loc) + } + ptr = &bool_value + len = size_of(bool_value) + case + .Linger, + .Send_Timeout, + .Receive_Timeout: + t, ok := value.(time.Duration) + if !ok do panic("set_option() value must be a time.Duration here", loc) + + nanos := time.duration_nanoseconds(t) + timeval_value.nanoseconds = int(nanos % 1e9) + timeval_value.seconds = (nanos - i64(timeval_value.nanoseconds)) / 1e9 + + ptr = &timeval_value + len = size_of(timeval_value) + case + .Receive_Buffer_Size, + .Send_Buffer_Size: + // TODO: check for out of range values and return .Value_Out_Of_Range? + switch i in value { + case i8, u8: i2 := i; int_value = os.socklen_t((^u8)(&i2)^) + case i16, u16: i2 := i; int_value = os.socklen_t((^u16)(&i2)^) + case i32, u32: i2 := i; int_value = os.socklen_t((^u32)(&i2)^) + case i64, u64: i2 := i; int_value = os.socklen_t((^u64)(&i2)^) + case i128, u128: i2 := i; int_value = os.socklen_t((^u128)(&i2)^) + case int, uint: i2 := i; int_value = os.socklen_t((^uint)(&i2)^) + case: + panic("set_option() value must be an integer here", loc) + } + ptr = &int_value + len = size_of(int_value) + } + + skt := any_socket_to_socket(s) + res := os.setsockopt(Platform_Socket(skt), int(level), int(option), ptr, len) + if res != os.ERROR_NONE { + return Socket_Option_Error(res) + } + + return nil +}
\ No newline at end of file diff --git a/core/net/socket_openbsd.odin b/core/net/socket_openbsd.odin new file mode 100644 index 000000000..746b886ef --- /dev/null +++ b/core/net/socket_openbsd.odin @@ -0,0 +1,515 @@ +// +build openbsd +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. + + + IMPORTANT/TODO: This is a carbon copy of `socket_darwin.odin`. Adjust if necessary. + +*/ +package net + +import "core:c" +import "core:os" +import "core:time" + +Platform_Socket :: os.Socket + +Create_Socket_Error :: enum c.int { + Family_Not_Supported_For_This_Socket = c.int(os.EAFNOSUPPORT), + No_Socket_Descriptors_Available = c.int(os.EMFILE), + No_Buffer_Space_Available = c.int(os.ENOBUFS), + No_Memory_Available_Available = c.int(os.ENOMEM), + Protocol_Unsupported_By_System = c.int(os.EPROTONOSUPPORT), + Wrong_Protocol_For_Socket = c.int(os.EPROTONOSUPPORT), + Family_And_Socket_Type_Mismatch = c.int(os.EPROTONOSUPPORT), +} + +create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) { + c_type, c_protocol, c_family: int + + switch family { + case .IP4: c_family = os.AF_INET + case .IP6: c_family = os.AF_INET6 + case: + unreachable() + } + + switch protocol { + case .TCP: c_type = os.SOCK_STREAM; c_protocol = os.IPPROTO_TCP + case .UDP: c_type = os.SOCK_DGRAM; c_protocol = os.IPPROTO_UDP + case: + unreachable() + } + + sock, ok := os.socket(c_family, c_type, c_protocol) + if ok != os.ERROR_NONE { + err = Create_Socket_Error(ok) + return + } + + switch protocol { + case .TCP: return TCP_Socket(sock), nil + case .UDP: return UDP_Socket(sock), nil + case: + unreachable() + } +} + + +Dial_Error :: enum c.int { + Port_Required = -1, + + Address_In_Use = c.int(os.EADDRINUSE), + In_Progress = c.int(os.EINPROGRESS), + Cannot_Use_Any_Address = c.int(os.EADDRNOTAVAIL), + Wrong_Family_For_Socket = c.int(os.EAFNOSUPPORT), + Refused = c.int(os.ECONNREFUSED), + Is_Listening_Socket = c.int(os.EACCES), + Already_Connected = c.int(os.EISCONN), + Network_Unreachable = c.int(os.ENETUNREACH), // Device is offline + Host_Unreachable = c.int(os.EHOSTUNREACH), // Remote host cannot be reached + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Not_Socket = c.int(os.ENOTSOCK), + Timeout = c.int(os.ETIMEDOUT), + Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata? +} + +dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) { + if endpoint.port == 0 { + return 0, .Port_Required + } + + family := family_from_endpoint(endpoint) + sock := create_socket(family, .TCP) or_return + skt = sock.(TCP_Socket) + + // NOTE(tetra): This is so that if we crash while the socket is open, we can + // bypass the cooldown period, and allow the next run of the program to + // use the same address immediately. + _ = set_option(skt, .Reuse_Address, true) + + sockaddr := endpoint_to_sockaddr(endpoint) + res := os.connect(Platform_Socket(skt), (^os.SOCKADDR)(&sockaddr), size_of(sockaddr)) + if res != os.ERROR_NONE { + err = Dial_Error(res) + return + } + + return +} + + +Bind_Error :: enum c.int { + // Another application is currently bound to this endpoint. + Address_In_Use = c.int(os.EADDRINUSE), + // The address is not a local address on this machine. + Given_Nonlocal_Address = c.int(os.EADDRNOTAVAIL), + // To bind a UDP socket to the broadcast address, the appropriate socket option must be set. + Broadcast_Disabled = c.int(os.EACCES), + // The address family of the address does not match that of the socket. + Address_Family_Mismatch = c.int(os.EFAULT), + // The socket is already bound to an address. + Already_Bound = c.int(os.EINVAL), + // There are not enough ephemeral ports available. + No_Ports_Available = c.int(os.ENOBUFS), +} + +bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Network_Error) { + sockaddr := endpoint_to_sockaddr(ep) + s := any_socket_to_socket(skt) + res := os.bind(Platform_Socket(s), (^os.SOCKADDR)(&sockaddr), size_of(sockaddr)) + if res != os.ERROR_NONE { + err = Bind_Error(res) + } + return +} + + +// This type of socket becomes bound when you try to send data. +// This is likely what you want if you want to send data unsolicited. +// +// This is like a client TCP socket, except that it can send data to any remote endpoint without needing to establish a connection first. +make_unbound_udp_socket :: proc(family: Address_Family) -> (skt: UDP_Socket, err: Network_Error) { + sock := create_socket(family, .UDP) or_return + skt = sock.(UDP_Socket) + return +} + +// This type of socket is bound immediately, which enables it to receive data on the port. +// Since it's UDP, it's also able to send data without receiving any first. +// +// This is like a listening TCP socket, except that data packets can be sent and received without needing to establish a connection first. +// +// The bound_address is the address of the network interface that you want to use, or a loopback address if you don't care which to use. +make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (skt: UDP_Socket, err: Network_Error) { + skt = make_unbound_udp_socket(family_from_address(bound_address)) or_return + bind(skt, {bound_address, port}) or_return + return +} + + + +Listen_Error :: enum c.int { + Address_In_Use = c.int(os.EADDRINUSE), + Already_Connected = c.int(os.EISCONN), + No_Socket_Descriptors_Available = c.int(os.EMFILE), + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Nonlocal_Address = c.int(os.EADDRNOTAVAIL), + Not_Socket = c.int(os.ENOTSOCK), + Listening_Not_Supported_For_This_Socket = c.int(os.EOPNOTSUPP), +} + +listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_Socket, err: Network_Error) { + assert(backlog > 0 && i32(backlog) < max(i32)) + + family := family_from_endpoint(interface_endpoint) + sock := create_socket(family, .TCP) or_return + skt = sock.(TCP_Socket) + + // NOTE(tetra): This is so that if we crash while the socket is open, we can + // bypass the cooldown period, and allow the next run of the program to + // use the same address immediately. + // + // TODO(tetra, 2022-02-15): Confirm that this doesn't mean other processes can hijack the address! + set_option(sock, .Reuse_Address, true) or_return + + bind(sock, interface_endpoint) or_return + + res := os.listen(Platform_Socket(skt), backlog) + if res != os.ERROR_NONE { + err = Listen_Error(res) + return + } + + return +} + + + +Accept_Error :: enum c.int { + Reset = c.int(os.ECONNRESET), // TODO(tetra): Is this error actually possible here? Or is like Linux, in which case we can remove it. + Not_Listening = c.int(os.EINVAL), + No_Socket_Descriptors_Available_For_Client_Socket = c.int(os.EMFILE), + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Not_Socket = c.int(os.ENOTSOCK), + Not_Connection_Oriented_Socket = c.int(os.EOPNOTSUPP), + Would_Block = c.int(os.EWOULDBLOCK), // TODO: we may need special handling for this; maybe make a socket a struct with metadata? +} + +accept_tcp :: proc(sock: TCP_Socket) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) { + sockaddr: os.SOCKADDR_STORAGE_LH + sockaddrlen := c.int(size_of(sockaddr)) + + client_sock, ok := os.accept(Platform_Socket(sock), cast(^os.SOCKADDR) &sockaddr, &sockaddrlen) + if ok != os.ERROR_NONE { + err = Accept_Error(ok) + return + } + client = TCP_Socket(client_sock) + source = sockaddr_to_endpoint(&sockaddr) + return +} + + + +close :: proc(skt: Any_Socket) { + s := any_socket_to_socket(skt) + os.close(os.Handle(Platform_Socket(s))) +} + + + +TCP_Recv_Error :: enum c.int { + Shutdown = c.int(os.ESHUTDOWN), + Not_Connected = c.int(os.ENOTCONN), + Connection_Broken = c.int(os.ENETRESET), // TODO(tetra): Is this error actually possible here? + Not_Socket = c.int(os.ENOTSOCK), + Aborted = c.int(os.ECONNABORTED), + Connection_Closed = c.int(os.ECONNRESET), // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them? + Offline = c.int(os.ENETDOWN), + Host_Unreachable = c.int(os.EHOSTUNREACH), + Interrupted = c.int(os.EINTR), + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... +} + +recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) { + if len(buf) <= 0 { + return + } + res, ok := os.recv(Platform_Socket(skt), buf, 0) + if ok != os.ERROR_NONE { + err = TCP_Recv_Error(ok) + return + } + return int(res), nil +} + +UDP_Recv_Error :: enum c.int { + // The buffer is too small to fit the entire message, and the message was truncated. + Truncated = c.int(os.EMSGSIZE), + // The so-called socket is not an open socket. + Not_Socket = c.int(os.ENOTSOCK), + // The so-called socket is, in fact, not even a valid descriptor. + Not_Descriptor = c.int(os.EBADF), + // The buffer did not point to a valid location in memory. + Bad_Buffer = c.int(os.EFAULT), + // A signal occurred before any data was transmitted. + // See signal(7). + Interrupted = c.int(os.EINTR), + // The send timeout duration passed before all data was sent. + // See Socket_Option.Send_Timeout. + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... + // The socket must be bound for this operation, but isn't. + Socket_Not_Bound = c.int(os.EINVAL), +} + +recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) { + if len(buf) <= 0 { + return + } + + from: os.SOCKADDR_STORAGE_LH + fromsize := c.int(size_of(from)) + res, ok := os.recvfrom(Platform_Socket(skt), buf, 0, cast(^os.SOCKADDR) &from, &fromsize) + if ok != os.ERROR_NONE { + err = UDP_Recv_Error(ok) + return + } + + bytes_read = int(res) + remote_endpoint = sockaddr_to_endpoint(&from) + return +} + +recv :: proc{recv_tcp, recv_udp} + + + +// TODO +TCP_Send_Error :: enum c.int { + Aborted = c.int(os.ECONNABORTED), // TODO: merge with other errors? + Connection_Closed = c.int(os.ECONNRESET), + Not_Connected = c.int(os.ENOTCONN), + Shutdown = c.int(os.ESHUTDOWN), + // The send queue was full. + // This is usually a transient issue. + // + // This also shouldn't normally happen on Linux, as data is dropped if it + // doesn't fit in the send queue. + No_Buffer_Space_Available = c.int(os.ENOBUFS), + Offline = c.int(os.ENETDOWN), + Host_Unreachable = c.int(os.EHOSTUNREACH), + // A signal occurred before any data was transmitted. + // See signal(7). + Interrupted = c.int(os.EINTR), + // The send timeout duration passed before all data was sent. + // See Socket_Option.Send_Timeout. + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... +} + +// Repeatedly sends data until the entire buffer is sent. +// If a send fails before all data is sent, returns the amount +// sent up to that point. +send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) { + for bytes_written < len(buf) { + limit := min(int(max(i32)), len(buf) - bytes_written) + remaining := buf[bytes_written:][:limit] + res, ok := os.send(Platform_Socket(skt), remaining, 0) + if ok != os.ERROR_NONE { + err = TCP_Send_Error(ok) + return + } + bytes_written += int(res) + } + return +} + +// TODO +UDP_Send_Error :: enum c.int { + // The message is too big. No data was sent. + Truncated = c.int(os.EMSGSIZE), + // TODO: not sure what the exact circumstances for this is yet + Network_Unreachable = c.int(os.ENETUNREACH), + // There are no more emphemeral outbound ports available to bind the socket to, in order to send. + No_Outbound_Ports_Available = c.int(os.EAGAIN), + // The send timeout duration passed before all data was sent. + // See Socket_Option.Send_Timeout. + Timeout = c.int(os.EWOULDBLOCK), // NOTE: No, really. Presumably this means something different for nonblocking sockets... + // The so-called socket is not an open socket. + Not_Socket = c.int(os.ENOTSOCK), + // The so-called socket is, in fact, not even a valid descriptor. + Not_Descriptor = c.int(os.EBADF), + // The buffer did not point to a valid location in memory. + Bad_Buffer = c.int(os.EFAULT), + // A signal occurred before any data was transmitted. + // See signal(7). + Interrupted = c.int(os.EINTR), + // The send queue was full. + // This is usually a transient issue. + // + // This also shouldn't normally happen on Linux, as data is dropped if it + // doesn't fit in the send queue. + No_Buffer_Space_Available = c.int(os.ENOBUFS), + // No memory was available to properly manage the send queue. + No_Memory_Available = c.int(os.ENOMEM), +} + +send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) { + toaddr := endpoint_to_sockaddr(to) + for bytes_written < len(buf) { + limit := min(1<<31, len(buf) - bytes_written) + remaining := buf[bytes_written:][:limit] + res, ok := os.sendto(Platform_Socket(skt), remaining, 0, cast(^os.SOCKADDR)&toaddr, size_of(toaddr)) + if ok != os.ERROR_NONE { + err = UDP_Send_Error(ok) + return + } + bytes_written += int(res) + } + return +} + +send :: proc{send_tcp, send_udp} + + + + +Shutdown_Manner :: enum c.int { + Receive = c.int(os.SHUT_RD), + Send = c.int(os.SHUT_WR), + Both = c.int(os.SHUT_RDWR), +} + +Shutdown_Error :: enum c.int { + Aborted = c.int(os.ECONNABORTED), + Reset = c.int(os.ECONNRESET), + Offline = c.int(os.ENETDOWN), + Not_Connected = c.int(os.ENOTCONN), + Not_Socket = c.int(os.ENOTSOCK), + Invalid_Manner = c.int(os.EINVAL), +} + +shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) { + s := any_socket_to_socket(skt) + res := os.shutdown(Platform_Socket(s), int(manner)) + if res != os.ERROR_NONE { + return Shutdown_Error(res) + } + return +} + + + + +Socket_Option :: enum c.int { + Reuse_Address = c.int(os.SO_REUSEADDR), + Keep_Alive = c.int(os.SO_KEEPALIVE), + Out_Of_Bounds_Data_Inline = c.int(os.SO_OOBINLINE), + TCP_Nodelay = c.int(os.TCP_NODELAY), + + Linger = c.int(os.SO_LINGER), + + Receive_Buffer_Size = c.int(os.SO_RCVBUF), + Send_Buffer_Size = c.int(os.SO_SNDBUF), + Receive_Timeout = c.int(os.SO_RCVTIMEO), + Send_Timeout = c.int(os.SO_SNDTIMEO), +} + +Socket_Option_Error :: enum c.int { + Offline = c.int(os.ENETDOWN), + Timeout_When_Keepalive_Set = c.int(os.ENETRESET), + Invalid_Option_For_Socket = c.int(os.ENOPROTOOPT), + Reset_When_Keepalive_Set = c.int(os.ENOTCONN), + Not_Socket = c.int(os.ENOTSOCK), +} + +set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error { + level := os.SOL_SOCKET if option != .TCP_Nodelay else os.IPPROTO_TCP + + // NOTE(tetra, 2022-02-15): On Linux, you cannot merely give a single byte for a bool; + // it _has_ to be a b32. + // I haven't tested if you can give more than that. + bool_value: b32 + int_value: i32 + timeval_value: os.Timeval + + ptr: rawptr + len: os.socklen_t + + switch option { + case + .Reuse_Address, + .Keep_Alive, + .Out_Of_Bounds_Data_Inline, + .TCP_Nodelay: + // TODO: verify whether these are options or not on Linux + // .Broadcast, + // .Conditional_Accept, + // .Dont_Linger: + switch x in value { + case bool, b8: + x2 := x + bool_value = b32((^bool)(&x2)^) + case b16: + bool_value = b32(x) + case b32: + bool_value = b32(x) + case b64: + bool_value = b32(x) + case: + panic("set_option() value must be a boolean here", loc) + } + ptr = &bool_value + len = size_of(bool_value) + case + .Linger, + .Send_Timeout, + .Receive_Timeout: + t, ok := value.(time.Duration) + if !ok do panic("set_option() value must be a time.Duration here", loc) + + nanos := time.duration_nanoseconds(t) + timeval_value.nanoseconds = int(nanos % 1e9) + timeval_value.seconds = (nanos - i64(timeval_value.nanoseconds)) / 1e9 + + ptr = &timeval_value + len = size_of(timeval_value) + case + .Receive_Buffer_Size, + .Send_Buffer_Size: + // TODO: check for out of range values and return .Value_Out_Of_Range? + switch i in value { + case i8, u8: i2 := i; int_value = os.socklen_t((^u8)(&i2)^) + case i16, u16: i2 := i; int_value = os.socklen_t((^u16)(&i2)^) + case i32, u32: i2 := i; int_value = os.socklen_t((^u32)(&i2)^) + case i64, u64: i2 := i; int_value = os.socklen_t((^u64)(&i2)^) + case i128, u128: i2 := i; int_value = os.socklen_t((^u128)(&i2)^) + case int, uint: i2 := i; int_value = os.socklen_t((^uint)(&i2)^) + case: + panic("set_option() value must be an integer here", loc) + } + ptr = &int_value + len = size_of(int_value) + } + + skt := any_socket_to_socket(s) + res := os.setsockopt(Platform_Socket(skt), int(level), int(option), ptr, len) + if res != os.ERROR_NONE { + return Socket_Option_Error(res) + } + + return nil +}
\ No newline at end of file diff --git a/core/net/socket_windows.odin b/core/net/socket_windows.odin new file mode 100644 index 000000000..a08248d91 --- /dev/null +++ b/core/net/socket_windows.odin @@ -0,0 +1,577 @@ +// +build windows +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:c" +import win "core:sys/windows" +import "core:time" + +Platform_Socket :: win.SOCKET + +Create_Socket_Error :: enum c.int { + Network_Subsystem_Failure = win.WSAENETDOWN, + Family_Not_Supported_For_This_Socket = win.WSAEAFNOSUPPORT, + No_Socket_Descriptors_Available = win.WSAEMFILE, + No_Buffer_Space_Available = win.WSAENOBUFS, + Protocol_Unsupported_By_System = win.WSAEPROTONOSUPPORT, + Wrong_Protocol_For_Socket = win.WSAEPROTOTYPE, + Family_And_Socket_Type_Mismatch = win.WSAESOCKTNOSUPPORT, +} + +@(init, private) +ensure_winsock_initialized :: proc() { + win.ensure_winsock_initialized() +} + +create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Network_Error) { + c_type, c_protocol, c_family: c.int + + switch family { + case .IP4: c_family = win.AF_INET + case .IP6: c_family = win.AF_INET6 + case: + unreachable() + } + + switch protocol { + case .TCP: c_type = win.SOCK_STREAM; c_protocol = win.IPPROTO_TCP + case .UDP: c_type = win.SOCK_DGRAM; c_protocol = win.IPPROTO_UDP + case: + unreachable() + } + + sock := win.socket(c_family, c_type, c_protocol) + if sock == win.INVALID_SOCKET { + err = Create_Socket_Error(win.WSAGetLastError()) + return + } + + switch protocol { + case .TCP: return TCP_Socket(sock), nil + case .UDP: return UDP_Socket(sock), nil + case: + unreachable() + } +} + + +Dial_Error :: enum c.int { + Port_Required = -1, + + Address_In_Use = win.WSAEADDRINUSE, + In_Progress = win.WSAEALREADY, + Cannot_Use_Any_Address = win.WSAEADDRNOTAVAIL, + Wrong_Family_For_Socket = win.WSAEAFNOSUPPORT, + Refused = win.WSAECONNREFUSED, + Is_Listening_Socket = win.WSAEINVAL, + Already_Connected = win.WSAEISCONN, + Network_Unreachable = win.WSAENETUNREACH, // Device is offline + Host_Unreachable = win.WSAEHOSTUNREACH, // Remote host cannot be reached + No_Buffer_Space_Available = win.WSAENOBUFS, + Not_Socket = win.WSAENOTSOCK, + Timeout = win.WSAETIMEDOUT, + Would_Block = win.WSAEWOULDBLOCK, // TODO: we may need special handling for this; maybe make a socket a struct with metadata? +} + +dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := default_tcp_options) -> (skt: TCP_Socket, err: Network_Error) { + if endpoint.port == 0 { + err = .Port_Required + return + } + + family := family_from_endpoint(endpoint) + sock := create_socket(family, .TCP) or_return + skt = sock.(TCP_Socket) + + // NOTE(tetra): This is so that if we crash while the socket is open, we can + // bypass the cooldown period, and allow the next run of the program to + // use the same address immediately. + _ = set_option(skt, .Reuse_Address, true) + + sockaddr := endpoint_to_sockaddr(endpoint) + res := win.connect(Platform_Socket(skt), &sockaddr, size_of(sockaddr)) + if res < 0 { + err = Dial_Error(win.WSAGetLastError()) + return + } + + if options.no_delay { + _ = set_option(sock, .TCP_Nodelay, true) // NOTE(tetra): Not vital to succeed; error ignored + } + + return +} + +Bind_Error :: enum c.int { + // Another application is currently bound to this endpoint. + Address_In_Use = win.WSAEADDRINUSE, + // The address is not a local address on this machine. + Given_Nonlocal_Address = win.WSAEADDRNOTAVAIL, + // To bind a UDP socket to the broadcast address, the appropriate socket option must be set. + Broadcast_Disabled = win.WSAEACCES, + // The address family of the address does not match that of the socket. + Address_Family_Mismatch = win.WSAEFAULT, + // The socket is already bound to an address. + Already_Bound = win.WSAEINVAL, + // There are not enough ephemeral ports available. + No_Ports_Available = win.WSAENOBUFS, +} + +bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Network_Error) { + sockaddr := endpoint_to_sockaddr(ep) + s := any_socket_to_socket(skt) + res := win.bind(Platform_Socket(s), &sockaddr, size_of(sockaddr)) + if res < 0 { + err = Bind_Error(win.WSAGetLastError()) + } + return +} + + +// This type of socket becomes bound when you try to send data. +// This is likely what you want if you want to send data unsolicited. +// +// This is like a client TCP socket, except that it can send data to any remote endpoint without needing to establish a connection first. +make_unbound_udp_socket :: proc(family: Address_Family) -> (skt: UDP_Socket, err: Network_Error) { + sock := create_socket(family, .UDP) or_return + skt = sock.(UDP_Socket) + return +} + +// This type of socket is bound immediately, which enables it to receive data on the port. +// Since it's UDP, it's also able to send data without receiving any first. +// +// This is like a listening TCP socket, except that data packets can be sent and received without needing to establish a connection first. +// +// The bound_address is the address of the network interface that you want to use, or a loopback address if you don't care which to use. +make_bound_udp_socket :: proc(bound_address: Address, port: int) -> (skt: UDP_Socket, err: Network_Error) { + skt = make_unbound_udp_socket(family_from_address(bound_address)) or_return + bind(skt, {bound_address, port}) or_return + return +} + + + +Listen_Error :: enum c.int { + Address_In_Use = win.WSAEADDRINUSE, + Already_Connected = win.WSAEISCONN, + No_Socket_Descriptors_Available = win.WSAEMFILE, + No_Buffer_Space_Available = win.WSAENOBUFS, + Nonlocal_Address = win.WSAEADDRNOTAVAIL, + Not_Socket = win.WSAENOTSOCK, + Listening_Not_Supported_For_This_Socket = win.WSAEOPNOTSUPP, +} + +listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_Socket, err: Network_Error) { + assert(backlog > 0 && i32(backlog) < max(i32)) + + family := family_from_endpoint(interface_endpoint) + sock := create_socket(family, .TCP) or_return + skt = sock.(TCP_Socket) + + // NOTE(tetra): While I'm not 100% clear on it, my understanding is that this will + // prevent hijacking of the server's endpoint by other applications. + set_option(skt, .Exclusive_Addr_Use, true) or_return + + bind(sock, interface_endpoint) or_return + + res := win.listen(Platform_Socket(skt), i32(backlog)) + if res == win.SOCKET_ERROR { + err = Listen_Error(win.WSAGetLastError()) + return + } + + return +} + + + +Accept_Error :: enum c.int { + Not_Listening = win.WSAEINVAL, + No_Socket_Descriptors_Available_For_Client_Socket = win.WSAEMFILE, + No_Buffer_Space_Available = win.WSAENOBUFS, + Not_Socket = win.WSAENOTSOCK, + Not_Connection_Oriented_Socket = win.WSAEOPNOTSUPP, + Would_Block = win.WSAEWOULDBLOCK, // TODO: we may need special handling for this; maybe make a socket a struct with metadata? +} + +accept_tcp :: proc(sock: TCP_Socket, options := default_tcp_options) -> (client: TCP_Socket, source: Endpoint, err: Network_Error) { + for { + sockaddr: win.SOCKADDR_STORAGE_LH + sockaddrlen := c.int(size_of(sockaddr)) + client_sock := win.accept(Platform_Socket(sock), &sockaddr, &sockaddrlen) + if int(client_sock) == win.SOCKET_ERROR { + e := win.WSAGetLastError() + if e == win.WSAECONNRESET { + // NOTE(tetra): Reset just means that a client that connection immediately lost the connection. + // There's no need to concern the user with this, so we handle it for them. + // On Linux, this error isn't possible in the first place according the man pages, so we also + // can do this to match the behaviour. + continue + } + err = Accept_Error(e) + return + } + client = TCP_Socket(client_sock) + source = sockaddr_to_endpoint(&sockaddr) + if options.no_delay { + _ = set_option(client, .TCP_Nodelay, true) // NOTE(tetra): Not vital to succeed; error ignored + } + return + } +} + + + +close :: proc(skt: Any_Socket) { + if s := any_socket_to_socket(skt); s != {} { + win.closesocket(Platform_Socket(s)) + } +} + + + +TCP_Recv_Error :: enum c.int { + Network_Subsystem_Failure = win.WSAENETDOWN, + Not_Connected = win.WSAENOTCONN, + Bad_Buffer = win.WSAEFAULT, + Keepalive_Failure = win.WSAENETRESET, + Not_Socket = win.WSAENOTSOCK, + Shutdown = win.WSAESHUTDOWN, + Would_Block = win.WSAEWOULDBLOCK, + Aborted = win.WSAECONNABORTED, // TODO: not functionally different from Reset; merge? + Timeout = win.WSAETIMEDOUT, + Connection_Closed = win.WSAECONNRESET, // TODO(tetra): Determine when this is different from the syscall returning n=0 and maybe normalize them? + Host_Unreachable = win.WSAEHOSTUNREACH, // TODO: verify can actually happen +} + +recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: Network_Error) { + if len(buf) <= 0 { + return + } + res := win.recv(Platform_Socket(skt), raw_data(buf), c.int(len(buf)), 0) + if res < 0 { + err = TCP_Recv_Error(win.WSAGetLastError()) + return + } + return int(res), nil +} + +UDP_Recv_Error :: enum c.int { + Network_Subsystem_Failure = win.WSAENETDOWN, + Aborted = win.WSAECONNABORTED, // TODO: not functionally different from Reset; merge? + // UDP packets are limited in size, and the length of the incoming message exceeded it. + Truncated = win.WSAEMSGSIZE, + // The machine at the remote endpoint doesn't have the given port open to receiving UDP data. + Remote_Not_Listening = win.WSAECONNRESET, + Shutdown = win.WSAESHUTDOWN, + // A broadcast address was specified, but the .Broadcast socket option isn't set. + Broadcast_Disabled = win.WSAEACCES, + Bad_Buffer = win.WSAEFAULT, + No_Buffer_Space_Available = win.WSAENOBUFS, + // The socket is not valid socket handle. + Not_Socket = win.WSAENOTSOCK, + Would_Block = win.WSAEWOULDBLOCK, + // The remote host cannot be reached from this host at this time. + Host_Unreachable = win.WSAEHOSTUNREACH, + // The network cannot be reached from this host at this time. + Offline = win.WSAENETUNREACH, + Timeout = win.WSAETIMEDOUT, + // The socket isn't bound; an unknown flag specified; or MSG_OOB specified with SO_OOBINLINE enabled. + Incorrectly_Configured = win.WSAEINVAL, // TODO: can this actually happen? + // The message took more hops than was allowed (the Time To Live) to reach the remote endpoint. + TTL_Expired = win.WSAENETRESET, +} + +recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: Network_Error) { + if len(buf) <= 0 { + return + } + + from: win.SOCKADDR_STORAGE_LH + fromsize := c.int(size_of(from)) + res := win.recvfrom(Platform_Socket(skt), raw_data(buf), c.int(len(buf)), 0, &from, &fromsize) + if res < 0 { + err = UDP_Recv_Error(win.WSAGetLastError()) + return + } + + bytes_read = int(res) + remote_endpoint = sockaddr_to_endpoint(&from) + return +} + +recv :: proc{recv_tcp, recv_udp} + + +// +// TODO: consider merging some errors to make handling them easier +// TODO: verify once more what errors to actually expose +// + +TCP_Send_Error :: enum c.int { + Aborted = win.WSAECONNABORTED, // TODO: not functionally different from Reset; merge? + Not_Connected = win.WSAENOTCONN, + Shutdown = win.WSAESHUTDOWN, + Connection_Closed = win.WSAECONNRESET, + No_Buffer_Space_Available = win.WSAENOBUFS, + Network_Subsystem_Failure = win.WSAENETDOWN, + Host_Unreachable = win.WSAEHOSTUNREACH, + Offline = win.WSAENETUNREACH, // TODO: verify possible, as not mentioned in docs + Timeout = win.WSAETIMEDOUT, + // A broadcast address was specified, but the .Broadcast socket option isn't set. + Broadcast_Disabled = win.WSAEACCES, + Bad_Buffer = win.WSAEFAULT, + // Connection is broken due to keepalive activity detecting a failure during the operation. + Keepalive_Failure = win.WSAENETRESET, // TODO: not functionally different from Reset; merge? + // The so-called socket is not an open socket. + Not_Socket = win.WSAENOTSOCK, +} + +// Repeatedly sends data until the entire buffer is sent. +// If a send fails before all data is sent, returns the amount +// sent up to that point. +send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: Network_Error) { + for bytes_written < len(buf) { + limit := min(int(max(i32)), len(buf) - bytes_written) + remaining := buf[bytes_written:] + res := win.send(Platform_Socket(skt), raw_data(remaining), c.int(limit), 0) + if res < 0 { + err = TCP_Send_Error(win.WSAGetLastError()) + return + } + bytes_written += int(res) + } + return +} + +UDP_Send_Error :: enum c.int { + Network_Subsystem_Failure = win.WSAENETDOWN, + Aborted = win.WSAECONNABORTED, // TODO: not functionally different from Reset; merge? + // UDP packets are limited in size, and len(buf) exceeded it. + Message_Too_Long = win.WSAEMSGSIZE, + // The machine at the remote endpoint doesn't have the given port open to receiving UDP data. + Remote_Not_Listening = win.WSAECONNRESET, + Shutdown = win.WSAESHUTDOWN, + // A broadcast address was specified, but the .Broadcast socket option isn't set. + Broadcast_Disabled = win.WSAEACCES, + Bad_Buffer = win.WSAEFAULT, + // Connection is broken due to keepalive activity detecting a failure during the operation. + Keepalive_Failure = win.WSAENETRESET, // TODO: not functionally different from Reset; merge? + No_Buffer_Space_Available = win.WSAENOBUFS, + // The socket is not valid socket handle. + Not_Socket = win.WSAENOTSOCK, + // This socket is unidirectional and cannot be used to send any data. + // TODO: verify possible; decide whether to keep if not + Receive_Only = win.WSAEOPNOTSUPP, + Would_Block = win.WSAEWOULDBLOCK, + // The remote host cannot be reached from this host at this time. + Host_Unreachable = win.WSAEHOSTUNREACH, + // Attempt to send to the Any address. + Cannot_Use_Any_Address = win.WSAEADDRNOTAVAIL, + // The address is of an incorrect address family for this socket. + Family_Not_Supported_For_This_Socket = win.WSAEAFNOSUPPORT, + // The network cannot be reached from this host at this time. + Offline = win.WSAENETUNREACH, + Timeout = win.WSAETIMEDOUT, +} + +// Sends a single UDP datagram packet. +// +// Datagrams are limited in size; attempting to send more than this limit at once will result in a Message_Too_Long error. +// UDP packets are not guarenteed to be received in order. +send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: Network_Error) { + if len(buf) > int(max(c.int)) { + // NOTE(tetra): If we don't guard this, we'll return (0, nil) instead, which is misleading. + err = .Message_Too_Long + return + } + toaddr := endpoint_to_sockaddr(to) + res := win.sendto(Platform_Socket(skt), raw_data(buf), c.int(len(buf)), 0, &toaddr, size_of(toaddr)) + if res < 0 { + err = UDP_Send_Error(win.WSAGetLastError()) + return + } + bytes_written = int(res) + return +} + +send :: proc{send_tcp, send_udp} + + + + +Shutdown_Manner :: enum c.int { + Receive = win.SD_RECEIVE, + Send = win.SD_SEND, + Both = win.SD_BOTH, +} + +Shutdown_Error :: enum c.int { + Aborted = win.WSAECONNABORTED, + Reset = win.WSAECONNRESET, + Offline = win.WSAENETDOWN, + Not_Connected = win.WSAENOTCONN, + Not_Socket = win.WSAENOTSOCK, + Invalid_Manner = win.WSAEINVAL, +} + +shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Network_Error) { + s := any_socket_to_socket(skt) + res := win.shutdown(Platform_Socket(s), c.int(manner)) + if res < 0 { + return Shutdown_Error(win.WSAGetLastError()) + } + return +} + + + + +Socket_Option :: enum c.int { + // bool: Whether the address that this socket is bound to can be reused by other sockets. + // This allows you to bypass the cooldown period if a program dies while the socket is bound. + Reuse_Address = win.SO_REUSEADDR, + // bool: Whether other programs will be inhibited from binding the same endpoint as this socket. + Exclusive_Addr_Use = win.SO_EXCLUSIVEADDRUSE, + // bool: When true, keepalive packets will be automatically be sent for this connection. + // TODO: verify this understanding + Keep_Alive = win.SO_KEEPALIVE, + // bool: When true, client connections will immediately be sent a TCP/IP RST response, rather than + // being accepted. + Conditional_Accept = win.SO_CONDITIONAL_ACCEPT, + // bool: If true, when the socket is closed, but data is still waiting to be sent, discard that data. + Dont_Linger = win.SO_DONTLINGER, + // bool: When true, 'out-of-band' data sent over the socket will be read by a normal net.recv() call, + // the same as normal 'in-band' data. + Out_Of_Bounds_Data_Inline = win.SO_OOBINLINE, + // bool: When true, disables send-coalescing, therefore reducing latency. + TCP_Nodelay = win.TCP_NODELAY, + // win.LINGER: Customizes how long (if at all) the socket will remain open when there is some remaining data + // waiting to be sent, and net.close() is called. + Linger = win.SO_LINGER, + // win.DWORD: The size, in bytes, of the OS-managed receive-buffer for this socket. + Receive_Buffer_Size = win.SO_RCVBUF, + // win.DWORD: The size, in bytes, of the OS-managed send-buffer for this socket. + Send_Buffer_Size = win.SO_SNDBUF, + // win.DWORD: For blocking sockets, the time in milliseconds to wait for incoming data to be received, before giving up and returning .Timeout. + // For non-blocking sockets, ignored. + // Use a value of zero to potentially wait forever. + Receive_Timeout = win.SO_RCVTIMEO, + // win.DWORD: For blocking sockets, the time in milliseconds to wait for outgoing data to be sent, before giving up and returning .Timeout. + // For non-blocking sockets, ignored. + // Use a value of zero to potentially wait forever. + Send_Timeout = win.SO_SNDTIMEO, + // bool: Allow sending to, receiving from, and binding to, a broadcast address. + Broadcast = win.SO_BROADCAST, +} + +Socket_Option_Error :: enum c.int { + Linger_Only_Supports_Whole_Seconds = 1, + // The given value is too big or small to be given to the OS. + Value_Out_Of_Range, + + Network_Subsystem_Failure = win.WSAENETDOWN, + Timeout_When_Keepalive_Set = win.WSAENETRESET, + Invalid_Option_For_Socket = win.WSAENOPROTOOPT, + Reset_When_Keepalive_Set = win.WSAENOTCONN, + Not_Socket = win.WSAENOTSOCK, +} + +set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Network_Error { + level := win.SOL_SOCKET if option != .TCP_Nodelay else win.IPPROTO_TCP + + bool_value: b32 + int_value: i32 + linger_value: win.LINGER + + ptr: rawptr + len: c.int + + switch option { + case + .Reuse_Address, + .Exclusive_Addr_Use, + .Keep_Alive, + .Out_Of_Bounds_Data_Inline, + .TCP_Nodelay, + .Broadcast, + .Conditional_Accept, + .Dont_Linger: + switch x in value { + case bool, b8: + x2 := x + bool_value = b32((^bool)(&x2)^) + case b16: + bool_value = b32(x) + case b32: + bool_value = b32(x) + case b64: + bool_value = b32(x) + case: + panic("set_option() value must be a boolean here", loc) + } + ptr = &bool_value + len = size_of(bool_value) + case .Linger: + t, ok := value.(time.Duration) + if !ok do panic("set_option() value must be a time.Duration here", loc) + + num_secs := i64(time.duration_seconds(t)) + if time.Duration(num_secs * 1e9) != t do return .Linger_Only_Supports_Whole_Seconds + if num_secs > i64(max(u16)) do return .Value_Out_Of_Range + linger_value.l_onoff = 1 + linger_value.l_linger = c.ushort(num_secs) + + ptr = &linger_value + len = size_of(linger_value) + case + .Receive_Timeout, + .Send_Timeout: + t, ok := value.(time.Duration) + if !ok do panic("set_option() value must be a time.Duration here", loc) + + int_value = i32(time.duration_milliseconds(t)) + ptr = &int_value + len = size_of(int_value) + + case + .Receive_Buffer_Size, + .Send_Buffer_Size: + switch i in value { + case i8, u8: i2 := i; int_value = c.int((^u8)(&i2)^) + case i16, u16: i2 := i; int_value = c.int((^u16)(&i2)^) + case i32, u32: i2 := i; int_value = c.int((^u32)(&i2)^) + case i64, u64: i2 := i; int_value = c.int((^u64)(&i2)^) + case i128, u128: i2 := i; int_value = c.int((^u128)(&i2)^) + case int, uint: i2 := i; int_value = c.int((^uint)(&i2)^) + case: + panic("set_option() value must be an integer here", loc) + } + ptr = &int_value + len = size_of(int_value) + } + + skt := any_socket_to_socket(s) + res := win.setsockopt(Platform_Socket(skt), c.int(level), c.int(option), ptr, len) + if res < 0 { + return Socket_Option_Error(win.WSAGetLastError()) + } + + return nil +} diff --git a/core/net/url.odin b/core/net/url.odin new file mode 100644 index 000000000..ffd1bbf83 --- /dev/null +++ b/core/net/url.odin @@ -0,0 +1,235 @@ +/* + Copyright 2022 Tetralux <tetraluxonpc@gmail.com> + Copyright 2022 Colin Davidson <colrdavidson@gmail.com> + Copyright 2022 Jeroen van Rijn <nom@duclavier.com>. + Made available under Odin's BSD-3 license. + + List of contributors: + Tetralux: Initial implementation + Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver + Jeroen van Rijn: Cross platform unification, code style, documentation +*/ + +/* + Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. + For other protocols and their features, see subdirectories of this package. +*/ +package net + +import "core:strings" +import "core:strconv" +import "core:unicode/utf8" +import "core:mem" + +split_url :: proc(url: string, allocator := context.allocator) -> (scheme, host, path: string, queries: map[string]string) { + s := url + + i := strings.last_index(s, "://") + if i != -1 { + scheme = s[:i] + s = s[i+3:] + } + + i = strings.index(s, "?") + if i != -1 { + query_str := s[i+1:] + s = s[:i] + if query_str != "" { + queries_parts := strings.split(query_str, "&") + queries = make(map[string]string, len(queries_parts), allocator) + for q in queries_parts { + parts := strings.split(q, "=") + switch len(parts) { + case 1: queries[parts[0]] = "" // NOTE(tetra): Query not set to anything, was but present. + case 2: queries[parts[0]] = parts[1] // NOTE(tetra): Query set to something. + case: break + } + } + } + } + + i = strings.index(s, "/") + if i == -1 { + host = s + path = "/" + } else { + host = s[:i] + path = s[i:] + } + + return +} + +join_url :: proc(scheme, host, path: string, queries: map[string]string, allocator := context.allocator) -> string { + using strings + + b := make_builder(allocator) + grow_builder(&b, len(scheme) + 3 + len(host) + 1 + len(path)) + + write_string(&b, scheme) + write_string(&b, "://") + write_string(&b, trim_space(host)) + + if path != "" { + if path[0] != '/' do write_string(&b, "/") + write_string(&b, trim_space(path)) + } + + + if len(queries) > 0 do write_string(&b, "?") + for query_name, query_value in queries { + write_string(&b, query_name) + if query_value != "" { + write_string(&b, "=") + write_string(&b, query_value) + } + } + + return to_string(b) +} + +percent_encode :: proc(s: string, allocator := context.allocator) -> string { + using strings + + b := make_builder(allocator) + grow_builder(&b, len(s) + 16) // NOTE(tetra): A reasonable number to allow for the number of things we need to escape. + + for ch in s { + switch ch { + case 'A'..='Z', 'a'..='z', '0'..='9', '-', '_', '.', '~': + write_rune_builder(&b, ch) + case: + bytes, n := utf8.encode_rune(ch) + for byte in bytes[:n] { + buf: [2]u8 = --- + t := strconv.append_int(buf[:], i64(byte), 16) + write_rune_builder(&b, '%') + write_string(&b, t) + } + } + } + + return to_string(b) +} + +percent_decode :: proc(encoded_string: string, allocator := context.allocator) -> (decoded_string: string, ok: bool) { + using strings + + b := make_builder(allocator) + grow_builder(&b, len(encoded_string)) + defer if !ok do destroy_builder(&b) + + stack_buf: [4]u8 + pending := mem.buffer_from_slice(stack_buf[:]) + s := encoded_string + + for len(s) > 0 { + i := index_rune(s, '%') + if i == -1 { + write_string(&b, s) // no '%'s; the string is already decoded + break + } + + write_string(&b, s[:i]) + s = s[i:] + + if len(s) == 0 do return // percent without anything after it + s = s[1:] + + if s[0] == '%' { + write_rune_builder(&b, '%') + s = s[1:] + continue + } + + if len(s) < 2 do return // percent without encoded value + + n: int + n, _ = strconv.parse_int(s[:2], 16) + switch n { + case 0x20: write_rune_builder(&b, ' ') + case 0x21: write_rune_builder(&b, '!') + case 0x23: write_rune_builder(&b, '#') + case 0x24: write_rune_builder(&b, '$') + case 0x25: write_rune_builder(&b, '%') + case 0x26: write_rune_builder(&b, '&') + case 0x27: write_rune_builder(&b, '\'') + case 0x28: write_rune_builder(&b, '(') + case 0x29: write_rune_builder(&b, ')') + case 0x2A: write_rune_builder(&b, '*') + case 0x2B: write_rune_builder(&b, '+') + case 0x2C: write_rune_builder(&b, ',') + case 0x2F: write_rune_builder(&b, '/') + case 0x3A: write_rune_builder(&b, ':') + case 0x3B: write_rune_builder(&b, ';') + case 0x3D: write_rune_builder(&b, '=') + case 0x3F: write_rune_builder(&b, '?') + case 0x40: write_rune_builder(&b, '@') + case 0x5B: write_rune_builder(&b, '[') + case 0x5D: write_rune_builder(&b, ']') + case: + // utf-8 bytes + // TODO(tetra): Audit this - 4 bytes??? + append(&pending, s[0]) + append(&pending, s[1]) + if len(pending) == 4 { + r, _ := utf8.decode_rune(pending[:]) + write_rune_builder(&b, r) + clear(&pending) + } + } + s = s[2:] + } + + ok = true + decoded_string = to_string(b) + return +} + +// +// TODO: encoding/base64 is broken... +// + +// // TODO(tetra): The whole "table" stuff in encoding/base64 is too impenetrable for me to +// // make a table for this ... sigh - so this'll do for now. +/* +base64url_encode :: proc(data: []byte, allocator := context.allocator) -> string { + out := transmute([]byte) base64.encode(data, base64.ENC_TABLE, allocator); + for b, i in out { + switch b { + case '+': out[i] = '-'; + case '/': out[i] = '_'; + } + } + i := len(out)-1; + for ; i >= 0; i -= 1 { + if out[i] != '=' do break; + } + return string(out[:i+1]); +} + +base64url_decode :: proc(s: string, allocator := context.allocator) -> []byte { + size := len(s); + padding := 0; + for size % 4 != 0 { + size += 1; // TODO: SPEED + padding += 1; + } + + temp := make([]byte, size, context.temp_allocator); + copy(temp, transmute([]byte) s); + + for b, i in temp { + switch b { + case '-': temp[i] = '+'; + case '_': temp[i] = '/'; + } + } + + for in 0..padding-1 { + temp[len(temp)-1] = '='; + } + + return base64.decode(string(temp), base64.DEC_TABLE, allocator); +} +*/
\ No newline at end of file |