├── examples ├── config.nims ├── example3.nim ├── example5.nim ├── example4.nim ├── example1.nim ├── example2.nim ├── opt_rr.nim └── resolver.nim ├── ndns.nimble ├── LICENSE ├── src ├── ndns │ └── platforms │ │ ├── winapi.nim │ │ └── resolv.nim └── ndns.nim └── README.md /examples/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") 2 | -------------------------------------------------------------------------------- /examples/example3.nim: -------------------------------------------------------------------------------- 1 | import ndns 2 | 3 | let client = initDnsClient() 4 | 5 | echo resolveIpv4(client, "nim-lang.org") 6 | -------------------------------------------------------------------------------- /examples/example5.nim: -------------------------------------------------------------------------------- 1 | import ndns 2 | 3 | let client = initSystemDnsClient() 4 | 5 | echo resolveIpv4(client, "nim-lang.org") 6 | -------------------------------------------------------------------------------- /examples/example4.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch, ndns 2 | 3 | let client = initDnsClient() 4 | 5 | echo waitFor asyncResolveIpv4(client, "nim-lang.org") 6 | -------------------------------------------------------------------------------- /ndns.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2.0" 4 | author = "rockcavera" 5 | description = "A pure Nim Domain Name System (DNS) client" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.4.0" 14 | requires "dnsprotocol >= 0.2.2" 15 | requires "stew" 16 | -------------------------------------------------------------------------------- /examples/example1.nim: -------------------------------------------------------------------------------- 1 | import ndns 2 | 3 | let header = initHeader(randId(), rd = true) 4 | 5 | let question = initQuestion("nim-lang.org", QType.A, QClass.IN) 6 | # If the last character of "nim-lang.org" is not a '.', the initializer will 7 | # add, as it is called the DNS root. 8 | 9 | let msg = initMessage(header, @[question]) 10 | # The initializer automatically changes `header.qdcount` to `1'u16` 11 | 12 | let client = initDnsClient() 13 | 14 | var rmsg = dnsQuery(client, msg) 15 | 16 | echo repr(rmsg) 17 | -------------------------------------------------------------------------------- /examples/example2.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch, ndns 2 | 3 | let header = initHeader(randId(), rd = true) 4 | 5 | let question = initQuestion("nim-lang.org", QType.A, QClass.IN) 6 | # If the last character of "nim-lang.org" is not a '.', the initializer will 7 | # add, as it is called the DNS root. 8 | 9 | let msg = initMessage(header, @[question]) 10 | # The initializer automatically changes `header.qdcount` to `1'u16` 11 | 12 | let client = initDnsClient() 13 | 14 | var rmsg = waitFor dnsAsyncQuery(client, msg) 15 | 16 | echo repr(rmsg) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rockcavera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/opt_rr.nim: -------------------------------------------------------------------------------- 1 | import ndns 2 | 3 | import std/[asyncdispatch, macros] 4 | 5 | # code taken from std/enumutils 6 | macro enumFullRange(a: typed): untyped = 7 | newNimNode(nnkBracket).add(a.getType[1][1..^1]) 8 | 9 | proc dumpDnsMessage(msg: Message) = 10 | echo "Header: ", msg.header 11 | echo "Questions: ", msg.questions 12 | echo "Authorities: ", msg.authorities 13 | echo "Additionals: ", msg.additionals 14 | echo "Answers:" 15 | 16 | for res in msg.answers: 17 | if res.`type` in enumFullRange(Type): 18 | echo res 19 | else: 20 | echo "Unknown `Type`: ", cast[int](res.`type`) 21 | 22 | proc udpPayloadSize() = 23 | let 24 | header = initHeader(randId(), rd = true) 25 | question = initQuestion("google.com", QType.ANY, QClass.IN) 26 | opt = initOptRR(1280, 0, 0, false, 0, nil) 27 | msg = initMessage(header, @[question], additionals = @[opt]) 28 | client = initDnsClient() 29 | smsg = dnsQuery(client, msg) 30 | 31 | echo "---------Synchronous----------" 32 | dumpDnsMessage(smsg) 33 | 34 | echo "---------Asynchronous----------" 35 | let amsg = waitFor dnsAsyncQuery(client, msg, 5000) 36 | dumpDnsMessage(amsg) 37 | 38 | udpPayloadSize() 39 | -------------------------------------------------------------------------------- /src/ndns/platforms/winapi.nim: -------------------------------------------------------------------------------- 1 | ## Minimal implementation to get System DNS Server (IPv4 only) 2 | ## 3 | ## This implementation uses the winapi function `GetNetworkParams` and should 4 | ## work for Windows. 5 | ## 6 | ## References: 7 | ## - https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getnetworkparams 8 | 9 | const 10 | MAX_HOSTNAME_LEN = 128 11 | MAX_DOMAIN_NAME_LEN = 128 12 | MAX_SCOPE_ID_LEN = 256 13 | 14 | ERROR_SUCCESS = 0 15 | ERROR_BUFFER_OVERFLOW = 111 16 | 17 | type 18 | # https://learn.microsoft.com/en-us/windows/win32/api/iptypes/ns-iptypes-ip_address_string 19 | IP_ADDRESS_STRING = object 20 | `string`: array[16, char] 21 | 22 | IP_MASK_STRING = IP_ADDRESS_STRING 23 | 24 | # https://learn.microsoft.com/en-us/windows/win32/api/iptypes/ns-iptypes-ip_addr_string 25 | IP_ADDR_STRING = object 26 | next: PIP_ADDR_STRING 27 | ipAddress: IP_ADDRESS_STRING 28 | ipMask: IP_MASK_STRING 29 | context: int32 30 | 31 | PIP_ADDR_STRING = ptr IP_ADDR_STRING 32 | 33 | # https://learn.microsoft.com/en-us/windows/win32/api/iptypes/ns-iptypes-fixed_info_w2ksp1 34 | FIXED_INFO_W2KSP1 = object 35 | hostName: array[MAX_HOSTNAME_LEN + 4, char] 36 | domainName: array[MAX_DOMAIN_NAME_LEN + 4, char] 37 | currentDnsServer: PIP_ADDR_STRING 38 | dnsServerList: IP_ADDR_STRING 39 | nodeType: uint32 40 | scopeId: array[MAX_SCOPE_ID_LEN + 4, char] 41 | enableRouting: uint32 42 | enableProxy: uint32 43 | enableDns: uint32 44 | 45 | FIXED_INFO = FIXED_INFO_W2KSP1 46 | PFIXED_INFO = ptr FIXED_INFO 47 | 48 | # https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getnetworkparams 49 | proc getNetworkParams(pFixedInfo: PFIXED_INFO, pOutBufLen: var uint32): int32 {.importc: "GetNetworkParams", stdcall, dynlib: "Iphlpapi.dll".} 50 | 51 | proc getSystemDnsServer*(): string = 52 | ## Returns the IPv4 used by the system for DNS resolution. Otherwise it 53 | ## returns an empty string `""`. 54 | var 55 | bufLen = uint32(sizeof(FIXED_INFO) and 0xFFFFFFFF) 56 | buf = cast[PFIXED_INFO](alloc0(int(bufLen))) 57 | 58 | if isNil(buf): 59 | raise newException(CatchableError, "Error allocating memory needed to call GetNetworkParams") 60 | 61 | var success = getNetworkParams(buf, bufLen) 62 | 63 | if success == ERROR_BUFFER_OVERFLOW: 64 | buf = cast[PFIXED_INFO](realloc0(buf, sizeof(FIXED_INFO), int(bufLen))) 65 | 66 | if isNil(buf): 67 | raise newException(CatchableError, "Error allocating memory needed to call GetNetworkParams") 68 | 69 | success = getNetworkParams(buf, bufLen) 70 | 71 | if success == ERROR_SUCCESS: 72 | result = $cast[cstring](addr buf.dnsServerList.ipAddress.`string`) 73 | 74 | dealloc(buf) 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A pure Nim Domain Name System (DNS) client implemented with [dnsprotocol](https://github.com/rockcavera/nim-dnsprotocol). 2 | 3 | This implementation has synchronous and asynchronous (async) procedures (procs) for transmitting data over the internet, using both UDP and TCP protocol. 4 | # Install 5 | `nimble install ndns` 6 | 7 | or 8 | 9 | `nimble install https://github.com/rockcavera/nim-ndns.git` 10 | # Basic Use 11 | Resolving IPv4 addresses for nim-lang.org (**not async**): 12 | ```nim 13 | import ndns 14 | 15 | let client = initDnsClient() 16 | 17 | echo resolveIpv4(client, "nim-lang.org") 18 | ``` 19 | 20 | Resolving IPv4 addresses for nim-lang.org (**async**): 21 | ```nim 22 | import asyncdispatch, ndns 23 | 24 | let client = initDnsClient() 25 | 26 | echo waitFor asyncResolveIpv4(client, "nim-lang.org") 27 | ``` 28 | 29 | For a "real-life" async example, see [resolver.nim](/examples/resolver.nim). In this example I have made as many comments as possible, even if they look silly. I think it might help someone, as a similar example I provided privately for a newcomer to Nim. It can also be compiled with `-d:showLoopLog` to show the async workflow. 30 | # Advanced Use 31 | Creating a `Message` object with a `QType.A` query for the domain name nim-lang.org, transmitting the `Message` and receiving the response (**not async**): 32 | ```nim 33 | import ndns 34 | 35 | let header = initHeader(randId(), rd = true) 36 | 37 | let question = initQuestion("nim-lang.org", QType.A, QClass.IN) 38 | # If the last character of "nim-lang.org" is not a '.', the initializer will 39 | # add, as it is called the DNS root. 40 | 41 | let msg = initMessage(header, @[question]) 42 | # The initializer automatically changes `header.qdcount` to `1'u16` 43 | 44 | let client = initDnsClient() 45 | 46 | var rmsg = dnsQuery(client, msg) 47 | 48 | echo repr(rmsg) 49 | ``` 50 | 51 | Creating a `Message` object with a `QType.A` query for the domain name nim-lang.org, transmitting the `Message` and receiving the response (**async**): 52 | ```nim 53 | import asyncdispatch, ndns 54 | 55 | let header = initHeader(randId(), rd = true) 56 | 57 | let question = initQuestion("nim-lang.org", QType.A, QClass.IN) 58 | # If the last character of "nim-lang.org" is not a '.', the initializer will 59 | # add, as it is called the DNS root. 60 | 61 | let msg = initMessage(header, @[question]) 62 | # The initializer automatically changes `header.qdcount` to `1'u16` 63 | 64 | let client = initDnsClient() 65 | 66 | var rmsg = waitFor dnsAsyncQuery(client, msg) 67 | 68 | echo repr(rmsg) 69 | ``` 70 | # Using System DNS Server 71 | You can initialize the DNS client with the DNS resolver server used by the system. To do this, start the client with `initSystemDnsClient`. 72 | ```nim 73 | import ndns 74 | 75 | let client = initSystemDnsClient() 76 | 77 | echo resolveIpv4(client, "nim-lang.org") 78 | ``` 79 | # Documentation 80 | https://rockcavera.github.io/nim-ndns/ndns.html 81 | -------------------------------------------------------------------------------- /examples/resolver.nim: -------------------------------------------------------------------------------- 1 | # Usage: resolver domain.com [domain2.com ... domainN.com] 2 | # 3 | # For those who don't quite understand the async workflow in Nim, you can compile 4 | # with `-d:showLoopLog`. 5 | # 6 | # It is important to understand that within the infinite loop of events you can perform other 7 | # operations that may or may not block the thread. 8 | # Examples: 9 | # 1) you can change the code to resolve 2 domains at a time; 10 | # 2) you can resolve a domain and maintain an IRC connection; 11 | # 3) you can have an HTTP server and a domain resolution. 12 | # 13 | # There are several possibilities. 14 | 15 | import std/[asyncdispatch, os, strformat] # Importing modules from stdlib 16 | 17 | import ndns # pkg/ndns - Importing the `ndns` package 18 | 19 | var isResolvingAnyDns = false # A boolean to know if there is already a domain being resolved 20 | 21 | proc resolveDomain(client: DnsClient, strDomain: string) {.async.} = 22 | # Declaring an asynchronous procedure to perform resolution of `strDomain` and print the IPv4 23 | # received as a response. See https://nim-lang.org/docs/asyncdispatch.html#asynchronous-procedures 24 | isResolvingAnyDns = true # Setting to `true` to resolve only one at a time 25 | 26 | echo fmt"Resolving `{strDomain}`..." 27 | 28 | let allIpv4 = await asyncResolveIpv4(client, strDomain) # Calls the procedure `asyncResolveIpv4`, 29 | # asynchronously, using `await`, which 30 | # also doesn't lock the thread, but makes 31 | # return here when `Future[seq[string]]` 32 | # is ready, which is the return value of 33 | # called procedure. 34 | when defined(showLoopLog): 35 | let domainName = fmt"`{strDomain}`" 36 | else: 37 | let domainName = fmt" " 38 | 39 | if len(allIpv4) == 0: 40 | echo fmt"{domainName} did not return IPv4. Possibly it does not exist.{'\n'}" 41 | elif len(allIpv4) == 1: 42 | echo fmt"{domainName} Address:{'\n'} {allIpv4[0]}{'\n'}" 43 | else: 44 | echo fmt"{domainName} Addresses:" 45 | for ip in allIpv4: # Print all IPv4 returned in `allIpv4` 46 | echo fmt" {ip}" 47 | 48 | echo "" 49 | 50 | isResolvingAnyDns = false # Setting it to `false`, so the event loop can resolve the next domain 51 | 52 | proc main() = 53 | let argsCount = paramCount() # https://nim-lang.org/docs/os.html#paramCount 54 | 55 | if argsCount == 0: 56 | echo fmt"""Usage: 57 | {getAppFilename().extractFilename} domain.com [domain2.com ... domainN.com]""" 58 | quit(0) 59 | 60 | let client = initSystemDnsClient() # https://rockcavera.github.io/nim-ndns/ndns.html#initSystemDnsClient 61 | 62 | echo fmt"DNS client initialized using DNS Server: {getIp(client)}{'\n'}" 63 | 64 | var 65 | countLoop = 0 # Counter of how many loops it will take to resolve all domains 66 | x = 1 # Current index of passed arguments 67 | 68 | while true: # Infinite loop of events. It will resolve only one DNS at a time asynchronously, but 69 | # the loop will continue... 70 | when defined(showLoopLog): 71 | inc(countLoop) # Increasing the counter 72 | echo fmt"Starting loop {countLoop}" 73 | 74 | if not isResolvingAnyDns: # If it's not resolving any domain... 75 | while (x <= argsCount) and (paramStr(x) == ""): inc(x) # Skip empty `paramStr(x)`. 76 | 77 | if x <= argsCount: # If the index is less than or equal to the number of arguments passed 78 | when defined(showLoopLog): 79 | echo "Before calling `resolveDomain`" 80 | asyncCheck resolveDomain(client, paramStr(x)) # Calls the async procedure `resolveDomain`, 81 | # without blocking the thread, to resolve the 82 | # domain present in `paramStr(x)`. 83 | when defined(showLoopLog): 84 | echo "After calling `resolveDomain`" 85 | 86 | inc(x) # Increase index 87 | else: 88 | when defined(showLoopLog): 89 | echo fmt"Work finished in loop {countLoop}" 90 | break # Exits the infinite loop of events as it has no more domains to resolve 91 | 92 | if hasPendingOperations(): # https://nim-lang.org/docs/asyncdispatch.html#hasPendingOperations 93 | when defined(showLoopLog): 94 | echo "Calling `poll`..." 95 | poll(15) # https://nim-lang.org/docs/asyncdispatch.html#poll%2Cint 96 | 97 | when defined(showLoopLog): 98 | echo fmt"Ending loop {countLoop}{'\n'}" 99 | 100 | main() 101 | -------------------------------------------------------------------------------- /src/ndns/platforms/resolv.nim: -------------------------------------------------------------------------------- 1 | ## Minimal implementation to get System DNS Server (IPv4 and IPv6). This implementation is heavily 2 | ## influenced by glibc. 3 | ## 4 | ## Implements a parser to capture the first nameserver in the resolver configuration file, which is, 5 | ## by default, /etc/resolv.conf. You can change this file by passing to compile 6 | ## `-d:ndnsPathResConf=/etc/myresolv.conf`. 7 | ## 8 | ## Checking for changes in the `ndnsPathResConf` file performed at each new `getSystemDnsServer()` 9 | ## call makes the code 2x faster, if there is no change. 10 | ## 11 | ## This implementation should work for systems that adopt resolver. Currently this implementation is 12 | ## imported into Linux and BSD. If your platform uses a resolver configuration file, compile with 13 | ## `-d:ndnsUseResolver`. 14 | ## 15 | ## References: 16 | ## - https://man7.org/linux/man-pages/man5/resolv.conf.5.html 17 | import std/[os, parseutils, times] 18 | 19 | type 20 | FileChangeDetection = object 21 | ## Object for `ndnsPathResConf` file information. 22 | fileId: FileId ## Serial ID 23 | size: BiggestInt ## Size 24 | lastWriteTime: Time ## Last write time 25 | creationTime: Time ## Creation time 26 | 27 | ResolvConfGlobal = object 28 | ## Object for global resolver information. 29 | nameserver: string ## The first nameserver caught in the last parse of `ndnsPathResConf` 30 | fileResolvConf: FileChangeDetection ## `ndnsPathResConf` file information during last parse. 31 | initialized: bool ## Determines whether the `ndnsPathResConf` file has already been parsed 32 | 33 | const ndnsPathResConf* {.strdefine.} = "/etc/resolv.conf" 34 | ## Resolver configuration file. You can change by compiling with 35 | ## `-d:ndnsPathResConf=/etc/myresolv.conf`. 36 | 37 | var resolvGlobal: ResolvConfGlobal 38 | ## Keeps information from the `ndnsPathResConf` file and if it has already been parsed. 39 | 40 | proc fileResolvIsUnchanged(): bool = 41 | ## Returns `true` if the `ndnsPathResConf` file has not changed since the last parse. 42 | let fileInfo = getFileInfo(ndnsPathResConf) 43 | 44 | result = (fileInfo.id.file == resolvGlobal.fileResolvConf.fileId) and 45 | (fileInfo.size == resolvGlobal.fileResolvConf.size) and 46 | (fileInfo.creationTime == resolvGlobal.fileResolvConf.creationTime) and 47 | (fileInfo.lastWriteTime == resolvGlobal.fileResolvConf.lastWriteTime) 48 | 49 | proc getSystemDnsServer*(): string = 50 | ## Returns the first nameserver found in the `ndnsPathResConf` file. Will return `""` if not 51 | ## found. 52 | const 53 | comments = { ';', '#' } 54 | whiteSpaces = { ' ', '\t', '\v', '\r', '\n', '\f' } 55 | commentsAndWhiteSpaces = comments + whiteSpaces 56 | 57 | if resolvGlobal.initialized and fileResolvIsUnchanged(): 58 | result = resolvGlobal.nameserver 59 | else: 60 | if fileExists(ndnsPathResConf): 61 | let fileInfo = getFileInfo(ndnsPathResConf) 62 | 63 | for line in lines(ndnsPathResConf): 64 | if line == "": continue # skipe empty line 65 | if line[0] in comments: continue # skip comments 66 | 67 | var strConf: string 68 | 69 | let count = parseUntil(line, strConf, whiteSpaces) 70 | 71 | if count > 0: 72 | case strConf 73 | of "nameserver": 74 | if parseUntil(line, result, commentsAndWhiteSpaces, count + skipWhitespace(line, count)) > 0: 75 | break 76 | else: # for now there is no interest in implementing: domain, search and options 77 | discard 78 | 79 | resolvGlobal.nameserver = result 80 | resolvGlobal.fileResolvConf.fileId = fileInfo.id.file 81 | resolvGlobal.fileResolvConf.size = fileInfo.size 82 | resolvGlobal.fileResolvConf.creationTime = fileInfo.creationTime 83 | resolvGlobal.fileResolvConf.lastWriteTime = fileInfo.lastWriteTime 84 | resolvGlobal.initialized = true 85 | 86 | when false: 87 | # Discontinued implementation. Reasons: 88 | # - Systems based on musl libc do not have `res_init()` implemented; 89 | # - Different implementations of resolv.h. 90 | # - Operating systems using deprecated resolv implementation 91 | # - OpenBSD has its own `struct __res_state` 92 | 93 | ## Minimal implementation to get System DNS Server (IPv4 only) 94 | ## 95 | ## This implementation uses the resolv library, which should work on Linux and 96 | ## BSD. 97 | ## 98 | ## References: 99 | ## - https://man7.org/linux/man-pages/man3/resolver.3.html 100 | ## - https://www.freebsd.org/cgi/man.cgi?query=resolver 101 | ## 102 | ## Using this module implies passing `-lresolv` or `-lc` to the linkage process. 103 | ## 104 | ## Notes: 105 | ## - To use the interface deprecated by the resolv library, compile with 106 | ## `-d:useDeprecatedResolv`. On **OpenBSD** it is recommended to define this symbol 107 | ## when compiling, since the `resolv.h` used on this platform has its own 108 | ## definitions that will only be used when this symbol is defined. 109 | ## - Unfortunately systems based on musl libc do not have `res_init()` 110 | ## implemented. Such a libc loads the settings from "/etc/resolv.conf", when 111 | ## needed, through `__get_resolv_conf()` which is not compatible with 112 | ## `struct __res_state`. Faced with so many divergences found using 113 | ## `resolv.h`, I believe it is better to implement a parser for 114 | ## "/etc/resolv.conf". TODO 115 | when defined(nimdoc): 116 | import std/[net, winlean] 117 | else: 118 | import std/[net, posix] 119 | 120 | when defined(bsd): 121 | {.passL: "-lc".} 122 | else: 123 | {.passL: "-lresolv".} 124 | 125 | const 126 | useOpenBSDResolv = when defined(useDeprecatedResolv) and defined(openbsd): true 127 | else: false 128 | # The structure of res_state in OpenBSD has several peculiarities, as well 129 | # as currently adopts the deprecated version with res_init(). 130 | # https://github.com/openbsd/src/blob/e3c5fa921ef394179421471c88eb2be26d8a6692/include/resolv.h 131 | 132 | MAXNS = 3 133 | MAXDNSRCH = 6 134 | MAXRESOLVSORT = 10 135 | 136 | RES_INIT = 1 137 | 138 | when useOpenBSDResolv: 139 | {.emit: """/*INCLUDESECTION*/ 140 | #include 141 | """.} # See https://github.com/troglobit/inadyn/issues/241 142 | 143 | const MAXDNSLUS = 4 144 | 145 | type 146 | CulongOrCuint = cuint 147 | 148 | ResTimeSpecObj = object 149 | resSec: Time 150 | resNSec: clong 151 | else: 152 | type CulongOrCuint = culong 153 | 154 | type 155 | # Commented for being currently in disuse 156 | #ResSendhookact {.size: 4.} = enum 157 | # ResGoahead, ResNextns, ResModified, ResDone, ResError 158 | 159 | #ResSendQhook = proc (ns: ptr ptr Sockaddr_in, query: ptr ptr uint8, 160 | # querylen: ptr cint, ans: ptr uint8, anssiz: cint, 161 | # resplen: ptr cint): ResSendhookact {.cdecl.} 162 | 163 | #ResSendRhook = proc (ns: ptr Sockaddr_in, query: ptr uint8, querylen: cint, 164 | # ans: ptr uint8, anssiz: cint, resplen: ptr cint): ResSendhookact {.cdecl.} 165 | 166 | ResSendQhook = pointer 167 | ResSendRhook = pointer 168 | 169 | SortListObj = object 170 | `addr`: InAddr 171 | mask: uint32 # uint32_t 172 | 173 | Ext = object 174 | nscount: uint16 # u_int16_t 175 | nsmap: array[MAXNS, uint16] 176 | nssocks: array[MAXNS, cint] 177 | nscount6: uint16 178 | nsinit: uint16 179 | nsaddrs: array[MAXNS, ptr Sockaddr_in6] 180 | initstamp: array[2, cuint] 181 | 182 | UUnion {.union.} = object 183 | pad: array[52, cchar] 184 | ext: Ext 185 | 186 | ResState = object 187 | retrans: cint 188 | retry: cint 189 | options: CulongOrCuint 190 | nscount: cint 191 | when useOpenBSDResolv: 192 | family: array[2, cint] 193 | nsaddrList: array[MAXNS, Sockaddr_in] 194 | id: cushort 195 | dnsrch: array[MAXDNSRCH + 1, cstring] 196 | defdname: array[256, cchar] 197 | pfcode: CulongOrCuint 198 | ndots {.bitsize:4.}: cuint 199 | nsort {.bitsize:4.}: cuint 200 | when useOpenBSDResolv: 201 | unused: array[3, cchar] 202 | else: 203 | ipv6_unavail {.bitsize:1.}: cuint 204 | unused {.bitsize:23.}: cuint 205 | sortList: array[MAXRESOLVSORT, SortListObj] 206 | when useOpenBSDResolv: 207 | lookups: array[MAXDNSLUS, cchar] 208 | restimespec: ResTimeSpecObj 209 | reschktime: Time 210 | else: 211 | qhook: ResSendQhook 212 | rhook: ResSendRhook 213 | resHErrno: cint 214 | vcsock: cint 215 | flags: cuint 216 | u: UUnion 217 | 218 | {.push header: "".} 219 | type SResState {.importc: "struct __res_state".} = object 220 | 221 | when not defined(useDeprecatedResolv): 222 | proc resNInit(statep: ptr SResState): cint {.importc: "res_ninit".} 223 | proc resNClose(rstatep: ptr SResState) {.importc: "res_nclose".} 224 | else: 225 | when useOpenBSDResolv: 226 | var res {.importc: "_res".}: SResState # currently it is a C macro for `struct __res_state __res_state(void)` 227 | else: 228 | proc resState(): ptr SResState {.importc: "__res_state".} 229 | 230 | proc resInit(): cint {.importc: "res_init".} 231 | {.pop.} 232 | 233 | proc getSystemDnsServer*(): string = 234 | ## Returns the IPv4 used by the system for DNS resolution. Otherwise it 235 | ## returns an empty string `""`. 236 | var 237 | rs: ResState 238 | ip: IpAddress 239 | port: Port 240 | rInit = when defined(useDeprecatedResolv): resInit() 241 | else: resNInit(cast[ptr SResState](addr rs)) 242 | 243 | if rInit == 0: 244 | when useOpenBSDResolv: 245 | rs = cast[ResState](res) 246 | elif defined(useDeprecatedResolv): 247 | rs = cast[ResState](resState()[]) # If nim compiled with `--threads:on`, on NetBSD it will result in SIGABRT 248 | 249 | if (rs.options and RES_INIT) == RES_INIT: 250 | fromSockAddr(rs.nsaddrList[0], sizeof(Sockaddr_in).SockLen, ip, port) 251 | 252 | result = $ip 253 | 254 | when not defined(useDeprecatedResolv): 255 | resNClose(cast[ptr SResState](addr rs)) 256 | -------------------------------------------------------------------------------- /src/ndns.nim: -------------------------------------------------------------------------------- 1 | ## A pure Nim Domain Name System (DNS) client implemented with 2 | ## [dnsprotocol](https://github.com/rockcavera/nim-dnsprotocol). 3 | ## 4 | ## This implementation has synchronous and asynchronous (async) procedures 5 | ## (procs) for transmitting data over the internet, using both UDP and TCP 6 | ## protocol. 7 | ## 8 | ## Basic Use 9 | ## ========= 10 | ## Resolving IPv4 addresses for nim-lang.org (**not async**): 11 | ## ```nim 12 | ## import ndns 13 | ## 14 | ## let client = initDnsClient() 15 | ## 16 | ## echo resolveIpv4(client, "nim-lang.org") 17 | ## ``` 18 | ## 19 | ## Resolving IPv4 addresses for nim-lang.org (**async**): 20 | ## ```nim 21 | ## import asyncdispatch, ndns 22 | ## 23 | ## let client = initDnsClient() 24 | ## 25 | ## echo waitFor asyncResolveIpv4(client, "nim-lang.org") 26 | ## ``` 27 | ## 28 | ## Advanced Use 29 | ## ============ 30 | ## Creating a `Message` object with a `QType.A` query for the domain name 31 | ## nim-lang.org, transmitting the `Message` and receiving the response (**not 32 | ## async**): 33 | ## ```nim 34 | ## import ndns 35 | ## 36 | ## let header = initHeader(randId(), rd = true) 37 | ## 38 | ## let question = initQuestion("nim-lang.org", QType.A, QClass.IN) 39 | ## # If the last character of "nim-lang.org" is not a '.', the initializer will 40 | ## # add, as it is called the DNS root. 41 | ## 42 | ## let msg = initMessage(header, @[question]) 43 | ## # The initializer automatically changes `header.qdcount` to `1'u16` 44 | ## 45 | ## let client = initDnsClient() 46 | ## 47 | ## var rmsg = dnsQuery(client, msg) 48 | ## 49 | ## echo repr(rmsg) 50 | ## ``` 51 | ## 52 | ## Creating a `Message` object with a `QType.A` query for the domain name 53 | ## nim-lang.org, transmitting the `Message` and receiving the response 54 | ## (**async**): 55 | ## ```nim 56 | ## import asyncdispatch, ndns 57 | ## 58 | ## let header = initHeader(randId(), rd = true) 59 | ## 60 | ## let question = initQuestion("nim-lang.org", QType.A, QClass.IN) 61 | ## # If the last character of "nim-lang.org" is not a '.', the initializer will 62 | ## # add, as it is called the DNS root. 63 | ## 64 | ## let msg = initMessage(header, @[question]) 65 | ## # The initializer automatically changes `header.qdcount` to `1'u16` 66 | ## 67 | ## let client = initDnsClient() 68 | ## 69 | ## var rmsg = waitFor dnsAsyncQuery(client, msg) 70 | ## 71 | ## echo repr(rmsg) 72 | ## ``` 73 | ## 74 | ## Using System DNS Server 75 | ## ======================= 76 | ## You can initialize the DNS client with the DNS resolver server used by the 77 | ## system. To do this, start the client with `initSystemDnsClient`. 78 | ## ```nim 79 | ## import ndns 80 | ## 81 | ## let client = initSystemDnsClient() 82 | ## 83 | ## echo resolveIpv4(client, "nim-lang.org") 84 | ## ``` 85 | ## See `initSystemDnsClient()<#initSystemDnsClient>`_ for supported platforms. 86 | 87 | # Std imports 88 | import std/[asyncdispatch, asyncnet, nativesockets, net, random] 89 | 90 | # Nimble packages imports 91 | import pkg/[dnsprotocol, stew/endians2] 92 | 93 | export dnsprotocol, TimeoutError, Port 94 | 95 | # Internal 96 | when defined(nimdoc): 97 | import ./ndns/platforms/winapi 98 | import ./ndns/platforms/resolv except getSystemDnsServer 99 | else: 100 | when defined(linux) or defined(bsd) or defined(ndnsUseResolver): 101 | import ./ndns/platforms/resolv 102 | elif defined(windows): 103 | import ./ndns/platforms/winapi 104 | 105 | type 106 | DnsClient* = object ## Contains information about the DNS server. 107 | ip: string ## Dns server IP. 108 | port: Port ## DNS server listening port. 109 | domain: Domain 110 | 111 | UnexpectedDisconnectionError* = object of CatchableError 112 | ## Raised if an unexpected disconnect occurs (only TCP). 113 | ResponseIpNotEqualError* = object of CatchableError 114 | ## Raised if the IP that sent the response is different from the IP that 115 | ## received the query (only UDP). 116 | ResponsePortNotEqualError* = object of CatchableError 117 | ## Raised if the Port that sent the response is different from the Port that 118 | ## received the query (only UDP). 119 | ResponseIdNotEqualError* = object of CatchableError 120 | ## Raised if the query ID does not match the response ID. 121 | IsNotAResponseError* = object of CatchableError 122 | ## Raised if not a response (!= QR.Response). 123 | IsNotAnResponseError* {.deprecated: "Use `IsNotAResponseError`".} = IsNotAResponseError # typo 124 | ## Use `IsNotAResponseError`. Raised if not a response (!= QR.Response). 125 | OpCodeNotEqualError* = object of CatchableError 126 | ## Raised if the OpCode is different between the query and the response. 127 | 128 | const 129 | ipv4Arpa = "in-addr.arpa" 130 | ## Special domain reserved for reverse IP lookup for IPv4 131 | ipv6Arpa = "ip6.arpa" 132 | ## Special domain reserved for IP reverse query for IPv6 133 | ndnsDnsServerIp* {.strdefine.} = "8.8.8.8" 134 | ## Default dns server ip for queries. You can change by compiling with 135 | ## `-d:ndnsDnsServerIp=1.1.1.1`. 136 | defaultIpDns* {.deprecated: "Use `ndnsDnsServerIp`".} = ndnsDnsServerIp 137 | ## Kept only for compatibility reasons. 138 | ndnsDnsServerIpDomain = case parseIpAddress(ndnsDnsServerIp).family 139 | of IpAddressFamily.IPv6: AF_INET6 140 | of IpAddressFamily.IPv4: AF_INET 141 | ndnsClient = DnsClient(ip: ndnsDnsServerIp, port: Port(53), domain: ndnsDnsServerIpDomain) 142 | 143 | randomize() 144 | 145 | template initDnsClientImpl() = 146 | let ip = parseIpAddress(strIp) 147 | 148 | result.ip = strIp 149 | result.port = port 150 | 151 | case ip.family 152 | of IpAddressFamily.IPv6: 153 | result.domain = AF_INET6 154 | of IpAddressFamily.IPv4: 155 | result.domain = AF_INET 156 | 157 | proc initDnsClientImpl(strIp: string, port: Port, raiseExceptions: static[bool]): DnsClient = 158 | when raiseExceptions: 159 | initDnsClientImpl() 160 | else: 161 | try: 162 | initDnsClientImpl() 163 | except ValueError: 164 | result = ndnsClient 165 | 166 | proc initDnsClient*(ip: string = ndnsDnsServerIp, port: Port = Port(53)): DnsClient = 167 | ## Returns a created `DnsClient` object. 168 | ## 169 | ## **Parameters** 170 | ## - `ip` is a DNS server IP. It can be IPv4 or IPv6. It cannot be a domain 171 | ## name. 172 | ## - `port` is a DNS server listening port. 173 | initDnsClientImpl(ip, port, true) 174 | 175 | proc initSystemDnsClient*(): DnsClient = 176 | ## Returns a `DnsClient` object, in which the dns server IP is the first one 177 | ## used by the system. If it is not possible to determine a dns server IP by 178 | ## the system, it will be initialized with `ndnsDnsServerIp`. 179 | ## 180 | ## Currently implemented for: 181 | ## - `Windows`_ 182 | ## - `Linux`_ 183 | ## - `BSD`_ 184 | ## 185 | ## Notes: 186 | ## - If your platform is not listed above and uses a `resolver configuration 187 | ## file`_, compile with `-d:ndnsUseResolver`. 188 | ## - It just creates a `DnsClient` object with the IP used by the system. Does 189 | ## not use the system's native DNS resolution implementation unless the 190 | ## system provides a proxy. 191 | ## - The `ip` field in the `DnsClient` object does not change automatically if 192 | ## the IP used by the system changes. 193 | when declared(getSystemDnsServer): 194 | let ipServDns = getSystemDnsServer() 195 | 196 | if ipServDns == "": 197 | result = ndnsClient 198 | else: 199 | result = initDnsClientImpl(ipServDns, Port(53), false) 200 | else: 201 | result = ndnsClient 202 | 203 | proc getIp*(client: DnsClient): string = 204 | ## Returns the IP defined in the `client`. 205 | client.ip 206 | 207 | proc getPort*(client: DnsClient): Port = 208 | ## Returns the port defined in the `client`. 209 | client.port 210 | 211 | template newSocketTmpl(sockType: SockType, protocol: Protocol) = 212 | when socket is AsyncSocket: 213 | socket = newAsyncSocket(client.domain, sockType, protocol, false) 214 | elif socket is Socket: 215 | socket = newSocket(client.domain, sockType, protocol, false) 216 | 217 | template checkResponse(protocol: Protocol) = 218 | when IPPROTO_UDP == protocol: 219 | if fromIp != client.ip: 220 | raise newException(ResponseIpNotEqualError, 221 | "The IP that sent the response is different from the IP that received the query") 222 | 223 | if fromPort != client.port: 224 | raise newException(ResponsePortNotEqualError, 225 | "The Port that sent the response is different from the Port that received the query") 226 | 227 | result = parseMessage(rBinMsg) 228 | 229 | if result.header.id != msg.header.id: 230 | raise newException(ResponseIdNotEqualError, 231 | "The query ID does not match the response ID") 232 | 233 | if result.header.flags.qr != QR.Response: 234 | raise newException(IsNotAResponseError, "Not a response (!= QR.Response)") 235 | 236 | if result.header.flags.opcode != msg.header.flags.opcode: 237 | raise newException(OpCodeNotEqualError, 238 | "The OpCode is different between the query and the response") 239 | 240 | proc dnsTcpQuery*(client: DnsClient, msg: Message, timeout: int = -1): Message = 241 | ## Returns a `Message` of the DNS query response performed using the TCP 242 | ## protocol. 243 | ## 244 | ## **Parameters** 245 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 246 | ## server. 247 | ## - `msg` is a `Message` object that contains the DNS query. 248 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 249 | ## DNS server. When it is `-1`, it will try to connect for an unlimited 250 | ## time. 251 | let qBinMsg = toBinMsg(msg, true) 252 | 253 | var socket: Socket 254 | 255 | newSocketTmpl(SOCK_STREAM, IPPROTO_TCP) 256 | 257 | setSockOpt(socket, OptNoDelay, true, cint(IPPROTO_TCP)) 258 | 259 | setBlocking(getFd(socket), false) 260 | 261 | try: 262 | connect(socket, client.ip, client.port, timeout) 263 | except TimeoutError: 264 | close(socket) 265 | 266 | raise newException(TimeoutError, "Connection timeout has been reached") 267 | 268 | send(socket, qBinMsg) 269 | 270 | let lenRecv = recv(socket, 2) 271 | 272 | if "" == lenRecv: 273 | close(socket) 274 | 275 | raise newException(UnexpectedDisconnectionError, 276 | "An unexpected disconnect occurs") 277 | 278 | var 279 | remaiderRecv = int(fromBytes(uint16, [uint8(ord(lenRecv[0])), 280 | uint8(ord(lenRecv[1]))], bigEndian)) 281 | rBinMsg = newStringOfCap(remaiderRecv) 282 | 283 | while remaiderRecv >= BufferSize: 284 | let recv = recv(socket, BufferSize) 285 | 286 | if recv == "": 287 | close(socket) 288 | 289 | raise newException(UnexpectedDisconnectionError, 290 | "An unexpected disconnect occurs") 291 | 292 | add(rBinMsg, recv) 293 | 294 | remaiderRecv = remaiderRecv - len(recv) 295 | 296 | while remaiderRecv > 0: 297 | let recv = recv(socket, remaiderRecv) 298 | 299 | if recv == "": 300 | close(socket) 301 | 302 | raise newException(UnexpectedDisconnectionError, 303 | "An unexpected disconnect occurs") 304 | 305 | add(rBinMsg, recv) 306 | 307 | remaiderRecv = remaiderRecv - len(recv) 308 | 309 | close(socket) 310 | 311 | checkResponse(IPPROTO_TCP) 312 | 313 | proc udpPayloadSize(msg: Message): int = 314 | ## Returns the UDP payload size. 315 | ## 316 | ## The payload will always be 512 bytes. However, if the `msg` has an OPT RR 317 | ## in additionals, the payload will be the value specified in the `udpSize` 318 | ## field. 319 | result = 512 320 | 321 | for additional in msg.additionals: 322 | if additional.`type` == Type.OPT: 323 | result = int(additional.udpSize) 324 | break 325 | 326 | proc dnsQuery*(client: DnsClient, msg: Message, timeout: int = -1, 327 | retransmit = false): Message = 328 | ## Returns a `Message` of the DNS query response performed using the UDP 329 | ## protocol 330 | ## 331 | ## **Parameters** 332 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 333 | ## server. 334 | ## - `msg` is a `Message` object that contains the DNS query. 335 | ## - `timeout` is the maximum waiting time, in milliseconds, to receive the 336 | ## response from the DNS server. When it is `-1`, it will try to receive the 337 | ## response for an unlimited time. 338 | ## - `retransmit` when `true`, determine the retransmission of the query to 339 | ## TCP protocol when the received response is truncated 340 | ## (`header.flags.tc == true`). 341 | let 342 | payloadSize = udpPayloadSize(msg) 343 | qBinMsg = toBinMsg(msg) 344 | 345 | var socket: Socket 346 | 347 | newSocketTmpl(SOCK_DGRAM, IPPROTO_UDP) 348 | 349 | sendTo(socket, client.ip, client.port, qBinMsg) 350 | 351 | var 352 | sRead = @[getFd(socket)] 353 | rBinMsg = newString(payloadSize) 354 | fromIp: string 355 | fromPort: Port 356 | lenReceived: int 357 | 358 | if selectRead(sRead, timeout) > 0: 359 | lenReceived = recvFrom(socket, rBinMsg, payloadSize, fromIp, fromPort) 360 | else: 361 | close(socket) 362 | 363 | raise newException(TimeoutError, "Response timeout has been reached") 364 | 365 | close(socket) 366 | 367 | setLen(rBinMsg, lenReceived) 368 | 369 | checkResponse(IPPROTO_UDP) 370 | 371 | if result.header.flags.tc and retransmit: 372 | result = dnsTcpQuery(client, msg, timeout) 373 | 374 | proc dnsAsyncTcpQuery*(client: DnsClient, msg: Message, timeout: int = 500): 375 | owned(Future[Message]) {.async.} = 376 | ## Returns a `Message` of the DNS query response performed using the TCP 377 | ## protocol 378 | ## 379 | ## **Parameters** 380 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 381 | ## server. 382 | ## - `msg` is a `Message` object that contains the DNS query. 383 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 384 | ## DNS server. When it is negative (less than 0), it will try to connect for 385 | ## an unlimited time. 386 | let qBinMsg = toBinMsg(msg, true) 387 | 388 | var socket: AsyncSocket 389 | 390 | newSocketTmpl(SOCK_STREAM, IPPROTO_TCP) 391 | 392 | setSockOpt(socket, OptNoDelay, true, cint(IPPROTO_TCP)) 393 | 394 | var fut = connect(socket, client.ip, client.port) 395 | 396 | if (timeout < 0): 397 | yield fut 398 | else: 399 | let waiting = await withTimeout(fut, timeout) 400 | 401 | if not waiting: 402 | close(socket) 403 | 404 | raise newException(TimeoutError, "Connection timeout has been reached") 405 | 406 | if fut.failed: 407 | close(socket) 408 | 409 | raise fut.readError() 410 | 411 | await send(socket, qBinMsg) 412 | 413 | let lenRecv = await recv(socket, 2) 414 | 415 | if "" == lenRecv: 416 | close(socket) 417 | 418 | raise newException(UnexpectedDisconnectionError, 419 | "An unexpected disconnect occurs") 420 | 421 | var 422 | remaiderRecv = int(fromBytes(uint16, [uint8(ord(lenRecv[0])), 423 | uint8(ord(lenRecv[1]))], bigEndian)) 424 | rBinMsg = newStringOfCap(remaiderRecv) 425 | 426 | while remaiderRecv >= BufferSize: 427 | let recv = await recv(socket, BufferSize) 428 | 429 | if recv == "": 430 | close(socket) 431 | 432 | raise newException(UnexpectedDisconnectionError, 433 | "An unexpected disconnect occurs") 434 | 435 | add(rBinMsg, recv) 436 | 437 | remaiderRecv = remaiderRecv - len(recv) 438 | 439 | while remaiderRecv > 0: 440 | let recv = await recv(socket, remaiderRecv) 441 | 442 | if recv == "": 443 | close(socket) 444 | 445 | raise newException(UnexpectedDisconnectionError, 446 | "An unexpected disconnect occurs") 447 | 448 | add(rBinMsg, recv) 449 | 450 | remaiderRecv = remaiderRecv - len(recv) 451 | 452 | close(socket) 453 | 454 | checkResponse(IPPROTO_TCP) 455 | 456 | proc dnsAsyncQuery*(client: DnsClient, msg: Message, timeout: int = 500, 457 | retransmit = false): owned(Future[Message]) {.async.} = 458 | ## Returns a `Message` of the DNS query response performed using the UDP 459 | ## protocol. 460 | ## 461 | ## **Parameters** 462 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 463 | ## server. 464 | ## - `msg` is a `Message` object that contains the DNS query. 465 | ## - `timeout` is the maximum waiting time, in milliseconds, to receive the 466 | ## response from the DNS server. When it is negative (less than 0), it will 467 | ## try to receive the response for an unlimited time. 468 | ## - `retransmit` when `true`, determine the retransmission of the query to 469 | ## TCP protocol when the received response is truncated 470 | ## (`header.flags.tc == true`). 471 | let 472 | payloadSize = udpPayloadSize(msg) 473 | qBinMsg = toBinMsg(msg) 474 | 475 | var socket: AsyncSocket 476 | 477 | newSocketTmpl(SOCK_DGRAM, IPPROTO_UDP) 478 | 479 | await sendTo(socket, client.ip, client.port, qBinMsg) 480 | 481 | var fut = recvFrom(socket, payloadSize) 482 | 483 | if timeout < 0: 484 | yield fut 485 | else: 486 | let waiting = await withTimeout(fut, timeout) 487 | 488 | if not waiting: 489 | close(socket) 490 | 491 | raise newException(TimeoutError, "Response timeout has been reached") 492 | 493 | if fut.failed: 494 | close(socket) 495 | 496 | raise fut.readError() 497 | 498 | let (rBinMsg, fromIp, fromPort) = fut.read() 499 | 500 | close(socket) 501 | 502 | checkResponse(IPPROTO_UDP) 503 | 504 | if result.header.flags.tc and retransmit: 505 | result = await dnsAsyncTcpQuery(client, msg, timeout) 506 | 507 | template domainNameRDns(domainV4, domainV6: string) = 508 | let ip = parseIpAddress(ip) 509 | 510 | case ip.family 511 | of IpAddressFamily.IPv4: 512 | # 15 characters for IPv4 + 513 | # 1 character for the dot of connection between IPv4 and `domainV4` + 514 | # `len(domainV4)` 515 | result = newStringOfCap(16 + len(domainV4)) 516 | 517 | for i in countdown(3, 0): 518 | result.add($ip.address_v4[i]) 519 | result.add('.') 520 | 521 | result.add(domainV4) 522 | of IpAddressFamily.IPv6: 523 | const hexDigits = "0123456789ABCDEF" 524 | # 63 characters for IPv6 + 525 | # 1 character for the dot of connection between IPv6 and `domainV6` + 526 | # `len(domainV6)` 527 | result = newStringOfCap(64 + len(domainV6)) 528 | 529 | for i in countdown(15, 0): 530 | let 531 | hi = (ip.address_v6[i] shr 4) and 0xF 532 | lo = ip.address_v6[i] and 0xF 533 | 534 | add(result, hexDigits[lo]) 535 | add(result, '.') 536 | add(result, hexDigits[hi]) 537 | add(result, '.') 538 | 539 | result.add(domainV6) 540 | 541 | proc prepareRDns*(ip: string): string = 542 | ## Returns a domain name for reverse DNS lookup. 543 | ## 544 | ## **Parameters** 545 | ## - `ip` is the IP address you want to query. It can be an IPv4 or IPv6. It 546 | ## cannot be a domain name. 547 | domainNameRDns(ipv4Arpa, ipv6Arpa) 548 | 549 | proc prepareDnsBL*(ip, dnsbl: string): string = 550 | ## Returns a domain name for DnsBL query. 551 | ## 552 | ## **Parameters** 553 | ## - `ip` is the IP address you want to query. It can be an IPv4 or IPv6. It 554 | ## cannot be a domain name. 555 | ## - `dnsbl` is the domain name that maintains the blacklist. 556 | domainNameRDns(dnsbl, dnsbl) 557 | 558 | proc randId*(): uint16 {.inline.} = 559 | ## Returns a `uint16`, randomly generated, to be used as an id. 560 | rand(1 .. 65535).uint16 561 | 562 | template resolveIpv4(async: bool) = 563 | let 564 | msg = initMessage(initHeader(id = randId(), rd = true), 565 | @[initQuestion(domain, QType.A, QClass.IN)]) 566 | rmsg = when async: await dnsAsyncQuery(client, msg, timeout, true) 567 | else: dnsQuery(client, msg, timeout, true) 568 | 569 | if rmsg.header.flags.rcode == RCode.NoError: 570 | for rr in rmsg.answers: 571 | if rr.`type` != Type.A or rr.class != Class.IN: continue 572 | 573 | let ip = IpAddress(family: IpAddressFamily.IPv4, 574 | address_v4: RDataA(rr.rdata).address) 575 | 576 | add(result, $ip) 577 | 578 | template resolveIpv6(async: bool) = 579 | let 580 | msg = initMessage(initHeader(id = randId(), rd = true), 581 | @[initQuestion(domain, QType.AAAA, QClass.IN)]) 582 | rmsg = when async: await dnsAsyncQuery(client, msg, timeout, true) 583 | else: dnsQuery(client, msg, timeout, true) 584 | 585 | if rmsg.header.flags.rcode == RCode.NoError: 586 | for rr in rmsg.answers: 587 | if rr.`type` != Type.AAAA or rr.class != Class.IN: continue 588 | 589 | let ip = IpAddress(family: IpAddressFamily.IPv6, 590 | address_v6: RDataAAAA(rr.rdata).address) 591 | 592 | add(result, $ip) 593 | 594 | template resolveRdns(async: bool) = 595 | let 596 | msg = initMessage(initHeader(id = randId(), rd = true), 597 | @[initQuestion(prepareRDns(ip), QType.PTR, QClass.IN)]) 598 | rmsg = when async: await dnsAsyncQuery(client, msg, timeout, true) 599 | else: dnsQuery(client, msg, timeout, true) 600 | 601 | if rmsg.header.flags.rcode == RCode.NoError: 602 | for rr in rmsg.answers: 603 | if rr.name != msg.questions[0].qname or rr.`type` != Type.PTR or 604 | rr.class != Class.IN: continue 605 | 606 | add(result, RDataPTR(rr.rdata).ptrdname) 607 | 608 | proc resolveIpv4*(client: DnsClient, domain: string, timeout: int = -1): 609 | seq[string] = 610 | ## Returns all IPv4 addresses, in a `seq[string]`, that have been resolved 611 | ## from `domain`. The `seq[string]` can be empty. 612 | ## 613 | ## **Parameters** 614 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 615 | ## server. 616 | ## - `domain` is the domain name that you wish to obtain IPv4 addresses. 617 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 618 | ## DNS server or to receive the response from the DNS server. When it is 619 | ## `-1`, it will try to connect for an unlimited time or to receive the 620 | ## response for an unlimited time. 621 | resolveIpv4(false) 622 | 623 | proc asyncResolveIpv4*(client: DnsClient, domain: string, timeout: int = 500): 624 | owned(Future[seq[string]]) {.async.} = 625 | ## Returns all IPv4 addresses, in a `seq[string]`, that have been resolved 626 | ## from `domain`. The `seq[string]` can be empty. 627 | ## 628 | ## **Parameters** 629 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 630 | ## server. 631 | ## - `domain` is the domain name that you wish to obtain IPv4 addresses. 632 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 633 | ## DNS server or to receive the response from the DNS server. When it is 634 | ## negative (less than 0), it will try to connect for an unlimited time or 635 | ## to receive the response for an unlimited time. 636 | resolveIpv4(true) 637 | 638 | proc resolveIpv6*(client: DnsClient, domain: string, timeout: int = -1): 639 | seq[string] = 640 | ## Returns all IPv6 addresses, in a `seq[string]`, that have been resolved 641 | ## from `domain`. The `seq[string]` can be empty. 642 | ## 643 | ## **Parameters** 644 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 645 | ## server. 646 | ## - `domain` is the domain name that you wish to obtain IPv6 addresses. 647 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 648 | ## DNS server or to receive the response from the DNS server. When it is 649 | ## `-1`, it will try to connect for an unlimited time or to receive the 650 | ## response for an unlimited time. 651 | resolveIpv6(false) 652 | 653 | proc asyncResolveIpv6*(client: DnsClient, domain: string, timeout: int = 500): 654 | owned(Future[seq[string]]) {.async.} = 655 | ## Returns all IPv6 addresses, in a `seq[string]`, that have been resolved 656 | ## from `domain`. The `seq[string]` can be empty. 657 | ## 658 | ## **Parameters** 659 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 660 | ## server. 661 | ## - `domain` is the domain name that you wish to obtain IPv6 addresses. 662 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 663 | ## DNS server or to receive the response from the DNS server. When it is 664 | ## negative (less than 0), it will try to connect for an unlimited time or 665 | ## to receive the response for an unlimited time. 666 | resolveIpv6(true) 667 | 668 | proc resolveRDns*(client: DnsClient, ip: string, timeout: int = -1): 669 | seq[string] = 670 | ## Returns all domain names, in a `seq[string]`, which is obtained by the 671 | ## "reverse" query of `ip`. The `seq[string]` can be empty. 672 | ## 673 | ## **Parameters** 674 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 675 | ## server. 676 | ## - `ip` is the IPv4 or IPv6 address that is intended to obtain the domain 677 | ## name, which represents the reverse address. 678 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 679 | ## DNS server or to receive the response from the DNS server. When it is 680 | ## `-1`, it will try to connect for an unlimited time or to receive the 681 | ## response for an unlimited time. 682 | resolveRdns(false) 683 | 684 | proc asyncResolveRDns*(client: DnsClient, ip: string, timeout: int = 500): 685 | owned(Future[seq[string]]) {.async.} = 686 | ## Returns all domain names, in a `seq[string]`, which is obtained by the 687 | ## "reverse" query of `ip`. The `seq[string]` can be empty. 688 | ## 689 | ## **Parameters** 690 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 691 | ## server. 692 | ## - `ip` is the IPv4 or IPv6 address that is intended to obtain the domain 693 | ## name, which represents the reverse address. 694 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 695 | ## DNS server or to receive the response from the DNS server. When it is 696 | ## negative (less than 0), it will try to connect for an unlimited time or 697 | ## to receive the response for an unlimited time. 698 | resolveRdns(true) 699 | 700 | proc resolveDnsBL*(client: DnsClient, ip, dnsbl: string, timeout: int = -1): 701 | seq[string] = 702 | ## Returns IPv4 addresses. Usually the loopback address (127.0.0.0/24), in 703 | ## which the last octet of IPv4 represents something on the black list. 704 | ## 705 | ## **Parameters** 706 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 707 | ## server. 708 | ## - `ip` is the IPv4 or IPv6 address that you want to know if it is 709 | ## blacklisted. 710 | ## - `dnsbl` is the domain name for DnsBL queries. 711 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 712 | ## DNS server or to receive the response from the DNS server. When it is 713 | ## `-1`, it will try to connect for an unlimited time or to receive the 714 | ## response for an unlimited time. 715 | resolveIpv4(client, prepareDnsBL(ip, dnsbl)) 716 | 717 | proc asyncResolveDnsBL*(client: DnsClient, ip, dnsbl: string, 718 | timeout: int = 500): owned(Future[seq[string]]) 719 | {.async.} = 720 | ## Returns IPv4 addresses. Usually the loopback address (127.0.0.0/24), in 721 | ## which the last octet of IPv4 represents something on the black list. 722 | ## 723 | ## **Parameters** 724 | ## - `client` is a `DnsClient` object that contains the IP and Port of the DNS 725 | ## server. 726 | ## - `ip` is the IPv4 or IPv6 address that you want to know if it is 727 | ## blacklisted. 728 | ## - `dnsbl` is the domain name for DnsBL queries. 729 | ## - `timeout` is the maximum waiting time, in milliseconds, to connect to the 730 | ## DNS server or to receive the response from the DNS server. When it is 731 | ## negative (less than 0), it will try to connect for an unlimited time or 732 | ## to receive the response for an unlimited time. 733 | result = await asyncResolveIpv4(client, prepareDnsBL(ip, dnsbl)) 734 | --------------------------------------------------------------------------------