diff --git a/src/sys/private/addresses_posix.nim b/src/sys/private/addresses_posix.nim index 80ff76c..af154c9 100644 --- a/src/sys/private/addresses_posix.nim +++ b/src/sys/private/addresses_posix.nim @@ -11,6 +11,7 @@ import syscall/posix type IP4Impl {.borrow: `.`.} = distinct InAddr IP6Impl {.borrow: `.`.} = distinct In6Addr + IP6Octet = char template ip4Word() {.dirty.} = result = ip.s_addr @@ -33,3 +34,30 @@ template ip4EndpointAddr() {.dirty.} = template ip4EndpointPort() {.dirty.} = result = Port fromBE(e.sin_port) + +template octets(ip: IP6Impl): untyped = + ip.s6_addr + +type IP6EndpointImpl {.requiresInit, borrow: `.`.} = distinct Sockaddr_in6 + +template ip6InitEndpoint() {.dirty.} = + result = IP6EndpointImpl: + Sockaddr_in6( + sin6_family: AF_INET6.TSa_Family, + sin6_addr: cast[In6AddrOrig](ip), + sin6_port: toBE(port.uint16), + sin6_flowinfo: uint32(flowId), + sin6_scope_id: uint32(scopeId) + ) + +template ip6EndpointAddr() {.dirty.} = + result = IP6 cast[IP6Impl](e.sin6_addr) + +template ip6EndpointPort() {.dirty.} = + result = Port fromBE(e.sin6_port) + +template ip6EndpointFlowId() {.dirty.} = + result = FlowId e.sin6_flowinfo + +template ip6EndpointScopeId() {.dirty.} = + result = ScopeId e.sin6_scope_id diff --git a/src/sys/private/addresses_windows.nim b/src/sys/private/addresses_windows.nim index 4de891a..9239bb6 100644 --- a/src/sys/private/addresses_windows.nim +++ b/src/sys/private/addresses_windows.nim @@ -34,4 +34,29 @@ template ip4EndpointAddr() {.dirty.} = template ip4EndpointPort() {.dirty.} = result = Port fromBE(e.sin_port) +template octets(ip: IP6Impl): untyped = + ip.Byte + +type IP6EndpointImpl {.requiresInit, borrow: `.`.} = distinct sockaddr_in6 + +template ip6InitEndpoint() {.dirty.} = + result = IP6EndpointImpl: + Sockaddr_in6( + sin6_family: AF_INET6, + sin6_addr: In6Addr(ip), + sin6_port: toBE(port.uint16), + sin6_flowinfo: uint32(flowId) + ) + result.union1.sin6_scope_id = uint32(scopeId) + +template ip6EndpointAddr() {.dirty.} = + result = IP6 e.sin6_addr + +template ip6EndpointPort() {.dirty.} = + result = Port fromBE(e.sin6_port) + +template ip6EndpointFlowId() {.dirty.} = + result = e.sin6_flowinfo +template ip6EndpointScopeId() {.dirty.} = + result = e.sin6_scope_id diff --git a/src/sys/private/sockets_posix.nim b/src/sys/private/sockets_posix.nim index 929724c..0561569 100644 --- a/src/sys/private/sockets_posix.nim +++ b/src/sys/private/sockets_posix.nim @@ -101,11 +101,17 @@ proc `=destroy`(r: var ResolverResultImpl) = freeaddrinfo(r.info) r.info = nil -template ip4Resolve() {.dirty.} = +template ipResolve() {.dirty.} = result = new ResolverResultImpl let hints = AddrInfo( - ai_family: AF_INET, + ai_family: + if isNone(kind): + AF_UNSPEC + else: + case kind.get + of V4: AF_INET + of V6: AF_INET6, ai_flags: AI_NUMERICSERV or AI_ADDRCONFIG ) @@ -129,8 +135,13 @@ template resolvedItems() {.dirty.} = var info = r.info while info != nil: if info.ai_addr != nil: - if info.ai_addr.sa_family == AF_INET.TSa_Family: - yield cast[ptr IP4Endpoint](info.ai_addr)[] + case info.ai_addr.sa_family + of AF_INET.TSa_Family: + yield IPEndpoint(kind: V4, v4: cast[ptr IP4Endpoint](info.ai_addr)[]) + of AF_INET6.TSa_Family: + yield IPEndpoint(kind: V6, v6: cast[ptr IP6Endpoint](info.ai_addr)[]) + else: + discard "Should not be possible, but harmless even if it is" info = info.ai_next @@ -153,7 +164,12 @@ proc handleAsyncConnectResult(fd: SocketFD) {.raises: [OSError].} = raise newOSError(error, $Error.Connect) template tcpConnect() {.dirty.} = - let sock = makeSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP) + const addressFamily = + when endpoint is IP4Endpoint: + AF_INET + elif endpoint is IP6Endpoint: + AF_INET6 + let sock = makeSocket(addressFamily, SOCK_STREAM, IPPROTO_TCP) if connect( SocketHandle(sock.fd), @@ -176,7 +192,12 @@ template tcpConnect() {.dirty.} = result = Conn[TCP] newSocket(sock) template tcpAsyncConnect() {.dirty.} = - var sock = makeSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, {sfNonBlock}) + const addressFamily = + when endpoint is IP4Endpoint: + AF_INET + elif endpoint is IP6Endpoint: + AF_INET6 + var sock = makeSocket(addressFamily, SOCK_STREAM, IPPROTO_TCP, {sfNonBlock}) if connect( SocketHandle(sock.fd), @@ -210,7 +231,13 @@ func maxBacklog(): Natural = SOMAXCONN template tcpListen() {.dirty.} = - let sock = makeSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP) + const addressFamily = + when endpoint is IP4Endpoint: + AF_INET + elif endpoint is IP6Endpoint: + AF_INET6 + + let sock = makeSocket(addressFamily, SOCK_STREAM, IPPROTO_TCP) # Bind the address to the socket posixChk bindSocket( @@ -227,7 +254,13 @@ template tcpListen() {.dirty.} = result = Listener[TCP] newSocket(sock) template tcpAsyncListen() {.dirty.} = - var sock = makeSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, {sfNonBlock}) + const addressFamily = + when endpoint is IP4Endpoint: + AF_INET + elif endpoint is IP6Endpoint: + AF_INET6 + + var sock = makeSocket(addressFamily, SOCK_STREAM, IPPROTO_TCP, {sfNonBlock}) if bindSocket( SocketHandle(sock.fd), @@ -303,13 +336,8 @@ proc commonAccept[T](fd: SocketFD, remoteAddr: var T, if conn.fd == InvalidFD: return - # TODO: Remove this once IPv6 support lands - # - # This is used to verify that we are getting IPv4 address. - assert remoteLen == SockLen(sizeof remoteAddr): - "The length of the endpoint structure does not match assumption. This is a nim-sys bug." - assert remoteAddr.sin_family == AF_INET.TSa_Family: - "The address is not IPv4. This is a nim-sys bug." + assert remoteLen <= SockLen(sizeof remoteAddr): + "The length of the endpoint structure is bigger than expected size. This is a nim-sys bug." when not declared(accept4): # On systems without accept4, flags have to be set manually. @@ -322,16 +350,25 @@ proc commonAccept[T](fd: SocketFD, remoteAddr: var T, result = conn template tcpAccept() {.dirty.} = - let conn = commonAccept[IP4Endpoint](l.fd, result.remote) + var saddr: SockaddrStorage + let conn = commonAccept(l.fd, saddr) if conn.fd == InvalidFD: raise newOSError(errno, $Error.Accept) result.conn = Conn[TCP] newSocket(conn) + case saddr.ss_family + of AF_INET.TSa_Family: + result.remote = IPEndpoint(kind: V4, v4: cast[IP4Endpoint](saddr)) + of AF_INET6.TSa_Family: + result.remote = IPEndpoint(kind: V6, v6: cast[IP6Endpoint](saddr)) + else: + doAssert false, "Unexpected remote address family: " & $saddr.ss_family template tcpAsyncAccept() {.dirty.} = # Loop until we get a connection while true: - var conn = commonAccept[IP4Endpoint](l.fd, result.remote, {sfNonBlock}) + var saddr: SockaddrStorage + var conn = commonAccept(l.fd, saddr, {sfNonBlock}) if conn.fd == InvalidFD: # If the socket signals that no connections are pending @@ -343,19 +380,34 @@ template tcpAsyncAccept() {.dirty.} = else: # We got a connection result.conn = AsyncConn[TCP] newAsyncSocket(move conn) + case saddr.ss_family + of AF_INET.TSa_Family: + result.remote = IPEndpoint(kind: V4, v4: cast[IP4Endpoint](saddr)) + of AF_INET6.TSa_Family: + result.remote = IPEndpoint(kind: V6, v6: cast[IP6Endpoint](saddr)) + else: + doAssert false, "Unexpected remote address family: " & $saddr.ss_family return template tcpLocalEndpoint() {.dirty.} = - var endpointLen = SockLen sizeof(result) + var + saddr: SockaddrStorage + endpointLen = SockLen sizeof(saddr) posixChk getsockname( SocketHandle l.fd, - cast[ptr SockAddr](addr result), + cast[ptr SockAddr](addr saddr), addr endpointLen ): $Error.LocalEndpoint - assert endpointLen == SockLen sizeof(result): - "The length of the endpoint structure does not match assumption. This is a nim-sys bug." - assert result.sin_family == TSa_Family(AF_INET): - "The address is not IPv4. This is a nim-sys bug." + assert endpointLen <= SockLen(sizeof saddr): + "The length of the endpoint structure is bigger than expected size. This is a nim-sys bug." + + case saddr.ss_family + of AF_INET.TSa_Family: + result = IPEndpoint(kind: V4, v4: cast[IP4Endpoint](saddr)) + of AF_INET6.TSa_Family: + result = IPEndpoint(kind: V6, v6: cast[IP6Endpoint](saddr)) + else: + doAssert false, "Unexpected remote address family: " & $saddr.ss_family diff --git a/src/sys/private/syscall/posix.nim b/src/sys/private/syscall/posix.nim index f1c8c2a..2546f1d 100644 --- a/src/sys/private/syscall/posix.nim +++ b/src/sys/private/syscall/posix.nim @@ -8,7 +8,7 @@ # Seems to be a compiler bug, it shouldn't trigger unused imports for this line. import std/posix as std_posix -export std_posix +export std_posix except In6Addr # XXX: Remove when we fully replace std/posix {.used.} @@ -30,6 +30,14 @@ when defined(bsd) or defined(linux): proc pipe2*(pipefd: var array[2, cint], flags: cint): cint {.importc, header: "".} +type + # Overrides the std/posix version to fix the type. + In6Addr* {.importc: "struct in6_addr", pure, final, + header: "".} = object + s6_addr*: array[16, byte] + + In6AddrOrig* = std_posix.In6Addr + template retryOnEIntr*(op: untyped): untyped = ## Given a POSIX operation that returns `-1` on error, automatically retry it ## if the error was `EINTR`. diff --git a/src/sys/sockets.nim b/src/sys/sockets.nim index ebcf7fb..5dae0f0 100644 --- a/src/sys/sockets.nim +++ b/src/sys/sockets.nim @@ -168,7 +168,7 @@ template derive(T, Base: typedesc): untyped = type Protocol* {.pure.} = enum - TCP + TCP ## Generic TCP socket Conn*[Protocol: static Protocol] = distinct Socket ## A connection with `Protocol`. @@ -351,12 +351,14 @@ proc `=copy`*(dst: var ResolverResultImpl, src: ResolverResultImpl) {.error.} ## Copying a `ResolverResult` is prohibited at the moment. This restriction ## might be lifted in the future. -proc resolveIP4*(host: string, port: Port = PortNone): ResolverResult - {.raises: [OSError, ResolverError].} = +proc resolveIP*(host: string, port: Port = PortNone, kind = none(IPEndpointKind)): ResolverResult + {.raises: [OSError, ResolverError].} = ## Resolve the endpoints of `host` for port `port`. ## ## `port` will be carried over to the result verbatim. ## + ## `kind` can be specified to limit the result to the specified endpoint address family. + ## ## On failure, either `OSError` or `ResolverError` will be raised, depending ## on whether the error was caused by the operating system or the resolver. ## @@ -367,17 +369,16 @@ proc resolveIP4*(host: string, port: Port = PortNone): ResolverResult ## - On Windows, the error code in `ResolverError` is one of the errors in this ## `list `_. ## Other errors are reported as `OSError`. - ip4Resolve() + ipResolve() -# In the future this should be either generic or use `Endpoint`. -iterator items*(r: ResolverResult): IP4Endpoint = +iterator items*(r: ResolverResult): IPEndpoint = ## Yields endpoints from the resolving result resolvedItems() -func closureItems(r: ResolverResult): iterator (): IP4Endpoint = +func closureItems(r: ResolverResult): iterator (): IPEndpoint = ## Produce a closure iterator for `r.items`. This is necessary for use in CPS. result = - iterator(): IP4Endpoint = + iterator(): IPEndpoint = for ep in r.items: yield ep @@ -395,6 +396,18 @@ proc connectTcp*(endpoint: IP4Endpoint): Conn[TCP] ## Create a TCP connection to `endpoint`. tcpConnect() +proc connectTcp*(endpoint: IP6Endpoint): Conn[TCP] + {.raises: [OSError].} = + ## Create a TCP connection to `endpoint`. + tcpConnect() + +proc connectTcp*(endpoint: IPEndpoint): Conn[TCP] + {.raises: [OSError].} = + ## Create a TCP connection to `endpoint`. + case endpoint.kind + of V4: connectTcp(endpoint.v4) + of V6: connectTcp(endpoint.v6) + proc connectTcp*(endpoints: ResolverResult): Conn[TCP] {.raises: [OSError, IncompatibleEndpointError].} = ## Connects via TCP to one of the compatible endpoint from `endpoints`. @@ -431,12 +444,17 @@ proc connectTcp*(host: IP4, port: Port): Conn[TCP] ## Create a TCP connection to `host` and `port`. connectTcp initEndpoint(host, port) +proc connectTcp*(host: IP6, port: Port): Conn[TCP] + {.inline, raises: [OSError].} = + ## Create a TCP connection to `host` and `port`. + connectTcp initEndpoint(host, port) + proc connectTcp*(host: string, port: Port): Conn[TCP] {.inline, raises: [OSError, ResolverError, IncompatibleEndpointError].} = ## Create a TCP connection to `host` and `port`. ## ## `host` will be resolved before connection. - connectTcp resolveIP4(host, port) + connectTcp resolveIP(host, port) proc connectTcpAsync*(endpoint: IP4Endpoint): AsyncConn[TCP] {.asyncio.} = @@ -445,6 +463,28 @@ proc connectTcpAsync*(endpoint: IP4Endpoint): AsyncConn[TCP] ## `OSError` is raised if the connection fails. tcpAsyncConnect() +proc connectTcpAsync*(endpoint: IP6Endpoint): AsyncConn[TCP] + {.asyncio.} = + ## Create an asynchronous TCP connection to `endpoint`. + ## + ## `OSError` is raised if the connection fails. + tcpAsyncConnect() + +proc connectTcpAsync*(endpoint: IPEndpoint): AsyncConn[TCP] + {.asyncio.} = + ## Create an asynchronous TCP connection to `endpoint`. + ## + ## `OSError` is raised if the connection fails. + case endpoint.kind + of V4: + # XXX: https://github.com/nim-works/cps/issues/301 + let v4 = endpoint.v4 + connectTcpAsync(v4) + of V6: + # XXX: https://github.com/nim-works/cps/issues/301 + let v6 = endpoint.v6 + connectTcpAsync(v6) + proc connectTcpAsync*(endpoints: ResolverResult): AsyncConn[TCP] {.asyncio.} = ## Connects via TCP to one of the compatible endpoint from `endpoints`. @@ -463,7 +503,7 @@ proc connectTcpAsync*(endpoints: ResolverResult): AsyncConn[TCP] attempted = false lastError: ref OSError {.warning: "Workaround for nim-works/cps#185".} - let next: iterator (): IP4Endpoint = closureItems(endpoints) + let next: iterator (): IPEndpoint = closureItems(endpoints) while true: attempted = true let ep = next() @@ -490,13 +530,18 @@ proc connectTcpAsync*(host: IP4, port: Port): AsyncConn[TCP] ## Create a TCP connection to `host` and `port`. connectTcpAsync initEndpoint(host, port) +proc connectTcpAsync*(host: IP6, port: Port): Conn[TCP] + {.inline, raises: [OSError].} = + ## Create a TCP connection to `host` and `port`. + connectTcp initEndpoint(host, port) + proc connectTcpAsync*(host: string, port: Port): AsyncConn[TCP] {.asyncio.} = ## Create a TCP connection to `host` and `port`. ## ## `host` will be resolved **synchronously** before connection. # A move have to be done or the compiler might think that this is a copy. - var resolverResult = resolveIP4(host, port) + var resolverResult = resolveIP(host, port) connectTcpAsync move(resolverResult) type @@ -527,6 +572,44 @@ proc listenTcp*(endpoint: IP4Endpoint, backlog = none(Natural)): Listener[TCP] ## If `backlog` is `0`, the OS will select a reasonable minimum. tcpListen() +proc listenTcp*(endpoint: IP6Endpoint, backlog = none(Natural)): Listener[TCP] + {.raises: [OSError].} = + ## Listen at `endpoint` for TCP connections. + ## + ## If the port of the endpoint is `PortNone`, an ephemeral port will be + ## reserved automatically by the operating system. `localEndpoint` can be + ## used to retrieve the port number. + ## + ## The `backlog` parameter defines the maximum amount of pending connections. + ## If a connection request arrives when the queue is full, the client might + ## receive a "Connection refused" error or the connection might be silently + ## dropped. This value is treated by most operating systems as a hint. + ## + ## If `backlog` is `None`, the maximum queue length will be selected. + ## + ## If `backlog` is `0`, the OS will select a reasonable minimum. + tcpListen() + +proc listenTcp*(endpoint: IPEndpoint, backlog = none(Natural)): Listener[TCP] + {.raises: [OSError].} = + ## Listen at `endpoint` for TCP connections. + ## + ## If the port of the endpoint is `PortNone`, an ephemeral port will be + ## reserved automatically by the operating system. `localEndpoint` can be + ## used to retrieve the port number. + ## + ## The `backlog` parameter defines the maximum amount of pending connections. + ## If a connection request arrives when the queue is full, the client might + ## receive a "Connection refused" error or the connection might be silently + ## dropped. This value is treated by most operating systems as a hint. + ## + ## If `backlog` is `None`, the maximum queue length will be selected. + ## + ## If `backlog` is `0`, the OS will select a reasonable minimum. + case endpoint.kind + of V4: listenTcp(endpoint.v4) + of V6: listenTcp(endpoint.v6) + proc listenTcp*(endpoints: ResolverResult, backlog = none(Natural)): Listener[TCP] {.raises: [OSError, IncompatibleEndpointError].} = ## Listen for TCP connections at one of the endpoint in `endpoints`. @@ -589,7 +672,26 @@ proc listenTcp*(host: IP4, port: Port, backlog = none(Natural)): Listener[TCP] ## If `backlog` is `0`, the OS will select a reasonable minimum. listenTcp(initEndpoint(host, port), backlog) -proc listenTcp*(host: string, port: Port, backlog = none(Natural)): Listener[TCP] +proc listenTcp*(host: IP6, port: Port, backlog = none(Natural)): Listener[TCP] + {.inline, raises: [OSError].} = + ## Listen at `host` and `port` for TCP connections. + ## + ## If the port of the endpoint is `PortNone`, an ephemeral port will be + ## reserved automatically by the operating system. `localEndpoint` can be + ## used to fetch this data. + ## + ## The `backlog` parameter defines the maximum amount of pending connections. + ## If a connection request arrives when the queue is full, the client might + ## receive a "Connection refused" error or the connection might be silently + ## dropped. This value is treated by most operating systems as a hint. + ## + ## If `backlog` is `None`, the maximum queue length will be selected. + ## + ## If `backlog` is `0`, the OS will select a reasonable minimum. + listenTcp(initEndpoint(host, port), backlog) + +proc listenTcp*(host: string, port: Port, kind = none(IPEndpointKind), + backlog = none(Natural)): Listener[TCP] {.inline, raises: [OSError, IncompatibleEndpointError, ResolverError].} = ## Listen at `host` and `port` for TCP connections. ## @@ -605,7 +707,7 @@ proc listenTcp*(host: string, port: Port, backlog = none(Natural)): Listener[TCP ## If `backlog` is `None`, the maximum queue length will be selected. ## ## If `backlog` is `0`, the OS will select a reasonable minimum. - listenTcp(resolveIP4(host, port), backlog) + listenTcp(resolveIP(host, port, kind), backlog) {.warning: "Compiler bug workaround, see https://github.com/nim-lang/Nim/issues/19118".} proc listenTcpAsync*(endpoint: IP4Endpoint, backlog: Option[Natural] = none(Natural)): AsyncListener[TCP] @@ -626,6 +728,52 @@ proc listenTcpAsync*(endpoint: IP4Endpoint, backlog: Option[Natural] = none(Natu ## If `backlog` is `0`, the OS will select a reasonable minimum. tcpAsyncListen() +{.warning: "Compiler bug workaround, see https://github.com/nim-lang/Nim/issues/19118".} +proc listenTcpAsync*(endpoint: IP6Endpoint, backlog: Option[Natural] = none(Natural)): AsyncListener[TCP] + {.asyncio.} = + ## Listen at `endpoint` for TCP connections asynchronously. + ## + ## If the port of the endpoint is `PortNone`, an ephemeral port will be + ## reserved automatically by the operating system. `localEndpoint` can be + ## used to retrieve the port number. + ## + ## The `backlog` parameter defines the maximum amount of pending connections. + ## If a connection request arrives when the queue is full, the client might + ## receive a "Connection refused" error or the connection might be silently + ## dropped. This value is treated by most operating systems as a hint. + ## + ## If `backlog` is `None`, the maximum queue length will be selected. + ## + ## If `backlog` is `0`, the OS will select a reasonable minimum. + tcpAsyncListen() + +{.warning: "Compiler bug workaround, see https://github.com/nim-lang/Nim/issues/19118".} +proc listenTcpAsync*(endpoint: IPEndpoint, backlog: Option[Natural] = none(Natural)): AsyncListener[TCP] + {.asyncio.} = + ## Listen at `endpoint` for TCP connections asynchronously. + ## + ## If the port of the endpoint is `PortNone`, an ephemeral port will be + ## reserved automatically by the operating system. `localEndpoint` can be + ## used to retrieve the port number. + ## + ## The `backlog` parameter defines the maximum amount of pending connections. + ## If a connection request arrives when the queue is full, the client might + ## receive a "Connection refused" error or the connection might be silently + ## dropped. This value is treated by most operating systems as a hint. + ## + ## If `backlog` is `None`, the maximum queue length will be selected. + ## + ## If `backlog` is `0`, the OS will select a reasonable minimum. + case endpoint.kind + of V4: + # XXX: https://github.com/nim-works/cps/issues/301 + let v4 = endpoint.v4 + listenTcpAsync(v4) + of V6: + # XXX: https://github.com/nim-works/cps/issues/301 + let v6 = endpoint.v6 + listenTcpAsync(v6) + proc listenTcpAsync*(endpoints: ResolverResult, backlog: Option[Natural] = none(Natural)): AsyncListener[TCP] {.asyncio.} = ## Listen for TCP connections at one of the endpoint in `endpoints`. @@ -648,7 +796,7 @@ proc listenTcpAsync*(endpoints: ResolverResult, backlog: Option[Natural] = none( attempted = false lastError: ref OSError {.warning: "Workaround for nim-works/cps#185".} - let next: iterator (): IP4Endpoint = closureItems(endpoints) + let next: iterator (): IPEndpoint = closureItems(endpoints) while true: attempted = true let ep = next() @@ -688,7 +836,27 @@ proc listenTcpAsync*(host: IP4, port: Port, backlog: Option[Natural] = none(Natu ## If `backlog` is `0`, the OS will select a reasonable minimum. listenTcpAsync(initEndpoint(host, port), backlog) -proc listenTcpAsync*(host: string, port: Port, backlog: Option[Natural] = none(Natural)): AsyncListener[TCP] +proc listenTcpAsync*(host: IP6, port: Port, backlog: Option[Natural] = none(Natural)): AsyncListener[TCP] + {.asyncio.} = + ## Listen at `host` and `port` for TCP connections. + ## + ## If the port of the endpoint is `PortNone`, an ephemeral port will be + ## reserved automatically by the operating system. `localEndpoint` can be + ## used to retrieve the port number. + ## + ## The `backlog` parameter defines the maximum amount of pending connections. + ## If a connection request arrives when the queue is full, the client might + ## receive a "Connection refused" error or the connection might be silently + ## dropped. This value is treated by most operating systems as a hint. + ## + ## If `backlog` is `None`, the maximum queue length will be selected. + ## + ## If `backlog` is `0`, the OS will select a reasonable minimum. + listenTcpAsync(initEndpoint(host, port), backlog) + +proc listenTcpAsync*(host: string, port: Port, + kind: Option[IPEndpointKind] = none(IPEndpointKind), + backlog: Option[Natural] = none(Natural)): AsyncListener[TCP] {.asyncio.} = ## Listen at `host` and `port` for TCP connections. ## @@ -706,20 +874,20 @@ proc listenTcpAsync*(host: string, port: Port, backlog: Option[Natural] = none(N ## If `backlog` is `0`, the OS will select a reasonable minimum. # A move have to be performed due to the compiler thinking that this is a # "copy". - listenTcpAsync(resolveIP4(host, port), backlog) + listenTcpAsync(resolveIP(host, port, kind), backlog) -proc accept*(l: Listener[TCP]): tuple[conn: Conn[TCP], remote: IP4Endpoint] {.raises: [OSError].} = +proc accept*(l: Listener[TCP]): tuple[conn: Conn[TCP], remote: IPEndpoint] {.raises: [OSError].} = ## Get the first connection from the queue of pending connections of `l`. ## ## Returns the connection and its endpoint. tcpAccept() -proc accept*(l: AsyncListener[TCP]): tuple[conn: AsyncConn[TCP], remote: IP4Endpoint] {.asyncio.} = +proc accept*(l: AsyncListener[TCP]): tuple[conn: AsyncConn[TCP], remote: IPEndpoint] {.asyncio.} = ## Get the first connection from the queue of pending connections of `l`. ## ## Returns the connection and its endpoint. tcpAsyncAccept() -proc localEndpoint*(l: AsyncListener[TCP] | Listener[TCP]): IP4Endpoint {.raises: [OSError].} = +proc localEndpoint*(l: AsyncListener[TCP] | Listener[TCP]): IPEndpoint {.raises: [OSError].} = ## Obtain the local endpoint of `l`. tcpLocalEndpoint() diff --git a/src/sys/sockets/addresses.nim b/src/sys/sockets/addresses.nim index 40d5552..8503065 100644 --- a/src/sys/sockets/addresses.nim +++ b/src/sys/sockets/addresses.nim @@ -9,6 +9,7 @@ ## Addresses and related utilities import pkg/stew/endians2 +import strformat when defined(posix): include ".."/private/addresses_posix @@ -100,3 +101,139 @@ proc ip*(e: IP4Endpoint): IP4 = proc port*(e: IP4Endpoint): Port = ## Returns the port of the endpoint. ip4EndpointPort() + +proc `$`*(e: IP4Endpoint): string = + &"{e.ip}:{e.port}" + +type + IP6* = IP6Impl + ## An IPv6 address. + +func ip6*(a, b, c, d, e, f, g, h: uint16): IP6 = + ## Creates an `IP4` object of the address `a:b:c:d:e:f:g:h`. + var idx = 0 + for x in [a, b, c, d, e, f, g, h]: + let bytes = x.toBytesBE() + result.octets[idx] = bytes[0] + result.octets[idx + 1] = bytes[1] + inc idx, 2 + +const + IP6Loopback* = ip6(0, 0, 0, 0, 0, 0, 0, 1) + ## The IPv6 loopback address. + IP6Any* = ip6(0, 0, 0, 0, 0, 0, 0, 0) + ## The IPv6 address used to specify binding to any address. + +func `==`*(a, b: IP6): bool = + ## Returns whether `a` and `b` points to the same address. + a.octets == b.octets + +func `[]`*(ip: IP6, idx: Natural): uint16 {.inline.} = + ## Returns the `uint16` at position `idx`. + fromBytesBE(uint16, ip.octets.toOpenArray(idx * 2, idx * 2 + 1)) + +func `[]=`*(ip: var IP6, idx: Natural, val: uint16) {.inline.} = + ## Set the `uint16` at position `idx` to `val`. + let bytes = toBytesBE(val) + ip.octets[idx * 2] = bytes[0] + ip.octets[idx * 2 + 1] = bytes[1] + +func len*(ip: IP6): int {.inline.} = + ## Returns the number of `uint16` in `ip`. + 8 + +func isV4Mapped*(ip: IP6): bool = + ## Returns whether `ip` is an IPv4-mapped address as described in RFC4291. + for x in 0 ..< 10: + if ip.octets[x] != 0: + return false + + result = ip.octets[10] == 0xff and ip.octets[11] == 0xff + +func `$`*(ip: IP6): string = + ## Returns the string representation of `ip`. + if ip.isV4Mapped(): + result = "::ffff:" & $ip.octets[12] & '.' & $ip.octets[13] & '.' & $ip.octets[14] & '.' & $ip.octets[15] + else: + var zeroSlice = -1 .. -2 + var idx = 0 + while idx < ip.len: + if ip[idx] == 0: + let start = idx + while idx < ip.len and ip[idx] == 0: + inc idx + let slice = start ..< idx + + if slice.len > zeroSlice.len: + zeroSlice = slice + else: + inc idx + + func addIp6Slice(s: var string, ip: IP6, slice: Slice[int]) = + if slice.len > 0: + var slice = slice + s.add &"{ip[slice.a]:x}" + inc slice.a + for idx in slice: + s.add &":{ip[idx]:x}" + + if zeroSlice.len > 1: + result.addIp6Slice(ip, 0 ..< zeroSlice.a) + result.add "::" + result.addIp6Slice(ip, zeroSlice.b + 1 ..< ip.len) + else: + result.addIp6Slice(ip, 0 ..< ip.len) + +type + IP6Endpoint* = IP6EndpointImpl + ## An IPv6 endpoint, which is a combination of an IPv4 address, a port, a + ## flow identifier and a scope identifier. + + FlowId* = distinct uint32 + ## A 20-bit flow identifier. As RFC3493 does not specify an interpretation, + ## the library treats this type as opaque and does not perform any + ## byte-ordering conversions. + ## + ## From testing, it appears that most operating systems use network byte + ## ordering for values of this type. + + ScopeId* = distinct uint32 + ## A 32-bit address scope identifier. As RFC3493 does not specify an + ## interpretation, the library treats this type as opaque and does not + ## perform any byte-ordering conversions. + ## + ## From testing, it appears that most operating systems use host byte + ## ordering for values of this type. + +proc initEndpoint*(ip: IP6, port: Port, flowId = 0.FlowId, + scopeId = 0.ScopeId): IP6Endpoint = + ## Creates an IPv6 endpoint. + ip6InitEndpoint() + +proc ip*(e: IP6Endpoint): IP6 = + ## Returns the IPv6 address of the endpoint. + ip6EndpointAddr() + +proc port*(e: IP6Endpoint): Port = + ## Returns the port of the endpoint. + ip6EndpointPort() + +proc flowId*(e: IP6Endpoint): FlowId = + ## Returns the flow identifier of the endpoint. + ip6EndpointFlowId() + +proc scopeId*(e: IP6Endpoint): ScopeId = + ## Returns the scope identifier of the endpoint. + ip6EndpointScopeId() + +type + IPEndpointKind* {.pure.} = enum + ## The address family of an endpoint. + V4 + V6 + + IPEndpoint* = object + ## An object containing either IPv4 or IPv6 endpoint. + case kind*: IPEndpointKind + of V4: v4*: IP4Endpoint + of V6: v6*: IP6Endpoint diff --git a/tests/sockets/tip.nim b/tests/sockets/tip.nim index 4974b43..e1adb1d 100644 --- a/tests/sockets/tip.nim +++ b/tests/sockets/tip.nim @@ -1,3 +1,4 @@ +import std/options import pkg/balls import sys/sockets @@ -7,9 +8,47 @@ suite "IP address testing": check $ip4(1, 1, 1, 1) == "1.1.1.1" test "Resolving localhost works": - block test: - for ep in resolveIP4("localhost").items: - if ep.ip == ip4(127, 0, 0, 1): - break test + var + foundV4 = false + foundV6 = false + for ep in resolveIP("localhost").items: + if ep.kind == V4 and ep.v4.ip == ip4(127, 0, 0, 1): + foundV4 = true + elif ep.kind == V6 and ep.v6.ip == ip6(0, 0, 0, 0, 0, 0, 0, 1): + foundV6 = true - check false, "did not find 127.0.0.1 when resolving for localhost" + check foundV4, "did not find 127.0.0.1 when resolving for localhost" + check foundV6, "did not find ::1 when resolving for localhost" + + test "Scoped resolve for localhost": + var foundV4 = false + for ep in resolveIP("localhost", kind = some(V4)).items: + if ep.kind == V4 and ep.v4.ip == ip4(127, 0, 0, 1): + foundV4 = true + elif ep.kind == V6: + check false, "found IPv6 for localhost but configured to resolve only IPv4 addresses" + + check foundV4, "did not find 127.0.0.1 when resolving for localhost" + + var foundV6 = false + for ep in resolveIP("localhost", kind = some(V6)).items: + if ep.kind == V6 and ep.v6.ip == ip6(0, 0, 0, 0, 0, 0, 0, 1): + foundV6 = true + elif ep.kind == V4: + check false, "found IPv4 for localhost but configured to resolve only IPv6 addresses" + + check foundV6, "did not find ::1 when resolving for localhost" + + test "IPv6 to string": + check $ip6(0, 0, 0, 0, 0, 0, 0, 0) == "::" + check $ip6(0, 0, 0, 0, 0, 0, 0, 1) == "::1" + check $ip6(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0) == "2001:db8::" + check $ip6(0x2001, 0xdb8, 0x1b, 0, 0x2, 0, 0, 0) == "2001:db8:1b:0:2::" + check $ip6(0x2001, 0xdb8, 0x1b, 0, 0, 0, 0, 0xfd) == "2001:db8:1b::fd" + check $ip6(0x2001, 0xdb8, 0, 0x3, 0x2, 0x4d, 0x5c, 0x6d) == "2001:db8:0:3:2:4d:5c:6d" + check $ip6(0x2001, 0xdb8, 0x1b, 0x3, 0x2, 0x4d, 0x5c, 0x6d) == "2001:db8:1b:3:2:4d:5c:6d" + + test "IPv4-mapped IPv6 to string": + check $ip6(0, 0, 0, 0, 0, 0xffff, 0x7f00, 1) == "::ffff:127.0.0.1" + check $ip6(0, 0, 0, 0, 0, 0xffff, 0xc000, 0x2a0) == "::ffff:192.0.2.160" + check $ip6(0xfe80, 0, 0, 0, 0, 0xffff, 0xc000, 0x2a0) == "fe80::ffff:c000:2a0" diff --git a/tests/sockets/tsockets.nim b/tests/sockets/tsockets.nim index dcae7f2..7499b3b 100644 --- a/tests/sockets/tsockets.nim +++ b/tests/sockets/tsockets.nim @@ -1,4 +1,4 @@ -import std/[locks, strutils] +import std/[locks, strutils, options] import pkg/balls import sys/[files, ioqueue, sockets] import ".."/helpers/io @@ -10,12 +10,12 @@ makeDelimRead(AsyncConn[TCP]) suite "TCP sockets": test "Listening on TCP port 0 will create a random port": let server = listenTcp(IP4Loopback, PortNone) - check server.localEndpoint.port != PortNone + check server.localEndpoint.v4.port != PortNone proc checkAsync() {.asyncio.} = let asyncServer = listenTcpAsync(IP4Loopback, PortNone) - check asyncServer.localEndpoint.port != PortNone - check asyncServer.localEndpoint.port != server.localEndpoint.port + check asyncServer.localEndpoint.v4.port != PortNone + check asyncServer.localEndpoint.v4.port != server.localEndpoint.v4.port checkAsync() run() @@ -27,13 +27,13 @@ suite "TCP sockets": # Accept then close connection immediately close s[].accept().conn - var server = listenTcp("localhost", PortNone) - check server.localEndpoint.port != PortNone + var server = listenTcp("localhost", PortNone, kind = some(V4)) + check server.localEndpoint.v4.port != PortNone var thr: Thread[ptr Listener[TCP]] thr.createThread(acceptWorker, addr server) # Connect then disconnect immediately - close connectTcp("localhost", server.localEndpoint.port) + close connectTcp("localhost", server.localEndpoint.v4.port) # Close the thread joinThread thr @@ -43,15 +43,18 @@ suite "TCP sockets": close s.accept().conn proc checkAsync() {.asyncio.} = - let asyncServer = listenTcpAsync("localhost", PortNone) + let asyncServer = listenTcpAsync("localhost", PortNone, kind = some(V4)) # Run until the worker dismisses to the background discard trampoline: whelp acceptWorker(asyncServer) - check asyncServer.localEndpoint.port != PortNone - check asyncServer.localEndpoint.port != server.localEndpoint.port + check asyncServer.localEndpoint.v4.port != PortNone + check asyncServer.localEndpoint.v4.port != server.localEndpoint.v4.port # Connect then disconnect immediately - close connectTcpAsync("localhost", asyncServer.localEndpoint.port) + let + lEndpoint = asyncServer.localEndpoint.v4 + port = lEndpoint.port + close connectTcpAsync("localhost", port) checkAsync() run()