├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── configure ├── main.c └── test ├── LaunchAgents ├── test.ondemand-socket.plist ├── test.port.plist └── test.socket.plist ├── bind-error.bats ├── custom-port.bats ├── custom-records.bats ├── default-records.bats ├── large-request.bats ├── launchctl-ondemand-socket.bats ├── launchctl-port.bats ├── launchctl-socket.bats ├── multiple-requests.bats ├── nxdomin.bats ├── plain-dns.bats ├── subdomains.bats ├── test_helper.bash ├── timeout.bats ├── usage.bats └── version.bats /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | 12 | - name: Install dnsutils 13 | run: sudo apt-get install dnsutils 14 | 15 | - name: make 16 | run: make 17 | 18 | - name: make test 19 | run: make test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.h 2 | launchdns 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/bats"] 2 | path = test/bats 3 | url = https://github.com/sstephenson/bats 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Joshua Peek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC = cc 2 | CFLAGS = -Wall 3 | INSTALL = /usr/bin/install 4 | PREFIX ?= /usr/local 5 | BINDIR = $(PREFIX)/bin 6 | BATS = test/bats/bin/bats 7 | CONFIG = config.h 8 | SOURCES = main.c 9 | EXECUTABLE = launchdns 10 | 11 | all: $(EXECUTABLE) 12 | 13 | $(CONFIG): 14 | ./configure 15 | 16 | $(EXECUTABLE): main.c $(CONFIG) 17 | $(CC) $(CFLAGS) -o $@ $< 18 | 19 | install: $(EXECUTABLE) 20 | $(INSTALL) -d $(BINDIR) 21 | $(INSTALL) $(EXECUTABLE) $(BINDIR)/$(EXECUTABLE) 22 | 23 | $(BATS): 24 | git submodule init 25 | git submodule update 26 | 27 | clean: 28 | rm -f $(CONFIG) $(EXECUTABLE) 29 | 30 | test: all $(BATS) 31 | ifdef $CI 32 | $(BATS) --taps ./test 33 | else 34 | $(BATS) ./test 35 | endif 36 | 37 | .PHONY: install clean test 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # launchdns 2 | 3 | A launchd friendly development DNS server. 4 | 5 | ``` 6 | launchdns [-spt46] 7 | ``` 8 | 9 | 10 | ## Installation 11 | 12 | ``` 13 | $ make 14 | $ make install [PREFIX=] 15 | ```` 16 | 17 | 18 | ## Usage 19 | 20 | Start DNS server on port 55353. 21 | 22 | ``` 23 | $ launchdns -p 55353 24 | ``` 25 | 26 | 27 | ## Options 28 | 29 | * `-s `, `--socket=` 30 | 31 | Name of key in Launchd Sockets dictionary to bind to. If name is set, the `--port` option is ignored. 32 | 33 | * `-p `, `--port=` 34 | 35 | Port number to bind to. Defaults to 55353. 36 | 37 | * `-t `, `--timeout=` 38 | 39 | Number of seconds to run for. Will run indefinitely by default. 40 | 41 | * `-4 `, `--a=` 42 | 43 | IPv4 address for all A responses. Defaults to 127.0.0.1. 44 | 45 | * `-6 `, `--aaaa=` 46 | 47 | IPv6 address for all AAAA responses. Defaults to ::1. 48 | 49 | 50 | ## Launch Agent configuration 51 | 52 | The most basic setup is running an agent that binds to the default port. 53 | 54 | **~/Library/LaunchAgents/com.github.josh.launchdns.plist** 55 | 56 | ``` xml 57 | 58 | 59 | 60 | 61 | Label 62 | com.github.josh.launchdns 63 | RunAtLoad 64 | 65 | ProgramArguments 66 | 67 | /usr/local/bin/launchdns 68 | 69 | 70 | 71 | ``` 72 | 73 | This will keep `launchdns` running on port 55353 anytime you log into your computer. 74 | 75 | Though, the preferred way to configure the server is to run on demand. 76 | 77 | **~/Library/LaunchAgents/com.github.josh.launchdns.plist** 78 | 79 | ``` xml 80 | 81 | 82 | 83 | 84 | Label 85 | com.github.josh.launchdns 86 | ProgramArguments 87 | 88 | /usr/local/bin/launchdns 89 | --socket=Listeners 90 | --timeout=30 91 | 92 | Sockets 93 | 94 | Listeners 95 | 96 | SockType 97 | dgram 98 | SockNodeName 99 | 127.0.0.1 100 | SockServiceName 101 | 55353 102 | 103 | 104 | 105 | 106 | ``` 107 | 108 | Instead of the server being started at load, it will automatically start on demand when theres activity on 127.0.0.1:55353. And after 30 seconds, it will quit until it needs to be launched again. Note the `--socket` name matches the `Listeners` name of the Socket dictionary key. 109 | 110 | Last, configure your system's resolver to use the nameserver for whatever TLDs you wish, for an example `.localhost`. 111 | 112 | **/etc/resolver/localhost** 113 | 114 | ``` 115 | nameserver 127.0.0.1 116 | port 55353 117 | ``` 118 | 119 | ## See Also 120 | 121 | launchd(8), launchctl(1), launchd.plist(5), launch(3), resolver(5) 122 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | while [ "$1" ]; do 3 | case "$1" in 4 | --with-launch-h) WITH_LAUNCH_H="1";; 5 | --with-launch-h-activate-socket) WITH_LAUNCH_H_ACTIVATE_SOCKET="1";; 6 | *) 7 | echo "Unknown argument '$1'!" >&2 8 | exit 1 9 | ;; 10 | esac 11 | shift 12 | done 13 | 14 | rm -f config.h 15 | touch config.h 16 | 17 | if [ -f "/usr/include/launch.h" ]; then 18 | LAUNCH_H="/usr/include/launch.h" 19 | else 20 | XCODE_SDK_PATH=$(xcrun --show-sdk-path 2>/dev/null) 21 | if [ -f "$XCODE_SDK_PATH/usr/include/launch.h" ]; then 22 | LAUNCH_H="$XCODE_SDK_PATH/usr/include/launch.h" 23 | fi 24 | fi 25 | 26 | if [ -n "$LAUNCH_H" ]; then 27 | echo "#define HAVE_LAUNCH_H 1" >> config.h 28 | 29 | cat "$LAUNCH_H" | grep -q "^launch_activate_socket" 30 | if [ "$?" = "0" ]; then 31 | echo "#define HAVE_LAUNCH_ACTIVATE_SOCKET 1" >> config.h 32 | elif [ -n "$WITH_LAUNCH_H_ACTIVATE_SOCKET" ]; then 33 | echo "Could not find 'launch_activate_socket' in '$LAUNCH_H'!" >&2 34 | exit 1 35 | fi 36 | elif [ -n "$WITH_LAUNCH_H" ] || [ -n "$WITH_LAUNCH_H_ACTIVATE_SOCKET" ]; then 37 | echo "Could not find 'launch.h'!" >&2 38 | exit 1 39 | fi 40 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #ifdef HAVE_LAUNCH_H 13 | #include 14 | #endif 15 | 16 | #define VERSION 1.0.4 17 | 18 | #define PORT 0x39d8 19 | #define TTL 600 20 | #define RECV_SIZE 512 21 | #define HEADER_SIZE 12 22 | 23 | #define INADDR_LOOPBACK_INIT { 0x0100007f } 24 | 25 | #define STR_HELPER(x) #x 26 | #define STR(x) STR_HELPER(x) 27 | 28 | static const struct option longoptions[] = { 29 | {"socket", 1, NULL, 's'}, 30 | {"timeout", 1, NULL, 't'}, 31 | {"port", 1, NULL, 'p'}, 32 | {"a", 1, NULL, '4'}, 33 | {"aaaa", 1, NULL, '6'}, 34 | {"version", 0, NULL, 'v'}, 35 | { NULL, 0, NULL, 0 } 36 | }; 37 | 38 | volatile sig_atomic_t quit = 0; 39 | 40 | void quit_handler() 41 | { 42 | quit = 1; 43 | } 44 | 45 | int main(int argc, char **argv) 46 | { 47 | char *name = NULL; 48 | 49 | // Default socket on 127.0.0.1:55353 50 | struct sockaddr_in addr = { 51 | .sin_family = AF_INET, 52 | .sin_port = PORT, 53 | .sin_addr = INADDR_LOOPBACK_INIT 54 | }; 55 | 56 | // Default A record to 127.0.0.1 57 | struct in_addr a = INADDR_LOOPBACK_INIT; 58 | 59 | // Default AAAA record to ::1 60 | struct in6_addr aaaa = IN6ADDR_LOOPBACK_INIT; 61 | 62 | int timeout = 0; 63 | 64 | int opt; 65 | 66 | while ((opt = getopt_long(argc, argv, "s:t:p:4:6:v", longoptions, NULL)) != EOF) { 67 | switch (opt) { 68 | case 's': 69 | name = optarg; 70 | break; 71 | case 't': 72 | timeout = atoi(optarg); 73 | break; 74 | case 'p': 75 | addr.sin_port = htons(atoi(optarg)); 76 | break; 77 | case '4': 78 | inet_pton(AF_INET, optarg, &a); 79 | break; 80 | case '6': 81 | inet_pton(AF_INET6, optarg, &aaaa); 82 | break; 83 | case 'v': 84 | printf("launchdns " STR(VERSION)); 85 | #ifndef HAVE_LAUNCH_ACTIVATE_SOCKET 86 | printf(" (without socket activation)"); 87 | #endif 88 | printf("\n"); 89 | exit(0); 90 | default: 91 | exit(1); 92 | break; 93 | } 94 | } 95 | 96 | int sd, err; 97 | if (name == NULL) { 98 | sd = socket(AF_INET, SOCK_DGRAM, 0); 99 | err = bind(sd, (struct sockaddr *)&addr, sizeof(addr)); 100 | if (err < 0) { 101 | fprintf(stderr, "Could not open socket on %i: %s\n", ntohs(addr.sin_port), strerror(errno)); 102 | return 1; 103 | } 104 | } else { 105 | #ifdef HAVE_LAUNCH_ACTIVATE_SOCKET 106 | int *c_fds; 107 | size_t c_cnt; 108 | 109 | err = launch_activate_socket(name, &c_fds, &c_cnt); 110 | if (err == 0) { 111 | sd = c_fds[0]; 112 | } else { 113 | fprintf(stderr, "Could not activate launchd socket `%s'\n", name); 114 | return 1; 115 | } 116 | #else 117 | fprintf(stderr, "launchd not supported\n"); 118 | return 1; 119 | #endif 120 | } 121 | 122 | struct sigaction sa; 123 | memset(&sa, 0, sizeof(sa)); 124 | sa.sa_handler = &quit_handler; 125 | 126 | if (timeout != 0) { 127 | sigaction(SIGALRM, &sa, NULL); 128 | alarm(timeout); 129 | } 130 | sigaction(SIGTERM, &sa, NULL); 131 | 132 | char msg[RECV_SIZE + 30]; 133 | struct sockaddr caddr; 134 | socklen_t len = sizeof(caddr); 135 | int flags = 0; 136 | 137 | while (quit == 0) { 138 | int n = recvfrom(sd, msg, RECV_SIZE, flags, &caddr, &len); 139 | 140 | if (n < HEADER_SIZE) { 141 | continue; 142 | } 143 | 144 | int qdcount = msg[5] | msg[4] << 8; 145 | 146 | if (qdcount < 1) { 147 | continue; 148 | } 149 | 150 | int i = HEADER_SIZE; 151 | 152 | while (i < n && msg[i] != 0) { 153 | i += msg[i] + 1; 154 | } 155 | 156 | int ans = i + 5; 157 | 158 | if (i == HEADER_SIZE || n < ans) { 159 | continue; 160 | } 161 | 162 | int qtype = msg[i+2] | msg[i+1] << 8; 163 | int qclass = msg[i+4] | msg[i+3] << 8; 164 | 165 | if (qclass == 0x01 && (qtype == 0x01 || qtype == 0x1c)) { 166 | // Opcode 167 | msg[2] = 0x81; 168 | msg[3] = 0x80; 169 | 170 | // ANCOUNT 171 | msg[6] = 0x00; 172 | msg[7] = 0x01; 173 | 174 | // NSCOUNT 175 | msg[8] = 0x00; 176 | msg[9] = 0x00; 177 | 178 | // ARCOUNT 179 | msg[10] = 0x00; 180 | msg[11] = 0x00; 181 | 182 | // RR Name 183 | msg[ans++] = 0xc0; 184 | msg[ans++] = 0x0c; 185 | 186 | // RR Type 187 | msg[ans++] = qtype >> 8; 188 | msg[ans++] = qtype; 189 | 190 | // RR Class 191 | msg[ans++] = qclass >> 8; 192 | msg[ans++] = qclass; 193 | 194 | // TTL 195 | uint32_t ttl = TTL; 196 | msg[ans++] = ttl >> 24; 197 | msg[ans++] = ttl >> 16; 198 | msg[ans++] = ttl >> 8; 199 | msg[ans++] = ttl; 200 | 201 | void *rdata; 202 | uint16_t rsize; 203 | if (qtype == 0x01) { 204 | rdata = &a.s_addr; 205 | rsize = sizeof(a.s_addr); 206 | } else { 207 | rdata = &aaaa.s6_addr; 208 | rsize = sizeof(aaaa.s6_addr); 209 | } 210 | 211 | // RD Length 212 | msg[ans++] = rsize >> 8; 213 | msg[ans++] = rsize; 214 | 215 | // RDATA 216 | memcpy(&msg[ans], rdata, rsize); 217 | ans += rsize; 218 | } else { 219 | // NXDomain 220 | msg[2] = 0x81; 221 | msg[3] = 0x03; 222 | } 223 | 224 | sendto(sd, msg, ans, flags, &caddr, len); 225 | } 226 | 227 | close(sd); 228 | return 0; 229 | } 230 | -------------------------------------------------------------------------------- /test/LaunchAgents/test.ondemand-socket.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | test.ondemand-socket 7 | RunAtLoad 8 | 9 | ProgramArguments 10 | 11 | $BIN 12 | --socket=Listeners 13 | 14 | Sockets 15 | 16 | Listeners 17 | 18 | SockType 19 | dgram 20 | SockNodeName 21 | 127.0.0.1 22 | SockServiceName 23 | 65353 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/LaunchAgents/test.port.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | test.port 7 | RunAtLoad 8 | 9 | ProgramArguments 10 | 11 | $BIN 12 | --port=65353 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/LaunchAgents/test.socket.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | test.socket 7 | RunAtLoad 8 | 9 | ProgramArguments 10 | 11 | $BIN 12 | --socket=Listeners 13 | 14 | Sockets 15 | 16 | Listeners 17 | 18 | SockType 19 | dgram 20 | SockNodeName 21 | 127.0.0.1 22 | SockServiceName 23 | 65353 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/bind-error.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | name=$(basename "$BIN") 6 | 7 | PORT="65455" 8 | 9 | setup() { 10 | $BIN --port "$PORT" & 11 | pid=$! 12 | } 13 | 14 | teardown() { 15 | kill -9 $pid 16 | sleep 0 17 | } 18 | 19 | @test "bind address already in use" { 20 | run "$BIN" --port "$PORT" 21 | [ "$status" -eq 1 ] 22 | [ "${lines[0]}" = "Could not open socket on $PORT: Address already in use" ] 23 | } 24 | -------------------------------------------------------------------------------- /test/custom-port.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | PORT="65454" 6 | 7 | setup() { 8 | $BIN --port "$PORT" & 9 | pid=$! 10 | } 11 | 12 | teardown() { 13 | kill -9 $pid 14 | sleep 0 15 | } 16 | 17 | @test "dig A record" { 18 | name="test" 19 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 20 | [ "$status" -eq 0 ] 21 | [[ "$output" == *"status: NOERROR,"* ]] 22 | [[ "$output" == *"test. 600 IN A 127.0.0.1"* ]] 23 | } 24 | 25 | @test "dig AAAA record" { 26 | name="test" 27 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +edns=0 +noall +answer +comments 28 | [ "$status" -eq 0 ] 29 | [[ "$output" == *"status: NOERROR,"* ]] 30 | [[ "$output" == *"test. 600 IN AAAA ::1"* ]] 31 | } 32 | -------------------------------------------------------------------------------- /test/custom-records.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | A="8.8.8.8" 6 | AAAA="2001:4860:4860::8888" 7 | 8 | setup() { 9 | $BIN --port "$PORT" --a "$A" --aaaa "$AAAA" & 10 | pid=$! 11 | } 12 | 13 | teardown() { 14 | kill -9 $pid 15 | sleep 0 16 | } 17 | 18 | @test "dig custom A record" { 19 | name="test" 20 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 21 | [ "$status" -eq 0 ] 22 | [[ "$output" == *"status: NOERROR,"* ]] 23 | [[ "$output" == *"test. 600 IN A $A"* ]] 24 | } 25 | 26 | @test "dig custom AAAA record" { 27 | name="test" 28 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +edns=0 +noall +answer +comments 29 | [ "$status" -eq 0 ] 30 | [[ "$output" == *"status: NOERROR,"* ]] 31 | [[ "$output" == *"test. 600 IN AAAA $AAAA"* ]] 32 | } 33 | -------------------------------------------------------------------------------- /test/default-records.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | setup() { 6 | $BIN --port "$PORT" & 7 | pid=$! 8 | } 9 | 10 | teardown() { 11 | kill -9 $pid 12 | sleep 0 13 | } 14 | 15 | @test "dig default A record" { 16 | name="test" 17 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 18 | [ "$status" -eq 0 ] 19 | [[ "$output" == *"status: NOERROR,"* ]] 20 | [[ "$output" == *"test. 600 IN A 127.0.0.1"* ]] 21 | } 22 | 23 | @test "dig default AAAA record" { 24 | name="test" 25 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +edns=0 +noall +answer +comments 26 | [ "$status" -eq 0 ] 27 | [[ "$output" == *"status: NOERROR,"* ]] 28 | [[ "$output" == *"test. 600 IN AAAA ::1"* ]] 29 | } 30 | -------------------------------------------------------------------------------- /test/large-request.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | setup() { 6 | $BIN --port "$PORT" & 7 | pid=$! 8 | } 9 | 10 | teardown() { 11 | kill -9 $pid 12 | sleep 0 13 | } 14 | 15 | name="ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 16 | 17 | @test "dig largest A record question (plain DNS)" { 18 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +noedns +noall +answer +comments 19 | [ "$status" -eq 0 ] 20 | [[ "$output" == *"status: NOERROR,"* ]] 21 | } 22 | 23 | @test "dig largest A record question (plain EDNS)" { 24 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 25 | [ "$status" -eq 0 ] 26 | [[ "$output" == *"status: NOERROR,"* ]] 27 | } 28 | 29 | @test "dig largest AAAA record question (plain DNS)" { 30 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +noedns +noall +answer +comments 31 | [ "$status" -eq 0 ] 32 | [[ "$output" == *"status: NOERROR,"* ]] 33 | } 34 | 35 | @test "dig largest AAAA record question (plain EDNS)" { 36 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +edns=0 +noall +answer +comments 37 | [ "$status" -eq 0 ] 38 | [[ "$output" == *"status: NOERROR,"* ]] 39 | } 40 | -------------------------------------------------------------------------------- /test/launchctl-ondemand-socket.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | plist=$(launch_plist "test.ondemand-socket.plist") 6 | 7 | setup() { 8 | if [ -n "$LAUNCHD" ]; then 9 | launchctl load "$plist" 10 | fi 11 | } 12 | 13 | teardown() { 14 | if [ -n "$LAUNCHD" ]; then 15 | launchctl unload "$plist" 16 | fi 17 | } 18 | 19 | @test "dig A record" { 20 | if [ -z "$LAUNCHD" ]; then 21 | skip "no launchd" 22 | fi 23 | 24 | name="test" 25 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 26 | [ "$status" -eq 0 ] 27 | [[ "$output" == *"status: NOERROR,"* ]] 28 | [[ "$output" == *"test. 600 IN A 127.0.0.1"* ]] 29 | } 30 | 31 | @test "dig AAAA record" { 32 | if [ -z "$LAUNCHD" ]; then 33 | skip "no launchd" 34 | fi 35 | 36 | name="test" 37 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +edns=0 +noall +answer +comments 38 | [ "$status" -eq 0 ] 39 | [[ "$output" == *"status: NOERROR,"* ]] 40 | [[ "$output" == *"test. 600 IN AAAA ::1"* ]] 41 | } 42 | -------------------------------------------------------------------------------- /test/launchctl-port.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | plist=$(launch_plist "test.port.plist") 6 | 7 | setup() { 8 | if [ -n "$LAUNCHD" ]; then 9 | launchctl load "$plist" 10 | fi 11 | } 12 | 13 | teardown() { 14 | if [ -n "$LAUNCHD" ]; then 15 | launchctl unload "$plist" 16 | fi 17 | } 18 | 19 | @test "dig A record" { 20 | if [ -z "$LAUNCHD" ]; then 21 | skip "no launchd" 22 | fi 23 | 24 | name="test" 25 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 26 | [ "$status" -eq 0 ] 27 | [[ "$output" == *"status: NOERROR,"* ]] 28 | [[ "$output" == *"test. 600 IN A 127.0.0.1"* ]] 29 | } 30 | 31 | @test "dig AAAA record" { 32 | if [ -z "$LAUNCHD" ]; then 33 | skip "no launchd" 34 | fi 35 | 36 | name="test" 37 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +edns=0 +noall +answer +comments 38 | [ "$status" -eq 0 ] 39 | [[ "$output" == *"status: NOERROR,"* ]] 40 | [[ "$output" == *"test. 600 IN AAAA ::1"* ]] 41 | } 42 | -------------------------------------------------------------------------------- /test/launchctl-socket.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | plist=$(launch_plist "test.socket.plist") 6 | 7 | setup() { 8 | if [ -n "$LAUNCHD" ]; then 9 | launchctl load "$plist" 10 | fi 11 | } 12 | 13 | teardown() { 14 | if [ -n "$LAUNCHD" ]; then 15 | launchctl unload "$plist" 16 | fi 17 | } 18 | 19 | @test "dig A record" { 20 | if [ -z "$LAUNCHD" ]; then 21 | skip "no launchd" 22 | fi 23 | 24 | name="test" 25 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 26 | [ "$status" -eq 0 ] 27 | [[ "$output" == *"status: NOERROR,"* ]] 28 | [[ "$output" == *"test. 600 IN A 127.0.0.1"* ]] 29 | } 30 | 31 | @test "dig AAAA record" { 32 | if [ -z "$LAUNCHD" ]; then 33 | skip "no launchd" 34 | fi 35 | 36 | name="test" 37 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +edns=0 +noall +answer +comments 38 | [ "$status" -eq 0 ] 39 | [[ "$output" == *"status: NOERROR,"* ]] 40 | [[ "$output" == *"test. 600 IN AAAA ::1"* ]] 41 | } 42 | -------------------------------------------------------------------------------- /test/multiple-requests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | setup() { 6 | $BIN --port "$PORT" & 7 | pid=$! 8 | } 9 | 10 | teardown() { 11 | kill -9 $pid 12 | sleep 0 13 | } 14 | 15 | @test "dig multiple requests (plain DNS and plain EDNS)" { 16 | name="test" 17 | 18 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +noedns +noall +answer +comments 19 | [ "$status" -eq 0 ] 20 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +noedns +noall +answer +comments 21 | [ "$status" -eq 0 ] 22 | 23 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 24 | [ "$status" -eq 0 ] 25 | 26 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +noedns +noall +answer +comments 27 | [ "$status" -eq 0 ] 28 | 29 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 30 | [ "$status" -eq 0 ] 31 | 32 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 33 | [ "$status" -eq 0 ] 34 | } 35 | -------------------------------------------------------------------------------- /test/nxdomin.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | setup() { 6 | $BIN --port="$PORT" & 7 | pid=$! 8 | } 9 | 10 | teardown() { 11 | kill -9 $pid 12 | sleep 0 13 | } 14 | 15 | @test "dig MX record" { 16 | name="dev" 17 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name MX +edns=0 +noall +answer +comments 18 | [ "$status" -eq 0 ] 19 | [[ "$output" == *"status: NXDOMAIN,"* ]] 20 | } 21 | 22 | @test "dig subdomain MX record" { 23 | name="foo.dev" 24 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name MX +edns=0 +noall +answer +comments 25 | [ "$status" -eq 0 ] 26 | [[ "$output" == *"status: NXDOMAIN,"* ]] 27 | } 28 | 29 | @test "dig CNAME record" { 30 | name="dev" 31 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name CNAME +edns=0 +noall +answer +comments 32 | [ "$status" -eq 0 ] 33 | [[ "$output" == *"status: NXDOMAIN,"* ]] 34 | } 35 | 36 | @test "dig subdomain CNAME record" { 37 | name="foo.dev" 38 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name CNAME +edns=0 +noall +answer +comments 39 | [ "$status" -eq 0 ] 40 | [[ "$output" == *"status: NXDOMAIN,"* ]] 41 | } 42 | 43 | @test "dig TXT record" { 44 | name="dev" 45 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name TXT +edns=0 +noall +answer +comments 46 | [ "$status" -eq 0 ] 47 | [[ "$output" == *"status: NXDOMAIN,"* ]] 48 | } 49 | 50 | @test "dig subdomain TXT record" { 51 | name="foo.dev" 52 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name TXT +edns=0 +noall +answer +comments 53 | [ "$status" -eq 0 ] 54 | [[ "$output" == *"status: NXDOMAIN,"* ]] 55 | } 56 | -------------------------------------------------------------------------------- /test/plain-dns.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | setup() { 6 | $BIN --port="$PORT" & 7 | pid=$! 8 | } 9 | 10 | teardown() { 11 | kill -9 $pid 12 | sleep 0 13 | } 14 | 15 | # subdomain tests 16 | 17 | @test "dig foo.dev A record (plain DNS)" { 18 | name="foo.dev" 19 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +noedns +noall +answer +comments 20 | [ "$status" -eq 0 ] 21 | [[ "$output" == *"status: NOERROR,"* ]] 22 | [[ "$output" == *"foo.dev. 600 IN A 127.0.0.1"* ]] 23 | } 24 | 25 | @test "dig foo.bar.dev A record (plain DNS)" { 26 | name="foo.bar.dev" 27 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +noedns +noall +answer +comments 28 | [ "$status" -eq 0 ] 29 | [[ "$output" == *"status: NOERROR,"* ]] 30 | [[ "$output" == *"foo.bar.dev. 600 IN A 127.0.0.1"* ]] 31 | } 32 | 33 | @test "dig foo.dev AAAA record (plain DNS)" { 34 | name="foo.dev" 35 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +noedns +noall +answer +comments 36 | [ "$status" -eq 0 ] 37 | [[ "$output" == *"status: NOERROR,"* ]] 38 | [[ "$output" == *"foo.dev. 600 IN AAAA ::1"* ]] 39 | } 40 | 41 | # NXDOMAIN tests 42 | 43 | @test "dig MX record (plain DNS)" { 44 | name="dev" 45 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name MX +noedns +noall +answer +comments 46 | [ "$status" -eq 0 ] 47 | [[ "$output" == *"status: NXDOMAIN,"* ]] 48 | } 49 | 50 | @test "dig subdomain MX record (plain DNS)" { 51 | name="foo.dev" 52 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name MX +noedns +noall +answer +comments 53 | [ "$status" -eq 0 ] 54 | [[ "$output" == *"status: NXDOMAIN,"* ]] 55 | } 56 | 57 | @test "dig subdomain CNAME record (plain DNS)" { 58 | name="foo.dev" 59 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name CNAME +noedns +noall +answer +comments 60 | [ "$status" -eq 0 ] 61 | [[ "$output" == *"status: NXDOMAIN,"* ]] 62 | } 63 | -------------------------------------------------------------------------------- /test/subdomains.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | setup() { 6 | $BIN --port="$PORT" & 7 | pid=$! 8 | } 9 | 10 | teardown() { 11 | kill -9 $pid 12 | sleep 0 13 | } 14 | 15 | @test "dig dev A record" { 16 | name="dev" 17 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 18 | [ "$status" -eq 0 ] 19 | [[ "$output" == *"status: NOERROR,"* ]] 20 | [[ "$output" == *"dev. 600 IN A 127.0.0.1"* ]] 21 | } 22 | 23 | @test "dig foo.dev A record" { 24 | name="foo.dev" 25 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 26 | [ "$status" -eq 0 ] 27 | [[ "$output" == *"status: NOERROR,"* ]] 28 | [[ "$output" == *"foo.dev. 600 IN A 127.0.0.1"* ]] 29 | } 30 | 31 | @test "dig bar.dev A record" { 32 | name="bar.dev" 33 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 34 | [ "$status" -eq 0 ] 35 | [[ "$output" == *"status: NOERROR,"* ]] 36 | [[ "$output" == *"bar.dev. 600 IN A 127.0.0.1"* ]] 37 | } 38 | 39 | @test "dig foo.bar.dev A record" { 40 | name="foo.bar.dev" 41 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name A +edns=0 +noall +answer +comments 42 | [ "$status" -eq 0 ] 43 | [[ "$output" == *"status: NOERROR,"* ]] 44 | [[ "$output" == *"foo.bar.dev. 600 IN A 127.0.0.1"* ]] 45 | } 46 | 47 | @test "dig dev AAAA record" { 48 | name="dev" 49 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +edns=0 +noall +answer +comments 50 | [ "$status" -eq 0 ] 51 | [[ "$output" == *"status: NOERROR,"* ]] 52 | [[ "$output" == *"dev. 600 IN AAAA ::1"* ]] 53 | } 54 | 55 | @test "dig foo.dev AAAA record" { 56 | name="foo.dev" 57 | run dig +tries=1 +time=1 -p $PORT @127.0.0.1 $name AAAA +edns=0 +noall +answer +comments 58 | [ "$status" -eq 0 ] 59 | [[ "$output" == *"status: NOERROR,"* ]] 60 | [[ "$output" == *"foo.dev. 600 IN AAAA ::1"* ]] 61 | } 62 | -------------------------------------------------------------------------------- /test/test_helper.bash: -------------------------------------------------------------------------------- 1 | expand_path() { 2 | { cd "$(dirname "$1")" 2>/dev/null 3 | local dirname="$PWD" 4 | cd "$OLDPWD" 5 | echo "$dirname/$(basename "$1")" 6 | } || echo "$1" 7 | } 8 | 9 | render() { 10 | eval "echo \"$(cat $1)\"" 11 | } 12 | 13 | launch_plist() { 14 | plist="$BATS_TMPDIR/$1" 15 | render "$BATS_TEST_DIRNAME/LaunchAgents/$1" > "$plist" 16 | echo "$plist" 17 | } 18 | 19 | LAUNCHD=$(which launchctl || echo) 20 | BIN=$(expand_path "$BATS_TEST_DIRNAME/../launchdns") 21 | PORT="65353" 22 | -------------------------------------------------------------------------------- /test/timeout.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "timeout in 1 second" { 6 | run $BIN --port "$PORT" --timeout 1 7 | [ "$status" -eq 0 ] 8 | } 9 | -------------------------------------------------------------------------------- /test/usage.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | name=$(basename "$BIN") 6 | 7 | normalize_error() { 8 | str="$1" 9 | TICK="\`" 10 | QUOTE="'" 11 | str="${str//$TICK/$QUOTE}" 12 | str="${str/$BIN/$name}" 13 | echo $str 14 | } 15 | 16 | @test "invalid option prints error" { 17 | run "$BIN" --invalid 18 | [ "$status" -eq 1 ] 19 | [ "$(normalize_error "${lines[0]}")" = "$name: unrecognized option '--invalid'" ] 20 | } 21 | 22 | @test "missing port argument prints error" { 23 | run "$BIN" --port 24 | [ "$status" -eq 1 ] 25 | [ "$(normalize_error "${lines[0]}")" = "$name: option '--port' requires an argument" ] 26 | } 27 | 28 | @test "missing a argument prints error" { 29 | run "$BIN" --a 30 | [ "$status" -eq 1 ] 31 | [ "$(normalize_error "${lines[0]}")" = "$name: option '--a' requires an argument" ] 32 | } 33 | 34 | @test "missing aaaa argument prints error" { 35 | run "$BIN" --aaaa 36 | [ "$status" -eq 1 ] 37 | [ "$(normalize_error "${lines[0]}")" = "$name: option '--aaaa' requires an argument" ] 38 | } 39 | -------------------------------------------------------------------------------- /test/version.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "prints version" { 6 | run "$BIN" --version 7 | echo "${lines[0]}" | grep "launchdns 1." 8 | [ "$status" -eq 0 ] 9 | } 10 | 11 | @test "prints version with shorthand" { 12 | run "$BIN" -v 13 | echo "${lines[0]}" | grep "launchdns 1." 14 | [ "$status" -eq 0 ] 15 | } 16 | --------------------------------------------------------------------------------