diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 000000000..7185a2136 --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,18 @@ +name: CI Tests + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build: + # Only run on main repo, not forks + if: github.repository == 'cunnie/sslip.io' + runs-on: self-hosted + container: cunnie/fedora-golang-bosh + steps: + - uses: actions/checkout@v4 + + - name: Test + run: HOME=/root ginkgo -r -p . diff --git a/.github/workflows/docker-fedora-golang-bosh.yml b/.github/workflows/docker-fedora-golang-bosh.yml new file mode 100644 index 000000000..b200aab37 --- /dev/null +++ b/.github/workflows/docker-fedora-golang-bosh.yml @@ -0,0 +1,44 @@ +name: Build cunnie/fedora-golang-bosh + +on: + push: + paths: + - "Docker/fedora-golang-bosh/Dockerfile" + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: self-hosted + container: + image: docker:24.0-dind + options: --privileged + services: + docker: + image: docker:24.0-dind + options: --privileged + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: cunnie + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: Docker/fedora-golang-bosh + platforms: linux/amd64,linux/arm64 + push: true + tags: cunnie/fedora-golang-bosh:latest diff --git a/.github/workflows/docker-fedora-ruby-bind-utils.yml b/.github/workflows/docker-fedora-ruby-bind-utils.yml new file mode 100644 index 000000000..1c74832b3 --- /dev/null +++ b/.github/workflows/docker-fedora-ruby-bind-utils.yml @@ -0,0 +1,44 @@ +name: Build cunnie/fedora-ruby-bind-utils + +on: + push: + paths: + - "Docker/fedora-ruby-bind-utils/Dockerfile" + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: self-hosted + container: + image: docker:24.0-dind + options: --privileged + services: + docker: + image: docker:24.0-dind + options: --privileged + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: cunnie + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: Docker/fedora-ruby-bind-utils + platforms: linux/amd64,linux/arm64 + push: true + tags: cunnie/fedora-ruby-bind-utils:latest diff --git a/.github/workflows/docker-sslip.io-dns-server.yml b/.github/workflows/docker-sslip.io-dns-server.yml new file mode 100644 index 000000000..ce480cc2c --- /dev/null +++ b/.github/workflows/docker-sslip.io-dns-server.yml @@ -0,0 +1,44 @@ +name: Build cunnie/sslip.io-dns-server + +on: + push: + tags: + - "*" # Trigger on any tag + workflow_dispatch: # Allow manual triggering + +jobs: + build-and-push: + runs-on: self-hosted + container: + image: docker:24.0-dind + options: --privileged + services: + docker: + image: docker:24.0-dind + options: --privileged + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: cunnie + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: Docker/sslip.io-dns-server + platforms: linux/amd64,linux/arm64 + push: true + tags: | + cunnie/sslip.io-dns-server:latest + cunnie/sslip.io-dns-server:${{ github.ref_name }} diff --git a/.github/workflows/nameservers.yml b/.github/workflows/nameservers.yml new file mode 100644 index 000000000..d8e3cac18 --- /dev/null +++ b/.github/workflows/nameservers.yml @@ -0,0 +1,21 @@ +name: Nameservers + +on: + schedule: + - cron: "0 */6 * * *" # Runs every 6 hours + workflow_dispatch: # Allows manual triggering + +jobs: + check-dns: + runs-on: self-hosted + container: + image: cunnie/fedora-ruby-bind-utils + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run DNS check + run: rspec --format documentation --color spec/ + env: + DOMAIN: sslip.io # You can set your domain here if needed diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..02f928a93 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +# We don't check-in JetBrains's Goland settings +.idea +bin/sslip.io-dns-server-* diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000..2f4b60750 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..19c77b841 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +### Contributing to sslip.io + +Thanks for your interest in sslip.io! This project is primarily a personal +"for fun" project and I'm not looking to grow a community or accept +contributions at this time. + +If you have an idea for an improvement: + +- Forking is encouraged! Please feel free to fork the repository and make the +changes you'd like to see. You can then use your own forked version. +- If you're feeling particularly ambitious, deploy servers running your forked +code. A diverse ecosystem is better. Period. + +I don't accept pull requests because **coding is the most fun** aspect of the +project, the last thing I'd want to outsource. Reviewing other people's +code? Not so fun. Dealing with abuse reports? Maintaining servers? Paying +hosting fees? Checking monitoring alerts? Not fun. + +However, I might consider pull requests for the following: + +- Critical security vulnerabilities: If you discover a serious security flaw, +please report it responsibly by opening an issue. +- Bugs: Include tests, use good commit messages, and squash your commits. +- Minor documentation fixes: Typos or small clarifications in the documentation +are welcome. + +Thanks for understanding! diff --git a/Docker/fedora-golang-bosh/Dockerfile b/Docker/fedora-golang-bosh/Dockerfile new file mode 100644 index 000000000..b072e72e6 --- /dev/null +++ b/Docker/fedora-golang-bosh/Dockerfile @@ -0,0 +1,86 @@ +# cunnie/fedora-golang-bosh + +# To build + +# docker buildx build --pull --platform=linux/amd64,linux/arm64 -t cunnie/fedora-golang-bosh . # OR +# docker build -t cunnie/fedora-golang-bosh . +# docker push cunnie/fedora-golang-bosh + +FROM fedora + +LABEL org.opencontainers.image.authors="Brian Cunnie " + +# need ruby to run dns-check.rb & bind-utils for dig & nslookup +RUN dnf update -y; \ + dnf groupinstall -y "Development Tools"; \ + dnf install -y \ + bind-utils \ + binutils \ + btrfs-progs \ + direnv \ + etcd \ + fd-find \ + gcc-g++ \ + git \ + golang \ + htop \ + iproute \ + iputils \ + jq \ + mysql-devel \ + neovim \ + net-tools \ + nmap-ncat \ + npm \ + openssl-devel \ + python \ + redhat-rpm-config \ + ripgrep \ + ruby \ + ruby-devel \ + rubygems \ + socat \ + strace \ + tcpdump \ + tmux \ + wget \ + zlib-devel \ + zsh \ + zsh-lovers \ + zsh-syntax-highlighting \ + ; + +RUN mkdir ~/workspace; \ + cd ~/workspace; \ + git clone https://github.com/clvv/fasd.git; \ + cd fasd; \ + sudo make install; \ + echo 'eval "\$(fasd --init posix-alias zsh-hook)"' >> ~/.zshrc; \ + echo 'alias z='fasd_cd -d' # cd, same functionality as j in autojump' >> ~/.zshrc \ +EOF + +RUN echo "" | SHELL=/usr/bin/zsh /usr/bin/zsh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"; \ + sed -i 's/robbyrussell/agnoster/' ~/.zshrc; \ + echo 'export EDITOR=nvim' >> ~/.zshrc; \ + echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.zshrc + +# amd64, arm64 (so I can run on AWS graviton2, natively on M1, M2 macs) +ARG TARGETARCH + +RUN curl -L https://github.com/cloudfoundry/bosh-cli/releases/download/v7.5.2/bosh-cli-7.5.2-linux-${TARGETARCH} -o /usr/local/bin/bosh; \ + chmod +x /usr/local/bin/bosh + +RUN dnf install -y dnf-plugins-core; \ + dnf-3 config-manager --add-repo https://rpm.releases.hashicorp.com/fedora/hashicorp.repo; \ + dnf -y install vault; \ + setcap -r /usr/bin/vault + +# https://packages.cloudfoundry.org/stable?release=redhat64&version=8.7.8&source=github-rel +# https://packages.cloudfoundry.org/stable?release=redhataarch64&version=8.7.8&source=github-rel +RUN ARCH=${TARGETARCH/amd64/64}; ARCH=${ARCH/arm64/aarch64} ; \ + curl -L "https://packages.cloudfoundry.org/stable?release=redhat${ARCH}&version=8.7.8&source=github-rel" -o cli.rpm; \ + rpm -i cli.rpm + +RUN CGO_ENABLED=0 GOBIN=/usr/local/bin go install github.com/onsi/ginkgo/v2/ginkgo@latest + +CMD [ "/usr/bin/zsh" ] diff --git a/ci/ruby-bind-utils/Dockerfile b/Docker/fedora-ruby-bind-utils/Dockerfile similarity index 57% rename from ci/ruby-bind-utils/Dockerfile rename to Docker/fedora-ruby-bind-utils/Dockerfile index b04e865c4..5d41d8cf7 100644 --- a/ci/ruby-bind-utils/Dockerfile +++ b/Docker/fedora-ruby-bind-utils/Dockerfile @@ -1,8 +1,9 @@ +# cunnie/fedora-ruby-bind-utils FROM fedora MAINTAINER Brian Cunnie # need ruby to run dns-check.rb & bind-utils for dig & nslookup RUN dnf update -y; \ - dnf install -y bind-utils ruby rubygems which whois; \ + dnf install -y bind-utils iproute iputils net-tools ruby rubygems tcpdump which whois; \ gem install rspec diff --git a/Docker/sslip.io-dns-server/Dockerfile b/Docker/sslip.io-dns-server/Dockerfile new file mode 100644 index 000000000..a131a6548 --- /dev/null +++ b/Docker/sslip.io-dns-server/Dockerfile @@ -0,0 +1,37 @@ +# +# cunnie/sslip.io-dns-server: sslip.io DNS server Dockerfile +# +# Dockerfile of a (Golang-based) DNS server that responds to DNS queries of +# hostnames with embedded IP addresses (e.g. "169.254.169.254.example.com") +# with the IP address ("169.254.169.254). See https://sslip.io for more +# information +# +# To build: +# +# docker buildx build -f Dockerfile-sslip.io-dns-server -t cunnie/sslip.io-dns-server --platform linux/arm64/v8,linux/amd64 --push . +# +# Typical start command: +# +# docker run -d --rm -p 3353:53/udp cunnie/sslip.io-dns-server +# +# To test from host: +# +# dig +short 127.0.0.1.example.com @localhost -p 3353 +# 127.0.0.1 +# +FROM fedora + +LABEL org.opencontainers.image.authors="Brian Cunnie " + +RUN dnf install -y bind-utils + +ARG TARGETARCH # amd64, arm64 (so I can run on AWS graviton2) +RUN curl -f -L https://github.com/cunnie/sslip.io/releases/download/4.1.0/sslip.io-dns-server-linux-$TARGETARCH \ + -o /usr/sbin/sslip.io-dns-server; \ + chmod 755 /usr/sbin/sslip.io-dns-server + +ENTRYPOINT ["/usr/sbin/sslip.io-dns-server"] + +# DNS listens on port 53 UDP +# The `EXPOSE` directive doesn't do much in our case. We use it for documentation. +EXPOSE 53/udp diff --git a/LICENSE b/LICENSE index 9591157b0..d64569567 100644 --- a/LICENSE +++ b/LICENSE @@ -1,662 +1,202 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 247d0e382..c58783a92 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,170 @@ # sslip.io -[![ci.nono.io](https://ci.nono.io/api/v1/pipelines/sslip.io/jobs/check-dns/badge)](https://ci.nono.io/?groups=sslip.io) +[![Production Nameservers](https://github.com/cunnie/sslip.io/actions/workflows/nameservers.yml/badge.svg)](https://github.com/cunnie/sslip.io/actions/workflows/nameservers.yml) +[![CI Tests](https://github.com/cunnie/sslip.io/actions/workflows/ci-tests.yml/badge.svg)](https://github.com/cunnie/sslip.io/actions/workflows/ci-tests.yml) -*sslip.io* is a domain that maps specially-crafted DNS A records to IP addresses -(e.g. "127-0-0-1.sslip.io" maps to 127.0.0.1). It is similar to, inspired by, -and uses much of the code of [xip.io](http://xip.io/). +_sslip.io_ is a DNS server that maps specially-crafted DNS A records to IP +addresses (e.g. "127-0-0-1.sslip.io" maps to 127.0.0.1). It is similar to, and +inspired by, [xip.io](http://xip.io/). -Refer to the website ([sslip.io](https://sslip.io)) for more information. +If you'd like to use sslip.io _as a service_, refer to the website +([sslip.io](https://sslip.io)) for more information. This README targets +developers; the website targets users. + +## Quick Start + +```bash +git clone https://github.com/cunnie/sslip.io.git +cd sslip.io +go mod tidy +sudo go run main.go + # sudo is required on Linux, but not on macOS, to bind to privileged port 53 +``` + +In another window: + +```bash +dig @localhost 192.168.0.1.sslip.io +short + # should return "192.168.0.1" +``` + +### Quick Start Tests + +```bash +go mod tidy +go generate +ginkgo -r -p . +``` + +## Running Your Own Nameservers + +We can customize our nameserver and address records (NS, A, and AAAA), which +can be particularly useful in an internetless (air-gapped) environment. This can +be done with a combination of the `-nameservers` flag and the `-addresses` flag. + +For example, let's say we're the DNS admin for pivotal.io, and we'd like to +have a subdomain, "xip.pivotal.io", that does sslip.io-style lookups (e.g. +"127.0.0.1.xip.pivotal.io" would resolve to "127.0.0.1"). Let's say we have two +servers that we've set aside for this purpose: + +- ns-sslip-0.pivotal.io, 10.8.8.8 (IPv4) +- ns-sslip-1.pivotal.io, fc88:: (IPv6) + +First, we delegate the subdomain "xip.pivotal.io" to our two nameservers, and +then we run the following command run on each of the two servers: + +```bash +# after we've cloned our repo & cd'ed into it +go run main.go \ + -nameservers=ns-sslip-0.pivotal.io,ns-sslip-1.pivotal.io \ + -addresses ns-sslip-0.pivotal.io=10.8.8.8,ns-sslip-1.pivotal.io=fc88:: +``` + +**Note: These nameservers are not general-purpose nameservers; for example, +they won't look up google.com. They are not recursive.** Don't ever configure a +machine to point to these nameservers. + +### Running with Docker + +Probably the easiest way to run the nameserver is with the official Docker +image, +[cunnie/sslip.io-dns-server](https://hub.docker.com/r/cunnie/sslip.io-dns-server): -- `document_root/` contains the HTML content of the sslip.io website -- `ci/` contains the [Concourse](https://concourse.ci/) continuous integration - (CI) pipeline and task. -- `spec/` contains the RSpec files for test driven development (TDD). - To run the tests: ```bash -DOMAIN=sslip.io rspec --format documentation --color spec +docker run \ + -it \ + --rm \ + -p 53:53/udp \ + cunnie/sslip.io-dns-server ``` -- `conf/sslip.io+nono.io.yml` contains the - [PowerDNS](https://www.powerdns.com/)'s [pipe - backend](https://doc.powerdns.com/md/authoritative/backend-pipe/)'s - configuration in YAML format for use with [BOSH](https://bosh.io). The - `pdns_pipe` key is the pipe backend script, and `pdns_pipe_conf` is its - configuration file. + +If we see the error, "`Error starting userland proxy: listen udp4 0.0.0.0:53: +bind: address already in use.`", we turn off the systemd resolver: `sudo +systemctl stop systemd-resolved` + +Let's try a more complicated setup: we're on our workstation, jammy.nono.io, +whose IP addresses are 10.9.9.114 and 2601:646:0100:69f0:0:ff:fe00:72. We'd like +our workstation to be the DNS server: + +```bash +docker run \ + -it \ + --rm \ + -p 53:53/udp \ + cunnie/sslip.io-dns-server \ + -nameservers jammy.nono.io \ + -addresses jammy.nono.io=10.9.9.114,jammy.nono.io=2601:646:100:69f0:0:ff:fe00:72 +``` + +From another machine, we look up the DNS NS record for "127.0.0.1.com", and we +see the expected reply: + +```bash +dig ns 127.0.0.1.com @jammy.nono.io +short +... + ;; ANSWER SECTION: + 127.0.0.1.com. 604800 IN NS jammy.nono.io. + + ;; ADDITIONAL SECTION: + jammy.nono.io. 604800 IN A 10.9.9.114 + jammy.nono.io. 604800 IN AAAA 2601:646:100:69f0:0:ff:fe00:72 +``` + +The Docker image is multi-platform, supporting both x86_64 architecture as well +as ARM64 (AWS Graviton, Apple M1/M2). + +## Command-line Flags + +- `-port` overrides the default port, 53, which the server binds to. This can + be especially useful when running as a non-privileged user, unable to bind to + privileged ports (<1024) ("`listen udp :53: bind: permission denied`"). For + example, to run the server on port 9553: `go run main.go -port 9553`. To + query, `dig @localhost 127.0.0.1.sslip.io -p 9553` +- `-nameservers` overrides the default NS records `ns-do-sg.sslip.io`, + `ns-gce.sslip.io`, `ns-hetzner.sslip.io`, and `ns-ovh.sslip.io`; flag, e.g. + `go run main.go -nameservers ns1.example.com,ns2.example.com`). If you're + running your own nameservers, you probably want to set this. Don't forget to + set address records for the new name servers with the `-addresses` flag (see + below). Exception: `_acme-challenge` records are handled differently to + accommodate the procurement of Let's Encrypt wildcard certificates; you can + read more about that procedure [here](docs/wildcard.md) +- `-addresses` overrides the default A/AAAA (IPv4/IPv6) address records. For + example, here's how we set the IPv4 record & IPv6 record for our nameserver + (in the `-nameservers` example above), ns1.example.com: `-addresses + ns1.example.com=10.8.8.8,ns1.example.com=fc::8888`. Note that you can set + many addresses for a single host, e.g. + `ns1.example.com=1.1.1.1,ns1.example.com=8.8.8.8,ns1.example.com=9.9.9.9` +- `-blocklistURL` overrides the default block list, + (). + It's not necessary to override this if you're in an internetless environment: + if the DNS server can't download the blocklist, it prints out a message and + continues to serve DNS queries + +## DNS Server Miscellany + +- it binds to both UDP and TCP. +- The SOA record is hard-coded except the _MNAME_ (primary master name server) + record, which is set to the queried hostname (e.g. `dig big.apple.com + @ns.sslip.io` would return an SOA with an _MNAME_ record of + `big.apple.com.` +- The MX records are hard-coded to the queried hostname with a preference of 0, + except `sslip.io` itself, which has custom MX records to enable email + delivery to ProtonMail +- There are no SRV records + +## Directory Structure + +- `spec/` contains the tests for the production nameservers. To run + the tests locally: + ```bash + DOMAIN=sslip.io rspec --format documentation --color spec/ + ``` +- `k8s/document_root_sslip.io/` contains the HTML content of the sslip.io + website. + +### Acknowledgements + +- Sam Stephenson (xip.io), the late Roopinder Singh (nip.io), and the other DNS + developers out there +- The contributors (@normanr, @jpambrun come to mind) who improved sslip.io +- Let's Encrypt for bumping our rate limits diff --git a/bin/make_all b/bin/make_all new file mode 100755 index 000000000..5306e94f2 --- /dev/null +++ b/bin/make_all @@ -0,0 +1,23 @@ +#!/bin/bash -x +# +# Build binaries for macOS, Windows, Linux, FreeBSD +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd $DIR/.. +ldflags="-X xip/xip.VersionSemantic=4.1.0 \ + -X xip/xip.VersionDate=$(date +%Y/%m/%d-%H:%M:%S%z) \ + -X xip/xip.VersionGitHash=$(git rev-parse --short HEAD)" +export GOOS GOARCH + +# Bring in ARM64 for my AWS Graviton2 instance +for GOARCH in amd64 arm64; do + for GOOS in darwin linux freebsd; do + go build \ + -ldflags="$ldflags" \ + -o $DIR/sslip.io-dns-server-$GOOS-$GOARCH \ + main.go & + done + GOOS=windows + go build -o $DIR/sslip.io-dns-server-$GOOS-$GOARCH.exe main.go & +done +wait diff --git a/ci/pipeline-badges.yml b/ci/pipeline-badges.yml deleted file mode 100644 index b6ecd637d..000000000 --- a/ci/pipeline-badges.yml +++ /dev/null @@ -1,78 +0,0 @@ -# pipeline for Concourse CI for badges -# -# fly -t nono sp -p badges -c pipeline-badges.yml -# fly -t nono expose-pipeline -p badges -# fly -t nono unpause-pipeline -p badges -groups: -- name: badges - jobs: - - passing - - failing - - unknown - - aborted - - errored - - -jobs: -- name: passing - # `true` always passes - plan: - - task: passing - config: - platform: linux - image_resource: - type: docker-image - source: - repository: fedora - run: - path: true -- name: failing - # `false` always fails - plan: - - task: failing - config: - platform: linux - image_resource: - type: docker-image - source: - repository: fedora - run: - path: false -- name: unknown - # never run this job and it will always be "unknown" - plan: - - task: unknown - config: - platform: linux - image_resource: - type: docker-image - source: - repository: fedora - run: - path: sleep - args: ["864000"] -- name: aborted - # run this job and then abort it. You'll have ten days to abort it. - plan: - - task: aborted - config: - platform: linux - image_resource: - type: docker-image - source: - repository: fedora - run: - path: sleep - args: ["864000"] -- name: errored - # Concourse will error if it can't find the `non-existent` executable - plan: - - task: errored - config: - platform: linux - image_resource: - type: docker-image - source: - repository: fedora - run: - path: non-existent diff --git a/ci/pipeline-bosh-pws_cf.yml b/ci/pipeline-bosh-pws_cf.yml deleted file mode 100644 index 076556c4f..000000000 --- a/ci/pipeline-bosh-pws_cf.yml +++ /dev/null @@ -1,266 +0,0 @@ -# fly -t nono sp -p bosh:pws_cf -c pipeline-bosh-pws_cf.yml -# fly -t nono expose-pipeline -p bosh:pws_cf -# fly -t nono unpause-pipeline -p bosh:pws_cf - -groups: [] - -resource_template: &resource_template - type: git - source: - uri: https://github.com/cunnie/docs.git - -resources: -- name: clamav-pivnet-release - <<: *resource_template -- name: pws-deployed-version - <<: *resource_template -- name: cf-deployment-cloudops - <<: *resource_template -- name: cf-sli-s3 - <<: *resource_template -- name: prod-configs - <<: *resource_template -- name: cloudops-ci - <<: *resource_template -- name: prod-aws - <<: *resource_template -- name: app-asset - <<: *resource_template -resource_types: [] -jobs: -- name: upload-clamav-release - build_logs_to_retain: 1000 - plan: - - get: cloudops-ci - - get: clamav-pivnet-release - trigger: true -- name: dry-runs-and-build-msg - serial: true - build_logs_to_retain: 1000 - plan: - - get: cloudops-ci - - get: cf-deployment-cloudops -- name: deploy-pws-isolation-cloudops - serial: true - build_logs_to_retain: 1000 - plan: - - get: cf-deployment-cloudops - passed: - - deploy-cf-cfapps-io2-donotuseapi - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-01 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-diego-overflow-az2 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-02 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-diego-overflow-az2 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-03 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-diego-overflow-az2 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-04 - serial: true - build_logs_to_retain: 1000 - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-diego-overflow-az2 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-05 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-diego-overflow-az2 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-06 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - delete-pws-diego-overflow-az2 - - deploy-pws-diego-overflow-az4 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-07 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - delete-pws-diego-overflow-az2 - - deploy-pws-diego-overflow-az4 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-08 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - delete-pws-diego-overflow-az2 - - deploy-pws-diego-overflow-az4 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-09 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - delete-pws-diego-overflow-az2 - - deploy-pws-diego-overflow-az4 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-10 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - delete-pws-diego-overflow-az2 - - deploy-pws-diego-overflow-az4 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-cf-cfapps-io2 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - dry-runs-and-build-msg - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-cf-cfapps-io2-donotuseapi - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-cf-cfapps-io2 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: update-statuspage-version - serial: true - plan: - - aggregate: - - get: cf-deployment-cloudops - passed: - - delete-pws-diego-overflow-az4 - trigger: true - - get: prod-aws - params: - submodules: none - - get: pws-deployed-version - - get: cloudops-ci -- name: deploy-pws-pivotal-internal-apps - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-isolation-cloudops - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-cellblock-windows - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-isolation-cloudops - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: update-environment-configs - serial: true - plan: - - aggregate: - - get: cloudops-ci - - get: prod-configs - trigger: true -- name: deploy-pws-diego-overflow-az2 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-isolation-cloudops - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: deploy-pws-diego-overflow-az4 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-isolation-cloudops - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: delete-pws-diego-overflow-az2 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-diego-cellblock-01 - - deploy-pws-diego-cellblock-02 - - deploy-pws-diego-cellblock-03 - - deploy-pws-diego-cellblock-04 - - deploy-pws-diego-cellblock-05 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci -- name: delete-pws-diego-overflow-az4 - serial: true - plan: - - get: cf-deployment-cloudops - passed: - - deploy-pws-diego-cellblock-06 - - deploy-pws-diego-cellblock-07 - - deploy-pws-diego-cellblock-08 - - deploy-pws-diego-cellblock-09 - - deploy-pws-diego-cellblock-10 - trigger: true - - get: cf-sli-s3 - - get: app-asset - - get: cloudops-ci diff --git a/ci/pipeline-simple.yml b/ci/pipeline-simple.yml deleted file mode 100644 index bded5e687..000000000 --- a/ci/pipeline-simple.yml +++ /dev/null @@ -1,45 +0,0 @@ -# fly -t nono sp -p simple -c pipeline-simple.yml -# fly -t nono expose-pipeline -p simple -# fly -t nono unpause-pipeline -p simple -groups: -- name: simple - jobs: - - unit - - integration - -jobs: -- name: unit - plan: - - get: src - trigger: true - - task: unit - config: - platform: linux - image_resource: - type: docker-image - source: - repository: fedora - run: - path: true - -- name: integration - plan: - - get: src - trigger: true - passed: [unit] - - task: integration - config: - platform: linux - image_resource: - type: docker-image - source: - repository: fedora - run: - path: false - -resources: -- name: src - type: git - source: - uri: https://github.com/cunnie/sslip.io - branch: master diff --git a/ci/pipeline-sslip.io.yml b/ci/pipeline-sslip.io.yml deleted file mode 100644 index 6153988ad..000000000 --- a/ci/pipeline-sslip.io.yml +++ /dev/null @@ -1,32 +0,0 @@ -# pipeline for Concourse CI for sslip.io -# -# fly -t nono sp -p sslip.io -c pipeline-sslip.io.yml -# fly -t nono expose-pipeline -p sslip.io -# fly -t nono unpause-pipeline -p sslip.io -groups: -- name: sslip.io - jobs: - - check-dns - -jobs: -- name: check-dns - public: true - plan: - - {get: sslip.io, trigger: true} - - {get: 6h, trigger: true} - - task: check-dns - file: sslip.io/ci/tasks/check-dns.yml - attempts: 10 - params: - DOMAIN: sslip.io - -resources: -- name: sslip.io - type: git - source: - uri: https://github.com/cunnie/sslip.io - branch: master - -- name: 6h - type: time - source: {interval: 6h} diff --git a/ci/tasks/check-dns.sh b/ci/tasks/check-dns.sh deleted file mode 100755 index 39ed7306f..000000000 --- a/ci/tasks/check-dns.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -xe - -rspec --format documentation --color sslip.io/spec diff --git a/ci/tasks/check-dns.yml b/ci/tasks/check-dns.yml deleted file mode 100644 index 52ad61328..000000000 --- a/ci/tasks/check-dns.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -platform: linux -image_resource: - type: docker-image - source: {repository: cunnie/fedora-ruby-bind-utils} - -inputs: -- name: sslip.io - -run: - path: sslip.io/ci/tasks/check-dns.sh - -params: - DOMAIN: "" diff --git a/conf/sslip.io+nono.io.yml b/conf/sslip.io+nono.io.yml deleted file mode 100644 index 42cf5b7f5..000000000 --- a/conf/sslip.io+nono.io.yml +++ /dev/null @@ -1,261 +0,0 @@ -pdns_named_conf: | - zone "nono.com" { - type slave; - file "/var/vcap/jobs/pdns/etc/nono.com"; - masters { 78.46.204.247; }; - }; - zone "nono.io" { - type slave; - file "/var/vcap/jobs/pdns/etc/nono.io"; - masters { 78.46.204.247; }; - }; -pdns_conf: | - launch=bind:first,pipe:second - slave=yes - bind-first-config=/var/vcap/jobs/pdns/etc/named.conf - pipe-second-command=/var/vcap/jobs/pdns/bin/pipe /var/vcap/jobs/pdns/etc/pipe.conf - # fixes `Fatal error: Unable to acquire TCPv6 socket: Address family not supported by protocol` - # undo when deployed with IPv6 - local-ipv6= -pdns_pipe: | - #!/usr/bin/env bash - # - # Originally written by Sam Stephenson for xip.io - set -e - shopt -s nocasematch - - # Configuration - # - # Increment this timestamp when the contents of the file change. - XIP_TIMESTAMP="2018030100" - - # The top-level domain for which the name server is authoritative. - # CHANGEME: change "sslip.io" to your domain - XIP_DOMAIN="sslip.io" - - # How long responses should be cached, in seconds. - XIP_TTL=300 - - # SOA record - XIP_SOA="briancunnie.gmail.com ns-he.nono.io $XIP_TIMESTAMP $XIP_TTL $XIP_TTL $XIP_TTL $XIP_TTL" - - # The public IP addresses (e.g. for the web site) of the top-level domain. - # `A` queries for the top-level domain will return this list of addresses. - # CHANGEME: change this to your domain's webserver's address - XIP_ROOT_ADDRESSES=( "78.46.204.247" ) - XIP_ROOT_ADDRESSES_AAAA=( "2a01:4f8:c17:b8f::2" ) - - # The public IP addresses on which this xip-pdns server will run. - # `NS` queries for the top-level domain will return this list of addresses. - # Each entry maps to a 1-based subdomain of the format `ns-1`, `ns-2`, etc. - # `A` queries for these subdomains map to the corresponding addresses here. - # CHANGEME: change this to match your NS records; one of these IP addresses - # should match the jobs(xip).networks.static_ips listed above - XIP_NS_ADDRESSES=( "52.0.56.137" "52.187.42.158" "104.155.144.4" "78.47.249.19" ) - XIP_NS=( "ns-aws.nono.io" "ns-azure.nono.io" "ns-gce.nono.io" "ns-he.nono.io" ) - - # These are the MX records for your domain. IF YOU'RE NOT SURE, - # don't set it at at all (comment it out)--it defaults to no - # MX records. - # XIP_MX_RECORDS=( - # "10" "mx.zoho.com" - # "20" "mx2.zoho.com" - # ) - XIP_MX_RECORDS=( ) - - if [ -a "$1" ]; then - source "$1" - fi - - # - # Protocol helpers - # - read_cmd() { - local IFS=$'\t' - local i=0 - local arg - - read -ra CMD - for arg; do - eval "$arg=\"\${CMD[$i]}\"" - let i=i+1 - done - } - - send_cmd() { - local IFS=$'\t' - printf "%s\n" "$*" - } - - fail() { - send_cmd "FAIL" - log "Exiting" - exit 1 - } - - read_helo() { - read_cmd HELO VERSION - [ "$HELO" = "HELO" ] && [ "$VERSION" = "1" ] - } - - read_query() { - read_cmd TYPE QNAME QCLASS QTYPE ID IP - } - - send_answer() { - local type="$1" - shift - send_cmd "DATA" "$QNAME" "$QCLASS" "$type" "$XIP_TTL" "$ID" "$@" - } - - log() { - printf "[xip-pdns:$$] %s\n" "$@" >&2 - } - - - # - # xip.io domain helpers - # - IP_PATTERN="(^|\.)(x{0}(x{0}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))($|\.)" - DASHED_IP_PATTERN="(^|-|\.)(x{0}(x{0}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)-){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))($|-|\.)" - # https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses - # We don't use "dotted" IPv6 because DNS doesn't allow two dots next to each other - # e.g. "::1" -> "1..sslip.io" isn't allowed (dig error: `is not a legal name (empty label)`) - DASHED_IPV6_PATTERN="(^|\.)(x{0}([0-9a-fA-F]{1,4}-){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}-){1,7}-|([0-9a-fA-F]{1,4}-){1,6}-[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}-){1,5}(-[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}-){1,4}(-[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}-){1,3}(-[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}-){1,2}(-[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}-(x{0}(-[0-9a-fA-F]{1,4}){1,6})|-(x{0}(-[0-9a-fA-F]{1,4}){1,7}|-))(\.|$)" - - qtype_is() { - [ "$QTYPE" = "$1" ] || [ "$QTYPE" = "ANY" ] - } - - qname_is_root_domain() { - [ "$QNAME" = "$XIP_DOMAIN" ] - } - - subdomain_is_ip() { - [[ "$QNAME" =~ $IP_PATTERN ]] - } - - subdomain_is_dashed_ip() { - [[ "$QNAME" =~ $DASHED_IP_PATTERN ]] - } - - subdomain_is_dashed_ipv6() { - [[ "$QNAME" =~ $DASHED_IPV6_PATTERN ]] - } - - resolve_ns_subdomain() { - local index="${SUBDOMAIN:3}" - echo "${XIP_NS_ADDRESSES[$index-1]}" - } - - resolve_ip_subdomain() { - [[ "$QNAME" =~ $IP_PATTERN ]] || true - echo "${BASH_REMATCH[2]}" - } - - resolve_dashed_ip_subdomain() { - [[ "$QNAME" =~ $DASHED_IP_PATTERN ]] || true - echo "${BASH_REMATCH[2]//-/.}" - } - - resolve_dashed_ipv6_subdomain() { - [[ "$QNAME" =~ $DASHED_IPV6_PATTERN ]] || true - echo "${BASH_REMATCH[2]//-/:}" - } - - answer_soa_query() { - send_answer "SOA" "$XIP_SOA" - } - - answer_ns_query() { - local i=1 - local ns_address - for ns in "${XIP_NS[@]}"; do - send_answer "NS" "$ns" - done - } - - answer_root_a_query() { - local address - for address in "${XIP_ROOT_ADDRESSES[@]}"; do - send_answer "A" "$address" - done - } - - answer_root_aaaa_query() { - local address - for address in "${XIP_ROOT_ADDRESSES_AAAA[@]}"; do - send_answer "AAAA" "$address" - done - } - - answer_mx_query() { - set -- "${XIP_MX_RECORDS[@]}" - while [ $# -gt 1 ]; do - send_answer "MX" "$1 $2" - shift 2 - done - } - - answer_subdomain_a_query_for() { - local type="$1" - local address="$(resolve_${type}_subdomain)" - if [ -n "$address" ]; then - send_answer "A" "$address" - fi - } - - answer_subdomain_aaaa_query_for() { - local type="$1" - local address="$(resolve_${type}_subdomain)" - if [ -n "$address" ]; then - send_answer "AAAA" "$address" - fi - } - - - # - # PowerDNS pipe backend implementation - # - trap fail err - read_helo - send_cmd "OK" "xip.io PowerDNS pipe backend (protocol version 1)" - - while read_query; do - log "Query: type=$TYPE qname=$QNAME qclass=$QCLASS qtype=$QTYPE id=$ID ip=$IP" - - if qtype_is "SOA"; then - answer_soa_query - fi - if qtype_is "NS"; then - answer_ns_query - fi - if qtype_is "MX"; then - answer_mx_query - fi - if qtype_is "A"; then - LC_QNAME=$(echo $QNAME | tr 'A-Z' 'a-z') - if [ $LC_QNAME == $XIP_DOMAIN ]; then - answer_root_a_query - else - if subdomain_is_dashed_ip; then - answer_subdomain_a_query_for dashed_ip - elif subdomain_is_ip; then - answer_subdomain_a_query_for ip - fi - fi - fi - - if qtype_is "AAAA"; then - LC_QNAME=$(echo $QNAME | tr 'A-Z' 'a-z') - if [ $LC_QNAME == $XIP_DOMAIN ]; then - answer_root_aaaa_query - else - if subdomain_is_dashed_ipv6; then - answer_subdomain_aaaa_query_for dashed_ipv6 - fi - fi - fi - - send_cmd "END" - done diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md new file mode 100644 index 000000000..7bb97e8d9 --- /dev/null +++ b/docs/DEVELOPER.md @@ -0,0 +1,130 @@ +## Release Procedure + +These instructions are meant primarily for me when deploying a new release; +they might not make sense unless you're on my workstation. + +```bash +export OLD_VERSION=4.0.0 +export VERSION=4.1.0 +cd ~/workspace/sslip.io +git pull -r --autostash +# update the version number for the TXT record for version.status.sslip.io +sed -i '' "s/$OLD_VERSION/$VERSION/g" \ + bin/make_all \ + spec/check-dns_spec.rb +# update the download instructions on the website +sed -i '' "s~/$OLD_VERSION/~/$VERSION/~g" \ + Docker/sslip.io-dns-server/Dockerfile +``` + +Optional: Update the version for the ns-gce, ns-hetzner, and ns-ovh install scripts + +```bash +pushd ~/bin +sed -i '' "s~/$OLD_VERSION/~/$VERSION/~g" \ + ~/bin/install_ns-{gce,hetzner,ovh}.sh ~/bin/install_common.sh +git add -p +git ci -m"Update sslip.io DNS server $OLD_VERSION → $VERSION" +git push +popd +``` + +Build & start the new executables: + +```bash +bin/make_all +# Start the server, assuming macOS M1. Adjust path for GOOS, GOARCH. Linux requires `sudo` +bin/sslip.io-dns-server-darwin-arm64 +``` + +Test from another window: + +```bash +export DNS_SERVER_IP=127.0.0.1 +export VERSION=4.1.0 +# quick sanity test +dig +short 127.0.0.1.example.com @$DNS_SERVER_IP +echo 127.0.0.1 +# NS ordering might be rotated +dig +short ns example.com @$DNS_SERVER_IP +printf "ns-do-sg.sslip.io.\nns-gce.sslip.io.\nns-hetzner.sslip.io.\nns-ovh.sslip.io.\n" +dig +short mx example.com @$DNS_SERVER_IP +echo "0 example.com." +dig +short mx sslip.io @$DNS_SERVER_IP +printf "10 mail.protonmail.ch.\n20 mailsec.protonmail.ch.\n" +dig +short txt sslip.io @$DNS_SERVER_IP +printf "\"protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26\"\n\"v=spf1 include:_spf.protonmail.ch mx ~all\"\n" +dig +short txt nip.io @$DNS_SERVER_IP +printf "\"protonmail-verification=19b0837cc4d9daa1f49980071da231b00e90b313\"\n\"v=spf1 include:_spf.protonmail.ch mx ~all\"\n" +dig +short txt 127.0.0.1.sslip.io @$DNS_SERVER_IP # no records +dig +short cname sslip.io @$DNS_SERVER_IP # no records +dig +short cname protonmail._domainkey.sslip.io @$DNS_SERVER_IP +echo protonmail.domainkey.dw4gykv5i2brtkjglrf34wf6kbxpa5hgtmg2xqopinhgxn5axo73a.domains.proton.ch. +dig a _Acme-ChallengE.127-0-0-1.sslip.io @$DNS_SERVER_IP | grep "^127" +echo "127-0-0-1.sslip.io. 604800 IN A 127.0.0.1" +dig +short sSlIp.Io +echo 78.46.204.247 +dig @$DNS_SERVER_IP txt ip.sslip.io +short | tr -d '"' +echo 127.0.0.1 +dig @$DNS_SERVER_IP txt version.status.sslip.io +short | grep $VERSION +echo "\"$VERSION\"" +echo " ===" # separator because the results are too similar +dig @$DNS_SERVER_IP 1.0.0.127.in-addr.arpa ptr +short +echo "127-0-0-1.sslip.io." +dig @$DNS_SERVER_IP _psl.sslip.io txt +short +echo "\"https://github.com/publicsuffix/list/pull/2206/"" +dig @$DNS_SERVER_IP 7f000001.nip.io +short +echo 127.0.0.1 +dig @$DNS_SERVER_IP metrics.status.sslip.io txt +short | grep '"Queries: ' +echo '"Queries: 13 (?.?/s)"' +``` + +Review the output then close the second window. Stop the server in the +original window. Commit our changes: + +```bash +GIT_MESSAGE="$VERSION: hexadecimal notation" +git add -p +git ci -vm"$GIT_MESSAGE" +git tag $VERSION +git push +git push --tags +scp bin/sslip.io-dns-server-linux-amd64 ns-do-sg: +scp bin/sslip.io-dns-server-linux-amd64 ns-gce: +scp bin/sslip.io-dns-server-linux-amd64 ns-hetzner: +scp bin/sslip.io-dns-server-linux-amd64 ns-ovh: +ssh ns-do-sg sudo install sslip.io-dns-server-linux-amd64 /usr/bin/sslip.io-dns-server +ssh ns-do-sg sudo shutdown -r now + # check version number: +sleep 10; while ! dig txt @ns-do-sg.sslip.io version.status.sslip.io +short; do sleep 5; done +ssh ns-gce sudo install sslip.io-dns-server-linux-amd64 /usr/bin/sslip.io-dns-server +ssh ns-gce sudo shutdown -r now + # check version number: +sleep 10; while ! dig txt @ns-gce.sslip.io version.status.sslip.io +short; do sleep 5; done # wait until it's back up before rebooting ns-hetzner +ssh ns-hetzner sudo install sslip.io-dns-server-linux-amd64 /usr/bin/sslip.io-dns-server +ssh ns-hetzner sudo shutdown -r now + # check version number: +sleep 10; while ! dig txt @ns-hetzner.sslip.io version.status.sslip.io +short; do sleep 5; done # wait until it's back up before rebooting ns-ovh +ssh ns-ovh sudo install sslip.io-dns-server-linux-amd64 /usr/bin/sslip.io-dns-server +ssh ns-ovh sudo shutdown -r now + # check version number: +sleep 10; while ! dig txt @ns-ovh.sslip.io version.status.sslip.io +short; do sleep 5; done +``` + +- Browse to to draft a new release +- Drag and drop the executables in `bin/` to the _Attach binaries..._ section. +- Click "Publish release" + +Trigger a new workflow to publish the Docker image: + +Update the webservers with the HTML with new versions: + +```bash +ssh nono.io curl -L -o /www/sslip.io/document_root/index.html https://raw.githubusercontent.com/cunnie/sslip.io/main/k8s/document_root_sslip.io/index.html +ssh nono.io curl -L -o /www/nip.io/document_root/index.html https://raw.githubusercontent.com/cunnie/sslip.io/main/k8s/document_root_nip.io/index.html +for HOST in {blocked,ns-do-sg,ns-gce,ns-hetzner,ns-ovh}.sslip.io; do + ssh $HOST curl -L -o /var/nginx/sslip.io/index.html https://raw.githubusercontent.com/cunnie/sslip.io/main/k8s/document_root_sslip.io/index.html +done +``` + +Browse to , trigger the workflow, and check that everything is green. diff --git a/docs/logs.md b/docs/logs.md new file mode 100644 index 000000000..bb3dcc35c --- /dev/null +++ b/docs/logs.md @@ -0,0 +1,48 @@ +### Tools for Exploring Log Files + +To generate log files on, say, ns-ovh: + +```zsh +sudo journalctl -u sslip.io-dns -S yesterday > /tmp/sslip.io.log +``` + +A file which I subsequently copy to my Mac (warning: uses BSD-variant of tools +like `sed`, so you may need to tweak the following commands if you're on Linux): + +[I use `cut` instead of `awk` because it's twice as fast (9.11s instead of 22.56s)] + +To find the domains queried (95% sslip.io): + +```zsh + # find all successful queries of A & AAAA records +grep -v '\. \? nil' < /tmp/sslip.io.log |\ + egrep "TypeA | TypeAAAA " |\ + cut -d " " -f 10 > /tmp/hosts.log +sed -E 's=.*(\.[^.]+\.[^.]+\.$)=\1=' < /tmp/hosts.log | tr 'A-Z' 'a-z' | sort | uniq -c | sort -n +``` + +```zsh + # find the most looked-up IP addresses using the above hosts.log +sort < /tmp/hosts.log | uniq -c | sort -n | tail -50 +``` + +```zsh + # Who's trying to find out their own IP via ip.sslip.io? + sudo journalctl --since yesterday -u sslip.io-dns | \ + grep -v "nil, SOA" | \ + grep "TypeTXT ip.sslip.io" | \ + sed 's/.*TypeTXT ip.sslip.io. ? \["//; s/"\]$//' | \ + sort | \ + uniq -c +``` + +```zsh + # Who's querying us the most? +awk '{print $8}' < /tmp/sslip.io.log | \ + grep -v "nil, SOA" | \ + sed 's/\.[0-9]*$//' | \ + sort | \ + uniq -c | \ + sort -n | \ + tail -50 +``` diff --git a/docs/wildcard.md b/docs/wildcard.md new file mode 100644 index 000000000..bd0a5911c --- /dev/null +++ b/docs/wildcard.md @@ -0,0 +1,194 @@ +## Procuring a Wildcard Certificate + +### Using a White Label Domain + +Let's say you have a domain that is hosted on Amazon Route53, lets call it +`example.com`. You have a few DNS entries set up like `foo.example.com`, and then +you have `xip.example.com` which is an NS record to `ns-ovh.sslip.io`. So you +are able to use both regular DNS records that are hardcoded, and then when you +need to use sslip you simply use your xip subdomain. + +To get a wildcard certificate for `*.xip.example.com`, simply go through the regular +Let's Encrypt DNS-01 challenge process. + +Let's Encrypt will query your name servers for the TXT record +`_acme-challenge.xip.example.com`, then your DNS server will respond with the +TXT record _that should have been created on Route53 as part of the challenge_, +otherwise it'll return the delegated nameservers (ns-ovh.sslip.io and so on). + +### Using the sslip.io domain + +You can procure a [wildcard](https://en.wikipedia.org/wiki/Wildcard_certificate) +certificate (e.g. `*.52-0-56-137.sslip.io`) from a certificate authority (e.g. +Let's Encrypt) using the [DNS-01 +challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge). + +You'll need the following: + +- An internet-accessible DNS server that's authoritative for its `sslip.io` + subdomain For example, if the DNS server's IP address is `52.187.42.158`, the + DNS server would need to be authoritative for the domain + `52-187-42-158.sslip.io`. Pro-tip: it only needs to be authoritative for the + `_acme-challenge` subdomain, e.g. `_acme-challenge.52-187-42-158.sslip.io`; + furthermore, it only needs to return TXT records. + + How to test that your DNS server is working properly (assuming you've set a + TXT record, "I love my dog"): + + ``` + dig _acme-challenge.52-187-42-158.sslip.io txt + ... + _acme-challenge.52-187-42-158.sslip.io 604800 IN TXT "I love my dog" + ... + ``` + +- An [ACME + v2](https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment) + protocol client; I use [acme.sh](https://github.com/acmesh-official/acme.sh). + The ACME client must be able to update the TXT records of your DNS server. + +### Using the Wildcard Certificate + +Once you've procured the wildcard certificate, you can install it on your +internal webservers for URLS of the following format: +https://*internal-ip.external-ip*.sslip.io (e.g. +). Note that the _internal-ip_ +portion of the URL _must_ be dash-separated, not dot-separated, for the wildcard +certificate to work properly. + +Tech note: wildcard certificates can be used for development for machines behind +a firewall using non-routable IP addresses (10/8, 172.16/12, 192.168/16) by +taking advantage of the manner which `sslip.io` parses hostnames with embedded +IP addresses: left-to-right. The internal IP address is parsed first and +returned as the IP address of the hostname. + +### How Do I Set Up an External DNS Server? + +The external IP might be from your local network (forward port 53 at your +router), or from a cloud provider (GCP, AWS, etc.). It might even be from a +public DNS service (e.g. [Cloudflare](https://www.cloudflare.com/), [AWS Route +53](https://aws.amazon.com/route53/), my perennial favorite +[easyDNS](https://easydns.com/), etc.). If not using a public DNS service, you +need to run your own DNS server (e.g. +[acme-dns](https://github.com/joohoi/acme-dns), the venerable +[BIND](https://en.wikipedia.org/wiki/BIND), the opinionated +[djbdns](https://cr.yp.to/djbdns.html), or my personal +[wildcard-dns-http-server](https://github.com/cunnie/sslip.io/tree/main/src/wildcard-dns-http-server), +etc.). You can use any ACME client +([acme.sh](https://github.com/acmesh-official/acme.sh), +[Certbot](https://certbot.eff.org/), etc.), but you must configure it to request +a wildcard certificate for \*._external-ip_.sslip.io, which requires configuring +the DNS-01 challenge to use DNS server chosen. + +#### Example + +In the following example, we create a webserver on Google Cloud Platform (GCP) +to acquire a wildcard certificate. We use the ACME client acme.sh and the +DNS server wildcard-dns-http-server: + +```bash +gcloud auth login + # set your project; mine is "blabbertabber" +gcloud config set project blabbertabber + # create your VM +gcloud compute instances create \ + --image-project "ubuntu-os-cloud" \ + --image-family "ubuntu-2004-lts" \ + --machine-type f1-micro \ + --boot-disk-size 40 \ + --boot-disk-type pd-ssd \ + --zone "us-west1-a" \ + sslip + # get the IP, e.g. 35.199.174.9 +export NAT_IP=$(gcloud compute instances list --filter="name=('sslip')" --format=json | \ + jq -r '.[0].networkInterfaces[0].accessConfigs[0].natIP') +echo $NAT_IP + # get the fully-qualified domain name, e.g. 35-199-174-9.sslip.io +export FQDN=${NAT_IP//./-}.sslip.io +echo $FQDN + # set IP & FQDN on the VM because we'll need them later +gcloud compute ssh --command="echo export FQDN=$FQDN IP=$IP >> ~/.bashrc" --zone=us-west1-a sslip + # create the rules to allow DNS (and ICMP/ping) inbound +gcloud compute firewall-rules create sslip-io-allow-dns \ + --allow udp:53,icmp \ + --network=default \ + --source-ranges 0.0.0.0/0 \ + # ssh onto the VM +gcloud compute ssh sslip -- -A + # install docker +sudo apt update && sudo apt upgrade -y && sudo apt install -y docker.io jq + # add us to the docker group +sudo addgroup $USER docker +newgrp docker + # Create the necessary directories +mkdir -p tls/ + # disable systemd-resolved to fix "Error starting userland proxy: listen tcp 0.0.0.0:53: bind: address already in use." + # thanks https://askubuntu.com/questions/907246/how-to-disable-systemd-resolved-in-ubuntu +sudo systemctl disable systemd-resolved +sudo systemctl stop systemd-resolved +echo nameserver 8.8.8.8 | sudo tee /etc/resolv.conf + # Let's start it up: +docker run -it --rm --name wildcard \ + -p 53:53/udp \ + -p 80:80 \ + cunnie/wildcard-dns-http-server & +dig +short TXT does.not.matter.example.com @localhost + # You should see `"Set this TXT record ..."` +export ACMEDNS_UPDATE_URL="http://localhost/update" +docker run --rm -it \ + -v $PWD/tls:/acme.sh \ + -e ACMEDNS_UPDATE_URL \ + --net=host \ + neilpang/acme.sh \ + --issue \ + --debug \ + -d $FQDN \ + -d *.$FQDN \ + --dns dns_acmedns +ls tls/$FQDN # you'll see the new cert, key, certificate +openssl x509 -in tls/$FQDN/$FQDN.cer -noout -text # read the cert info +``` + +Save the cert, key, certificate, intermediate ca, fullchain cert. They are in +`tls/$FQDN/`. + +Clean-up: + +``` +gcloud compute firewall-rules delete sslip-io-allow-dns +gcloud compute instances delete sslip +``` + +#### Troubleshooting / Debugging + +Run the server in one window so you can see the output, and then ssh into +another window and watch the log output in realtime. + +``` +gcloud compute ssh sslip -- -A +docker run -it --rm --name wildcard \ + -p 53:53/udp \ + -p 80:80 \ + cunnie/wildcard-dns-http-server +``` + +Notes about the logging output: any line that has the string "`TypeTXT →`" is +output from the DNS server; everything else is output from the HTTP server which +is used to create TXT records which the DNS server serves. + +Use `acme.sh`'s `--staging` flag to make sure it works (so you don't run into +Let's Encrypt's [rate limits](https://letsencrypt.org/docs/rate-limits/) with +failed attempts). + +``` +docker run --rm -it \ + -v $PWD/tls:/acme.sh \ + -e ACMEDNS_UPDATE_URL \ + --net=host \ + neilpang/acme.sh \ + --issue \ + --staging \ + --debug \ + -d *.$FQDN \ + --dns dns_acmedns +``` diff --git a/document_root/about.html b/document_root/about.html deleted file mode 100644 index f41149068..000000000 --- a/document_root/about.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - About sslip.io - - - - - - - - - - - - - - - -
-
-

About sslip.io

-

Tyler Schultz, - Alvaro Perez-Shirley, - and Brian Cunnie created sslip.io on Tuesday August 11, 2015 during a - Pivotal Software-sponsored Hack Day. Thanks Pivotal!

-

Sam Stephenson built xip.io, upon which - much of our code is based. He also suggested the name - sslip.io.

-

Justin Smith advised us on the security implications of releasing - an SSL certificate and key to the general public.

-
-

-
-

© 2015 Brian Cunnie, Pivotal Software

-
-
- - - - - - - - - - - - - - diff --git a/document_root/faq.html b/document_root/faq.html deleted file mode 100644 index 9c3b1890c..000000000 --- a/document_root/faq.html +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - - - - sslip.io FAQ - - - - - - - - - - - - - - - -
-
-

FAQ

-

Do I have to pay to use this service?

-

No, it's free.

-

Can I use this certificate on my commerce website?

-

Although there's no technical reason why you couldn't use - the sslip.io SSL key and certificate for your commerce - web, we strongly recommend against it: the key - is publicly available; your traffic isn't secure. sslip.io's - primary purpose is to assist developers who need to test - against valid SSL certs, not to safeguard content.

-

My webserver wants a certificate and an "intermediate certificate - chain"—where do I get that?

-

Certain web servers (e.g. Tenable's Nessus scanner) prefer to split the chained certificate file - (which has three concatenated certificates) into two - files: one file containing a single certificate for the - server itself (e.g. the "*.sslip.io" certificate), and - a second file containing the intermediate certificate - authorities (e.g. the two COMODO certificate authorities).

-

You can split the chained certificate file by hand, or - you can download them, pre-split, from GitHub:

-
    -
  • the server certificate ("*.sslip.io")
  • -
  • the intermediate certificate chain (the COMODO CAs)
  • -
-

Why can't I use dots in my hostname? xip.io lets me use - dots.

-

You can't have dots, but you can have dashes: for example, - "www-sf-ca-us-10-9-9-142.sslip.io" will work with sslip.io's - wildcard SSL certificate, but "www.sf.ca.us.10.9.9.142.sslip.io" - will not. This is a technical limitation of wildcard - certs and the manner in which browsers treat them (read - more here).

-

This restricts sslip.io's usage model. For example, it - won't work properly with Cloud Foundry's app domain or - system domain.

-

Does sslip.io work with name-based virtual hosting? We - have multiple projects but only one webserver.

- -

sslip.io interoperates quite well with name-based virtual hosting. - You can prepend identifying information to the sslip.io - hostname without jeopardizing the address resolution, and then use - those hostnames to distinguish the content being served. - For example, let's assume that your webserver's IP address - is 10.9.9.30, and that you have three projects you're - working on (Apple, Google, and Facebook). You would use - the following three sslip.io hostnames:

- -
    -
  • apple-10-9-9-30.sslip.io
  • -
  • facebook-10-9-9-30.sslip.io
  • -
  • google-10-9-9-30.sslip.io
  • -
-

Can you make the hostnames easier to remember? It's as - hard as memorizing IP addresses.

-

Unfortunately, no. We appreciate that "52-0-56-137.sslip.io" - is not an easy-to-remember hostname, whereas something - along the lines of "aws-server.sslip.io" would be much - simpler, but we don't see an easy solution—we need - to be able to extract the IP address from the hostname - in order for our DNS nameserver to reply with the proper - address when queried.

-

Do you have support for IPv6-style addresses?

-

Not yet, but if there's enough demand for it we might try - implementing it.

-

Why did you choose a 4096-bit key instead of a 2048-bit - key? -

-

We couldn't help ourselves—when it comes to keys, - longer is better. In retrospect there were flaws in our - thinking: certain hardware devices, e.g. YubiKeys, only - support keys of length 2048 bits or less. Also, there - was no technical value in making a long key—it's - publicly available on GitHub, so a zero-bit key would - have been equally secure.

-

Do I have to use the sslip.io domain? I'd rather have a - valid cert for my domain.

-

If you want valid SSL certificate, and you don't want to - use the sslip.io domain, then you'll need to purchase - a certificate for your domain. We purchased ours from - Cheap SSL Shop, - but use a vendor with whom you're comfortable.

-

What is the sslip.io certificate chain?

-

The sslip.io certificate chain is the series of certificates, - each signing the next, with a root certificate at the - top. It looks like the following:

-
-
-
-

-

Note that the "root" certificate is "AddTrust's External - CA Root", which issued a certificate to the "COMODO RSA - Certification Authority", which in turn issued a certificate - to the "COMODO RSA Domain Validation Secure Server CA" - which in turn issued our certificate, "*.sslip.io". -

-

How is "sslip.io" pronounced?

-

ESS-ESS-ELL-EYE-PEE-DOT-EYE-OH

-

Where do I report bugs? I think I found one.

-

Open an issue on GitHub; - we're tracking our issues there.

-

There's a typo/mistake on the sslip.io website.

-

Thanks! We love pull requests.

-
-

-
-

© 2015 Brian Cunnie, Pivotal Software

-
-
- - - - - - - - - - - - - - diff --git a/document_root/index.html b/document_root/index.html deleted file mode 100644 index f92045703..000000000 --- a/document_root/index.html +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - - - - Welcome to sslip.io - - - - - - - - - - -
-
-

sslip.io

-

Operational Status: ci.nono.io [Status]

-

sslip.io is a DNS (Domain Name System) - service that, when queried with a hostname with an embedded IP address, returns that IP Address. It was inspired - by and uses much of the code of xip.io, which was created by Sam Stephenson.

-

Here are some examples:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hostname / URLIP AddressNotes
192.168.0.1.sslip.io192.168.0.1dot separators
https://52-0-56-137.sslip.io52.0.56.137dash separators, sslip.io website mirror (IPv4)
www.192.168.0.1.sslip.io192.168.0.1subdomain
www.192-168-0-1.sslip.io192.168.0.1subdomain + dashes
https://www-78-46-204-247.sslip.io78.46.204.247embedded, sslip.io website mirror (IPv4)
–1.sslip.io::1IPv6 — always use dashes
https://2a01-4f8-c17-b8f--2.sslip.io2a01:4f8:c17:b8f::2sslip.io website mirror (IPv6)
-

Branding

-

sslip.io can be used to brand your own site (you don’t need to use the sslip.io domain). For example, say you - own the domain “example.com”, and you want your subdomain, “xip.example.com” to have xip.io-style features. To - accomplish this, set the following four DNS servers as NS records for the subdomain - “xip.example.com”

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
hostnameIP addressLocation
ns-aws.nono.io.52.0.56.137USA
ns-gce.nono.io.104.155.144.4USA
ns-azure.nono.io.52.187.42.158Singapore
ns-he.nono.io.78.46.204.247
2a01:4f8:c17:b8f::2
Germany
-

Let’s test it from the command line using dig:

-
-      dig +short 169-254-169-254.xip.example.com @ns-gce.nono.io.
-

Yields, hopefully: [connection timed out]

-
169.254.169.254
-

TLS (Transport Layer Security)

-

If you have a wildcard certificate for your sslip.io-style subdomain, you may install it on your machines for - TLS-verified connections.

-
-

When using a TLS wildcard certificate in conjunction with your branded sslip.io style subdomain, you must - use dashes not dots as separators. For example, if you have the TLS certificate for - *.xip.example.com, you could browse to https://https://52-0-56-137.xip.example.com/ but not - https://52.0.56.137.xip.example.com/.

-
-

For a real-world example of a TLS wildcard cert and sslip.io domain, browse https://52-0-56-137.sslip.io.

-

Pivotal employees can download the *.sslip.io TLS private key here.

-
-

Footnotes

-

[Status] A status of “build failing” rarely means the system is - failing. It’s more often an indication that when the servers were last checked (currently every six hours), the - CI (continuous integration) server had difficulty - reaching one of the four sslip.io nameservers. That’s normal. [connection timed out]

-

[connection timed out]

-

DNS runs over UDP which has no guaranteed - delivery, and it’s not uncommon for the packets to get lost in transmission. DNS clients are programmed to - seamlessly query a different server when that happens. That’s why DNS, by fiat, requires at least two nameservers - (for redundancy). From IETF (Internet Engineering Task Force) RFC - (Request for Comment) 1034:

-
-

A given zone will be available from several name servers to insure its availability in spite of host or - communication link failure. By administrative fiat, we require every zone to be available on at least two - servers, and many zones have more redundancy than that.

-
-
-
- - - - - - - - - - - - - diff --git a/etc/blocklist-test.txt b/etc/blocklist-test.txt new file mode 100644 index 000000000..56d2a4c4d --- /dev/null +++ b/etc/blocklist-test.txt @@ -0,0 +1,10 @@ +# TESTING ONLY: List of "Forbidden" (blocked) names & CIDRs + +# This is a shortened variant meant to be used for testing (`ginkgo`) because +# the legitimate one has grown so long it clutters the test output + +raiffeisen # https://www.rbinternational.com/en/homepage.html +43-134-66-67 # Netflix, https://nf-43-134-66-67.sslip.io/sg +43.134.66.67/24 # Netflix +2601:646:100:69f7:cafe:bebe:cafe:bebe/112 # personal (Comcast) IPv6 range for testing blocklist + diff --git a/etc/blocklist.txt b/etc/blocklist.txt new file mode 100644 index 000000000..74c54ab31 --- /dev/null +++ b/etc/blocklist.txt @@ -0,0 +1,737 @@ +# List of "Forbidden" (blocked) names & CIDRs + +# This is a list of forbidden names and CIDRs that are often used in phishing +# attacks. We won't resolve these hostnames to their embedded IP address (e.g. +# we won't resolve "raiffeisen.94.228.116.140.sslip.io" to 94.228.116.140); +# instead, we'll resolve it to one of our server's, blocked.sslip.io's, IP +# addresses, 52.0.56.137 or 2600:1f18:aaf:6900::a. Similarly, we won't resolve +# nf-43-134-66-67.sslip.io to 43.134.66.67 because it falls within one of our +# blocked CIDRs (43.134.66.67/24). + +# Forbidden names are resolved as expected for private networks (e.g. +# "raiffeisen.192.168.0.1.sslip.io" resolves to 192.168.0.1) because they +# aren't publicly accessible & thus can't be used for phishing attempts. + +# File format: blank lines are ignored, "#" are comments and are ignored. One +# name or CIDR per line. + +raiffeisen # https://www.rbinternational.com/en/homepage.html +43-134-66-67 # Netflix, https://nf-43-134-66-67.sslip.io/sg +43.134.66.67/24 # Netflix +2601:646:100:69f7:cafe:bebe:cafe:bebe/112 # personal (Comcast) IPv6 range for testing blocklist +139.198.158.74/32 # @yongzhi-weee: not obtain/acquiring a ICP license +20.55.32.72 # hxxps://bofa-tablas-v2.20-55-32-72[.]sslip[.]io/#/user +91.107.178.82/32 # Linkedin Phishing +45.82.251.70/32 # ?? +68.183.106.84/32 # EasyWeb Login +188.64.13.153/32 # https://188.64.13.153.sslip.io/business Facebook phishing +185.229.65.160/32 # ¿"benarponpombahk"? +185.248.144.24/32 # CommBank +5.206.224.115/32 # ABNAMRO.nl +37.28.157.12/32 # Belfius Bank, also Argenta.be +5.182.36.181/32 # Protonmail +34.125.139.171/32 # Linkedin +194.99.79.28/32 # LinkedIn +107.189.5.14/32 # amazon.co.jp +79.137.202.34/32 # "The Global Fund" +45.156.24.10/32 # amazon.co.jp +91.107.154.247/32 # United Nations Population Fund UNFPA +5.161.56.94/32 # nypost.com +5.250.187.205/32 # brave.com +194.116.214.162/32 # rt.com +49.12.41.51/32 # slate.com +104.234.196.206/32 # blizzard.com +45.87.41.92/32 # pogo.com +128.140.56.172/32 # n4g.com +128.140.77.135/32 # n4g.com +91.107.145.237/32 # nbcnews.com +209.141.40.61/32 # pytorch.org +89.110.65.182/32 # apple.com +172.86.70.223/32 # sfsi.org +79.132.135.157/32 # concern.net +38.180.104.125/32 # actionaid.org +167.235.149.231/32 # templatemo.com +66.245.197.41/32 # emailaddresses.com +164.90.229.173/32 # chirurgie-delitzsch.de +46.31.79.79/32 # onedio.com +49.12.204.135/32 # dailysabah.com +142.171.230.250/32 # lanacion.com.ar +65.109.200.95/32 # 2degrees.nz +78.47.48.110/32 # freeconvert.com +176.58.111.183/32 # this-person-does-not-exist.com +92.119.124.99/32 # arabnews.com +65.109.15.45/32 # speedtest.net +37.27.37.68/32 # winespectator.com +192.9.157.126/32 # obsproject.com +216.24.189.4/32 # undp.org +128.140.97.90/32 # kff.org +159.69.35.159/32 # defcad.com +139.59.88.201/32 # lbry.com +77.221.136.77/32 # firstpost.com +145.239.54.184/32 # witness.org +194.62.251.128/32 # reliefweb.int +95.164.16.5/32 # sbs.com.au +192.248.159.51/32 # ynet.co.il +45.61.134.40/32 # download.cnet.com +103.75.199.87/32 # arachemishop.com +5.75.205.208/32 # my-diary.org +91.107.181.88/32 # bat.com +88.99.108.110/32 # download.cnet.com +84.32.190.7/32 # slate.com +194.116.190.25/32 # wechat.com +154.16.16.103/32 # doctorswithoutborders.org +185.198.57.133/32 # pogo.com +5.75.233.237/32 # fhi360.org +91.107.224.123/32 # foreignpolicy.com +107.174.63.163/32 # visitdubai.com +172.232.159.186/32 # epa.gov +172.234.99.74/32 # ente.io +85.239.52.124/32 # sayt.ws +88.218.17.193/32 # architectureau.com +212.64.223.251/32 # heritage.org +206.189.51.165/32 # obsproject.com +23.94.57.87/32 # nypost.com +45.133.118.77/32 # unfpa.org +88.99.21.170/32 # csmonitor.com +5.42.106.54/32 # csmonitor.com +95.216.109.218/32 # beintheknow.org +77.91.78.26/32 # xenu.net +37.58.18.232/32 # download.cnet.com +172.86.77.127/32 # laco.org +192.99.210.176/32 # undp.org +146.59.192.162/32 # privacytools.io +207.246.83.11/32 # foreignpolicy.com +46.249.100.129/32 # theintercept.com +178.62.196.150/32 # aljazeera.net +37.27.41.197/32 # speedtest.net +144.126.145.43/32 # parsine.com +5.161.216.141/32 # foreignpolicy.com +65.108.58.0/32 # arvehaldus.ee +5.42.102.77/32 # e4gl.com +82.115.18.148/32 # flooringinstaller.org +164.92.136.56/32 # tommymyllymaki.se +128.140.13.221/32 # buyrealfollows.com +91.210.169.16/32 # overwiki.ru +54.36.119.4/32 # nypost.com +178.79.142.33/32 # aljazeera.net +78.153.139.39/32 # easycomposites.co.uk +80.87.206.55/32 # dalfak.com +116.202.8.64/32 # geschichtsdramaturgie.de +57.180.206.7/32 # infini-cloud.net +165.22.18.3/32 # musicema.com +164.90.187.137/32 # ekoyapidergisi.org +13.209.188.54/32 # mediamba.ssu.ac.kr +172.232.148.63/32 # aljazeera.net +174.138.27.137/32 # denniswu.com +45.9.250.228/32 # arabnews.com +188.132.209.133/32 # ebaumsworld.com +49.13.84.154/32 # slate.com +104.234.46.217/32 # ted.com +192.9.152.157/32 # slate.com +165.227.145.52/32 # credibilitycoalition.org +158.160.32.235/32 # atomyze.ru +144.202.10.114/32 # deerhunterforum.com +45.59.118.58/32 # news.gooya.com +45.159.248.190/32 # sliding.toys +65.21.179.255/32 # wsj.com +212.64.193.246/32 # ebaumsworld.com +5.78.110.219/32 # thestar.com +5.75.201.168/32 # witness.org +45.252.183.185/32 # nbcnews.com +37.221.125.82/32 # thestar.com +185.31.200.36/32 # worldwildlife.org +57.128.107.209/32 # wickr.com +128.140.39.65/32 # dia.mil +152.42.142.21/32 # tacticaltech.org +91.107.178.59/32 # isscr.org +91.107.138.86/32 # ddo.com +141.11.246.231/32 # un.org +176.120.64.60/32 # engenderhealth.org +51.89.156.250/32 # fosfeminista.org +147.45.43.91/32 # oca.org +142.93.46.4/32 # thestar.com +37.27.16.237/32 # reliefweb.int +167.235.157.229/32 # aljazeera.com +212.64.214.246/32 # tails.net +109.120.176.184/32 # arabnews.com +45.11.182.161/32 # thestar.com +147.45.78.152/32 # tails.net +209.38.194.217/32 # elpha.com +162.55.21.181/32 # khamenei.ir +92.246.137.149/32 # foreignpolicy.com +49.13.150.236/32 # internetsociety.org +178.32.224.219/32 # superuser.com +49.13.145.55/32 # glsen.org +188.116.25.165/32 # atlasofsurveillance.org +168.119.185.233/32 # wsj.com +156.255.1.134/32 # download.cnet.com +176.123.1.51/32 # bendodson.com +185.17.136.39/32 # ebaumsworld.com +78.135.80.144/32 # ebaumsworld.com +139.144.97.232/32 # dtdcaustralia.com.au +80.242.56.112/32 # dailywire.com +213.238.167.51/32 # ebaumsworld.com +91.107.156.115/32 # ebaumsworld.com +91.107.146.89/32 # mastodon.sdf.org +212.64.214.54/32 # ebaumsworld.com +212.64.214.22/32 # ebaumsworld.com +212.64.214.13/32 # ebaumsworld.com +116.202.15.101/32 # nextpit.com +147.189.131.181/32 # coolmathgames.com +46.101.72.21/32 # coolmathgames.com +172.93.185.171/32 # ectaco.com +134.209.185.94/32 # thestar.com +77.238.248.166/32 # gamespot.com +108.61.198.77/32 # download.cnet.com +167.71.134.82/32 # thestar.com +212.64.214.77/32 # ebaumsworld.com +213.238.167.197/32 # ebaumsworld.com +108.61.171.127/32 # ple.com.au +212.64.223.53/32 # ebaumsworld.com +13.80.65.209/32 # last.fm +192.119.64.121/32 # moeclipse.org +80.251.219.99/32 # kickasstorrents.to +128.140.52.186/32 # apple.com +94.103.92.125/32 # arriyadiyah.com +109.172.80.235/32 # slate.com +103.75.196.210/32 # soft98.ir +91.107.163.211/32 # pogo.com +85.159.230.171/32 # actionaid.org +45.92.111.164/32 # holakoueearchive.co +5.161.108.76/32 # shopmeramedical.com +193.163.200.15/32 # nbcnews.com +45.144.234.170/32 # rfa.org +34.80.136.10/32 # arabnews.com +43.225.108.140/32 # aimer-web.jp +212.83.46.41/32 # isscr.org +167.235.29.143/32 # blog.denic.de +62.113.255.125/32 # pogo.com +213.238.167.52/32 # ebaumsworld.com +104.167.215.178/32 # now.gg +138.68.180.24/32 # thestar.com +212.64.223.72/32 # ebaumsworld.com +212.64.193.61/32 # ebaumsworld.com +159.89.104.113/32 # ilo.org +107.174.172.254/32 # foreignpolicy.com +46.4.148.166/32 # unfpa.org +138.68.138.112/32 # thestar.com +135.148.113.128/32 # support.mozilla.org +146.19.75.114/32 # ashfootwear.co.uk +141.11.96.34/32 # nytimes.com +206.189.243.3/32 # gameignition.io +135.181.109.197/32 # projectveritas.com +142.171.81.119/32 # aljazeera.net +79.137.199.3/32 # iotdesignpro.com +37.27.46.150/32 # foreignpolicy.com +78.47.196.26/32 # dailywire.com +70.34.196.214/32 # foreignpolicy.com +45.61.154.115/32 # foreignpolicy.com +45.8.147.169/32 # csmonitor.com +195.16.74.5/32 # ebaumsworld.com +104.248.130.5/32 # dabrni.com +194.120.24.173/32 # my.webramz.com +5.42.82.86/32 # foreignpolicy.com +178.128.194.220/32 # uisp.com +78.135.80.217/32 # ebaumsworld.com +65.109.198.30/32 # nbcnewyork.com +212.64.214.161/32 # ebaumsworld.com +212.64.193.63/32 # ebaumsworld.com +87.247.170.234/32 # shenoto.com +176.105.255.204/32 # businesstoday.in +5.78.58.233/32 # csmonitor.com +109.61.95.86/32 # wickr.com +49.13.14.86/32 # csmonitor.com +212.64.214.87/32 # ebaumsworld.com +139.59.6.237/32 # gadgetsnow.com +91.107.154.162/32 # my-diary.org +213.238.167.10/32 # ebaumsworld.com +104.248.41.59/32 # app.teslacrypto.me +92.50.249.198/32 # mechel.ru +139.28.232.233/32 # khabarcanada.ca +37.27.10.56/32 # techniker-forum.de +77.221.136.78/32 # worldhealth.net +152.42.188.133/32 # nu.or.id +159.69.122.252/32 # junior-report.media +146.190.88.137/32 # rama168.co +78.135.104.164/32 # cesr.org +216.9.227.101/32 # saatkac.info.tr +174.138.15.168/32 # thecatsite.com +85.159.229.32/32 # pikabu.ru +167.114.90.232/32 # okala.com +109.120.188.67/32 # dppo.pro +46.17.102.186/32 # cadenza.ir +94.130.76.212/32 # pcgamesn.com +103.231.75.79/32 # pcgamesn.com +95.164.22.254/32 # getdrupe.com +128.140.36.89/32 # elbapit.it +164.90.211.63/32 # drudgereport.com +172.188.114.154/32 # jahannews.com +94.131.123.161/32 # drudgereport.com +212.64.214.90/32 # ebaumsworld.com +23.94.174.148/32 # nbcnews.com +91.107.152.217/32 # crypt.ee +91.107.139.71/32 # triller.co +92.246.136.54/32 # slate.com +212.64.223.40/32 # download.cnet.com +167.235.128.251/32 # spotmyenergy.com +13.235.0.237/32 # eduloanzm.com +51.178.236.99/32 # speedify.com +89.107.62.251/32 # limetorrents.lol +194.124.43.116/32 # arabnews.com +192.9.153.69/32 # store.steampowered.com +81.161.238.97/32 # arriyadiyah.com +134.209.228.38/32 # afilmywap.org.tw +168.119.121.187/32 # proton.me +139.84.167.74/32 # delta.chat +159.65.122.63/32 # upgrade.rs +45.12.108.203/32 # standardnotes.com +217.69.13.138/32 # download.cnet.com +148.113.3.154/32 # tacticaltech.org +95.179.217.91/32 # download.cnet.com +141.98.119.198/32 # gnupg.org +113.30.191.120/32 # arxiv.org +195.201.234.54/32 # nextpit.com +161.35.74.213/32 # cs-app.xyz +116.203.192.213/32 # inspavo.com +157.90.245.228/32 # sputnikglobe.com +170.64.140.55/32 # waihekecarrental.co.nz +172.96.191.241/32 # betflik5.info +164.92.168.210/32 # fysm.co +142.93.161.236/32 # landmarkproductions.live +94.154.33.158/32 # download.cnet.com +164.90.238.192/32 # schnauzerminiaturadelagonal.com +5.78.117.154/32 # nypost.com +164.92.225.172/32 # mp4moviez.legal +146.19.247.75/32 # slate.com +103.75.199.234/32 # freepik.com +170.64.170.123/32 # daronmont.com.au +94.131.123.13/32 # sbs.com.au +65.109.186.107/32 # coolmathgames.com +188.132.188.114/32 # ebaumsworld.com +212.64.223.51/32 # ebaumsworld.com +77.90.8.81/32 # element.io +5.161.149.183/32 # download.cnet.com +124.156.6.174/32 # lalgbtcenter.org +135.181.157.72/32 # waqfeya.net +83.217.9.85/32 # apt.ch +134.209.190.61/32 # thestar.com +146.190.18.201/32 # nytimes.com +65.109.221.11/32 # defcad.com +134.122.75.15/32 # bathelalnada.com +128.140.10.175/32 # alea-italia.it +89.185.85.193/32 # tomato.gg +165.227.225.17/32 # thestar.com +49.12.103.72/32 # reliefweb.int +164.92.139.32/32 # palmy-investing.com +46.4.162.14/32 # foreignpolicy.com +141.11.107.89/32 # linphone.org +77.238.238.216/32 # sci-hub.se +194.62.251.127/32 # goodreads.com +157.90.149.110/32 # beyondexgay.com +80.66.87.109/32 # NBC news +176.222.55.49/32 # slate.com +142.171.230.151/32 # whatsoproudlywehail.org +104.248.92.238/32 # esebari.it +5.180.30.211/32 # jadidonline.com +91.107.245.247/32 # ebaumsworld.com +154.12.61.243/32 # tvb.com +89.221.224.170/32 # foreignpolicy.com +91.107.172.136/32 # arabnews.com +46.17.101.214/32 # nytimes.com +92.118.114.109/32 # slate.com +13.49.239.35/32 # citylife.az +35.80.21.115/32 # crazygames.com +157.90.28.252/32 # fruitcraft.co +144.217.18.111/32 # wickr.com +79.137.202.87/32 # circumcision.org +65.20.103.236/32 # aljazeera.net +167.172.150.42/32 # mercari (Japanese) +35.85.65.126/32 # foreignpolicy.com +95.179.163.90/32 # foreignpolicy.com +91.107.255.196/32 # mastodon.xyz +81.19.137.196/32 # trezor +103.161.184.149/32 # UNFPA +38.180.62.143/32 # winespectator.com +167.235.65.210/32 # merriam-webster.com +109.120.156.173/32 # arabnews.com +164.90.223.13/32 # gadgetsnow.com +194.28.225.187/32 # ymca.int +49.13.159.110/32 # unicef.org +154.90.54.178/32 # khanacademy.org +45.252.183.181/32 # out.com +85.192.56.134/32 # kennedy-center.org +116.202.111.124/32 # mega.io +44.192.53.252/32 # imagene-ai.com +49.13.144.216/32 # oldschoololdfriends.com +45.145.4.81/32 # slate.com +139.84.171.117/32 # lgbtqnation.com +116.203.196.37/32 # speeddater.co.uk +49.13.214.82/32 # radiopaedia.org +78.135.80.210/32 # ebaumsworld.com +157.245.96.129/32 # atomapi.in +65.109.189.250/32 # afp.com +212.64.214.103/32 # ebaumsworld.com +91.107.137.210/32 # csmonitor.com +45.158.15.184/32 # ebaumsworld.com +139.59.176.152/32 # thestar.com +135.181.28.176/32 # marinetraffic.pro +23.177.136.104/32 # UNFPA +65.109.178.204/32 # UNFPA +78.135.104.19/32 # UNFPA +172.86.74.137/32 # UNFPA +188.166.174.56/32 # crypt.ee +65.109.238.97/32 # heyatonline.ir +152.70.94.211/32 # muckrock.com +185.219.84.223/32 # cpimlm.org +107.175.222.54/32 # download.ir +188.245.54.212/32 # bonbast.com +193.23.55.15/32 # plus.im +194.58.111.143/32 # prodoctorov.ru +5.75.200.8/32 # untappd.com +167.234.213.68/32 # brew.sh +38.60.208.121/32 # arabnews.com +151.80.106.167/32 # rg.ru +132.145.49.90/32 # tradingview.com +54.214.15.21/32 # untappd.com +188.132.188.25/32 # saatkac.info.tr +91.107.172.10/32 # unfpa.org +38.54.68.74/32 # hootsuite.com +92.119.59.97/32 # arthive.com +176.124.220.184/32 # arabnews.com +91.107.170.178/32 # timcast.com +129.151.147.169/32 # terredeshommes.org +107.174.102.113/32 # getsharex.com +77.83.203.86/32 # southcom.mil +93.127.223.129/32 # reliefweb.int +38.60.255.239/32 # actionaid.org +141.94.2.52/32 # defcad.com +94.130.78.56/32 # arabic.rt.com +89.251.9.57/32 # projectveritas.com +91.107.129.135/32 # onlinesim.io +91.107.182.82/32 # space.com +45.195.250.148/32 # cdnjs.com +93.188.161.162/32 # nypost.com +188.166.39.77/32 # aljazeera.net +95.216.186.128/32 # artforum.com +46.101.131.66/32 # gadgetsnow.com +206.189.140.121/32 # gadgetsnow.com +104.234.196.242/32 # unfpa.org +107.172.32.185/32 # arabnews.com +104.156.225.37/32 # forums.prsguitars.com +66.151.33.226/32 # sputniknews.in +49.13.84.255/32 # linguee.de +46.183.27.211/32 # akharinnews.com +91.241.49.48/32 # wordreference.com +107.175.183.191/32 # four-paws.org +91.107.175.212/32 # muckrock.com +45.76.95.134/32 # donya-e-eqtesad.com +107.172.167.252/32 # doctorswithoutborders.org +65.109.204.11/32 # worldhealth.net +52.220.134.5/32 # arabnews.com +159.69.150.196/32 # undp.org +91.107.253.226/32 # ebaumsworld.com +91.107.146.169/32 # ebaumsworld.com +136.244.84.246/32 # ilo.org +89.251.22.110/32 # Mercantil Servicios Financieros +107.189.16.42/32 # muckrock.com +178.62.79.170/32 # pcgamesn.com +80.253.255.2/32 # ebaumsworld.com +103.75.196.193/32 # ymca.int +64.176.189.63/32 # climaterealityproject.org +95.217.99.94/32 # gamespot.com +172.232.142.143/32 # f-droid.org +172.86.77.42/32 # f-droid.org +212.64.223.249/32 # ebaumsworld.com +45.159.249.169/32 # sheypoor.com +164.92.172.100/32 # slate.com +194.116.214.238/32 # videolan.org +5.78.51.159/32 # arriyadiyah.com +91.107.144.251/32 # ebaumsworld.com +142.93.238.38/32 # almanar.com.lb +54.152.242.38/32 # usatoday.com +65.109.218.141/32 # ebaumsworld.com +143.42.19.49/32 # gutschein724.de +188.132.209.230/32 # ebaumsworld.com +212.132.124.61/32 # key2gaming.com +109.120.157.43/32 # doctorswithoutborders.org +206.206.104.221/32 # doctorswithoutborders.org +38.54.8.58/32 # jami.net +91.192.81.2/32 # nypost.com +5.75.202.84/32 # ebaumsworld.com +91.107.128.116/32 # ilo.org +137.184.226.70/32 # blogease.co +45.158.15.124/32 # ebaumsworld.com +91.107.248.66/32 # ebaumsworld.com +80.253.254.10/32 # ebaumsworld.com +212.64.214.169/32 # ebaumsworld.com +194.146.123.175/32 # kickasstorrents.to +195.20.227.112/32 # digg.com +5.255.117.216/32 # soon.works +91.108.241.48/32 # digg.com +64.227.45.81/32 # thestar.com +185.17.136.135/32 # ebaumsworld.com +91.107.145.135/32 # ebaumsworld.com +139.59.20.162/32 # artgram.co +91.107.245.20/32 # ebaumsworld.com +38.54.30.247/32 # actionaid.org +68.183.46.0/32 # tacticaltech.org +193.24.209.143/32 # globalpolicy.org +209.38.38.51/32 # cesr.org +109.120.187.71/32 # now.org +65.109.186.19/32 # my-diary.org +37.27.80.4/32 # actionaid.org +104.168.68.225/32 # beparsi.com +107.174.92.107/32 # privacytools.io +212.64.214.31/32 # technorati.com +3.67.106.44/32 # nabaliaenergia.com +168.119.94.3/32 # pcgamesn.com +91.107.247.95/32 # ebaumsworld.com +# 23.137.104.229/32 # reviewscaribbean.com # See https://github.com/cunnie/sslip.io/issues/78 +91.107.156.231/32 # ebaumsworld.com +37.27.12.156/32 # uptvs.com +108.165.128.113/32 # concern.net +104.234.11.35/32 # camspark.com +207.154.229.160/32 # haqi.org +38.180.154.91/32 # earthwatch.org +38.180.15.105/32 # stonewall.org.uk +5.75.207.83/32 # ebaumsworld.com +91.107.240.195/32 # cesr.org +92.51.45.76/32 # rescue.org +49.12.241.120/32 # roam.news +91.107.139.245/32 # catholic.org +82.115.17.9/32 # ted.com +49.12.208.158/32 # dotabuff.com +103.35.191.94/32 # ctx.io +134.209.31.184/32 # gallerina-egypt.com +45.138.183.44/32 # kashoob.com +45.59.170.69/32 # isscr.org +43.229.150.11/32 # bicommunitynews.co.uk +167.235.158.56/32 # ai-techfusion.com +3.135.10.112/32 # popularenlinea.com +49.13.156.202/32 # vandermast.be +5.45.93.79/32 # indiatimes.com +216.9.227.8/32 # frontlineaids.org +77.105.147.35/32 # indiatimes.com +185.87.48.156/32 # penza-elegant.ru +91.207.183.233/32 # slate.com +78.135.80.44/32 # ebaumsworld.com +172.86.65.133/32 # lbry.com +91.107.245.231/32 # ebaumsworld.com +138.197.183.135/32 # actionaid.org +65.109.221.214/32 # gamespot.com +23.162.152.72/32 # fruitcraft.co +45.195.69.178/32 # sliding.toys +35.88.54.82/32 # crazygames.com +65.109.183.180/32 # superuser.com +195.219.251.74/32 # utoronto.ca +5.42.106.119/32 # arabnews.com +77.221.157.3/32 # indiatimes.com +45.158.15.135/32 # ebaumsworld.com +178.128.253.192/32 # aljazeera.net +176.222.52.130/32 # websplosion.com +104.248.156.203/32 # boyernews.com +49.13.196.224/32 # fliesenverlegung-hoertnagl.at +91.107.149.19/32 # ebaumsworld.com +137.184.224.51/32 # ford-market.com.ua +15.204.89.177/32 # boursemrooz.com +165.227.148.243/32 # foodizone.net +172.232.146.24/32 # unfpa.org +185.244.218.204/32 # csmonitor.com +23.254.161.20/32 # 2shared.com +65.109.198.80/32 # ebaumsworld.com +49.12.227.226/32 # f-secure.com +188.132.192.168/32 # usatoday.com +5.161.44.48/32 # nypost.com +128.140.92.102/32 # chinasentry.com +45.8.21.98/32 # doostihaa.com +45.67.86.25/32 # stonewall.org.uk +172.233.47.114/32 # doctorswithoutborders.org +139.59.34.114/32 # myawady.net.mm +212.24.110.231/32 # seclists.org +3.237.252.127/32 # cesr.org +80.240.21.194/32 # bigdev.ir +135.181.30.103/32 # meduza.io +78.141.246.36/32 # wccftech.com +65.109.176.33/32 # eventbrite.com +154.205.130.176/32 # climaterealityproject.org +64.226.82.36/32 # liveayurprana.com +91.107.148.133/32 # ebaumsworld.com +134.122.66.162/32 # flymizar.com +104.194.128.126/32 # durva.ir +104.168.54.193/32 # certbot.eff.org +38.60.255.84/32 # sputnikglobe.com +95.217.232.185/32 # mama.tv +159.69.53.79/32 # zapalean.com +165.227.133.84/32 # king.com +81.19.137.126/32 # rcsb.org +5.75.193.98/32 # muscleandfitness.com +45.63.117.88/32 # u4.no +38.54.84.228/32 # climaterealityproject.org +146.19.75.3/32 # slate.com +140.82.39.13/32 # ekonomiveyatirim.com +91.107.242.111/32 # keystone.guru +185.106.94.156/32 # indiatimes.com +194.26.232.15/32 # slate.com +85.159.226.73/32 # substack.com +89.208.106.240/32 # steam-currency.ru +172.105.107.43/32 # bestchildrensdentistsudbury.ca +91.107.247.129/32 # ebaumsworld.com +188.245.98.56/32 # news12.com +65.21.48.238/32 # itexcgroup.com +188.245.104.226/32 # afp.com +172.86.105.216/32 # thecafemeow.com +128.140.8.187/32 # phone.com +104.168.88.39/32 # bing.com +95.179.170.223/32 # credibilitycoalition.org +165.22.26.12/32 # ilo.org +194.15.152.82/32 # csmonitor.com +109.107.165.182/32 # uncaccoalition.org +95.216.147.91/32 # roozno.com +124.156.207.253/32 # terredeshommes.nl +5.75.200.244/32 # merriam-webster.com +79.132.139.29/32 # harvard.edu +45.8.147.233/32 # mojogem.com +185.230.143.180/32 # jhu.edu +152.42.226.149/32 # witness.org +141.144.204.180/32 # opendns.com +103.75.199.39/32 # lbry.org +45.138.74.44/32 # secfirst.org +136.244.84.68/32 # pirateparty.org.au +91.107.249.25/32 # ebaumsworld.com +199.247.14.108/32 # brothersroad.org +193.168.141.15/32 # minorityrights.org +132.145.89.187/32 # oneworld.net +135.181.89.225/32 # foodpix.ai +167.235.18.77/32 # smtb.io +91.107.152.37/32 # ebaumsworld.com +154.64.251.131/32 # panties.com +3.6.171.182/32 # hasura.io +128.140.85.249/32 # zibastyler.com +5.75.194.181/32 # childrensdefense.org +86.38.156.169/32 # sxyprn.com +5.255.117.196/32 # eln-voces.com +159.69.202.38/32 # kastanienhof-lemgo.de +94.131.123.230/32 # columbia.edu +46.29.239.47/32 # episcopalrelief.org +70.34.216.244/32 # reliefweb.int +65.109.232.185/32 # sierraclub.org +85.239.63.67/32 # tumblr.com +77.238.228.48/32 # UNFPA +128.140.89.200/32 # junior-report.media +194.61.120.156/32 # hellopoetry.com +89.22.224.202/32 # pcgamer.com +65.109.236.170/32 # pcgamesn.com +193.123.94.90/32 # sputnikglobe.com +5.42.78.43/32 # farayad.co +185.226.92.129/32 # metmuseum.org +77.223.98.77/32 # fhi360.org +172.86.106.182/32 # nypost.com +185.113.223.164/32 # mstdn.jp +141.98.210.133/32 # mastodon.xyz +95.179.140.173/32 # varzesh3.com +94.154.32.229/32 # jahannews.com +108.61.209.64/32 # knowyourmeme.com +77.92.151.38/32 # time.is +65.109.193.200/32 # tacticaltech.org +138.68.77.97/32 # laborrights.org +141.227.128.238/32 # UNFPA +154.205.148.163/32 # UNFPA +18.219.211.22/32 # velhaestancia.com.br +178.20.47.52/32 # arriyadiyah.com +209.38.250.14/32 # concern.net +81.29.149.131/32 # slate.com +65.108.247.57/32 # pcgamesn.com +104.245.12.38/32 # mastodon.sdf.org +178.73.210.233/32 # aljazeera.com +91.107.156.17/32 # panda.org +178.236.246.214/32 # madmaxworld.tv +5.75.195.227/32 # wildriftfire.com +108.59.199.190/32 # stthomasaquinassociety.org +188.34.167.93/32 # bazicenter.com +77.92.145.191/32 # atlaq.com +172.104.203.95/32 # unfpa.org +91.107.176.158/32 # photoroom.com +31.129.106.225/32 # 1800respect.org.au +65.109.198.229/32 # artsandculture.google.com +18.184.57.83/32 # shenoto.com +46.8.236.174/32 # fardanews.com +209.38.234.73/32 # fardanews.com +128.140.15.52/32 # account4web.com +5.161.43.35/32 # Abyss +82.115.18.250/32 # citizensclimatelobby.org +199.247.13.156/32 # rog.asus.com +91.107.244.181/32 # panda.org +49.12.0.222/32 # catholicsforchoice.org +198.181.36.122/32 # actionaid.org +38.60.196.184/32 # celebon.ir +109.176.198.98/32 # beparsi.com +194.36.170.226/32 # sputnikglobe.com +207.154.216.14/32 # unfpa.org +176.222.53.110/32 # freepik.com +184.174.96.189/32 # mozilla.org +185.222.241.107/32 # pcgamer.com +65.109.215.176/32 # heritage.org +138.197.128.25/32 # phoenixagritech.com +5.161.138.203/32 # starconfig.com.au +3.8.119.3/32 # optical.toys +104.248.31.5/32 # 47segundos.com +159.223.3.6/32 # oratie.com +82.115.5.6/32 # norml.org +54.229.43.6/32 # csmonitor.com +178.128.38.7/32 # gitlab-staging.riders.ai +217.144.188.8/32 # sputnikglobe.com +135.181.37.8/32 # cesr.org +45.14.247.9/32 # secondlife.com +66.245.194.10/32 # laborrights.org +179.61.249.10/32 # csmonitor.com +168.119.160.60/32 # akharinnews.com +104.244.78.172/32 # 9gag.com +91.225.217.17/32 # weather.com +185.198.234.74/32 # u4.no +134.209.181.141/32 # bluehedgerelocators.com +170.64.134.93/32 # clarenicholson.com +95.179.155.148/32 # roozno.com +15.235.207.99/32 # ghost.org +5.75.200.95/32 # ebaumsworld.com +152.53.47.35/32 # theintercept.com +91.107.136.156/32 # aljazeera.net +91.107.254.171/32 # ebaumsworld.com +78.135.80.254/32 # ebaumsworld.com +185.70.185.70/32 # newsvl.ru +149.248.15.223/32 # abc13.com +77.221.151.104/32 # sputnikglobe.com +91.107.252.71/32 # ebaumsworld.com +193.24.210.210/32 # uscg.mil +65.109.184.35/32 # 1800respect.org.au +195.201.223.122/32 # w3schools.com +68.183.120.73/32 # vezafy.com +91.107.245.21/32 # ebaumsworld.com +5.252.21.29/32 # giphy.com +205.172.56.59/32 # transformunow.org +45.59.118.106/32 # hootsuite.com +18.218.70.131/32 # nuevoleon.travel +135.181.197.31/32 # brothersroad.org +45.95.233.96/32 # proton.me +176.124.221.172/32 # arabnews.com +158.178.229.68/32 # usatoday.com +140.238.3.112/32 # usatoday.com +188.245.96.82/32 # jhu.edu +129.80.218.5/32 # actionaid.org +23.88.103.245/32 # laughfactory.com +45.116.14.101/32 # usatoday.com +141.95.103.173/32 # rebellion.global +65.109.208.36/32 # pof.com +188.245.148.2/32 # iranhiv.com +5.75.140.81/32 # kishss2.ir +141.94.173.106/32 # cvut.cz +66.23.193.126/32 # Interstellar +46.250.237.97/32 # UNFPA +185.140.12.132/32 # telegram +77.221.141.201/32 # telegram +89.23.103.58/32 # don't know, but Namecheap wants 'em shut down +93.127.160.4/32 # apple.com +48.216.217.16/32 # coinbase +135.237.45.132/32 # coinbase +181.214.208.102/32 # telegram +167.88.162.29/32 # Bank of America +95.179.245.161/32 # mobile.de diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..ca515edae --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module xip + +go 1.23.0 + +toolchain go1.24.2 + +require ( + github.com/onsi/ginkgo/v2 v2.23.4 + github.com/onsi/gomega v1.37.0 + golang.org/x/net v0.41.0 +) + +require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.34.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..871795da1 --- /dev/null +++ b/go.sum @@ -0,0 +1,41 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration_flags_test.go b/integration_flags_test.go new file mode 100644 index 000000000..f0be1dc29 --- /dev/null +++ b/integration_flags_test.go @@ -0,0 +1,269 @@ +package main_test + +import ( + "os/exec" + "strconv" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("flags", func() { + var serverCmd *exec.Cmd + var serverSession *Session + var port = getFreePort() + var flags []string + + JustBeforeEach(func() { + flags = append(flags, "-port", strconv.Itoa(port), "-blocklistURL", "file://etc/blocklist-test.txt") + serverCmd = exec.Command(serverPath, flags...) + serverSession, err = Start(serverCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + // takes 0.455s to start up on macOS Big Sur 3.7 GHz Quad Core 22-nm Xeon E5-1620v2 processor (2013 Mac Pro) + // takes 1.312s to start up on macOS Big Sur 2.0GHz quad-core 10th-generation Intel Core i5 processor (2020 13" MacBook Pro) + // 10 seconds should be long enough for slow container-on-a-VM-with-shared-core + Eventually(serverSession.Err, 10).Should(Say("Ready to answer queries")) + }) + AfterEach(func() { + serverSession.Terminate() + Eventually(serverSession).Should(Exit()) + }) + When("-nameservers is set", func() { + BeforeEach(func() { + flags = []string{"-nameservers=mickey.minnie.,daffy.duck"} + }) + It("returns all the NS records, appending dots as needed", func() { + digArgs := "@localhost example.com ns -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0`)) + Eventually(digSession).Should(Say(`;; ANSWER SECTION:`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(`mickey.minnie.\n`)) + Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(`daffy.duck.\n`)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`Adding nameserver "mickey\.minnie\."\n`)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`Adding nameserver "daffy\.duck\."\n`)) + // we don't know the order in which the nameservers will be returned, so we try both + Eventually(string(serverSession.Err.Contents())).Should(Or(MatchRegexp(`TypeNS example.com. \? mickey\.minnie\., daffy\.duck\.\n`), MatchRegexp(`TypeNS example.com. \? daffy\.duck\., mickey\.minnie\.\n`))) + }) + When("a nameserver is an empty string", func() { + BeforeEach(func() { + flags = []string{"-nameservers="} + }) + It("should message that it's skipping that nameserver and continue", func() { + Expect(string(serverSession.Err.Contents())).Should(MatchRegexp(`-nameservers: ignoring zero-length nameserver ""`)) + }) + }) + When("a nameserver is too long (>255 chars)", func() { + var tooLongDomainName = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789" + + "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789" + + "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789" + + "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789" + + BeforeEach(func() { + flags = []string{"-nameservers=" + tooLongDomainName} + }) + It("should message that it's skipping that nameserver and continue", func() { + Expect(string(serverSession.Err.Contents())).Should(MatchRegexp(`-nameservers: ignoring invalid nameserver "` + tooLongDomainName)) + }) + }) + }) + When("-addresses is set", func() { + BeforeEach(func() { + flags = []string{"-addresses=a.b.c=1.2.3.4,a.b.c=5.6.7.8,a.b.c=2600::"} + }) + It("returns the addresses when the A records of the hostnames are queried", func() { + digArgs := "@localhost a.b.c A -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0`)) + Eventually(digSession).Should(Say(`;; ANSWER SECTION:`)) + Eventually(digSession).Should(Say(`1.2.3.4\n`)) + Eventually(digSession).Should(Say(`5.6.7.8\n`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`Adding record "a.b.c.=1.2.3.4"\n`)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`Adding record "a.b.c.=5.6.7.8"\n`)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`Adding record "a.b.c.=2600::"\n`)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA a\.b\.c\. \? 1\.2\.3\.4, 5\.6\.7\.8\n`)) + }) + It("returns the addresses when the AAAA records of the hostnames are queried", func() { + digArgs := "@localhost a.b.c AAAA -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0`)) + Eventually(digSession).Should(Say(`;; ANSWER SECTION:`)) + Eventually(digSession).Should(Say(`2600::\n`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeAAAA a\.b\.c\. \? 2600::\n`)) + }) + When(`addresses don't include an "="`, func() { + BeforeEach(func() { + flags = []string{"-addresses=a.b.c"} + }) + It("should message that it's skipping that address and continue", func() { + Expect(string(serverSession.Err.Contents())).Should(MatchRegexp(`-addresses: arguments should be in the format "host=ip", not "a.b.c"`)) + }) + }) + }) + When("-quiet is set", func() { + BeforeEach(func() { + flags = []string{"-quiet"} + }) + It("doesn't print out log messages so that GCP doesn't charge $17/mo for storing them", func() { + digArgs := "@localhost 169.254.169.254 -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(Not(MatchRegexp(`169\.254\.169\.254`))) + }) + }) + When("-public is set to false", func() { + BeforeEach(func() { + flags = []string{"-public=false"} + }) + It("doesn't resolve public IPv4 addresses", func() { + digArgs := "@localhost 8-8-8-8.sslip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 8-8-8-8\.sslip\.io\. briancunnie\.gmail\.com\.`)) + }) + It("doesn't resolve public IPv6 addresses", func() { + digArgs := "@localhost aaaa 2600--.sslip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 2600--\.sslip\.io\. briancunnie\.gmail\.com\.`)) + }) + It("doesn't resolve public IPv4 addresses (hexadecimal)", func() { + digArgs := "@localhost 08080808.nip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 08080808\.nip\.io\. briancunnie\.gmail\.com\.`)) + }) + It("doesn't resolve public IPv6 addresses (hexadecimal)", func() { + digArgs := "@localhost aaaa 26010646010069f0042c6ab3cdd9e562.nip.io -p " + strconv.Itoa(port) // my laptop's IPv6 address + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`\? nil, SOA 26010646010069f0042c6ab3cdd9e562\.nip\.io\. briancunnie\.gmail\.com\.`)) + }) + It("resolves private IPv4 addresses", func() { + digArgs := "@localhost 192-168-0-1.sslip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`192-168-0-1\.sslip\.io\. \? 192\.168\.0\.1`)) + }) + It("resolves private IPv6 addresses", func() { + digArgs := "@localhost aaaa fc00--.sslip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`fc00--\.sslip\.io\. \? fc00::`)) + }) + It("resolves private IPv4 addresses (hexadecimal)", func() { + digArgs := "@localhost 7f000001.nip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA 7f000001.nip.io. \? 127.0.0.1`)) + }) + It("resolves private IPv6 addresses (hexadecimal)", func() { + digArgs := "@localhost aaaa 00000000000000000000000000000001.nip.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeAAAA 00000000000000000000000000000001.nip.io. \? ::1`)) + }) + }) + When("-delegates is set", func() { + BeforeEach(func() { + flags = []string{"-delegates=" + + "_acme-challenge.127-0-0-1.IP.io=ns.nono.io," + + "2600--.IP.IO=ns-1.nono.com," + + "_acme-challenge.73-189-219-4.ip.IO=ns-2.nono.com," + + "a.b.C=d.E.f"} + }) + When("the arguments are missing", func() { + BeforeEach(func() { + flags = []string{"-delegates="} + }) + It("should give an informative message", func() { + Expect(string(serverSession.Err.Contents())).Should(Not(MatchRegexp(`-delegates`))) + }) + }) + When("the arguments are mangled", func() { + BeforeEach(func() { + flags = []string{"-delegates=blahblah"} + }) + It("should give an informative message", func() { + Expect(string(serverSession.Err.Contents())).Should(MatchRegexp(`-delegates: arguments should be in the format "delegatedDomain=nameserver", not "blahblah"`)) + }) + }) + When("only some of the arguments are mangled", func() { + BeforeEach(func() { + flags = []string{"-delegates=a.b=c.d,blahblah"} + }) + It("adds the correct ones, gives an informative message for the mangled ones", func() { + Expect(string(serverSession.Err.Contents())).Should(MatchRegexp(`Adding delegated NS record "a.b.=c.d."`)) + Expect(string(serverSession.Err.Contents())).Should(MatchRegexp(`-delegates: arguments should be in the format "delegatedDomain=nameserver", not "blahblah"`)) + }) + }) + When("looking up a delegated domain", func() { + It("should return a non-authoritative NS record pointing to the nameserver", func() { + digArgs := "@localhost _acme-challenge.127-0-0-1.IP.io -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0`)) + Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:`)) + Eventually(digSession).Should(Say(`_acme-challenge.127-0-0-1.IP.io. 604800 IN NS ns.nono.io.\n`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`_acme-challenge\.127-0-0-1\.IP\.io\. \? nil, NS ns\.nono\.io\.`)) + }) + }) + When("looking up the subdomain of a delegated domain", func() { + It("should return a non-authoritative NS record pointing to the nameserver", func() { + digArgs := "@localhost subdomain.2600--.IP.IO -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0`)) + Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:`)) + Eventually(digSession).Should(Say(`subdomain.2600--.IP.IO. 604800 IN NS ns-1.nono.com.\n`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`subdomain\.2600--\.IP\.IO\. \? nil, NS ns-1\.nono\.com\.`)) + }) + }) + When("looking up a delegated domain that wouldn't have resolved to an IP address", func() { + It("it delegates", func() { + digArgs := "@localhost a.b.c -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0`)) + Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:`)) + Eventually(digSession).Should(Say(`a.b.c. 604800 IN NS d.e.f.`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`a\.b\.c\. \? nil, NS d\.e\.f\.`)) + }) + }) + }) +}) diff --git a/integration_metrics_test.go b/integration_metrics_test.go new file mode 100644 index 000000000..57f2adfdd --- /dev/null +++ b/integration_metrics_test.go @@ -0,0 +1,238 @@ +package main_test + +import ( + "fmt" + "os/exec" + "strconv" + "strings" + "time" + "xip/xip" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("IntegrationMetrics", func() { + When("the server is queried", func() { + // One big `It()` block because these tests cannot be run in parallel (singleton) + It("should update metrics", func() { + var actualMetrics xip.Metrics + expectedMetrics := getMetrics(port) + + // A updates .Queries, UDPQueries .AnsweredQueries, .AnsweredAQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics.AnsweredAQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost 127.0.0.1.sslip.io +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // A updates .Queries, TCPQueries .AnsweredQueries, .AnsweredAQueries + expectedMetrics.Queries++ + expectedMetrics.TCPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics.AnsweredAQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + // "+vc" runs the lookup over TCP, not UDP + actualMetrics = digAndGetMetrics("@localhost 127.0.0.1.sslip.io +short +vc -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // A (non-existent) record updates .Queries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost non-existent.sslip.io +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // A blocked updates .Queries, .AnsweredQueries, .AnsweredBlockedQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics.AnsweredBlockedQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + dig("@localhost bank-of-raiffeisen.127.0.0.1.sslip.io +short -p " + strconv.Itoa(port)) + actualMetrics = getMetrics(port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // AAAA updates .Queries, .AnsweredQueries, .AnsweredAAAAQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics.AnsweredAAAAQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost 2600--.sslip.io aaaa +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // AAAA (non-existent) updates .Queries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost non-existent.sslip.io aaaa +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // MX (customized) updates .Queries, .AnsweredQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost sslip.io mx +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // MX updates .Queries, AnsweredQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost non-existent.sslip.io mx +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // NS updates .Queries, AnsweredQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost non-existent.sslip.io ns +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // NS DNS-01 challenge record updates .Queries, .AnsweredNSDNS01ChallengeQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + // DNS-01 challenges don't count as successful because we're not authoritative; we're delegating + expectedMetrics.AnsweredNSDNS01ChallengeQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost _acme-challenge.fe80--.sslip.io NS +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // Always successful: SOA + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + dig("@localhost non-existent.sslip.io soa +short -p " + strconv.Itoa(port)) + actualMetrics = getMetrics(port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // TXT sslip.io (customized) updates .Queries, .AnsweredQueries, + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost sslip.io txt +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // TXT sslip.io (non-existent) updates .Queries, .AnsweredQueries, + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost non-existent.sslip.io txt +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // TXT ip.sslip.io updates .Queries, .AnsweredQueries, .AnsweredTXTSrcIPQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics.AnsweredTXTSrcIPQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost ip.sslip.io txt +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // TXT version.sslip.io updates .Queries, .AnsweredQueries, .AnsweredTXTVersionQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics.AnsweredTXTVersionQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost version.status.sslip.io txt +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // PTR version.sslip.io updates .Queries, .AnsweredQueries, .AnsweredPTRQueriesIPv4 + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics.AnsweredPTRQueriesIPv4++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost 1.2.3.4.in-addr.arpa ptr +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // PTR version.sslip.io updates .Queries, .AnsweredQueries, .AnsweredPTRQueriesIPv6 + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredQueries++ + expectedMetrics.AnsweredPTRQueriesIPv6++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa ptr +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + + // TXT DNS-01 challenge record updates .Queries, .AnsweredNSDNS01ChallengeQueries + expectedMetrics.Queries++ + expectedMetrics.UDPQueries++ + expectedMetrics.AnsweredNSDNS01ChallengeQueries++ + expectedMetrics = bumpExpectedToAccountForMetricsQuery(expectedMetrics) + actualMetrics = digAndGetMetrics("@localhost _acme-challenge.fe80--.sslip.io txt +short -p "+strconv.Itoa(port), port) + Expect(expectedMetrics.MostlyEquals(actualMetrics)).To(BeTrue()) + }) + }) +}) + +// bumpExpectedToAccountForMetricsQuery takes into account that +// digging for the metrics endpoint affects the metrics. It's like +// the Heisenberg uncertainty principle (observing changes the values) +func bumpExpectedToAccountForMetricsQuery(metrics xip.Metrics) xip.Metrics { + metrics.Queries++ + metrics.UDPQueries++ + metrics.AnsweredQueries++ + return metrics +} + +func digAndGetMetrics(digArgs string, port int) xip.Metrics { + dig(digArgs) + return getMetrics(port) +} + +func dig(digArgs string) { + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err := Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) +} + +func getMetrics(port int) (m xip.Metrics) { + digArgs := "@localhost metrics.status.sslip.io txt +short -p " + strconv.Itoa(port) + digCmd := exec.Command("dig", strings.Split(digArgs, " ")...) + stdout, err := digCmd.Output() + Expect(err).ToNot(HaveOccurred()) + var uptime int + var junk string + _, err = fmt.Sscanf(string(stdout), + "\"Uptime: %d\"\n"+ + "\"Blocklist: %s %s %s\n"+ + "\"Queries: %d (%s\n"+ // %s "swallows" the `/s"` at the end + "\"TCP/UDP: %d/%d\"\n"+ + "\"Answer > 0: %d (%s\n"+ // %s "swallows" the `/s"` at the end + "\"A: %d\"\n"+ + "\"AAAA: %d\"\n"+ + "\"TXT Source: %d\"\n"+ + "\"TXT Version: %d\"\n"+ + "\"PTR IPv4/IPv6: %d/%d\"\n"+ + "\"NS DNS-01: %d\"\n"+ + "\"Blocked: %d\"\n", + &uptime, + &junk, &junk, &junk, + &m.Queries, &junk, + &m.TCPQueries, &m.UDPQueries, + &m.AnsweredQueries, &junk, + &m.AnsweredAQueries, + &m.AnsweredAAAAQueries, + &m.AnsweredTXTSrcIPQueries, + &m.AnsweredTXTVersionQueries, + &m.AnsweredPTRQueriesIPv4, &m.AnsweredPTRQueriesIPv6, + &m.AnsweredNSDNS01ChallengeQueries, + &m.AnsweredBlockedQueries, + ) + Expect(err).ToNot(HaveOccurred()) + m.Start = time.Now().Add(-time.Duration(uptime) * time.Second) + //_, err = fmt.Fscanf(digSession.Out, "queries: %d", &m.Queries) + return m +} diff --git a/integration_speed_test.go b/integration_speed_test.go new file mode 100644 index 000000000..3b94e6e42 --- /dev/null +++ b/integration_speed_test.go @@ -0,0 +1,85 @@ +package main_test + +import ( + "net" + "os/exec" + "strconv" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" + . "github.com/onsi/gomega/gexec" + "golang.org/x/net/dns/dnsmessage" +) + +var _ = Describe("speed", func() { + var serverCmd *exec.Cmd + var serverSession *Session + var port = getFreePort() + var flags []string + + JustBeforeEach(func() { + flags = append(flags, "-port", strconv.Itoa(port), "-blocklistURL", "file://etc/blocklist-test.txt") + serverCmd = exec.Command(serverPath, flags...) + serverSession, err = Start(serverCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + // takes 0.455s to start up on macOS Big Sur 3.7 GHz Quad Core 22-nm Xeon E5-1620v2 processor (2013 Mac Pro) + // takes 1.312s to start up on macOS Big Sur 2.0GHz quad-core 10th-generation Intel Core i5 processor (2020 13" MacBook Pro) + // 10 seconds should be long enough for slow container-on-a-VM-with-shared-core + Eventually(serverSession.Err, 10).Should(Say("Ready to answer queries")) + }) + AfterEach(func() { + serverSession.Terminate() + Eventually(serverSession).Should(Exit()) + }) + When("we want test the throughput", func() { + var loopbackAddr *net.UDPAddr + var conn *net.UDPConn + var msg dnsmessage.Message + const numQueries = 5000 + const minThroughput = 1000 + + BeforeEach(func() { + loopbackAddr, err = net.ResolveUDPAddr("udp", "localhost:"+strconv.Itoa(port)) + Expect(err).ToNot(HaveOccurred()) + conn, err = net.DialUDP("udp", nil, loopbackAddr) + Expect(err).ToNot(HaveOccurred()) + }) + It("runs "+strconv.Itoa(numQueries)+" queries and the throughput is > "+strconv.Itoa(minThroughput)+" queries/sec", func() { + msg = dnsmessage.Message{ + Questions: []dnsmessage.Question{ + { + Name: dnsmessage.MustNewName("127-0-0-1.sslip.io."), + Type: dnsmessage.TypeA, + Class: dnsmessage.ClassINET, + }, + }, + } + responseBuf := make([]byte, 512) + queryBuf, err := msg.Pack() + Expect(err).ToNot(HaveOccurred()) + startTime := time.Now() + // The queries/second is conservative, realistically should be higher + // - queries are done sequentially, not in parallel + // - each query includes an overhead of 4 Expect() + // current max queries is 2047/second (ns-ovh.sslip.io.) + // ~19k Apple M2 + // ~8k vSphere Xeon D-1736 2.7GHz + // ~6k AWS Graviton T2 + // ~5k Azure Xeon E5-2673 v4 @ 2.30GHz + for i := 0; i < numQueries; i += 1 { + bytesWritten, err := conn.Write(queryBuf) + Expect(err).ToNot(HaveOccurred()) + Expect(bytesWritten).To(Equal(len(queryBuf))) + bytesRead, err := conn.Read(responseBuf) + Expect(err).ToNot(HaveOccurred()) + Expect(bytesRead).To(Equal(52)) // The A record response "127.0.0.1" is 52 bytes + } + elapsedSeconds := time.Since(startTime).Seconds() + Eventually(serverSession.Err).Should(Say(`TypeA 127-0-0-1\.sslip\.io\. \? 127\.0\.0\.1`)) + //fmt.Fprintf(os.Stderr, "Queries/second: %.2f\n", float64(numQueries)/elapsedSeconds) + Expect(float64(numQueries) / elapsedSeconds).Should(BeNumerically(">", minThroughput)) + }) + }) +}) diff --git a/integration_suite_test.go b/integration_suite_test.go new file mode 100644 index 000000000..ddc56beb7 --- /dev/null +++ b/integration_suite_test.go @@ -0,0 +1,13 @@ +package main_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestSslipIoDnsServer(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "SslipIoDnsServer Suite") +} diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 000000000..afb421658 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,633 @@ +package main_test + +import ( + "log" + "net" + "os/exec" + "regexp" + "runtime" + "strconv" + "strings" + "time" + "xip/testhelper" + "xip/xip" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" + . "github.com/onsi/gomega/gbytes" + . "github.com/onsi/gomega/gexec" +) + +var err error +var serverCmd *exec.Cmd +var serverSession *Session +var port = getFreePort() +var serverPath, _ = Build("main.go") + +var _ = BeforeSuite(func() { + format.MaxLength = 0 // need more output, 4000 is the default + Expect(err).ToNot(HaveOccurred()) + serverCmd = exec.Command(serverPath, "-port", strconv.Itoa(port), "-blocklistURL", "file://etc/blocklist-test.txt") + serverSession, err = Start(serverCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + // takes 0.455s to start up on macOS Big Sur 3.7 GHz Quad Core 22-nm Xeon E5-1620v2 processor (2013 Mac Pro) + // takes 1.312s to start up on macOS Big Sur 2.0GHz quad-core 10th-generation Intel Core i5 processor (2020 13" MacBook Pro) + // 10 seconds should be long enough for slow container-on-a-VM-with-shared-core + Eventually(serverSession.Err, 10).Should(Say(` version \d+\.\d+\.\d+ starting`)) + Eventually(serverSession.Err, 10).Should(Say("Ready to answer queries")) +}) + +var _ = AfterSuite(func() { + serverSession.Terminate() + Eventually(serverSession).Should(Exit()) +}) + +var _ = Describe("sslip.io-dns-server", func() { + //var stdin io.WriteCloser + var digCmd *exec.Cmd + var digSession *Session + var digArgs string + + Describe("Integration tests", func() { + DescribeTable("when the DNS server is queried", + func(digArgs string, digResults string, serverLogMessage string) { + digArgs += " -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + // we want to make sure digSession has exited because we + // want to compare the _full_ contents of the stdout in the case + // of negative assertions (e.g. "^$") + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(digResults)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(serverLogMessage)) + }, + Entry("A (customized) for sslip.io", + "@localhost sslip.io +short", + `\A78.46.204.247\n\z`, + `TypeA sslip.io. \? 78.46.204.247\n`), + Entry("A (or lack thereof) for example.com", + "@localhost example.com +short", + `\A\z`, + `TypeA example.com. \? nil, SOA example.com. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry("A for www-127-0-0-1.sslip.io", + "@localhost www-127-0-0-1.sslip.io +short", + `\A127.0.0.1\n\z`, + `TypeA www-127-0-0-1.sslip.io. \? 127.0.0.1\n`), + Entry("A for www.192.168.0.1.sslip.io", + "@localhost www.192.168.0.1.sslip.io +short", + `\A192.168.0.1\n\z`, + `TypeA www.192.168.0.1.sslip.io. \? 192.168.0.1\n`), + Entry("AAAA (customized) for sslip.io", + "@localhost sslip.io aaaa +short", + `\A2a01:4f8:c17:b8f::2\n\z`, + `TypeAAAA sslip.io. \? 2a01:4f8:c17:b8f::2\n`), + Entry("AAAA not found for example.com", + "@localhost example.com aaaa +short", + `\A\z`, + `TypeAAAA example.com. \? nil, SOA example.com. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry("AAAA for www-2601-646-100-69f0-1c09-bae7-aa42-146c.sslip.io", + "@localhost www-2601-646-100-69f0-1c09-bae7-aa42-146c.sslip.io aaaa +short", + `\A2601:646:100:69f0:1c09:bae7:aa42:146c\n\z`, + `TypeAAAA www-2601-646-100-69f0-1c09-bae7-aa42-146c.sslip.io. \? 2601:646:100:69f0:1c09:bae7:aa42:146c\n`), + Entry("ALL (ANY) is NOT implemented", + // `+notcp` required for dig 9.11.25-RedHat-9.11.25-2.fc32 to avoid "connection refused" + "@localhost sslip.io any +notcp", + ` status: NOTIMP,`, + `TypeALL sslip.io. \? NotImplemented\n`), + Entry("CNAME (customized) for protonmail._domainkey.sslip.io", + "@localhost protonmail._domainkey.sslip.io cname +short", + `\Aprotonmail.domainkey.dw4gykv5i2brtkjglrf34wf6kbxpa5hgtmg2xqopinhgxn5axo73a.domains.proton.ch.\n\z`, + `TypeCNAME protonmail._domainkey.sslip.io. \? protonmail.domainkey.dw4gykv5i2brtkjglrf34wf6kbxpa5hgtmg2xqopinhgxn5axo73a.domains.proton.ch.\n`), + Entry("CNAME not found for example.com", + "@localhost example.com cname +short", + `\A\z`, + `TypeCNAME example.com. \? nil, SOA example.com. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry("MX for example.com", + "@localhost example.com mx +short", + `\A0 example.com.\n\z`, + `TypeMX example.com. \? 0 example.com.\n`), + Entry("SOA for sslip.io", + "@localhost sslip.io soa +short", + `\Asslip.io. briancunnie.gmail.com. 20250615 900 900 1800 180\n\z`, + `TypeSOA sslip.io. \? sslip.io. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry("SOA for example.com", + "@localhost example.com soa +short", + `\Aexample.com. briancunnie.gmail.com. 20250615 900 900 1800 180\n\z`, + `TypeSOA example.com. \? example.com. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry("SRV (or other record that we don't implement) for example.com", + "@localhost example.com srv +short", + `\A\z`, + `TypeSRV example.com. \? nil, SOA example.com. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry(`TXT for version.status.sslip.io is the version number of the xip software (which gets overwritten during linking)`, + "@127.0.0.1 version.status.sslip.io txt +short", + `\A"0.0.0"\n"0001/01/01-99:99:99-0800"\n"cafexxx"\n\z`, + `TypeTXT version.status.sslip.io. \? \["0.0.0"\], \["0001/01/01-99:99:99-0800"\], \["cafexxx"\]`), + Entry(`TXT is the querier's IPv4 address and the domain "ip.sslip.io"`, + "@127.0.0.1 ip.sslip.io txt +short", + `127.0.0.1`, + `TypeTXT ip.sslip.io. \? \["127.0.0.1"\]`), + Entry(`TXT is the querier's IPv4 address and the domain is NOT "ip.sslip.io"`, + "@127.0.0.1 example.com txt +short", + `\A\z`, + `TypeTXT example.com. \? nil, SOA example.com. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry(`get a PTR for 1.0.168.192.in-addr.arpa returns 192-168-0-1.sslip.io`, + "@127.0.0.1 ptr -x 192.168.0.1 +short", + `\A192-168-0-1.sslip.io.\n\z`, + `TypePTR 1.0.168.192.in-addr.arpa. \? 192-168-0-1.sslip.io.`), + Entry(`get a PTR for 1.0.0.127.blah.in-addr.arpa returns no records; "blah.in-addr.arpa is not a valid domain."`, + "@127.0.0.1 1.0.0.127.blah.in-addr.arpa ptr +short", + `\A\z`, + `TypePTR 1.0.0.127.blah.in-addr.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry(`get a PTR for blah.1.0.0.127.in-addr.arpa returns no records; "blah" isn't a valid subdomain' `, + "@127.0.0.1 blah.1.0.0.127.in-addr.arpa ptr +short", + `\A\z`, + `TypePTR blah.1.0.0.127.in-addr.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry(`get a PTR for 0.0.127.in-addr.arpa returns no records; should have 4 octets, not 3`, + "@127.0.0.1 0.0.127.in-addr.arpa ptr +short", + `\A\z`, + `TypePTR 0.0.127.in-addr.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry(`get a PTR for 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa returns 2601-646-100-69f0-14ce-6eea-9204-bba2.sslip.io`, + "@127.0.0.1 ptr -x 2601:646:100:69f0:14ce:6eea:9204:bba2 +short", + `\A2601-646-100-69f0-14ce-6eea-9204-bba2.sslip.io.\n\z`, + `TypePTR 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa. \? 2601-646-100-69f0-14ce-6eea-9204-bba2.sslip.io.`), + Entry(`get a PTR for 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.blah.ip6.arpa returns no records; "blah isn't a valid subdomain'"`, + "@127.0.0.1 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.blah.ip6.arpa ptr +short", + `\A\z`, + `TypePTR 2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.blah.ip6.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry(`get a PTR for b2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa returns no records; "b2" isn't a valid subdomain'`, + "@127.0.0.1 b2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa ptr +short", + `\A\z`, + `TypePTR b2.a.b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry(`get a PTR for b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa returns no records; has too few numbers`, + "@127.0.0.1 b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa ptr +short", + `\A\z`, + `TypePTR b.b.4.0.2.9.a.e.e.6.e.c.4.1.0.f.9.6.0.0.1.0.6.4.6.0.1.0.6.2.ip6.arpa. \? nil, SOA sslip.io. briancunnie.gmail.com. 20250615 900 900 1800 180\n`), + Entry(`TODO: should, but doesn't, return an IDNA2008-compliant record for ::1`, + "@127.0.0.1 -x ::1 +short", + `\A--1.sslip.io.\n\z`, + `TypePTR 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa. \? --1.sslip.io.\n`), + Entry(`TODO: should, but doesn't, return an IDNA2008-compliant record for 2600::`, + "@127.0.0.1 -x 2600:: +short", + `\A2600--.sslip.io.\n\z`, + `TypePTR 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.6.2.ip6.arpa. \? 2600--.sslip.io.\n`), + Entry(`over TCP, A (customized) for sslip.io`, + "@localhost sslip.io +short +vc", + `\A78.46.204.247\n\z`, + `TypeA sslip.io. \? 78.46.204.247\n`), + Entry(`TXT for _psl sslip.io is a link to the pull request for putting sslip.io on the Public Suffix List`, + "@localhost _psl.sslip.io txt +short", + `\A"https://github.com/publicsuffix/list/pull/2206"\n\z`, + `TypeTXT _psl.sslip.io. \? \["https://github.com/publicsuffix/list/pull/2206"\]`), + ) + }) + Describe("for more complex assertions", func() { + When("we want to make sure our TTL is an hour if we need to block ", func() { + It("returns a TTL of 3600, at least for the non-RFC 1918 non-localhost IPv4 adresses", func() { + digArgs = "@localhost 52.0.56.138.sslip.io -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`52\.0\.56\.138\.sslip\.io\.\s+3600\s+IN\s+A\s+52\.0\.56\.138\n`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA 52\.0\.56\.138\.sslip\.io\. \? 52\.0\.56\.138\n`)) + }) + It("returns a TTL of 3600, at least for the non-RFC 4193 non-localhost IPv6 addresses", func() { + digArgs = "@localhost aaaa 2600-1f18-aaf-6900--b.sslip.io -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`2600-1f18-aaf-6900--b.sslip.io.\s+3600\s+IN\s+AAAA\s+2600:1f18:aaf:6900::b`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeAAAA 2600-1f18-aaf-6900--b\.sslip\.io\. \? 2600:1f18:aaf:6900::b\n`)) + }) + }) + When("our test is run on a machine which has IPv6", func() { + cmd := exec.Command("ping6", "-c", "1", "::1") + err := cmd.Run() // if the command succeeds, we have IPv6 + if err == nil { + It("returns a TXT of the querier's IPv6 address when querying ip.sslip.io", func() { + digCmd = exec.Command("dig", "@::1", "ip.sslip.io", "txt", "+short", "-p", strconv.Itoa(port)) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(`::1`)) + Eventually(serverSession.Err).Should(Say(`TypeTXT ip\.sslip\.io\. \? \["::1"\]`)) + Expect(digSession).To(Exit()) + }) + } + }) + When("we do reverse lookups (PTR) on a random series of IPv6 addresses (fuzz testing)", func() { + It("should succeed every time", func() { + for i := 0; i < 50; i++ { + addr := testhelper.RandomIPv6Address() + digArgs = "@localhost -x " + addr.String() + " -p " + strconv.Itoa(port) + " +short" + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + expectedPtr := strings.ReplaceAll(addr.String(), ":", "-") + ".sslip.io." + Eventually(digSession).Should(Say(expectedPtr)) + Eventually(digSession, 1).Should(Exit(0)) + } + }) + }) + When("ns.sslip.io is queried", func() { + It("returns all the A records", func() { + digArgs = "@localhost ns.sslip.io +short -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`146.190.110.69`)) + Eventually(digSession).Should(Say(`104.155.144.4`)) + Eventually(digSession).Should(Say(`5.78.115.44`)) + Eventually(digSession).Should(Say(`51.75.53.19`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA ns.sslip.io. \? 146.190.110.69, 104.155.144.4, 5.78.115.44, 51.75.53.19\n`)) + }) + It("returns all the AAAA records", func() { + digArgs = "@localhost aaaa ns.sslip.io +short -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`2400:6180:0:d2:0:1:da21:d000`)) + Eventually(digSession).Should(Say(`2600:1900:4000:4d12::`)) + Eventually(digSession).Should(Say(`2a01:4ff:1f0:c920::`)) + Eventually(digSession).Should(Say(`2001:41d0:602:2313::1`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeAAAA ns.sslip.io. \? 2400:6180:0:d2:0:1:da21:d000, 2600:1900:4000:4d12::, 2a01:4ff:1f0:c920::, 2001:41d0:602:2313::1\n`)) + }) + }) + When("there are multiple MX records returned (e.g. sslip.io)", func() { + It("returns all the records", func() { + digArgs = "@localhost sslip.io mx +short -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`10 mail.protonmail.ch.`)) + Eventually(digSession).Should(Say(`20 mailsec.protonmail.ch.\n$`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeMX sslip.io. \? 10 mail.protonmail.ch., 20 mailsec.protonmail.ch.\n`)) + }) + }) + When("there are multiple NS records returned (e.g. almost any NS query)", func() { + It("returns all the records", func() { + digArgs = "@localhost example.com ns -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr aa rd; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 8`)) + Eventually(digSession).Should(Say(`;; ANSWER SECTION:`)) + Eventually(digSession).Should(Say(`;; ADDITIONAL SECTION:`)) + Eventually(digSession).Should(Say(`ns-do-sg.sslip.io..*146.190.110.69\n`)) + Eventually(digSession).Should(Say(`ns-do-sg.sslip.io..*2400:6180:0:d2:0:1:da21:d000\n`)) + Eventually(digSession).Should(Say(`ns-gce.sslip.io..*104.155.144.4\n`)) + Eventually(digSession).Should(Say(`ns-gce.sslip.io..*2600:1900:4000:4d12::\n`)) + Eventually(digSession).Should(Say(`ns-hetzner.sslip.io..*5.78.115.44\n`)) + Eventually(digSession).Should(Say(`ns-hetzner.sslip.io..*2a01:4ff:1f0:c920::\n`)) + Eventually(digSession).Should(Say(`ns-ovh.sslip.io..*51.75.53.19\n`)) + Eventually(digSession).Should(Say(`ns-ovh.sslip.io..*2001:41d0:602:2313::1\n`)) + Eventually(digSession, 1).Should(Exit(0)) + // the server names may appear out-of-order + Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(`NS\tns-do-sg.sslip.io.\n`)) + Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(`NS\tns-gce.sslip.io.\n`)) + Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(`NS\tns-hetzner.sslip.io.\n`)) + Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(`NS\tns-ovh.sslip.io.\n`)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeNS example.com. \? ns-do-sg.sslip.io., ns-gce.sslip.io., ns-hetzner.sslip.io., ns-ovh.sslip.io.\n`)) + }) + }) + When(`there are multiple TXT records returned (e.g. SPF for sslip.io)`, func() { + It("returns the custom TXT records", func() { + digArgs = "@localhost sslip.io txt +short -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`"protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26"`)) + Eventually(digSession).Should(Say(`"v=spf1 include:_spf.protonmail.ch mx ~all"`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT sslip.io. \? \["protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26"\], \["v=spf1 include:_spf.protonmail.ch mx ~all"\]\n`)) + }) + }) + When(`a record for an "_acme-challenge" domain is queried`, func() { + When(`it's an NS record`, func() { + It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() { + digArgs = "@localhost _acme-challenge.fe80--.sslip.io ns -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1`)) + Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:`)) + Eventually(digSession).Should(Say(`fe80--.sslip.io.`)) + Eventually(digSession).Should(Say(`;; ADDITIONAL SECTION:`)) + Eventually(digSession).Should(Say(`fe80--.sslip.io..*fe80::\n`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeNS _acme-challenge.fe80--.sslip.io. \? nil, NS fe80--.sslip.io.\n`)) + }) + }) + When(`it's a TXT record`, func() { + It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() { + digArgs = "@localhost _acme-challenge.127-0-0-1.sslip.io txt -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1,`)) + Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:\n`)) + Eventually(digSession).Should(Say(`^_acme-challenge.127-0-0-1.sslip.io. 604800 IN NS 127-0-0-1.sslip.io.\n`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT _acme-challenge.127-0-0-1.sslip.io. \? nil, NS 127-0-0-1.sslip.io.\n`)) + }) + }) + When(`it's a A record`, func() { + It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() { + digArgs = "@localhost _acme-challenge.127-0-0-1.sslip.io a -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1,`)) + Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:\n`)) + Eventually(digSession).Should(Say(`^_acme-challenge.127-0-0-1.sslip.io. 604800 IN NS 127-0-0-1.sslip.io.\n`)) + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA _acme-challenge.127-0-0-1.sslip.io. \? nil, NS 127-0-0-1.sslip.io.\n`)) + }) + }) + }) + When(`a TXT record for an "metrics.status.sslip.io" domain is repeatedly queried`, func() { + It("rate-limits the queries after some amount requests", func() { + // typically ~9 milliseconds / query, ~125 queries / sec on 4-core Xeon + var start, stop time.Time + throttled := false + // double the the number of queries to make sure we exhaust the channel's buffers + for i := 0; i < xip.MetricsBufferSize*2; i++ { + start = time.Now() + digArgs = "@localhost metrics.status.sslip.io txt -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + _, err := digCmd.Output() + Expect(err).ToNot(HaveOccurred()) + stop = time.Now() + // we currently buffer at 250 milliseconds, so for our test we use a smidgen less because jitter + if stop.Sub(start) > 240*time.Millisecond { + throttled = true + break + } + } + Expect(throttled).To(BeTrue()) + }) + }) + }) + Describe(`The domain blocklist`, func() { + DescribeTable("when queried", + func(digArgs string, digResults string, serverLogMessage string) { + digArgs += " -p " + strconv.Itoa(port) + digCmd = exec.Command("dig", strings.Split(digArgs, " ")...) + digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + // we want to make sure digSession has exited because we + // want to compare the _full_ contents of the stdout in the case + // of negative assertions (e.g. "^$") + Eventually(digSession, 1).Should(Exit(0)) + Eventually(string(digSession.Out.Contents())).Should(MatchRegexp(digResults)) + Eventually(serverSession.Err).Should(Say(serverLogMessage)) + }, + Entry("an A record with a forbidden string on the left-hand side is redirected", + "@localhost raiffeisen.94.228.116.140.sslip.io +short", + `\A52.0.56.137\n\z`, + `TypeA raiffeisen.94.228.116.140.sslip.io. \? 52.0.56.137\n$`), + Entry("an A record with a forbidden string on the right-hand side is redirected", + "@localhost www.94-228-116-140.raiffeisen.com +short", + `\A52.0.56.137\n\z`, + `TypeA www.94-228-116-140.raiffeisen.com. \? 52.0.56.137\n$`), + Entry("an A record with a forbidden string embedded is redirected", + "@localhost international-raiffeisen-bank.94.228.116.140.sslip.io +short", + `\A52.0.56.137\n\z`, + `TypeA international-raiffeisen-bank.94.228.116.140.sslip.io. \? 52.0.56.137\n$`), + Entry("an A record with a forbidden string with a private IP is not redirected", + "@localhost raiffeisen.192.168.0.20.sslip.io +short", + `\A192.168.0.20\n\z`, + `TypeA raiffeisen.192.168.0.20.sslip.io. \? 192.168.0.20\n$`), + Entry("an AAAA record with a forbidden string is redirected", + "@localhost international-raiffeisen-bank.2600--.sslip.io aaaa +short", + `\A2600:1f18:aaf:6900::a\n\z`, + `TypeAAAA international-raiffeisen-bank.2600--.sslip.io. \? 2600:1f18:aaf:6900::a\n$`), + Entry("an AAAA record with a forbidden string with a private IP is NOT redirected", + "@localhost international-raiffeisen-bank.fc00--.sslip.io aaaa +short", + `\Afc00::\n\z`, + `TypeAAAA international-raiffeisen-bank.fc00--.sslip.io. \? fc00::\n$`), + // use regex to account for rotated nameserver order + Entry("an NS record with acme_challenge with a forbidden string is not delegated", + "@localhost _acme-challenge.raiffeisen.fe80--.sslip.io ns +short", + `\Ans-[a-z-]+.sslip.io.\nns-[a-z-]+.sslip.io.\nns-[a-z-]+.sslip.io.\nns-[a-z-]+.sslip.io.\n\z`, + `TypeNS _acme-challenge.raiffeisen.fe80--.sslip.io. \? ns-do-sg.sslip.io., ns-gce.sslip.io., ns-hetzner.sslip.io., ns-ovh.sslip.io.\n$`), + Entry("an A record with a forbidden CIDR is redirected", + "@localhost nf.43.134.66.67.sslip.io +short", + `\A52.0.56.137\n\z`, + `TypeA nf.43.134.66.67.sslip.io. \? 52.0.56.137\n$`), + Entry("an AAAA record with a forbidden CIDR is redirected", + "@localhost 2601-646-100-69f7-cafe-bebe-cafe-baba.sslip.io aaaa +short", + `\A2600:1f18:aaf:6900::a\n\z`, + `TypeAAAA 2601-646-100-69f7-cafe-bebe-cafe-baba.sslip.io. \? 2600:1f18:aaf:6900::a\n$`), + ) + }) + When("it can't bind to any UDP port", func() { + It("prints an error message and exits", func() { + Expect(err).ToNot(HaveOccurred()) + secondServerCmd := exec.Command(serverPath, "-port", strconv.Itoa(port), "-blocklistURL", "file://etc/blocklist-test.txt") + secondServerSession, err := Start(secondServerCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(secondServerSession.Err, 10).Should(Say("I couldn't bind via UDP to any IPs")) + Eventually(secondServerSession).Should(Exit(1)) + }) + }) + When("it can't bind to any TCP port", func() { + var squatters []net.Listener + var newPort = getFreePort() // I need a new free port to bind on because a server is running on the old port + BeforeEach(func() { + squatters, err = squatOnTcp(newPort) + Expect(err).ToNot(HaveOccurred()) + }) + AfterEach(func() { + for _, squatter := range squatters { + err = squatter.Close() + Expect(err).ToNot(HaveOccurred()) + } + }) + It("prints an error message and continues running", func() { + Expect(err).ToNot(HaveOccurred()) + secondServerCmd := exec.Command(serverPath, "-port", strconv.Itoa(newPort), "-blocklistURL", "file://etc/blocklist-test.txt") + secondServerSession, err := Start(secondServerCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(secondServerSession.Err, 10).Should(Say(` version \d+\.\d+\.\d+ starting`)) + Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via TCP to "\[::\]:\d+" \(INADDR_ANY, all interfaces\), so I'll try to bind to each address individually.`)) + Eventually(secondServerSession.Err, 10).Should(Say("I couldn't bind via TCP to any IPs")) + Eventually(secondServerSession.Err, 10).Should(Say("Ready to answer queries")) + secondServerSession.Terminate() + Eventually(secondServerSession).Should(Exit()) + }) + }) + When("it can't bind via UDP to the loopback address", func() { + var newPort = getFreePort() // I need a new free port to bind on because the server has already bound to the old port + var squatter *net.UDPConn + BeforeEach(func() { + squatter, err = squatOnUdpLoopbackPort(newPort) + Expect(err).ToNot(HaveOccurred()) + }) + It("prints an informative message and binds to the addresses it can", func() { + Expect(err).ToNot(HaveOccurred()) + secondServerCmd := exec.Command(serverPath, "-port", strconv.Itoa(newPort), "-blocklistURL", "file://etc/blocklist-test.txt") + secondServerSession, err := Start(secondServerCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(secondServerSession.Err, 10).Should(Say(` version \d+\.\d+\.\d+ starting`)) + Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via UDP to "\[::\]:\d+" \(INADDR_ANY, all interfaces\), so I'll try to bind to each address individually.`)) + Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via UDP to the following IPs:.* "(::1|127\.0\.0\.1)"`)) + err = squatter.Close() + Expect(err).ToNot(HaveOccurred()) + Eventually(secondServerSession.Err, 10).Should(Say("Ready to answer queries")) + secondServerSession.Terminate() + Eventually(secondServerSession).Should(Exit()) + }) + }) + When("it can't bind via TCP to the loopback address", func() { + var newPort = getFreePort() // I need a new free port to bind on because the server has already bound to the old port + var squatters []net.Listener + BeforeEach(func() { + squatters = squatOnTcpLoopback(newPort) + Expect(err).ToNot(HaveOccurred()) + Expect(err).ToNot(HaveOccurred()) + }) + It("prints an informative message and binds to the addresses it can", func() { + Expect(err).ToNot(HaveOccurred()) + secondServerCmd := exec.Command(serverPath, "-port", strconv.Itoa(newPort), "-blocklistURL", "file://etc/blocklist-test.txt") + secondServerSession, err := Start(secondServerCmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(secondServerSession.Err, 10).Should(Say(` version \d+\.\d+\.\d+ starting`)) + Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via TCP to "\[::\]:\d+" \(INADDR_ANY, all interfaces\), so I'll try to bind to each address individually.`)) + Eventually(secondServerSession.Err, 10).Should(Say(`I couldn't bind via TCP to the following IPs:.* "(::1|127\.0\.0\.1)"`)) + for _, squatter := range squatters { + err = squatter.Close() + Expect(err).ToNot(HaveOccurred()) + } + Eventually(secondServerSession.Err, 10).Should(Say("Ready to answer queries")) + secondServerSession.Terminate() + Eventually(secondServerSession).Should(Exit()) + }) + }) +}) + +func squatOnUdpLoopbackPort(port int) (squatter *net.UDPConn, err error) { + // try IPv6's loopback + udpAddr := net.UDPAddr{ + IP: net.ParseIP("::1"), + Port: port, + } + squatter, err = net.ListenUDP("udp", &udpAddr) + if err != nil { + // try IPv4's loopback + udpAddr = net.UDPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: port, + } + squatter, err = net.ListenUDP("udp", &udpAddr) + } + return squatter, err +} + +func squatOnTcpLoopback(port int) (squatters []net.Listener) { + var addrsToBind = []string{"[::1]", "127.0.0.1"} + // Get the GOOS environment variable to determine the operating system. + goos := strings.ToLower(runtime.GOOS) + if goos == "darwin" { + // macOS needs to bind to _all_ interfaces as well; don't know why + addrsToBind = append([]string{"[::]"}, addrsToBind...) + } + // try to bind to both IPv4 and IPv6's loopback + for _, addr := range addrsToBind { + addrPort := addr + ":" + strconv.Itoa(port) + squatter, err := net.Listen("tcp", addrPort) + if err != nil { + continue // probably + } + squatters = append(squatters, squatter) + } + return squatters +} + +// squatOnTcp(port) makes any subsequent attempt to bind to that port to fail, for testing purposes +func squatOnTcp(port int) (squatters []net.Listener, err error) { + /* + on macOS, not only do I need to listen on ALL addresses, but I also + need to listen to addresses individually. This isn't the case with Linux. + On a typical macOS dual-stack machine, I'll be able to create ~7 Listeners + (INADDR_ANY, 2 loopback, 1 IPv4, 3 IPv6) + */ + var squatter net.Listener + squatter, err = net.Listen("tcp", ":"+strconv.Itoa(port)) + if err != nil { + //log.Println(err.Error()) + } else { + squatters = append(squatters, squatter) + } + addrCIDRs, err := net.InterfaceAddrs() // typical addrCIDR "10.9.9.161/24" + ipv6regex := regexp.MustCompile(`:`) + for _, addrCIDR := range addrCIDRs { + ip, _, err := net.ParseCIDR(addrCIDR.String()) + if err != nil { + return squatters, err + } + ipv6 := ipv6regex.MatchString(addrCIDR.String()) + // accommodate IPv6's requirements for brackets: "[::1]:1024" vs "127.0.0.1:1024" + if ipv6 { + squatter, err = net.Listen("tcp", "["+ip.String()+"]"+":"+strconv.Itoa(port)) + //log.Println("[" + ip.String() + "]" + ":" + strconv.Itoa(port)) + if err != nil { + //log.Println(err.Error()) + // ignore errors on IPv6 bind attempts; it's probably a link-local, which needs a scope + // https://stackoverflow.com/questions/2455762/why-cant-i-bind-ipv6-socket-to-a-linklocal-address + continue + } + } else { + squatter, err = net.Listen("tcp", ip.String()+":"+strconv.Itoa(port)) + //log.Println(ip.String() + ":" + strconv.Itoa(port)) + if err != nil { + //log.Println(err.Error()) + continue + } + } + squatters = append(squatters, squatter) + } + //log.Println(len(squatters)) + return squatters, err +} + +// getFreePort should always succeed unless something awful has happened, e.g. port exhaustion +func getFreePort() int { + // we use a time-based seed to generate a random port to avoid collisions in our test + // we also bind for a millisecond (in `isPortFree()` to make sure we don't collide + // with another test running in parallel + listenPort := (time.Now().Nanosecond() % (65536 - 1024)) + 1023 + for { + listenPort += 1 + switch { + case listenPort > 65535: + listenPort = 1023 // we've reached the highest port, start over + // 1024 (lowest unprivileged port) - 1 (immediately incremented) + case isPortFree(listenPort): + return listenPort + } + } +} + +func isPortFree(port int) bool { + conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port}) + if err != nil { + return false + } + // we must Sleep() in order to avoid a race condition when tests + // are run in parallel (`ginkgo -p`) and the `ListenUDP()` and `Close()` + // we sleep for a millisecond because the port is randomized based on the millisecond. + time.Sleep(1 * time.Millisecond) + err = conn.Close() + if err != nil { + log.Printf("I couldn't close port %d", port) + return false + } + return true +} diff --git a/k8s/.gitignore b/k8s/.gitignore new file mode 100644 index 000000000..753b1a880 --- /dev/null +++ b/k8s/.gitignore @@ -0,0 +1,2 @@ +# don't checkin artifact from Docker build process +wildcard-dns-http-server diff --git a/k8s/Dockerfile-ntp b/k8s/Dockerfile-ntp new file mode 100644 index 000000000..4f98533d7 --- /dev/null +++ b/k8s/Dockerfile-ntp @@ -0,0 +1,15 @@ +# +# cunnie/sslip.io-ntp +# +# sslip.io NTP Dockerfile +# +# Much was from here: + +FROM alpine:3.11.3 AS sslip.io-ntp +LABEL org.opencontainers.image.authors="Brian Cunnie " +RUN apk update +RUN apk add openntpd +RUN mkdir -m 1777 /var/empty/tmp +ADD ./entrypoint-ntp.sh ./entrypoint-ntp.sh +RUN chmod 755 ./entrypoint-ntp.sh +ENTRYPOINT ["./entrypoint-ntp.sh"] diff --git a/k8s/Dockerfile-sslip.io-nginx b/k8s/Dockerfile-sslip.io-nginx new file mode 100644 index 000000000..380fb34f8 --- /dev/null +++ b/k8s/Dockerfile-sslip.io-nginx @@ -0,0 +1,43 @@ +# +# cunnie/sslip.io-nginx +# +# sslip.io nginx Dockerfile +# +# Dockerfile of an nginx server that serves the web +# pages of the sslip.io domain. +# +# Typical start command: +# +# docker run --rm -p 8080:80 cunnie/sslip.io-nginx +# +# To test from host: +# +# curl -I http://localhost:8080 +# +FROM fedora AS sslip.io-nginx + +LABEL org.opencontainers.image.authors="Brian Cunnie " + +RUN dnf install -y \ + bind-utils \ + iproute \ + less \ + lsof \ + neovim \ + net-tools \ + nginx \ + nmap-ncat \ + procps-ng + +RUN mv /usr/share/nginx/html /usr/share/nginx/html-orig + +COPY document_root_sslip.io /usr/share/nginx/html + +ENTRYPOINT [ "/usr/sbin/nginx", "-g", "daemon off;" ] + +# for testing: +# ENTRYPOINT /bin/bash + +# nginx listens on port 80 +# The `EXPOSE` directive doesn't do much in our case. We use it for documentation. +EXPOSE 80/tcp diff --git a/k8s/Dockerfile-wildcard-dns-http-server b/k8s/Dockerfile-wildcard-dns-http-server new file mode 100644 index 000000000..1242915fb --- /dev/null +++ b/k8s/Dockerfile-wildcard-dns-http-server @@ -0,0 +1,51 @@ +# cunnie/wildcard-dns-http-server: sslip.io wildcard DNS/HTTP server Dockerfile + +# This DNS/HTTP server enables the procurement of wildcard certs for sslip.io +# subdomains. It's meant to be run on the server whose IP address is the +# subdomain. e.g. if the subdomain was '207-44-147-10.sslip.io', then this +# should be run on the server whose IP address is 207.44.147.10, and this will +# procure a wildcard cert for *.207-44-147-10.sslip.io + +# This won't work for private addresses such as 10.0.1.10 or 192.168.0.1. + +# Dockerfile of a (Golang-based) DNS/HTTP server. + +# - the DNS server only responds to TXT queries, and always responds to TXT queries, +# and always responds with the same TXT record +# - the HTTP server allows you to update the TXT record by POST'ing to the /update +# endpoint with a JSON body of `{"txt":"the-new-TXT-record"}`. The endpoint +# is compatible with acme-dns. +# - acme.sh can be configured to update the DNS TXT record via HTTPS. + +# To build: + +# DOCKER_BUILD_DIR=$PWD +# pushd ../src/wildcard-dns-http-server/ +# GOOS=linux GOARCH=amd64 go build -o $DOCKER_BUILD_DIR/wildcard-dns-http-server +# popd +# docker build . -f Dockerfile-wildcard-dns-http-server -t cunnie/wildcard-dns-http-server + +# Typical start command: + +# docker run -it --rm -p 53:53/udp -p 80:80 cunnie/wildcard-dns-http-server + +# To test from host: + +# dig +short txt 127-0-0-1.example.com @localhost +# "Set this TXT record: curl -X POST http://localhost/update -d '{\"txt\":\"Certificate Authority's validation token\"}'" +# curl -X POST http://localhost/update -d '{"txt":"new-TXT-record"}' +# dig +short txt any-domain-you-want @localhost +# "new-TXT-record" + +FROM alpine AS sslip.io + +LABEL org.opencontainers.image.authors="Brian Cunnie " + +COPY wildcard-dns-http-server /usr/sbin/wildcard-dns-http-server + +ENTRYPOINT ["/usr/sbin/wildcard-dns-http-server"] + +# DNS listens on port 53 UDP +# The `EXPOSE` directive doesn't do much in our case. We use it for documentation. +EXPOSE 53/udp +EXPOSE 80/tcp diff --git a/k8s/document_root_k-v.io/img/favicon.svg b/k8s/document_root_k-v.io/img/favicon.svg new file mode 100644 index 000000000..9437e8415 --- /dev/null +++ b/k8s/document_root_k-v.io/img/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/k8s/document_root_nip.io/index.html b/k8s/document_root_nip.io/index.html new file mode 100644 index 000000000..cbbc499c4 --- /dev/null +++ b/k8s/document_root_nip.io/index.html @@ -0,0 +1,165 @@ + + + + + + + + + + nip.io - wildcard DNS for any IP Address + + + + + + + +
+

+ nip.io is now hosted by sslip.io. This page, the original + nip.io page, is kept as a memorial to the late, great Roopinder Singh, who created & ran nip.io. +

+
+
+

+ Watch out! The behavior has changed. For example, parsing is done from left to right + instead of right to left + (e.g. "1.127.0.0.1.nip.io" resolves to "1.127.0.0" not "127.0.0.1" ). +

+
+
+
+
+
+

nip.io

+
+ +
+

Dead simple wildcard DNS for any IP Address

+ +

+ Stop editing your etc/hosts file with custom hostname and IP address mappings. +

+ +

+ nip.io allows you to do that by mapping any IP + Address to a hostname using the following formats: +

+ +

Without a name:

+ +
    +
  • 10.0.0.1.nip.io maps to 10.0.0.1
  • +
  • 192-168-1-250.nip.io maps to 192.168.1.250
  • +
  • 0a000803.nip.io maps to 10.0.8.3
  • +
+ +

With a name:

+ +
    +
  • app.10.8.0.1.nip.io maps to 10.8.0.1
  • +
  • app-116-203-255-68.nip.io maps to 116.203.255.68
  • +
  • app-c0a801fc.nip.io maps to 192.168.1.252
  • +
  • customer1.app.10.0.0.1.nip.io maps to 10.0.0.1
  • +
  • customer2-app-127-0-0-1.nip.io maps to 127.0.0.1
  • +
  • customer3-app-7f000101.nip.io maps to 127.0.1.1
  • +
+ +

+ nip.io maps <anything>[.-]<IP Address>.nip.io in + "dot", "dash" or + "hexadecimal" notation to the corresponding + <IP Address>: +

+ +
    +
  • dot notation: magic.127.0.0.1.nip.io
  • +
  • dash notation: magic-127-0-0-1.nip.io
  • +
  • hexadecimal notation: magic-7f000001.nip.io
  • +
+ +

+ The "dash" and "hexadecimal" notation is especially useful when using services + like + LetsEncrypt as it's just a regular sub-domain + of nip.io +

+ +

About this service

+ +

+ nip.io is powered by PowerDNS + with a simple, + custom + PipeBackend + written in Python: + backend.py +

+ +

+ It's open source, licensed under Apache 2.0: + https://github.com/exentriquesolutions/nip.io + — pull requests are welcome. +

+ +

+ This is a free service provided by + Exentrique Solutions + (the same people who run XP-Dev.com which offer + Git, Mercurial and Subversion hosting). +

+ +

+ Feedback is appreciated, just + + raise an issue in GitHub + . +

+ +

Troubleshooting

+ +

DNS Rebinding Protection

+ +

+ Some DNS resolvers, forwarders and routers have + DNS rebinding protection + which may result in failure to resolve local and private IP addresses. + This service won't work in those situations. +

+ +

Related Services

+
    +
  • + localtls: + A DNS server in Python3 to provide TLS to webservices on local addresses. + It resolves addresses such as '192-168-0-1.yourdomain.net' to 192.168.0.1 and has a valid TLS + certificate for them. +
  • +
  • + sslip.io: Alternative to this service, supports + IPv6 and custom domains. +
  • +
  • + local.gd: Alternative to this service, where + everything is mapped to localhost/127.0.0.1. +
  • +
+
+
+
+
+ + + \ No newline at end of file diff --git a/document_root/css/starter-template.css b/k8s/document_root_sslip.io/css/starter-template.css similarity index 100% rename from document_root/css/starter-template.css rename to k8s/document_root_sslip.io/css/starter-template.css diff --git a/document_root/img/cert_chain.png b/k8s/document_root_sslip.io/img/cert_chain.png similarity index 100% rename from document_root/img/cert_chain.png rename to k8s/document_root_sslip.io/img/cert_chain.png diff --git a/document_root/img/favicon.ico b/k8s/document_root_sslip.io/img/favicon.ico similarity index 100% rename from document_root/img/favicon.ico rename to k8s/document_root_sslip.io/img/favicon.ico diff --git a/document_root/img/green_lock.png b/k8s/document_root_sslip.io/img/green_lock.png similarity index 100% rename from document_root/img/green_lock.png rename to k8s/document_root_sslip.io/img/green_lock.png diff --git a/document_root/img/red_lock.png b/k8s/document_root_sslip.io/img/red_lock.png similarity index 100% rename from document_root/img/red_lock.png rename to k8s/document_root_sslip.io/img/red_lock.png diff --git a/k8s/document_root_sslip.io/index.html b/k8s/document_root_sslip.io/index.html new file mode 100644 index 000000000..2f67c4500 --- /dev/null +++ b/k8s/document_root_sslip.io/index.html @@ -0,0 +1,384 @@ + + + + + + + + + Welcome to sslip.io + + + + + + + + + + +
+ +
+

nip.io & sslip.io

+

Operational Status: GitHub Actions [Status]

+

nip.io and sslip.io are a DNS (Domain Name System) + service that, when queried with a hostname with an embedded IP address, returns that IP address. It was inspired + by xip.io, which was created by Sam + Stephenson.

+

Here are some examples (the domains nip.io and sslip.io are interchangeable):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Hostname / URLIP AddressNotes
+ https://52.0.56.137.sslip.io + 52.0.56.137dot separators, sslip.io website mirror (IPv4)
+ https://52-0-56-137.sslip.io + 52.0.56.137dash separators, sslip.io website mirror (IPv4)
www.192.168.0.1.sslip.io192.168.0.1subdomain
www.192-168-0-1.sslip.io192.168.0.1subdomain + dashes
+ https://www-78-46-204-247.sslip.io + 78.46.204.247dash prefix, sslip.io website mirror (IPv4)
--1.sslip.io::1IPv6 — always use dashes, never dots
+ https://2a01-4f8-c17-b8f--2.sslip.io + 2a01:4f8:c17:b8f::2sslip.io website mirror (IPv6)
+ https://334B3513.nip.io/ + 51.75.53.19sslip.io website mirror (hexadecimal notation)
+ +

Branding / White Label / Custom Domains

+

sslip.io can be used to brand your own site (you don’t need to use the sslip.io domain). For example, say you + own the domain “example.com”, and you want your subdomain, “xip.example.com” to have xip.io-style features. To + accomplish this, set the following three DNS servers as NS records for the subdomain “xip.example.com”

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
hostnameIP addressLocation
ns-do-sg.sslip.io.146.190.110.69
+ 2400:6180:0:d2:0:1:da21:d000
Singapore
ns-hetzner.sslip.io.5.78.115.44
+ 2a01:4ff:1f0:c920::
USA
ns-ovh.sslip.io.51.75.53.19
+ 2001:41d0:602:2313::1
Poland
+

Let’s test it from the command line using dig:

+
dig @ns-ovh.sslip.io. 169-254-169-254.xip.example.com +short
+

Yields, hopefully: [connection timed out]

+
169.254.169.254
+

But I Want My Own DNS Server!

+

If you want to run your own DNS server, it's simple: you can compile from source or you can use one of our pre-built binaries. In the following example, we + install & run + our server within a docker container:

+
+docker run -it --rm fedora
+curl -L https://github.com/cunnie/sslip.io/releases/download/3.2.7/sslip.io-dns-server-linux-amd64 -o dns-server
+chmod +x dns-server
+./dns-server 2> dns-server.log &
+dnf install -y bind-utils
+dig @localhost 127-0-0-1.sslip.io +short # returns "127.0.0.1"
+

TLS

+

You can acquire TLS certificates for your externally-accessible hosts from certificate authorities (CAs) such + as Let's Encrypt. The easiest mechanism to acquire a certificate would be to use the HTTP-01 challenge. It requires, at + a + minimum, a web server running on your machine. The Caddy web server is + one + of the most popular examples. For example, if you had a webserver with the IP address 52.0.56.137, you could + obtain a TLS certificate for "52.0.56.137.sslip.io", or "www.52.0.56.137.sslip.io", or + "prod.www-52-0-56-137.sslip.io".

+ +

If you have procured a wildcard certificate for your branded / white label / custom sslip.io-style subdomain, + you may install it on your machines for TLS-verified connections.

+
+

When using a TLS wildcard certificate in conjunction with your branded sslip.io style subdomain, you must + use dashes not dots as separators. For example, if you have the TLS certificate for + *.xip.example.com, you could browse to https://www-52-0-56-137.xip.example.com/ but not + https://www.52.0.56.137.xip.example.com/. +

+
+

if you're interested in acquiring a wildcard certificate for your sslip.io domain, e.g. + "*.52-0-56-137.sslip.io", the procedure is described here.

+

Experimental Features

+

Experimental features can change; don't depend on them.

+

Determining Your External IP Address via DNS Lookup

+

You can use sslip.io's DNS servers (ns.sslip.io) to determine your public IP address by querying + the TXT record of ip.sslip.io:

+
+dig @ns.sslip.io txt ip.sslip.io +short    # sample reply "2607:fb90:464:ae1e:ed60:29c:884c:4b52"
+dig @ns.sslip.io txt ip.sslip.io +short -4 # forces IPv4 lookup; sample reply "172.58.35.231"
+dig @ns.sslip.io txt ip.sslip.io +short -6 # forces IPv6 lookup; sample reply "2607:fb90:464:ae1e:ed60:29c:884c:4b52"
+ +

This feature was inspired by Google's DNS lookup, i.e. dig txt o-o.myaddr.l.google.com @8.8.8.8 + +short. There are also popular HTTP-based services for determining your public IP address:

+ +

A big advantage of using DNS queries instead of HTTP queries is bandwidth: querying + ns-ovh.sslip.io requires a mere 594 bytes spread over 2 packets; Querying https://icanhazip.com/ requires 8692 bytes spread out over 34 packets—over + 14 times + as much! Admittedly bandwidth usage is a bigger concern for the one hosting the service than the one using the + service. +

+

Determining The Server Version of Software

You can determine the server version of the + sslip.io software by querying the TXT record of version.status.sslip.io: +
+dig @ns-ovh.nono.io version.status.sslip.io txt +short
+  "2.7.0"
+  "2023/10/04-18:51:49-0700"
+  "8f7f2df"
+
+

The first number, ("2.6.1"), is the version of the sslip.io DNS software, and is most relevant. The other two + numbers are the date compiled and the most recent git hash, but those values can differ across servers due to + the + manner in which the software is deployed.

+

Server Metrics

You can retrieve metrics from a given server by querying the TXT records of + metrics.status.sslip.io +
+dig @ns-ovh.sslip.io metrics.status.sslip.io txt +short
+  "Uptime: 165655"
+  "Blocklist: 2023-10-04 07:37:50-07 3,6"
+  "Queries: 14295231 (86.3/s)"
+  "TCP/UDP: 5231/14290000"
+  "Answer > 0: 4872793 (29.4/s)"
+  "A: 4025711"
+  "AAAA: 247215"
+  "TXT Source: 57"
+  "TXT Version: 24"
+  "PTR IPv4/IPv6: 318/22"
+  "NS DNS-01: 135"
+  "Blocked: 175"
+      
+
Explanation of Metrics
+
+
Uptime
+
The time since the DNS server has been started, in seconds
+
Blocklist
+
+ The first value ("2023-10-04 07:37:50-07") is the date the blocklist was last downloaded. The following two + numbers are the number of string matches that are blocked (e.g. "raiffeisen" is a string that is blocked if + it appears in the queried hostname) and the number of CIDR matches that are blocked (e.g. "43.134.66.67/24" + is blocked). The blocklist can be found here +
+
Queries
+
This consists of two numbers: The first is the raw number of DNS queries that the server has responded to + since starting operation, and the second is the first number divided by the uptime (i.e. queries/second)
+
TCP/UDP
+
This is the number of queries received on the TCP protocol versus the UDP protocol. The sum should equal + the number of queries. DNS typically uses the UDP protocol
+
Answer > 0
+
This consists of two numbers: the first is the number of queries we responded to with at least one record + in the answer section, and the second is the first number divided by the uptime (i.e. queries/second). Note + that the number of responses with an answer record is typically a fourth the size of the overall responses. + This is normal. One reason for this disparity is that often both the IPv4 (A) and IPv6 (AAAA) records will be + checked, but only one reply will have a record in the answer section . For example, browsing to + "127.0.0.1.sslip.io" generates two lookups, one with an answer (IPv4), and one without (IPv6). Another reason + is that lookups follow a chain, e.g. looking up "127.0.0.1.sslip.io" may generate up to four queries for A + records ("1.sslip.io", "0.1.sslip.io", "0.0.1.sslip.io" and "127.0.0.1.sslip.io"), only the last of which + returns a record in the answer section. Pro-tip: if you want to shave milliseconds off name resolution, use + dashes not dots in your hostname (e.g. "10-9-9-30.sslip.io" instead of "10.9.9.30.sslip.io")
+
A
+
The number of responses which included an A (IPv4) record in the answer section since starting operation + (e.g. "dig 127.0.0.1.sslip.io")
+
AAAA
+
The number of responses which included an AAAA (IPv6) record in the answer section since starting operation + (e.g. "dig --1.sslip.io aaaa")
+
TXT Source
+
The number of responses which included a TXT record of the querier's IP address since starting operation + (e.g. "dig @ns.sslip.io ip.sslip.io txt")
+
TXT Version
+
The number of responses which included a TXT record of the DNS's servers version since starting operation + (e.g. "dig @ns-hetzner.sslip.io version.status.sslip.io txt")
+
PTR IPv4/IPv6
+
This consists of two numbers; the first is the number of responses to IPv4 PTR queries + (1.0.0.127.in-addr.arpa.127-0-0-1.sslip.io.), the second, IPv6 PTR queries
+
NS DNS-01
+
The number of responses which included a delegation of the NS (name server) to satisfy a certificate + authority's DNS-01 challenge. This lookup is used for generating wildcard certificates from Let's Encrypt and + other certificate authority. Technically this is not a "successful" query in that we don't return a record in + the ANSWER section, but we do return an NS record in the AUTHORITY section. (e.g. "dig @ns-ovh.sslip.io + _acme-challenge.192.168.0.1.sslip.io. soa")
+
+ +
    +
  • + xip.io: the inspiration for sslip.io. Sadly, this appears to be no longer + maintained after Sam Stephenson left + Basecamp. +
  • +
  • + nip.io: formerly a separate service, but now incorporated into sslip.io. The + service previously used PowerDNS combined with a backend written in elegant Python. +
  • +
  • + Let's Encrypt: A Certificate Authority providing TLS certificates; + they have never failed to increase our rate limits when asked. If you can, donate. +
  • +
+
+

Footnotes

+

[Status] A status of “build failing” rarely means the system is failing. It’s + more often an indication that when the servers were last checked (currently every six hours), the CI (continuous + integration) server had difficulty reaching one + of the three sslip.io name servers. That’s normal. [connection timed + out]

+

[connection timed out]

+

DNS runs over UDP which has no guaranteed + delivery, and it’s not uncommon for the packets to get lost in transmission. DNS clients are programmed to + seamlessly query a different server when that happens. That’s why DNS, by fiat, requires at least two name + servers (for redundancy). From IETF (Internet Engineering Task + Force) RFC (Request for Comment) 1034:

+
+

A given zone will be available from several name servers to insure its availability in spite of host or + communication link failure. By administrative fiat, we require every zone to be available on at least two + servers, and many zones have more redundancy than that.

+
+
+
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s/document_root_sslip.io/phishing.html b/k8s/document_root_sslip.io/phishing.html new file mode 100644 index 000000000..e4d4ab129 --- /dev/null +++ b/k8s/document_root_sslip.io/phishing.html @@ -0,0 +1,33 @@ + + + + + Blocked Site! + + +

This Site Has Been Blocked!

+

We have identified the site you tried to visit, , as either a scam site, a phishing site, or + unauthorized copy of an existing site.

+

If you own this site and feel that it's been wrongly identified, please do the following:

+
    +
  • disable indexing of your site; disable indexing by either including the X-Robots-Tag: + noindex in your HTTP headers or include a robots.txt at the root of your website with the + following contents:
    + User-agent: *
    + Disallow: /
  • +
  • Open a GitHub issue & explain why your site + should be unblocked. +
  • +
+

We apologize in advance for accidentally blocking a legitimate site; we try our best.

+ + + diff --git a/k8s/entrypoint-ntp.sh b/k8s/entrypoint-ntp.sh new file mode 100755 index 000000000..9287d92ea --- /dev/null +++ b/k8s/entrypoint-ntp.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if [ -z "${NTP_CONF_FILE}" ] +then + NTP_CONF_FILE="/etc/ntpd.conf" +fi +ntpd -v -d -s -f ${NTP_CONF_FILE} diff --git a/main.go b/main.go new file mode 100644 index 000000000..743591261 --- /dev/null +++ b/main.go @@ -0,0 +1,296 @@ +package main + +import ( + "encoding/binary" + "errors" + "flag" + "log" + "net" + "os" + "runtime" + "strings" + "syscall" + "xip/xip" +) + +func main() { + var blocklistURL = flag.String("blocklistURL", + "https://raw.githubusercontent.com/cunnie/sslip.io/main/etc/blocklist.txt", + `URL containing a list of non-resolvable IPs/names/CIDRs, usually phishing or scamming sites. Example "file://etc/blocklist.txt"`) + var nameservers = flag.String("nameservers", "ns-do-sg.sslip.io.,ns-gce.sslip.io.,ns-hetzner.sslip.io.,ns-ovh.sslip.io.", + "comma-separated list of FQDNs of nameservers. If you're running your own sslip.io nameservers, set them here") + var addresses = flag.String("addresses", + "nip.io=78.46.204.247,"+ + "sslip.io=78.46.204.247,"+ + "nip.io=2a01:4f8:c17:b8f::2,"+ + "sslip.io=2a01:4f8:c17:b8f::2,"+ + "ns.sslip.io=146.190.110.69,"+ + "ns.sslip.io=2400:6180:0:d2:0:1:da21:d000,"+ + "ns.sslip.io=104.155.144.4,"+ + "ns.sslip.io=2600:1900:4000:4d12::,"+ + "ns.sslip.io=5.78.115.44,"+ + "ns.sslip.io=2a01:4ff:1f0:c920::,"+ + "ns.sslip.io=51.75.53.19,"+ + "ns.sslip.io=2001:41d0:602:2313::1,"+ + "blocked.sslip.io=52.0.56.137,"+ + "blocked.sslip.io=2600:1f18:aaf:6900::a,"+ + "ns-do-sg.sslip.io=146.190.110.69,"+ + "ns-do-sg.sslip.io=2400:6180:0:d2:0:1:da21:d000,"+ + "ns-gce.sslip.io=104.155.144.4,"+ + "ns-gce.sslip.io=2600:1900:4000:4d12::,"+ + "ns-hetzner.sslip.io=5.78.115.44,"+ + "ns-hetzner.sslip.io=2a01:4ff:1f0:c920::,"+ + "ns-ovh.sslip.io=51.75.53.19,"+ + "ns-ovh.sslip.io=2001:41d0:602:2313::1,"+ + "ns1.nip.io=51.75.53.19,"+ + "ns1.nip.io=2001:41d0:602:2313::1,"+ + "ns2.nip.io=5.78.115.44,"+ + "ns2.nip.io=2a01:4ff:1f0:c920::,", + "comma-separated list of hosts and corresponding IPv4 and/or IPv6 address(es). If you're running your own sslip.io nameservers, add their hostnames and addresses here. If unsure, add to the list rather than replace") + var delegates = flag.String("delegates", "", "comma-separated list of domains you own "+ + "and nameservers you control to which to delegate, often used to acquire wildcard certificates from "+ + "Let's Encrypt via DNS challenge. Example: "+ + `-delegates=_acme-challenge.73-189-219-4.xip.nono.io=ns-437.awsdns-54.com.,_acme-challenge.73-189-219-4.xip.nono.io=ns-1097.awsdns-09.org."`) + var bindPort = flag.Int("port", 53, "port the DNS server should bind to") + var quiet = flag.Bool("quiet", false, "suppresses logging of each DNS response. Use this to avoid Google Cloud charging you $30/month to retain the logs of your GKE-based sslip.io server") + var public = flag.Bool("public", true, "allows resolution of public IP addresses. If false, only resolves private IPs including localhost (127/8, ::1), link-local (169.254/16, fe80::/10), CG-NAT (100.64/12), private (10/8, 172.16/12, 192.168/16, fc/7). Set to false if you don't want miscreants impersonating you via public IPs. If unsure, set to false") + flag.Parse() + log.Printf("%s version %s starting", os.Args[0], xip.VersionSemantic) + log.Printf("blocklist URL: %s, name servers: %s, bind port: %d, quiet: %t", + *blocklistURL, *nameservers, *bindPort, *quiet) + + x, logmessages := xip.NewXip(*blocklistURL, strings.Split(*nameservers, ","), strings.Split(*addresses, ","), strings.Split(*delegates, ",")) + x.Public = *public + for _, logmessage := range logmessages { + log.Println(logmessage) + } + + var udpConns []*net.UDPConn + var tcpListeners []*net.TCPListener + var unboundUDPIPs []string + var unboundTCPIPs []string + udpConn, err := net.ListenUDP("udp", &net.UDPAddr{Port: *bindPort}) + switch { + case err == nil: // success! We've bound to all interfaces + udpConns = append(udpConns, udpConn) + case isErrorPermissionsError(err): + log.Printf("Try invoking me with `sudo` because I don't have permission to bind to UDP port %d.\n", *bindPort) + log.Fatal(err.Error()) + case isErrorAddressAlreadyInUse(err): + log.Printf("I couldn't bind via UDP to \"[::]:%d\" (INADDR_ANY, all interfaces), so I'll try to bind to each address individually.\n", *bindPort) + udpConns, unboundUDPIPs = bindUDPAddressesIndividually(*bindPort) + if len(unboundUDPIPs) > 0 { + log.Printf(`I couldn't bind via UDP to the following IPs: "%s"`, strings.Join(unboundUDPIPs, `", "`)) + } + default: + log.Fatal(err.Error()) + } + tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: *bindPort}) + switch { + case err == nil: // success! We've bound to all interfaces + tcpListeners = append(tcpListeners, tcpListener) + case isErrorPermissionsError(err): // unnecessary because it should've bombed out earlier when attempting to bind UDP + log.Printf("Try invoking me with `sudo` because I don't have permission to bind to TCP port %d.\n", *bindPort) + log.Println(err.Error()) + case isErrorAddressAlreadyInUse(err): + log.Printf("I couldn't bind via TCP to \"[::]:%d\" (INADDR_ANY, all interfaces), so I'll try to bind to each address individually.\n", *bindPort) + tcpListeners, unboundTCPIPs = bindTCPAddressesIndividually(*bindPort) + if len(unboundTCPIPs) > 0 { + log.Printf(`I couldn't bind via TCP to the following IPs: "%s"`, strings.Join(unboundTCPIPs, `", "`)) + } + default: + log.Println(err.Error()) // Unlike UDP, we don't exit on TCP errors, we merely log + } + if len(tcpListeners) == 0 { + // unlike UDP failure to bind, we don't exit because TCP is optional, UDP, mandatory + log.Printf("I couldn't bind via TCP to any IPs on port %d", *bindPort) + } + + // Log the list of IPs that we've bound to because it helps troubleshooting + var boundUDPIPs []string + for _, udpConn := range udpConns { + boundUDPIPs = append(boundUDPIPs, udpConn.LocalAddr().String()) + } + log.Printf(`I bound via UDP to the following IPs: "%s"`, strings.Join(boundUDPIPs, `", "`)) + var boundTCPIPs []string + for _, tcpListener := range tcpListeners { + boundTCPIPs = append(boundTCPIPs, tcpListener.Addr().String()) + } + log.Printf(`I bound via TCP to the following IPs: "%s"`, strings.Join(boundTCPIPs, `", "`)) + + if len(udpConns) == 0 { // couldn't bind to UDP anywhere? exit + log.Fatalf("I couldn't bind via UDP to any IPs on port %d, so I'm exiting", *bindPort) + } + if len(tcpListeners) == 0 { // couldn't bind to TCP anywhere? don't exit; TCP is optional + log.Printf("I couldn't bind via TCP to any IPs on port %d", *bindPort) + } + + // Read from the UDP connections & TCP Listeners + // use goroutines to read from all the UDP connections EXCEPT the first; we don't use a goroutine for that + // one because we use the first one to keep this program from exiting + for _, udpConn := range udpConns[1:] { + go readFromUDP(udpConn, x, *quiet) + } + for _, tcpListener := range tcpListeners { + go readFromTCP(tcpListener, x, *quiet) + } + log.Printf("Ready to answer queries") + readFromUDP(udpConns[0], x, *quiet) // refrain from exiting; There should always be a udpConns[0], and readFromUDP() _never_ returns +} + +func readFromUDP(conn *net.UDPConn, x *xip.Xip, quiet bool) { + for { + query := make([]byte, 512) + _, addr, err := conn.ReadFromUDP(query) + if err != nil { + log.Println(err.Error()) + continue + } + go func() { + response, logMessage, err := x.QueryResponse(query, addr.IP) + if err != nil { + log.Println(err.Error()) + return + } + _, err = conn.WriteToUDP(response, addr) + if err != nil { + log.Println(err.Error()) + return + } + if !quiet { + log.Printf("%v.%d %s", addr.IP, addr.Port, logMessage) + } + x.Metrics.UDPQueries += 1 + }() + } +} + +func readFromTCP(tcpListener *net.TCPListener, x *xip.Xip, quiet bool) { + for { + query := make([]byte, 65535) // 2-byte length field means largest size is 65535 + tcpConn, err := tcpListener.AcceptTCP() + if err != nil { + log.Println(err.Error()) + continue + } + _, err = tcpConn.Read(query) + query = query[2:] // remove the 2-byte length at the beginning of the query + if err != nil { + log.Println(err.Error()) + continue + } + remoteAddrPort := tcpConn.RemoteAddr().String() + addr, port, err := net.SplitHostPort(remoteAddrPort) + if err != nil { + log.Println(err.Error()) + continue + } + + go func() { + defer func(tcpConn *net.TCPConn) { + _ = tcpConn.Close() + }(tcpConn) + response, logMessage, err := x.QueryResponse(query, net.ParseIP(addr)) + if err != nil { + log.Println(err.Error()) + return + } + // insert the 2-byte length to the beginning of the response + responseSize := uint16(len(response)) + responseSizeBigEndianBytes := make([]byte, 2) + binary.BigEndian.PutUint16(responseSizeBigEndianBytes, responseSize) + response = append(responseSizeBigEndianBytes, response...) + _, err = tcpConn.Write(response) + if err != nil { + log.Println(err.Error()) + return + } + if !quiet { + log.Printf("%s.%s %s", addr, port, logMessage) + } + x.Metrics.TCPQueries += 1 + }() + } +} + +func bindUDPAddressesIndividually(bindPort int) (udpConns []*net.UDPConn, unboundIPs []string) { + // typical value of net.Addr.String() → "::1/128" "172.19.0.17/23" + // (don't worry about the port numbers in https://pkg.go.dev/net#Addr; they won't appear) + interfaceAddrs, err := net.InterfaceAddrs() + if err != nil { + log.Printf(`I couldn't get the local interface addresses: "%s"`, err.Error()) + return nil, nil + } + for _, interfaceAddr := range interfaceAddrs { + ip, _, err := net.ParseCIDR(interfaceAddr.String()) + if err != nil { + log.Printf(`I couldn't parse the local interface "%s".`, interfaceAddr.String()) + continue + } + udpConn, err := net.ListenUDP("udp", &net.UDPAddr{ + IP: ip, + Port: bindPort, + Zone: "", + }) + if err != nil { + unboundIPs = append(unboundIPs, ip.String()) + } else { + udpConns = append(udpConns, udpConn) + } + } + return udpConns, unboundIPs +} + +func bindTCPAddressesIndividually(bindPort int) (tcpListeners []*net.TCPListener, unboundIPs []string) { + // typical value of net.Addr.String() → "::1/128" "172.19.0.17/23" + // (don't worry about the port numbers in https://pkg.go.dev/net#Addr; they won't appear) + interfaceAddrs, err := net.InterfaceAddrs() + if err != nil { + log.Printf(`I couldn't get the local interface addresses: "%s"`, err.Error()) + return nil, nil + } + for _, interfaceAddr := range interfaceAddrs { + ip, _, err := net.ParseCIDR(interfaceAddr.String()) + if err != nil { + log.Printf(`I couldn't parse the local interface "%s".`, interfaceAddr.String()) + continue + } + listener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: ip, Port: bindPort}) + if err != nil { + unboundIPs = append(unboundIPs, ip.String()) + } else { + tcpListeners = append(tcpListeners, listener) + } + } + return tcpListeners, unboundIPs +} + +// Thanks https://stackoverflow.com/a/52152912/2510873 +func isErrorAddressAlreadyInUse(err error) bool { + var eOsSyscall *os.SyscallError + if !errors.As(err, &eOsSyscall) { + return false + } + var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr) + if !errors.As(eOsSyscall, &errErrno) { + return false + } + if errors.Is(errErrno, syscall.EADDRINUSE) { + return true + } + const WSAEADDRINUSE = 10048 + if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE { + return true + } + return false +} + +func isErrorPermissionsError(err error) bool { + var eOsSyscall *os.SyscallError + if errors.As(err, &eOsSyscall) { + if os.IsPermission(eOsSyscall) { + return true + } + } + return false +} diff --git a/spec/check-dns_spec.rb b/spec/check-dns_spec.rb index 0ca7e8a43..e46d0a392 100644 --- a/spec/check-dns_spec.rb +++ b/spec/check-dns_spec.rb @@ -11,25 +11,18 @@ def get_whois_nameservers(domain) soa = nil whois_lines = whois_output.split(/\n+/) nameserver_lines = whois_lines.select { |line| line =~ /^Name Server:/ } - nameservers = nameserver_lines.map { |line| line.split.last.downcase } + nameservers = nameserver_lines.map { |line| line.split.last.downcase }.uniq # whois records don't have trail '.'; NS records do; add trailing '.' nameservers.map { |ns| ns << '.' } nameservers end -def idn_dig? - system("dig -h | grep idn") -end - domain = ENV['DOMAIN'] || 'example.com' +sslip_version = '4.1.0' whois_nameservers = get_whois_nameservers(domain) describe domain do soa = nil - idn_dig = `dig -h | grep idn` - dig_args = "+short" - dig_args += idn_dig? ? " +noidnin" : "" - context "when evaluating $DOMAIN (\"#{domain}\") environment variable" do let (:domain) { ENV['DOMAIN'] } @@ -46,55 +39,80 @@ def idn_dig? end whois_nameservers.each do |whois_nameserver| - it "nameserver #{whois_nameserver}'s NS records match whois's, " + - "`dig #{dig_args} ns sslip.io @#{whois_nameserver}`" do - dig_nameservers = `dig #{dig_args} ns sslip.io @#{whois_nameserver}`.split(/\n+/) - expect(dig_nameservers.sort).to eq(whois_nameservers.sort) + it "nameserver #{whois_nameserver}'s NS records include all whois nameservers #{whois_nameservers}, " + + "`dig @#{whois_nameserver} ns #{domain} +short`" do + dig_nameservers = `dig @#{whois_nameserver} ns #{domain} +short`.split(/\n+/) + expect(whois_nameservers - dig_nameservers).to be_empty end it "nameserver #{whois_nameserver}'s SOA record match" do - dig_soa = `dig #{dig_args} soa sslip.io @#{whois_nameserver}` + dig_soa = `dig @#{whois_nameserver} soa #{domain} +short` soa = soa || dig_soa expect(dig_soa).to eq(soa) end + it "nameserver #{whois_nameserver}'s has an A record" do + expect(`dig @#{whois_nameserver} a #{domain} +short`.chomp).not_to eq('') + expect($?.success?).to be true + end + + it "nameserver #{whois_nameserver}'s has an AAAA record" do + expect(`dig @#{whois_nameserver} a #{domain} +short`.chomp).not_to eq('') + expect($?.success?).to be true + end + a = [ rand(256), rand(256), rand(256), rand(256) ] - it "nameserver #{whois_nameserver} resolves #{a.join(".")}.sslip.io to #{a.join(".")}" do - expect(`dig #{dig_args} #{a.join(".") + "." + domain} @#{whois_nameserver}`.chomp).to eq(a.join(".")) + it "resolves #{a.join(".")}.#{domain} to #{a.join(".")}" do + expect(`dig @#{whois_nameserver} #{a.join(".") + "." + domain} +short`.chomp).to eq(a.join(".")) end a = [ rand(256), rand(256), rand(256), rand(256) ] - it "nameserver #{whois_nameserver} resolves #{a.join("-")}.sslip.io to #{a.join(".")}" do - expect(`dig #{dig_args} #{a.join("-") + "." + domain} @#{whois_nameserver}`.chomp).to eq(a.join(".")) + it "resolves #{a.join("-")}.#{domain} to #{a.join(".")}" do + expect(`dig @#{whois_nameserver} #{a.join("-") + "." + domain} +short`.chomp).to eq(a.join(".")) end a = [ rand(256), rand(256), rand(256), rand(256) ] b = [ ('a'..'z').to_a, ('0'..'9').to_a ].flatten.shuffle[0,8].join - it "nameserver #{whois_nameserver} resolves #{b}.#{a.join("-")}.sslip.io to #{a.join(".")}" do - expect(`dig #{dig_args} #{b}.#{a.join("-") + "." + domain} @#{whois_nameserver}`.chomp).to eq(a.join(".")) + it "resolves #{b}.#{a.join("-")}.#{domain} to #{a.join(".")}" do + expect(`dig @#{whois_nameserver} #{b}.#{a.join("-") + "." + domain} +short`.chomp).to eq(a.join(".")) end a = [ rand(256), rand(256), rand(256), rand(256) ] b = [ ('a'..'z').to_a, ('0'..'9').to_a ].flatten.shuffle[0,8].join - it "nameserver #{whois_nameserver} resolves #{a.join("-")}.#{b} to #{a.join(".")}" do - expect(`dig #{dig_args} #{a.join("-") + "." + b} @#{whois_nameserver}`.chomp).to eq(a.join(".")) + it "resolves #{a.join("-")}.#{b} to #{a.join(".")}" do + expect(`dig @#{whois_nameserver} #{a.join("-") + "." + b} +short`.chomp).to eq(a.join(".")) end # don't begin the hostname with a double-dash -- `dig` mistakes it for an argument - it "nameserver #{whois_nameserver} resolves api.--.sslip.io' to eq ::)}" do - expect(`dig #{dig_args} AAAA api.--.sslip.io @#{whois_nameserver}`.chomp).to eq("::") + it "resolves api.--.#{domain}' to eq ::)}" do + expect(`dig @#{whois_nameserver} AAAA api.--.#{domain} +short`.chomp).to eq("::") + end + + it "resolves localhost.--1.#{domain}' to eq ::1)}" do + expect(`dig @#{whois_nameserver} AAAA localhost.api.--1.#{domain} +short`.chomp).to eq("::1") + end + + it "resolves 2001-4860-4860--8888.#{domain}' to eq 2001:4860:4860::8888)}" do + expect(`dig @#{whois_nameserver} AAAA 2001-4860-4860--8888.#{domain} +short`.chomp).to eq("2001:4860:4860::8888") + end + + it "resolves 2601-646-100-69f0--24.#{domain}' to eq 2601:646:100:69f0::24)}" do + expect(`dig @#{whois_nameserver} AAAA 2601-646-100-69f0--24.#{domain} +short`.chomp).to eq("2601:646:100:69f0::24") end - it "nameserver #{whois_nameserver} resolves localhost.--1.sslip.io' to eq ::1)}" do - expect(`dig #{dig_args} AAAA localhost.api.--1.sslip.io @#{whois_nameserver}`.chomp).to eq("::1") + it "gets the expected version number, #{sslip_version}" do + expect(`dig @#{whois_nameserver} TXT version.status.#{domain} +short`).to include(sslip_version) end - it "nameserver #{whois_nameserver} resolves 2001-4860-4860--8888.sslip.io' to eq 2001:4860:4860::8888)}" do - expect(`dig #{dig_args} AAAA 2001-4860-4860--8888.sslip.io @#{whois_nameserver}`.chomp).to eq("2001:4860:4860::8888") + it "gets the source (querier's) IP address" do + # Look on my Regular Expressions, ye mighty, and despair! + expect(`dig @#{whois_nameserver} TXT ip.#{domain} +short`).to match(/^"(\d+\.\d+\.\d+\.\d+)|(([[:xdigit:]]*:){2,7}[[:xdigit:]]*)"$/) end - it "nameserver #{whois_nameserver} resolves 2601-646-100-69f0--24.sslip.io' to eq 2601:646:100:69f0::24)}" do - expect(`dig #{dig_args} AAAA 2601-646-100-69f0--24.sslip.io @#{whois_nameserver}`.chomp).to eq("2601:646:100:69f0::24") + # check the website + it "is able to reach https://#{domain} and get a valid response (2xx)" do + `curl -If https://#{domain} 2> /dev/null` + expect($?.success?).to be true end end end diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 000000000..9f11b755a --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/src/wildcard-dns-http-server/go.mod b/src/wildcard-dns-http-server/go.mod new file mode 100644 index 000000000..011d7c252 --- /dev/null +++ b/src/wildcard-dns-http-server/go.mod @@ -0,0 +1,5 @@ +module github.com/cunnie/sslip.io/src/wildcard-dns-http-server + +go 1.15 + +require golang.org/x/net v0.7.0 diff --git a/src/wildcard-dns-http-server/go.sum b/src/wildcard-dns-http-server/go.sum new file mode 100644 index 000000000..4b6f2314f --- /dev/null +++ b/src/wildcard-dns-http-server/go.sum @@ -0,0 +1,28 @@ +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/src/wildcard-dns-http-server/main.go b/src/wildcard-dns-http-server/main.go new file mode 100644 index 000000000..4e26ae296 --- /dev/null +++ b/src/wildcard-dns-http-server/main.go @@ -0,0 +1,236 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "runtime" + "strings" + "sync" + "syscall" + + "golang.org/x/net/dns/dnsmessage" +) + +var txts = []string{`Set this TXT record: curl -X POST http://localhost/update -d '{"txt":"Certificate Authority validation token"}'`} + +// Txt is for parsing the JSON POST to set the DNS TXT record +type Txt struct { + Txt string `json:"txt"` +} + +func main() { + var wg sync.WaitGroup + log.Println("DNS: starting up.") + conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: 53}) + switch { + case err == nil: + log.Println(`DNS: Successfully bound to all interfaces, port 53.`) + wg.Add(1) + go dnsServer(conn, &wg) + case isErrorPermissionsError(err): + log.Println("DNS: Try invoking me with `sudo` because I don't have permission to bind to port 53.") + log.Fatal("DNS: " + err.Error()) + case isErrorAddressAlreadyInUse(err): + log.Println(`DNS: I couldn't bind to "0.0.0.0:53" (INADDR_ANY, all interfaces), so I'll try to bind to each address individually.`) + ipCIDRs := listLocalIPCIDRs() + var boundIPsPorts, unboundIPs []string + for _, ipCIDR := range ipCIDRs { + ip, _, err := net.ParseCIDR(ipCIDR) + if err != nil { + log.Printf(`DNS: I couldn't parse the local interface "%s".`, ipCIDR) + continue + } + conn, err = net.ListenUDP("udp", &net.UDPAddr{ + IP: ip, + Port: 53, + Zone: "", + }) + if err != nil { + unboundIPs = append(unboundIPs, ip.String()) + } else { + wg.Add(1) + boundIPsPorts = append(boundIPsPorts, conn.LocalAddr().String()) + go dnsServer(conn, &wg) + } + } + if len(boundIPsPorts) > 0 { + log.Printf(`DNS: I bound to the following: "%s"`, strings.Join(boundIPsPorts, `", "`)) + } + if len(unboundIPs) > 0 { + log.Printf(`DNS: I couldn't bind to the following IPs: "%s"`, strings.Join(unboundIPs, `", "`)) + } + default: + log.Fatal("DNS: " + err.Error()) + } + wg.Add(1) + go httpServer(&wg) + wg.Wait() +} + +func dnsServer(conn *net.UDPConn, group *sync.WaitGroup) { + var query dnsmessage.Message + + defer group.Done() + queryRaw := make([]byte, 512) + for { + _, addr, err := conn.ReadFromUDP(queryRaw) + if err != nil { + log.Println("DNS: " + err.Error()) + continue + } + err = query.Unpack(queryRaw) + if err != nil { + log.Println("DNS: " + err.Error()) + continue + } + // Technically, there can be multiple questions in a DNS message; practically, there's only one + if len(query.Questions) != 1 { + log.Printf("DNS: I expected one question but got %d.\n", len(query.Questions)) + continue + } + // We only return answers to TXT queries, nothing else + if query.Questions[0].Type != dnsmessage.TypeTXT { + log.Println("DNS: I expected a question for a TypeTXT record but got a question for a " + query.Questions[0].Type.String() + " record.") + continue + } + var txtAnswers = []dnsmessage.Resource{} + for _, txt := range txts { + txtAnswers = append(txtAnswers, dnsmessage.Resource{ + Header: dnsmessage.ResourceHeader{ + Name: query.Questions[0].Name, + Type: dnsmessage.TypeTXT, + Class: dnsmessage.ClassINET, + TTL: 60, + }, + Body: &dnsmessage.TXTResource{TXT: []string{txt}}, + }) + } + reply := dnsmessage.Message{ + Header: dnsmessage.Header{ + ID: query.ID, + Response: true, + Authoritative: true, + RecursionDesired: query.RecursionDesired, + }, + Questions: query.Questions, + Answers: txtAnswers, + } + replyRaw, err := reply.Pack() + if err != nil { + log.Println("DNS: " + err.Error()) + continue + } + _, err = conn.WriteToUDP(replyRaw, addr) + if err != nil { + log.Println("DNS: " + err.Error()) + continue + } + log.Printf("DNS: %v.%d %s → \"%v\"\n", addr.IP, addr.Port, query.Questions[0].Type.String(), txts) + } +} + +func httpServer(group *sync.WaitGroup) { + defer group.Done() + log.Println("HTTP: starting up.") + http.HandleFunc("/", usageHandler) + http.HandleFunc("/update", updateTxtHandler) + log.Fatal("HTTP: " + http.ListenAndServe(":80", nil).Error()) +} + +func usageHandler(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintln(w, `Set the TXT record: curl -X POST http://localhost/update -d '{"txt":"Certificate Authority's validation token"}'`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println("HTTP: " + err.Error()) + } + log.Printf("HTTP: wrong path (%s) with method (%s).\n", r.URL.Path, r.Method) +} + +func updateTxtHandler(w http.ResponseWriter, r *http.Request) { + var err error + if r.Method != http.MethodPost { + err = errors.New("/update requires POST method, not " + r.Method + " method") + http.Error(w, err.Error(), http.StatusBadRequest) + log.Println("HTTP: " + err.Error()) + return + } + var body []byte + if body, err = ioutil.ReadAll(r.Body); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println("HTTP: " + err.Error()) + return + } + var updateTxt Txt + if err := json.Unmarshal(body, &updateTxt); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println("HTTP: " + err.Error()) + return + } + if body, err = json.Marshal(updateTxt); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println("HTTP: " + err.Error()) + return + } + if _, err = fmt.Fprintf(w, string(body)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println("HTTP: " + err.Error()) + return + } + log.Println("HTTP: Creating new TXT record \"" + updateTxt.Txt + "\".") + // this is the money shot, where we create a new DNS TXT record to what was in the POST request + txts = append(txts, updateTxt.Txt) +} +func listLocalIPCIDRs() []string { + var ifaces []net.Interface + var cidrStrings []string + var err error + if ifaces, err = net.Interfaces(); err != nil { + panic(err) + } + for _, iface := range ifaces { + var cidrs []net.Addr + if cidrs, err = iface.Addrs(); err != nil { + panic(err) + } + for _, cidr := range cidrs { + cidrStrings = append(cidrStrings, cidr.String()) + } + } + return cidrStrings +} + +// Thanks https://stackoverflow.com/a/52152912/2510873 +func isErrorAddressAlreadyInUse(err error) bool { + var eOsSyscall *os.SyscallError + if !errors.As(err, &eOsSyscall) { + return false + } + var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr) + if !errors.As(eOsSyscall, &errErrno) { + return false + } + if errErrno == syscall.EADDRINUSE { + return true + } + const WSAEADDRINUSE = 10048 + if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE { + return true + } + return false +} + +func isErrorPermissionsError(err error) bool { + var eOsSyscall *os.SyscallError + if errors.As(err, &eOsSyscall) { + if os.IsPermission(eOsSyscall) { + return true + } + } + return false +} diff --git a/testhelper/helpers.go b/testhelper/helpers.go new file mode 100644 index 000000000..23a6f1aeb --- /dev/null +++ b/testhelper/helpers.go @@ -0,0 +1,38 @@ +package testhelper + +import ( + "encoding/binary" + "math/rand" + "net" +) + +// RandomIPv6Address is used for fuzz testing +func RandomIPv6Address() net.IP { + upperHalf := make([]byte, 8) + lowerHalf := make([]byte, 8) + binary.LittleEndian.PutUint64(upperHalf, rand.Uint64()) + binary.LittleEndian.PutUint64(lowerHalf, rand.Uint64()) + ipv6 := net.IP(append(upperHalf, lowerHalf...)) + // IPv6 addrs have a lot of all-zero two-byte sections + // So we zero-out ~50% of the sections + for i := 0; i < 8; i++ { + if rand.Int()%2 == 0 { + for j := 0; j < 2; j++ { + ipv6[i*2+j] = 0 + } + } + } + // avoid pathological case: an IPv4 address []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, ?, ?, ?, ?}) + ipv6[10] &= 0xfe + return ipv6 +} + +// Random8ByteString() returns an 8-char mixed-case string consisting solely of the letters a-z. +func Random8ByteString() string { + var randomString []byte + for i := 0; i < 8; i++ { + // 65 == ascii 'A', +32 (96) == ascii 'a', there are 26 letters in the alphabet. Mix upper case, too. + randomString = append(randomString, byte(65+32*rand.Intn(2)+rand.Intn(26))) + } + return string(randomString) +} diff --git a/tools.go b/tools.go new file mode 100644 index 000000000..573351d9a --- /dev/null +++ b/tools.go @@ -0,0 +1,6 @@ +//go:generate go install github.com/onsi/ginkgo/v2/ginkgo + +package main + +// This file imports packages that are used when running go generate, or used +// during the development process but not otherwise depended on by built code. diff --git a/xip/xip.go b/xip/xip.go new file mode 100644 index 000000000..19f023932 --- /dev/null +++ b/xip/xip.go @@ -0,0 +1,1384 @@ +// Package xip provides functions to create a DNS server which, when queried +// with a hostname with an embedded IP address, returns that IP Address. It +// was inspired by xip.io, which was created by Sam Stephenson +package xip + +import ( + "bufio" + "encoding/hex" + "errors" + "fmt" + "io" + "log" + "net" + "net/http" + "net/netip" + "os" + "regexp" + "strconv" + "strings" + "time" + + "golang.org/x/net/dns/dnsmessage" +) + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate + +// Xip is meant to be a singleton that holds global state for the DNS server +type Xip struct { + DnsAmplificationAttackDelay chan struct{} // for throttling metrics.status.sslip.io + Metrics Metrics // DNS server metrics + BlocklistStrings []string // list of blacklisted strings that shouldn't appear in public hostnames + BlocklistCIDRs []net.IPNet // list of blacklisted CIDRs; no A/AAAA records should resolve to IPs in these CIDRs + BlocklistUpdated time.Time // The most recent time the Blocklist was updated + NameServers []dnsmessage.NSResource // The list of authoritative name servers (NS) + Public bool // Whether to resolve public IPs; set to false if security-conscious +} + +// Metrics contains the counters of the important/interesting queries +type Metrics struct { + Start time.Time + Queries int + TCPQueries int + UDPQueries int + AnsweredQueries int + AnsweredAQueries int + AnsweredAAAAQueries int + AnsweredTXTSrcIPQueries int + AnsweredTXTVersionQueries int + AnsweredNSDNS01ChallengeQueries int + AnsweredBlockedQueries int + AnsweredPTRQueriesIPv4 int + AnsweredPTRQueriesIPv6 int +} + +// DomainCustomization is a value that is returned for a specific query. +// The map key is the domain in question, e.g. "sslip.io." (always include trailing dot). +// For example, when querying for MX records for "sslip.io", return the protonmail servers, +// but when querying for MX records for generic queries, e.g. "127.0.0.1.sslip.io", return the +// default (which happens to be no MX records). +// +// Noticeably absent are the NS records and SOA records. They don't need to be customized +// because they are always the same, regardless of the domain being queried. +type DomainCustomization struct { + A []dnsmessage.AResource + AAAA []dnsmessage.AAAAResource + CNAME dnsmessage.CNAMEResource + MX []dnsmessage.MXResource + NS []dnsmessage.NSResource + TXT func(*Xip, net.IP) ([]dnsmessage.TXTResource, error) + // Unlike the other record types, TXT is a function in order to enable more complex behavior + // e.g. IP address of the query's source +} + +// DomainCustomizations is a lookup table for specially-crafted records +// e.g. MX records for sslip.io. +// The string key should always be lower-cased +// DomainCustomizations{"sslip.io": ...} NOT DomainCustomizations{"sSLip.iO": ...} +// DNS hostnames are technically case-insensitive +type DomainCustomizations map[string]DomainCustomization + +// There's nothing like global variables to make my heart pound with joy. +// Some of these are global because they are, in essence, constants which +// I don't want to waste time recreating with every function call. +var ( + ipv4REDots = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d))($|[.-])`) + ipv4REDashes = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?\d)?\d)-){3}(25[0-5]|(2[0-4]|1?\d)?\d))($|[.-])`) + ipv4REHex = regexp.MustCompile(`(^|\.)([[:xdigit:]]{8})($|\.)`) // no dash separators, only dots + // https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses + ipv6RE = regexp.MustCompile(`(^|[.-])(([[:xdigit:]]{1,4}-){7}[[:xdigit:]]{1,4}|([[:xdigit:]]{1,4}-){1,7}-|([[:xdigit:]]{1,4}-){1,6}-[[:xdigit:]]{1,4}|([[:xdigit:]]{1,4}-){1,5}(-[[:xdigit:]]{1,4}){1,2}|([[:xdigit:]]{1,4}-){1,4}(-[[:xdigit:]]{1,4}){1,3}|([[:xdigit:]]{1,4}-){1,3}(-[[:xdigit:]]{1,4}){1,4}|([[:xdigit:]]{1,4}-){1,2}(-[[:xdigit:]]{1,4}){1,5}|[[:xdigit:]]{1,4}-((-[[:xdigit:]]{1,4}){1,6})|-((-[[:xdigit:]]{1,4}){1,7}|-)|fe80-(-[[:xdigit:]]{0,4}){0,4}%[\da-zA-Z]+|--(ffff(-0{1,4})?-)?((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d)|([[:xdigit:]]{1,4}-){1,4}-((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d))($|[.-])`) + ipv6REHex = regexp.MustCompile(`(^|\.)([[:xdigit:]]{32})($|\.)`) // no dash separators, only dots + ipv4ReverseRE = regexp.MustCompile(`^(.*)\.in-addr\.arpa\.$`) + ipv6ReverseRE = regexp.MustCompile(`^(([[:xdigit:]]\.){32})ip6\.arpa\.`) + dns01ChallengeRE = regexp.MustCompile(`(?i)_acme-challenge\.`) // (?i) → non-capturing case insensitive + + mbox, _ = dnsmessage.NewName("briancunnie.gmail.com.") + mx1, _ = dnsmessage.NewName("mail.protonmail.ch.") + mx2, _ = dnsmessage.NewName("mailsec.protonmail.ch.") + dkim1Sslip, _ = dnsmessage.NewName("protonmail.domainkey.dw4gykv5i2brtkjglrf34wf6kbxpa5hgtmg2xqopinhgxn5axo73a.domains.proton.ch.") + dkim2Sslip, _ = dnsmessage.NewName("protonmail2.domainkey.dw4gykv5i2brtkjglrf34wf6kbxpa5hgtmg2xqopinhgxn5axo73a.domains.proton.ch.") + dkim3Sslip, _ = dnsmessage.NewName("protonmail3.domainkey.dw4gykv5i2brtkjglrf34wf6kbxpa5hgtmg2xqopinhgxn5axo73a.domains.proton.ch.") + dkim1Nip, _ = dnsmessage.NewName("protonmail.domainkey.di5fzneyjbxuzcqcrbw2f63m34itvf6lmjde2s4maty3hdt6664dq.domains.proton.ch.") + dkim2Nip, _ = dnsmessage.NewName("protonmail2.domainkey.di5fzneyjbxuzcqcrbw2f63m34itvf6lmjde2s4maty3hdt6664dq.domains.proton.ch.") + dkim3Nip, _ = dnsmessage.NewName("protonmail3.domainkey.di5fzneyjbxuzcqcrbw2f63m34itvf6lmjde2s4maty3hdt6664dq.domains.proton.ch.") + + VersionSemantic = "0.0.0" + VersionDate = "0001/01/01-99:99:99-0800" + VersionGitHash = "cafexxx" + + MetricsBufferSize = 200 // big enough to run our tests, and small enough to prevent DNS amplification attacks + + Customizations = DomainCustomizations{ + "nip.io.": { + MX: []dnsmessage.MXResource{ + { + Pref: 10, + MX: mx1, + }, + { + Pref: 20, + MX: mx2, + }, + }, + TXT: TXTNipIoSPF, + }, + "sslip.io.": { + MX: []dnsmessage.MXResource{ + { + Pref: 10, + MX: mx1, + }, + { + Pref: 20, + MX: mx2, + }, + }, + TXT: TXTSslipIoSPF, + }, + // nameserver addresses; we get queries for those every once in a while + // CNAMEs for nip.io/sslip.io for DKIM signing + "protonmail._domainkey.nip.io.": { + CNAME: dnsmessage.CNAMEResource{ + CNAME: dkim1Nip, + }, + }, + "protonmail2._domainkey.nip.io.": { + CNAME: dnsmessage.CNAMEResource{ + CNAME: dkim2Nip, + }, + }, + "protonmail3._domainkey.nip.io.": { + CNAME: dnsmessage.CNAMEResource{ + CNAME: dkim3Nip, + }, + }, + "protonmail._domainkey.sslip.io.": { + CNAME: dnsmessage.CNAMEResource{ + CNAME: dkim1Sslip, + }, + }, + "protonmail2._domainkey.sslip.io.": { + CNAME: dnsmessage.CNAMEResource{ + CNAME: dkim2Sslip, + }, + }, + "protonmail3._domainkey.sslip.io.": { + CNAME: dnsmessage.CNAMEResource{ + CNAME: dkim3Sslip, + }, + }, + // Special-purpose TXT records + "ip.sslip.io.": { + TXT: TXTIp, + }, + "version.status.sslip.io.": { + TXT: func(x *Xip, _ net.IP) ([]dnsmessage.TXTResource, error) { + x.Metrics.AnsweredTXTVersionQueries++ + return []dnsmessage.TXTResource{ + {TXT: []string{VersionSemantic}}, // e.g. "2.2.1' + {TXT: []string{VersionDate}}, // e.g. "2021/10/03-15:08:54+0100" + {TXT: []string{VersionGitHash}}, // e.g. "9339c0d" + }, nil + }, + }, + "_psl.sslip.io.": { // avoid Let's Encrypt rate limits by joining https://publicsuffix.org + TXT: func(x *Xip, _ net.IP) ([]dnsmessage.TXTResource, error) { + x.Metrics.AnsweredTXTVersionQueries++ + return []dnsmessage.TXTResource{ + {TXT: []string{"https://github.com/publicsuffix/list/pull/2206"}}, + }, nil + }, + }, + + "metrics.status.sslip.io.": { + TXT: TXTMetrics, + }, + } +) + +// Response Why do I have a crazy struct of fields of arrays of functions? +// It's because I can't use dnsmessage.Builder as I had hoped; specifically +// I need to set the Header _after_ I process the message, but Builder expects +// it to be set first, so I use the functions as a sort of batch process to +// create the Builder. What in Header needs to be tweaked? Certain TXT records +// need to unset the authoritative field, and queries for ANY record need +// to set the rcode. +type Response struct { + Header dnsmessage.Header + Answers []func(*dnsmessage.Builder) error + Authorities []func(*dnsmessage.Builder) error + Additionals []func(*dnsmessage.Builder) error +} + +// NewXip follows convention for constructors: https://go.dev/doc/effective_go#allocation_new +func NewXip(blocklistURL string, nameservers []string, addresses []string, delegates []string) (x *Xip, logmessages []string) { + x = &Xip{Metrics: Metrics{Start: time.Now()}} + + // Download the blocklist + logmessages = append(logmessages, x.downloadBlockList(blocklistURL)) + // re-download the blocklist every hour so I don't need to restart servers after updating blocklist + go func() { + for { + time.Sleep(1 * time.Hour) + _ = x.downloadBlockList(blocklistURL) // uh-oh, I lose the log message. + } + }() + + // Parse and set our nameservers + for _, ns := range nameservers { + if len(ns) == 0 { + logmessages = append(logmessages, `-nameservers: ignoring zero-length nameserver ""`) + continue + } + // all nameservers must be absolute (end in ".") + if ns[len(ns)-1] != '.' { + ns += "." + } + // nameservers must be DNS-compliant + nsName, err := dnsmessage.NewName(ns) + if err != nil { + logmessages = append(logmessages, fmt.Sprintf(`-nameservers: ignoring invalid nameserver "%s"`, ns)) + continue + } + x.NameServers = append(x.NameServers, dnsmessage.NSResource{ + NS: nsName}) + logmessages = append(logmessages, fmt.Sprintf(`Adding nameserver "%s"`, ns)) + } + // Parse and set our addresses + for _, address := range addresses { + hostAddr := strings.Split(address, "=") + if len(hostAddr) != 2 { + logmessages = append(logmessages, fmt.Sprintf(`-addresses: arguments should be in the format "host=ip", not "%s"`, address)) + continue + } + host := hostAddr[0] + ip := net.ParseIP(hostAddr[1]) + // all hosts must be absolute (end in ".") + if host[len(host)-1] != '.' { + host += "." + } + if ip == nil { // bad IP delegate + logmessages = append(logmessages, fmt.Sprintf(`-addresses: "%s" is not assigned a valid IP`, hostAddr)) + continue + } + if ip.To4() != nil { // we have an IPv4 + var ABytes [4]byte + // copy the _last_ four bytes of the 16-byte IP, not the first four bytes. Cost me 2 hours. + copy(ABytes[0:4], ip[12:]) + // Thanks https://stackoverflow.com/questions/42605337/cannot-assign-to-struct-field-in-a-map + var hostEntry = DomainCustomization{} + if _, ok := Customizations[host]; ok { + hostEntry = Customizations[host] + } + hostEntry.A = append(hostEntry.A, dnsmessage.AResource{A: ABytes}) + Customizations[host] = hostEntry + } else { + // We're pretty sure it's IPv6 at this point, but we check anyway + if ip.To16() == nil { // it's not IPv6, and I don't know what it is + logmessages = append(logmessages, fmt.Sprintf(`-addresses: "%s" is not IPv4 or IPv6 "%s"`, hostAddr, ip.String())) + continue + } + var AAAABytes [16]byte + copy(AAAABytes[0:16], ip) + // Thanks https://stackoverflow.com/questions/42605337/cannot-assign-to-struct-field-in-a-map + var hostEntry = DomainCustomization{} + if _, ok := Customizations[host]; ok { + hostEntry = Customizations[host] + } + hostEntry.AAAA = append(hostEntry.AAAA, dnsmessage.AAAAResource{AAAA: AAAABytes}) + Customizations[host] = hostEntry + } + // print out the added records in a manner similar to the way they're set on the cmdline + logmessages = append(logmessages, fmt.Sprintf(`Adding record "%s=%s"`, host, ip)) + } + // Parse and set the nameservers of our delegated domains + for _, delegate := range delegates { + if delegate == "" { // most common case: no delegates defined + continue + } + delegatedDomainAndNameserver := strings.Split(strings.ToLower(delegate), "=") + if len(delegatedDomainAndNameserver) != 2 { + logmessages = append(logmessages, fmt.Sprintf(`-delegates: arguments should be in the format "delegatedDomain=nameserver", not "%s"`, delegate)) + continue + } + delegatedDomain := delegatedDomainAndNameserver[0] + nameServer := delegatedDomainAndNameserver[1] + // all domains & nameservers must be absolute (end in ".") + if delegatedDomain[len(delegatedDomain)-1] != '.' { + delegatedDomain += "." + } + if nameServer[len(nameServer)-1] != '.' { + nameServer += "." + } + + // nameservers must be DNS-compliant + nsName, err := dnsmessage.NewName(nameServer) + if err != nil { + logmessages = append(logmessages, fmt.Sprintf(`-nameservers: ignoring invalid nameserver "%s"`, nameServer)) + continue + } + var domainEntry = DomainCustomization{} + if _, ok := Customizations[delegatedDomain]; ok { + domainEntry = Customizations[delegatedDomain] + } + domainEntry.NS = append(domainEntry.NS, dnsmessage.NSResource{NS: nsName}) + Customizations[delegatedDomain] = domainEntry + // print out the added records in a manner similar to the way they're set on the cmdline + logmessages = append(logmessages, fmt.Sprintf(`Adding delegated NS record "%s=%s"`, delegatedDomain, nsName.String())) + } + + // We want to make sure that our DNS server isn't used in a DNS amplification attack. + // The endpoint we're worried about is metrics.status.sslip.io, whose reply is + // ~400 bytes with a query of ~100 bytes (4x amplification). We accomplish this by + // using channels with a quarter-second delay. Max throughput 1.2 kBytes/sec. + // + // We want to balance this delay against our desire to run tests quickly, so we buffer + // the channel with enough room to accommodate our tests. + // + // We also want to have fun playing with channels + dnsAmplificationAttackDelay := make(chan struct{}, MetricsBufferSize) + x.DnsAmplificationAttackDelay = dnsAmplificationAttackDelay + go func() { + // fill up the channel's buffer so that our tests aren't slowed down (~85 tests) + for i := 0; i < MetricsBufferSize; i++ { + dnsAmplificationAttackDelay <- struct{}{} + } + // now put on the brakes for users trying to leverage our server in a DNS amplification attack + for { + dnsAmplificationAttackDelay <- struct{}{} + time.Sleep(250 * time.Millisecond) + } + }() + return x, logmessages +} + +// QueryResponse takes in a raw (packed) DNS query and returns a raw (packed) +// DNS response, a string (for logging) that describes the query and the +// response, and an error. It takes in the raw data to offload as much as +// possible from main(). main() is hard to unit test, but functions like +// QueryResponse are not as hard. +// +// Examples of log strings returned: +// +// 78.46.204.247.33654: TypeA 127-0-0-1.sslip.io ? 127.0.0.1 +// 78.46.204.247.33654: TypeA non-existent.sslip.io ? nil, SOA +// 78.46.204.247.33654: TypeNS www.example.com ? NS +// 78.46.204.247.33654: TypeSOA www.example.com ? SOA +// 2600::.33654: TypeAAAA --1.sslip.io ? ::1 +func (x *Xip) QueryResponse(queryBytes []byte, srcAddr net.IP) (responseBytes []byte, logMessage string, err error) { + var queryHeader dnsmessage.Header + var p dnsmessage.Parser + var response Response + + if queryHeader, err = p.Start(queryBytes); err != nil { + return nil, "", err + } + var q dnsmessage.Question + // we only answer the first question even though there technically may be more than one; + // de facto there's one and only one question + if q, err = p.Question(); err != nil { + return nil, "", err + } + response, logMessage, err = x.processQuestion(q, srcAddr) + if err != nil { + return nil, "", err + } + response.Header.ID = queryHeader.ID + response.Header.RecursionDesired = queryHeader.RecursionDesired + x.Metrics.Queries++ + + b := dnsmessage.NewBuilder(nil, response.Header) + b.EnableCompression() + if err = b.StartQuestions(); err != nil { + return nil, "", err + } + if err = b.Question(q); err != nil { + return + } + if err = b.StartAnswers(); err != nil { + return nil, "", err + } + for _, answer := range response.Answers { + if err = answer(&b); err != nil { + return nil, "", err + } + } + if err = b.StartAuthorities(); err != nil { + return nil, "", err + } + for _, authority := range response.Authorities { + if err = authority(&b); err != nil { + return nil, "", err + } + } + if err = b.StartAdditionals(); err != nil { + return nil, "", err + } + for _, additionals := range response.Additionals { + if err = additionals(&b); err != nil { + return nil, "", err + } + } + if responseBytes, err = b.Finish(); err != nil { + return nil, "", err + } + return responseBytes, logMessage, nil +} + +func (x *Xip) processQuestion(q dnsmessage.Question, srcAddr net.IP) (response Response, logMessage string, err error) { + logMessage = q.Type.String() + " " + q.Name.String() + " ? " + response = Response{ + Header: dnsmessage.Header{ + ID: 0, // this will later be replaced with query.ID + Response: true, + OpCode: 0, + Authoritative: true, // We're able to white label domains by always being authoritative + Truncated: false, + RecursionDesired: false, // this will later be replaced with query.RecursionDesired + RecursionAvailable: false, // We are not recursing servers, so recursion is never available. Prevents DDOS + RCode: dnsmessage.RCodeSuccess, // assume success, may be replaced later + }, + } + if IsDelegated(q.Name.String()) { + // if xip.pivotal.io has been delegated to ns-437.awsdns-54.com. + // and a query comes in for 127-0-0-1.cloudfoundry.xip.pivotal.io + // then don't resolve the A record; instead, return the delegated + // NS record, ns-437.awsdns-54.com. + response.Header.Authoritative = false + return x.NSResponse(q.Name, response, logMessage) + } + if IsAcmeChallenge(q.Name.String()) && !x.blocklist(q.Name.String()) { + // thanks, @NormanR + // delegate everything to its stripped (remove "_acme-challenge.") address, e.g. + // dig _acme-challenge.127-0-0-1.sslip.io mx → NS 127-0-0-1.sslip.io + response.Header.Authoritative = false + return x.NSResponse(q.Name, response, logMessage) + } + switch q.Type { + case dnsmessage.TypeA: + { + return x.nameToAwithBlocklist(q, response, logMessage) + } + case dnsmessage.TypeAAAA: + { + return x.nameToAAAAwithBlocklist(q, response, logMessage) + } + case dnsmessage.TypeALL: + { + // We don't implement type ANY, so return "NotImplemented" like CloudFlare (1.1.1.1) + // https://blog.cloudflare.com/rfc8482-saying-goodbye-to-any/ + // Google (8.8.8.8) returns every record they can find (A, AAAA, SOA, NS, MX, ...). + response.Header.RCode = dnsmessage.RCodeNotImplemented + return response, logMessage + "NotImplemented", nil + } + case dnsmessage.TypeCNAME: + { + // If there is a CNAME, there can only be 1, and only from Customizations + cname := CNAMEResource(q.Name.String()) + if cname == nil { + // No Answers, only 1 Authorities + soaHeader, soaResource := SOAAuthority(q.Name) + response.Authorities = append(response.Authorities, + func(b *dnsmessage.Builder) error { + if err = b.SOAResource(soaHeader, soaResource); err != nil { + return err + } + return nil + }) + return response, logMessage + "nil, SOA " + soaLogMessage(soaResource), nil + } + x.Metrics.AnsweredQueries++ + response.Answers = append(response.Answers, + // 1 CNAME record, via Customizations + func(b *dnsmessage.Builder) error { + err = b.CNAMEResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeCNAME, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, *cname) + if err != nil { + return err + } + return nil + }) + return response, logMessage + cname.CNAME.String(), nil + } + case dnsmessage.TypeMX: + { + mailExchangers := MXResources(q.Name.String()) + var logMessages []string + + // We can be sure that len(mailExchangers) > 1, but we check anyway + if len(mailExchangers) == 0 { + return response, "", errors.New("no MX records, but there should be one") + } + x.Metrics.AnsweredQueries++ + response.Answers = append(response.Answers, + // 1 or more A records; A records > 1 only available via Customizations + func(b *dnsmessage.Builder) error { + for _, mailExchanger := range mailExchangers { + err = b.MXResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeMX, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, mailExchanger) + } + if err != nil { + return err + } + return nil + }) + for _, mailExchanger := range mailExchangers { + logMessages = append(logMessages, strconv.Itoa(int(mailExchanger.Pref))+" "+mailExchanger.MX.String()) + } + return response, logMessage + strings.Join(logMessages, ", "), nil + } + case dnsmessage.TypeNS: + { + return x.NSResponse(q.Name, response, logMessage) + } + case dnsmessage.TypeSOA: + { + x.Metrics.AnsweredQueries++ + soaResource := SOAResource(q.Name) + response.Answers = append(response.Answers, + func(b *dnsmessage.Builder) error { + err = b.SOAResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeSOA, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, soaResource) + if err != nil { + return err + } + return nil + }) + return response, logMessage + soaLogMessage(soaResource), nil + } + case dnsmessage.TypeTXT: + { + // if it's an "_acme-challenge." TXT, we return no answer but an NS authority & not authoritative + // if it's customized records, we return them in the Answers + // otherwise we return no Answers and Authorities SOA + if IsAcmeChallenge(q.Name.String()) { + // No Answers, Not Authoritative, Authorities contain NS records + response.Header.Authoritative = false + nameServers := x.NSResources(q.Name.String()) + var logMessages []string + for _, nameServer := range nameServers { + response.Authorities = append(response.Authorities, + // 1 or more A records; A records > 1 only available via Customizations + func(b *dnsmessage.Builder) error { + err = b.NSResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeNS, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, nameServer) + if err != nil { + return err + } + return nil + }) + logMessages = append(logMessages, nameServer.NS.String()) + } + return response, logMessage + "nil, NS " + strings.Join(logMessages, ", "), nil + } + var txts []dnsmessage.TXTResource + txts, err = x.TXTResources(q.Name.String(), srcAddr) + if err != nil { + return response, "", err + } + if len(txts) > 0 { + x.Metrics.AnsweredQueries++ + } + response.Answers = append(response.Answers, + // 1 or more TXT records via Customizations + // Technically there can be more than one TXT record, but practically there can only be one record + // but with multiple strings + func(b *dnsmessage.Builder) error { + for _, txt := range txts { + err = b.TXTResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeTXT, + Class: dnsmessage.ClassINET, + TTL: 180, // 3 minutes to allow key-value to propagate + Length: 0, + }, txt) + if err != nil { + return err + } + } + return nil + }) + var logMessageTXTss []string + for _, txt := range txts { + var logMessageTXTs []string + logMessageTXTs = append(logMessageTXTs, txt.TXT...) + logMessageTXTss = append(logMessageTXTss, `["`+strings.Join(logMessageTXTs, `", "`)+`"]`) + } + if len(logMessageTXTss) == 0 { + return response, logMessage + "nil, SOA " + soaLogMessage(SOAResource(q.Name)), nil + } + return response, logMessage + strings.Join(logMessageTXTss, ", "), nil + } + case dnsmessage.TypePTR: + { + ptr := x.PTRResource([]byte(q.Name.String())) + if ptr == nil { + // No Answers, only 1 Authorities + soaHeader, soaResource := SOAAuthority(dnsmessage.MustNewName("sslip.io.")) + response.Authorities = append(response.Authorities, + func(b *dnsmessage.Builder) error { + if err = b.SOAResource(soaHeader, soaResource); err != nil { + return err + } + return nil + }) + return response, logMessage + "nil, SOA " + soaLogMessage(soaResource), nil + } + //x.Metrics.AnsweredQueries++ + response.Answers = append(response.Answers, + // 1 CNAME record, via Customizations + func(b *dnsmessage.Builder) error { + err = b.PTRResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypePTR, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, *ptr) + if err != nil { + return err + } + return nil + }) + return response, logMessage + ptr.PTR.String(), nil + } + default: + { + // default is the same case as an A/AAAA record which is not found, + // i.e. we return no answers, but we return an authority section + // No Answers, only 1 Authorities + soaHeader, soaResource := SOAAuthority(q.Name) + response.Authorities = append(response.Authorities, + func(b *dnsmessage.Builder) error { + if err = b.SOAResource(soaHeader, soaResource); err != nil { + return err + } + return nil + }) + return response, logMessage + "nil, SOA " + soaLogMessage(soaResource), nil + } + } +} + +// NSResponse sets the Answers/Authorities depending upon whether we're delegating or authoritative +func (x *Xip) NSResponse(name dnsmessage.Name, response Response, logMessage string) (Response, string, error) { + nameServers := x.NSResources(name.String()) + var logMessages []string + if response.Header.Authoritative { + // we're authoritative, so we reply with the answers + // but we rotate the nameservers every second so one server doesn't bear the brunt of the traffic + epoch := time.Now().UTC().Unix() + index := int(epoch) % len(x.NameServers) + rotatedNameservers := append(x.NameServers[index:], x.NameServers[0:index]...) + response.Answers = append(response.Answers, + func(b *dnsmessage.Builder) error { + return buildNSRecords(b, name, rotatedNameservers) + }) + } else { + // we're NOT authoritative, so we reply who is authoritative + response.Authorities = append(response.Authorities, + func(b *dnsmessage.Builder) error { + return buildNSRecords(b, name, nameServers) + }) + logMessage += "nil, NS " // we're not supplying an answer; we're supplying the NS record that's authoritative + } + response.Additionals = append(response.Additionals, + func(b *dnsmessage.Builder) error { + for _, nameServer := range nameServers { + for _, aResource := range NameToA(nameServer.NS.String(), true) { + err := b.AResource(dnsmessage.ResourceHeader{ + Name: nameServer.NS, + Type: dnsmessage.TypeA, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, aResource) + if err != nil { + return err + } + } + for _, aaaaResource := range NameToAAAA(nameServer.NS.String(), true) { + err := b.AAAAResource(dnsmessage.ResourceHeader{ + Name: nameServer.NS, + Type: dnsmessage.TypeAAAA, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, aaaaResource) + if err != nil { + return err + } + } + } + return nil + }) + for _, nameServer := range nameServers { + logMessages = append(logMessages, nameServer.NS.String()) + } + return response, logMessage + strings.Join(logMessages, ", "), nil +} + +func buildNSRecords(b *dnsmessage.Builder, name dnsmessage.Name, nameServers []dnsmessage.NSResource) error { + for _, nameServer := range nameServers { + err := b.NSResource(dnsmessage.ResourceHeader{ + Name: name, + Type: dnsmessage.TypeNS, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, nameServer) + if err != nil { + return err + } + } + return nil +} + +// NameToA returns an []AResource that matched the hostname; it returns an array of zero-or-one records +// possibly more if it's a customized record (e.g. the addresses of "ns.sslip.io.") +// if "allowPublicIPs" is false, and the IP address is public, it'll return an empty array +func NameToA(fqdnString string, allowPublicIPs bool) []dnsmessage.AResource { + fqdn := []byte(fqdnString) + // is it a customized A record? If so, return early + if domain, ok := Customizations[strings.ToLower(fqdnString)]; ok && len(domain.A) > 0 { + return domain.A + } + for _, ipv4RE := range []*regexp.Regexp{ipv4REDashes, ipv4REDots} { + if ipv4RE.Match(fqdn) { + match := string(ipv4RE.FindSubmatch(fqdn)[2]) + match = strings.Replace(match, "-", ".", -1) + ipv4address := net.ParseIP(match).To4() + // We shouldn't reach here because `match` should always be valid, but we're not optimists + if ipv4address == nil { + // e.g. "ubuntu20.04.235.249.181-notify.sslip.io." <- the leading zero is the problem + log.Printf("----> Should be valid A but isn't: %s\n", fqdn) // TODO: delete this + return []dnsmessage.AResource{} + } + if (!allowPublicIPs) && IsPublic(ipv4address) { + return []dnsmessage.AResource{} + } + return []dnsmessage.AResource{ + {A: [4]byte{ipv4address[0], ipv4address[1], ipv4address[2], ipv4address[3]}}, + } + } + } + if match := ipv4REHex.FindSubmatch(fqdn); match != nil { + hexes := match[2] // strip out leading & trailing "." by using only the 2nd capture group, e.g. "7f000001" + ipBytes := make([]byte, 4) + _, err := hex.Decode(ipBytes, []byte(hexes)) + if err != nil || len(ipBytes) != 4 { + return []dnsmessage.AResource{} + } + ipv4address := net.IPv4(ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3]) + if ipv4address == nil { + return []dnsmessage.AResource{} + } + ipv4address = ipv4address.To4() + if ipv4address == nil { + return []dnsmessage.AResource{} + } + if (!allowPublicIPs) && IsPublic(ipv4address) { + return []dnsmessage.AResource{} + } + return []dnsmessage.AResource{ + {A: [4]byte{ipv4address[0], ipv4address[1], ipv4address[2], ipv4address[3]}}, + } + } + return []dnsmessage.AResource{} +} + +// NameToAAAA returns an []AAAAResource that matched the hostname; it returns an array of zero-or-one records +// possibly more if it's a customized record (e.g. the addresses of "ns.sslip.io.") +// if "allowPublicIPs" is false, and the IP address is public, it'll return an empty array +func NameToAAAA(fqdnString string, allowPublicIPs bool) []dnsmessage.AAAAResource { + fqdn := []byte(fqdnString) + // is it a customized AAAA record? If so, return early + if domain, ok := Customizations[strings.ToLower(fqdnString)]; ok && len(domain.AAAA) > 0 { + return domain.AAAA + } + if ipv6RE.Match(fqdn) { + ipv6RE.Longest() + match := string(ipv6RE.FindSubmatch(fqdn)[2]) + match = strings.Replace(match, "-", ":", -1) + ipv16address := net.ParseIP(match).To16() + if ipv16address == nil { + // We shouldn't reach here because `match` should always be valid, but we're not optimists + log.Printf("----> Should be valid AAAA but isn't: %s\n", fqdn) // TODO: delete this + return []dnsmessage.AAAAResource{} + } + if (!allowPublicIPs) && IsPublic(ipv16address) { + return []dnsmessage.AAAAResource{} + } + AAAAR := dnsmessage.AAAAResource{} + for i := range ipv16address { + AAAAR.AAAA[i] = ipv16address[i] + } + return []dnsmessage.AAAAResource{AAAAR} + } + if match := ipv6REHex.FindSubmatch(fqdn); match != nil { + hexes := match[2] // strip out leading & trailing "." by using only the 2nd capture group + ipBytes := make([]byte, 16) + _, err := hex.Decode(ipBytes, []byte(hexes)) + if err != nil || len(ipBytes) != 16 { + return []dnsmessage.AAAAResource{} + } + ipv6address := net.IP(ipBytes) + if ipv6address == nil { + return []dnsmessage.AAAAResource{} + } + if (!allowPublicIPs) && IsPublic(ipv6address) { + return []dnsmessage.AAAAResource{} + } + return []dnsmessage.AAAAResource{ + {AAAA: [16]byte{ipv6address[0], ipv6address[1], ipv6address[2], ipv6address[3], + ipv6address[4], ipv6address[5], ipv6address[6], ipv6address[7], + ipv6address[8], ipv6address[9], ipv6address[10], ipv6address[11], + ipv6address[12], ipv6address[13], ipv6address[14], ipv6address[15]}}, + } + } + return []dnsmessage.AAAAResource{} +} + +// CNAMEResource returns the CNAME via Customizations, otherwise nil +func CNAMEResource(fqdnString string) *dnsmessage.CNAMEResource { + if domain, ok := Customizations[strings.ToLower(fqdnString)]; ok && domain.CNAME != (dnsmessage.CNAMEResource{}) { + return &domain.CNAME + } + return nil +} + +// MXResources returns either 1 or more MX records set via Customizations or +// an MX record pointing to the queried record +func MXResources(fqdnString string) []dnsmessage.MXResource { + if domain, ok := Customizations[strings.ToLower(fqdnString)]; ok && len(domain.MX) > 0 { + return domain.MX + } + mx, _ := dnsmessage.NewName(fqdnString) + return []dnsmessage.MXResource{ + { + Pref: 0, + MX: mx, + }, + } +} + +func IsAcmeChallenge(fqdnString string) bool { + fqdnStringLowerCased := strings.ToLower(fqdnString) + if dns01ChallengeRE.MatchString(fqdnStringLowerCased) { + ipv4s := NameToA(fqdnStringLowerCased, true) + ipv6s := NameToAAAA(fqdnStringLowerCased, true) + if len(ipv4s) > 0 || len(ipv6s) > 0 { + return true + } + } + return false +} + +func IsDelegated(fqdnString string) bool { + fqdnStringLowerCased := strings.ToLower(fqdnString) + for domain := range Customizations { + if Customizations[domain].NS == nil { // no nameserver? then it can't be delegated + continue + } + // the "." prevents "where.com" from being mistakenly recognized as a subdomain of "here.com" + if strings.HasSuffix(fqdnStringLowerCased, "."+domain) || fqdnStringLowerCased == domain { + return true + } + } + return false +} + +func (x *Xip) NSResources(fqdnString string) []dnsmessage.NSResource { + fqdnStringLowerCased := strings.ToLower(fqdnString) + if x.blocklist(fqdnStringLowerCased) { + x.Metrics.AnsweredQueries++ + x.Metrics.AnsweredBlockedQueries++ + return x.NameServers + } + // Is this a delegated domain? Let's return the delegated nameservers + for domain := range Customizations { + if Customizations[domain].NS == nil { // no nameserver? then it can't be delegated + continue + } + // the "." prevents "where.com" from being mistakenly recognized as a subdomain of "here.com" + if strings.HasSuffix(fqdnStringLowerCased, "."+domain) || fqdnStringLowerCased == domain { + return Customizations[domain].NS + } + } + if IsAcmeChallenge(fqdnStringLowerCased) { + x.Metrics.AnsweredNSDNS01ChallengeQueries++ + strippedFqdn := dns01ChallengeRE.ReplaceAllString(fqdnStringLowerCased, "") + ns, _ := dnsmessage.NewName(strippedFqdn) + return []dnsmessage.NSResource{{NS: ns}} + } + x.Metrics.AnsweredQueries++ + return x.NameServers +} + +// TXTResources returns TXT records from Customizations +func (x *Xip) TXTResources(fqdn string, ip net.IP) ([]dnsmessage.TXTResource, error) { + if domain, ok := Customizations[strings.ToLower(fqdn)]; ok { + // Customizations[strings.ToLower(fqdn)] returns a _function_, + // we call that function, which has the same return signature as this method + if domain.TXT != nil { + return domain.TXT(x, ip) + } + } + return nil, nil +} + +func SOAAuthority(name dnsmessage.Name) (dnsmessage.ResourceHeader, dnsmessage.SOAResource) { + return dnsmessage.ResourceHeader{ + Name: name, + Type: dnsmessage.TypeSOA, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; it's not gonna change + Length: 0, + }, SOAResource(name) +} + +// SOAResource returns the hard-coded (except MNAME) SOA +func SOAResource(name dnsmessage.Name) dnsmessage.SOAResource { + return dnsmessage.SOAResource{ + NS: name, + MBox: mbox, + Serial: 20250615, + // cribbed the Refresh/Retry/Expire from google.com. + // MinTTL was 300, but I dropped to 180 for faster + // key-value propagation + Refresh: 900, + Retry: 900, + Expire: 1800, + MinTTL: 180, + } +} + +// PTRResource returns the PTR record, otherwise nil +func (x *Xip) PTRResource(fqdn []byte) *dnsmessage.PTRResource { + // "reverse", for example, means "1.0.0.127", as in "1.0.0.127.in-addr.arpa" + // the regular IP would be "127.0.0.1" + if ipv4ReverseRE.Match(fqdn) { + reversedIPv4 := ipv4ReverseRE.FindSubmatch(fqdn)[1] + reversedIPv4address := net.ParseIP(string(reversedIPv4)).To4() + if reversedIPv4address == nil { + return nil + } + ip := netip.AddrFrom4([4]byte{ + reversedIPv4address[3], + reversedIPv4address[2], + reversedIPv4address[1], + reversedIPv4address[0], + }) + ptrName, err := dnsmessage.NewName(strings.ReplaceAll(ip.String(), ".", "-") + ".sslip.io.") + if err != nil { + return nil + } + x.Metrics.AnsweredQueries++ + x.Metrics.AnsweredPTRQueriesIPv4++ + return &dnsmessage.PTRResource{ + PTR: ptrName, + } + } + if ipv6ReverseRE.Match(fqdn) { + b := ipv6ReverseRE.FindSubmatch(fqdn)[1] + reversed := []byte{ + b[62], b[60], b[58], b[56], ':', + b[54], b[52], b[50], b[48], ':', + b[46], b[44], b[42], b[40], ':', + b[38], b[36], b[34], b[32], ':', + b[30], b[28], b[26], b[24], ':', + b[22], b[20], b[18], b[16], ':', + b[14], b[12], b[10], b[8], ':', + b[6], b[4], b[2], b[0], + } + ip := net.ParseIP(string(reversed)).To16() + if ip == nil { + return nil + } + ptrName, err := dnsmessage.NewName(strings.ReplaceAll(ip.String(), ":", "-") + ".sslip.io.") + if err != nil { + return nil + } + x.Metrics.AnsweredQueries++ + x.Metrics.AnsweredPTRQueriesIPv6++ + return &dnsmessage.PTRResource{ + PTR: ptrName, + } + } + return nil +} + +// TXTSslipIoSPF SPF records for nip.io +func TXTNipIoSPF(_ *Xip, _ net.IP) ([]dnsmessage.TXTResource, error) { + // Although multiple TXT records with multiple strings are allowed, we're sticking + // with a multiple TXT records with a single string apiece because that's what ProtonMail requires + // and that's what google.com does. + return []dnsmessage.TXTResource{ + {TXT: []string{"protonmail-verification=19b0837cc4d9daa1f49980071da231b00e90b313"}}, // ProtonMail verification; don't delete + {TXT: []string{"v=spf1 include:_spf.protonmail.ch mx ~all"}}, + }, nil // Sender Policy Framework +} + +// TXTSslipIoSPF SPF records for sslio.io +func TXTSslipIoSPF(_ *Xip, _ net.IP) ([]dnsmessage.TXTResource, error) { + // Although multiple TXT records with multiple strings are allowed, we're sticking + // with a multiple TXT records with a single string apiece because that's what ProtonMail requires + // and that's what google.com does. + return []dnsmessage.TXTResource{ + {TXT: []string{"protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26"}}, // ProtonMail verification; don't delete + {TXT: []string{"v=spf1 include:_spf.protonmail.ch mx ~all"}}, + }, nil // Sender Policy Framework +} + +// TXTIp when TXT for "ip.sslip.io" is queried, return the IP address of the querier +func TXTIp(x *Xip, srcAddr net.IP) ([]dnsmessage.TXTResource, error) { + x.Metrics.AnsweredTXTSrcIPQueries++ + return []dnsmessage.TXTResource{{TXT: []string{srcAddr.String()}}}, nil +} + +// TXTMetrics when TXT for "metrics.sslip.io" is queried, return the cumulative metrics +func TXTMetrics(x *Xip, _ net.IP) (txtResources []dnsmessage.TXTResource, err error) { + <-x.DnsAmplificationAttackDelay + var metrics []string + uptime := time.Since(x.Metrics.Start) + metrics = append(metrics, fmt.Sprintf("Uptime: %.0f", uptime.Seconds())) + metrics = append(metrics, fmt.Sprintf("Blocklist: %s %d,%d", + x.BlocklistUpdated.Format("2006-01-02 15:04:05-07"), + len(x.BlocklistStrings), + len(x.BlocklistCIDRs))) + metrics = append(metrics, fmt.Sprintf("Queries: %d (%.1f/s)", x.Metrics.Queries, float64(x.Metrics.Queries)/uptime.Seconds())) + metrics = append(metrics, fmt.Sprintf("TCP/UDP: %d/%d", x.Metrics.TCPQueries, x.Metrics.UDPQueries)) + metrics = append(metrics, fmt.Sprintf("Answer > 0: %d (%.1f/s)", x.Metrics.AnsweredQueries, float64(x.Metrics.AnsweredQueries)/uptime.Seconds())) + metrics = append(metrics, fmt.Sprintf("A: %d", x.Metrics.AnsweredAQueries)) + metrics = append(metrics, fmt.Sprintf("AAAA: %d", x.Metrics.AnsweredAAAAQueries)) + metrics = append(metrics, fmt.Sprintf("TXT Source: %d", x.Metrics.AnsweredTXTSrcIPQueries)) + metrics = append(metrics, fmt.Sprintf("TXT Version: %d", x.Metrics.AnsweredTXTVersionQueries)) + metrics = append(metrics, fmt.Sprintf("PTR IPv4/IPv6: %d/%d", x.Metrics.AnsweredPTRQueriesIPv4, x.Metrics.AnsweredPTRQueriesIPv6)) + metrics = append(metrics, fmt.Sprintf("NS DNS-01: %d", x.Metrics.AnsweredNSDNS01ChallengeQueries)) + metrics = append(metrics, fmt.Sprintf("Blocked: %d", x.Metrics.AnsweredBlockedQueries)) + for _, metric := range metrics { + txtResources = append(txtResources, dnsmessage.TXTResource{TXT: []string{metric}}) + } + return txtResources, nil +} + +// soaLogMessage returns an easy-to-read string for logging SOA Answers/Authorities +func soaLogMessage(soaResource dnsmessage.SOAResource) string { + return soaResource.NS.String() + " " + + soaResource.MBox.String() + " " + + strconv.Itoa(int(soaResource.Serial)) + " " + + strconv.Itoa(int(soaResource.Refresh)) + " " + + strconv.Itoa(int(soaResource.Retry)) + " " + + strconv.Itoa(int(soaResource.Expire)) + " " + + strconv.Itoa(int(soaResource.MinTTL)) +} + +// MostlyEquals compares all fields except `Start` (timestamp) +func (a Metrics) MostlyEquals(b Metrics) bool { + if a.Queries == b.Queries && + a.TCPQueries == b.TCPQueries && + a.UDPQueries == b.UDPQueries && + a.AnsweredQueries == b.AnsweredQueries && + a.AnsweredAQueries == b.AnsweredAQueries && + a.AnsweredAAAAQueries == b.AnsweredAAAAQueries && + a.AnsweredTXTSrcIPQueries == b.AnsweredTXTSrcIPQueries && + a.AnsweredTXTVersionQueries == b.AnsweredTXTVersionQueries && + a.AnsweredPTRQueriesIPv4 == b.AnsweredPTRQueriesIPv4 && + a.AnsweredPTRQueriesIPv6 == b.AnsweredPTRQueriesIPv6 && + a.AnsweredNSDNS01ChallengeQueries == b.AnsweredNSDNS01ChallengeQueries && + a.AnsweredBlockedQueries == b.AnsweredBlockedQueries { + return true + } + return false +} + +func (x *Xip) downloadBlockList(blocklistURL string) string { + var err error + var blocklistReader io.ReadCloser + // file protocol's purpose: so I can run tests while flying with no internet + // secondary purpose: don't hammer GitHub when running tests + fileProtocolRE := regexp.MustCompile(`^file://`) + if fileProtocolRE.MatchString(blocklistURL) { + blocklistPath := strings.TrimPrefix(blocklistURL, "file://") + blocklistReader, err = os.Open(blocklistPath) + if err != nil { + return fmt.Sprintf(`failed to open blocklist "%s": %s`, blocklistPath, err.Error()) + } + //noinspection GoUnhandledErrorResult + defer blocklistReader.Close() + } else { + resp, err := http.Get(blocklistURL) + if err != nil { + return fmt.Sprintf(`failed to download blocklist "%s": %s`, blocklistURL, err.Error()) + } + blocklistReader = resp.Body + //noinspection GoUnhandledErrorResult + defer blocklistReader.Close() + if resp.StatusCode > 299 { + return fmt.Sprintf(`failed to download blocklist "%s", HTTP status: "%d"`, blocklistURL, resp.StatusCode) + } + } + blocklistStrings, blocklistCIDRs, err := ReadBlocklist(blocklistReader) + if err != nil { + return fmt.Sprintf(`failed to parse blocklist "%s": %s`, blocklistURL, err.Error()) + } + x.BlocklistStrings = blocklistStrings + x.BlocklistCIDRs = blocklistCIDRs + x.BlocklistUpdated = time.Now() + return fmt.Sprintf("Successfully downloaded blocklist from %s: %v, %v", blocklistURL, x.BlocklistStrings, x.BlocklistCIDRs) +} + +// ReadBlocklist "sanitizes" the block list, removing comments, invalid characters +// and lowercasing the names to be blocked. +// public to make testing easier +func ReadBlocklist(blocklist io.Reader) (stringBlocklists []string, cidrBlocklists []net.IPNet, err error) { + scanner := bufio.NewScanner(blocklist) + comments := regexp.MustCompile(`#.*`) + invalidDNSchars := regexp.MustCompile(`[^-\da-z]`) + invalidDNScharsWithSlashesDotsAndColons := regexp.MustCompile(`[^-_\da-z/.:]`) + + for scanner.Scan() { + line := scanner.Text() + line = strings.ToLower(line) + line = comments.ReplaceAllString(line, "") // strip comments + line = invalidDNScharsWithSlashesDotsAndColons.ReplaceAllString(line, "") // strip invalid characters + _, ipcidr, err := net.ParseCIDR(line) + if err != nil { + line = invalidDNSchars.ReplaceAllString(line, "") // strip invalid DNS characters + if line == "" { + continue + } + stringBlocklists = append(stringBlocklists, line) + } else { + cidrBlocklists = append(cidrBlocklists, *ipcidr) + } + } + if err = scanner.Err(); err != nil { + return []string{}, []net.IPNet{}, err + } + return stringBlocklists, cidrBlocklists, nil +} + +func (x *Xip) blocklist(hostname string) bool { + aResources := NameToA(hostname, true) + aaaaResources := NameToAAAA(hostname, true) + if len(aResources) == 0 && len(aaaaResources) == 0 { + return false + } + var ip net.IP + if len(aResources) == 1 { + ip = aResources[0].A[:] + } + if len(aaaaResources) == 1 { + ip = aaaaResources[0].AAAA[:] + } + if ip == nil { // placate linter who worries ip is nil; it should never be nil + return false + } + if ip.IsPrivate() { + return false + } + for _, blockstring := range x.BlocklistStrings { + if strings.Contains(hostname, blockstring) { + return true + } + } + for _, blockCIDR := range x.BlocklistCIDRs { + if blockCIDR.Contains(ip) { + return true + } + } + return false +} + +func (x *Xip) nameToAwithBlocklist(q dnsmessage.Question, response Response, logMessage string) (_ Response, _ string, err error) { + nameToAs := NameToA(q.Name.String(), x.Public) + if len(nameToAs) == 0 { + // No Answers, only 1 Authorities + soaHeader, soaResource := SOAAuthority(q.Name) + response.Authorities = append(response.Authorities, + func(b *dnsmessage.Builder) error { + if err = b.SOAResource(soaHeader, soaResource); err != nil { + return err + } + return nil + }) + return response, logMessage + "nil, SOA " + soaLogMessage(soaResource), nil + } + if x.blocklist(q.Name.String()) { + x.Metrics.AnsweredQueries++ + x.Metrics.AnsweredBlockedQueries++ + response.Answers = append(response.Answers, + // 1 or more A records; A records > 1 only available via Customizations + func(b *dnsmessage.Builder) error { + err = b.AResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeA, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, Customizations["blocked.sslip.io."].A[0]) + if err != nil { + return err + } + return nil + }) + return response, logMessage + net.IP(Customizations["blocked.sslip.io."].A[0].A[:]).String(), nil + } + x.Metrics.AnsweredQueries++ + x.Metrics.AnsweredAQueries++ + response.Answers = append(response.Answers, + // 1 or more A records; A records > 1 only available via Customizations + func(b *dnsmessage.Builder) error { + for _, nameToA := range nameToAs { + err = b.AResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeA, + Class: dnsmessage.ClassINET, + TTL: 3600, // 60 * 60 == 1 hour; short TTL in case we need to block them + Length: 0, + }, nameToA) + if err != nil { + return err + } + } + return nil + }) + var logMessages []string + for _, nameToA := range nameToAs { + ip := net.IP(nameToA.A[:]) + logMessages = append(logMessages, ip.String()) + } + return response, logMessage + strings.Join(logMessages, ", "), nil +} + +func IsPublic(ip net.IP) (isPublic bool) { + if ip.IsPrivate() { // RFC 1918, 4193 + return false + } + if ip4 := ip.To4(); ip4 != nil { + // IPv4 loopback + if ip4[0] == 127 { + return false + } + // IPv4 link-local + if ip4[0] == 169 && ip4[1] == 254 { + return false + } + // CG-NAT + if ip4[0] == 100 && ip4[1]&0xc0 == 64 { + return false + } + return true + } + // IPv6 loopback ::1 + if ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0 && + ip[4] == 0 && ip[5] == 0 && ip[6] == 0 && ip[7] == 0 && + ip[8] == 0 && ip[9] == 0 && ip[10] == 0 && ip[11] == 0 && + ip[12] == 0 && ip[13] == 0 && ip[14] == 0 && ip[15] == 1 { + return false + } + // IPv6 link-local fe80::/10 + if ip[0] == 0xfe && ip[1] == 0x80 && ip[2]&0xc0 == 0 { + return false + } + // IPv4/IPv6 Translation private internet 64:ff9b:1::/48 + if ip[0] == 0 && ip[1] == 0x64 && ip[2] == 0xff && ip[3] == 0x9b && + ip[4] == 0 && ip[5] == 1 && ip[6] == 0 && ip[7] == 0 && + ip[8] == 0 && ip[9] == 0 { + return false + } + // Teredo Tunneling 2001::/32 + // ORCHIDv2 (?) 2001:20::/28 + if ip[0] == 0x20 && ip[1] == 1 && ip[2] == 0 && ip[3]&0xf0 == 0x20 { + return false + } + // Documentation 2001:db8::/32 + if ip[0] == 0x20 && ip[1] == 1 && ip[2] == 0x0d && ip[3] == 0xb8 { + return false + } + // Private internets fc00::/7 + + return true +} + +func (x *Xip) nameToAAAAwithBlocklist(q dnsmessage.Question, response Response, logMessage string) (_ Response, _ string, err error) { + nameToAAAAs := NameToAAAA(q.Name.String(), x.Public) + if len(nameToAAAAs) == 0 { + // No Answers, only 1 Authorities + soaHeader, soaResource := SOAAuthority(q.Name) + response.Authorities = append(response.Authorities, + func(b *dnsmessage.Builder) error { + if err = b.SOAResource(soaHeader, soaResource); err != nil { + return err + } + return nil + }) + return response, logMessage + "nil, SOA " + soaLogMessage(soaResource), nil + } + if x.blocklist(q.Name.String()) { + x.Metrics.AnsweredQueries++ + x.Metrics.AnsweredBlockedQueries++ + response.Answers = append(response.Answers, + // 1 or more A records; A records > 1 only available via Customizations + func(b *dnsmessage.Builder) error { + err = b.AAAAResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeA, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change + Length: 0, + }, Customizations["blocked.sslip.io."].AAAA[0]) + if err != nil { + return err + } + return nil + }) + return response, logMessage + net.IP(Customizations["blocked.sslip.io."].AAAA[0].AAAA[:]).String(), nil + } + x.Metrics.AnsweredQueries++ + x.Metrics.AnsweredAAAAQueries++ + response.Answers = append(response.Answers, + // 1 or more AAAA records; AAAA records > 1 only available via Customizations + func(b *dnsmessage.Builder) error { + for _, nameToAAAA := range nameToAAAAs { + err = b.AAAAResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeAAAA, + Class: dnsmessage.ClassINET, + TTL: 3600, // 60 * 60 == 1 hour; short TTL in case we need to block them + Length: 0, + }, nameToAAAA) + if err != nil { + return err + } + } + return nil + }) + var logMessages []string + for _, nameToAAAA := range nameToAAAAs { + ip := net.IP(nameToAAAA.AAAA[:]) + logMessages = append(logMessages, ip.String()) + } + return response, logMessage + strings.Join(logMessages, ", "), nil +} diff --git a/xip/xip_suite_test.go b/xip/xip_suite_test.go new file mode 100644 index 000000000..08264ea17 --- /dev/null +++ b/xip/xip_suite_test.go @@ -0,0 +1,13 @@ +package xip_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestXip(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Xip Suite") +} diff --git a/xip/xip_test.go b/xip/xip_test.go new file mode 100644 index 000000000..0fdb48981 --- /dev/null +++ b/xip/xip_test.go @@ -0,0 +1,496 @@ +package xip_test + +import ( + "net" + "strings" + "xip/testhelper" + "xip/xip" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "golang.org/x/net/dns/dnsmessage" +) + +var _ = Describe("Xip", func() { + var ( + err error + ) + Describe("CNAMEResources()", func() { + It("returns nil by default", func() { + randomDomain := testhelper.Random8ByteString() + ".com." + cname := xip.CNAMEResource(randomDomain) + Expect(cname).To(BeNil()) + }) + When("querying one of sslip.io's DKIM CNAME's", func() { + It("returns the CNAME", func() { + cname := xip.CNAMEResource("protonmail._domainkey.SSlip.Io.") + Expect(cname.CNAME.String()).To(MatchRegexp("^protonmail\\.domainkey.*.domains\\.proton\\.ch\\.$")) + }) + }) + When("a domain has been customized but has no CNAMEs", func() { + It("returns nil", func() { + customizedDomain := testhelper.Random8ByteString() + ".com." + xip.Customizations[customizedDomain] = xip.DomainCustomization{} + cname := xip.CNAMEResource(customizedDomain) + Expect(cname).To(BeNil()) + delete(xip.Customizations, customizedDomain) + }) + }) + When("a domain has been customized with CNAMES", func() { + It("returns CNAME resources", func() { + customizedDomain := testhelper.Random8ByteString() + ".com." + xip.Customizations[strings.ToLower(customizedDomain)] = xip.DomainCustomization{ + CNAME: dnsmessage.CNAMEResource{ + CNAME: dnsmessage.Name{ + // google.com. + Length: 11, + Data: [255]byte{ + 103, 111, 111, 103, 108, 101, 46, 99, 111, 109, 46, + }, + }, + }, + } + cname := xip.CNAMEResource(customizedDomain) + Expect(cname.CNAME.String()).To(Equal("google.com.")) + delete(xip.Customizations, customizedDomain) // clean-up + }) + }) + }) + + Describe("MXResources()", func() { + It("returns the MX resource", func() { + randomDomain := testhelper.Random8ByteString() + ".com." + mx := xip.MXResources(randomDomain) + mxHostName := dnsmessage.MustNewName(randomDomain) + Expect(len(mx)).To(Equal(1)) + Expect(mx[0].MX).To(Equal(mxHostName)) + }) + When("sslip.io is the domain being queried", func() { + It("returns sslip.io's custom MX records", func() { + mx := xip.MXResources("sslIP.iO.") + Expect(len(mx)).To(Equal(2)) + Expect(mx[0].MX.Data).To(Equal(xip.Customizations["sslip.io."].MX[0].MX.Data)) + }) + }) + }) + + Describe("NSResources()", func() { + When("we use the default nameservers", func() { + var x, _ = xip.NewXip("file:///", []string{"ns-hetzner.sslip.io.", "ns-ovh.sslip.io.", "ns-do-sg.sslip.io."}, []string{}, []string{}) + It("returns the name servers", func() { + randomDomain := testhelper.Random8ByteString() + ".com." + ns := x.NSResources(randomDomain) + Expect(len(ns)).To(Equal(3)) + Expect(ns[0].NS.String()).To(Equal("ns-hetzner.sslip.io.")) + Expect(ns[1].NS.String()).To(Equal("ns-ovh.sslip.io.")) + Expect(ns[2].NS.String()).To(Equal("ns-do-sg.sslip.io.")) + }) + When(`the domain name contains "_acme-challenge."`, func() { + When("the domain name has an embedded IP", func() { + It(`returns an array of one NS record pointing to the domain name _sans_ "acme-challenge."`, func() { + randomDomain := "192.168.0.1." + testhelper.Random8ByteString() + ".com." + ns := x.NSResources("_acme-challenge." + randomDomain) + Expect(len(ns)).To(Equal(1)) + Expect(ns[0].NS.String()).To(Equal(strings.ToLower(randomDomain))) + aResources := xip.NameToA(randomDomain, true) + Expect(len(aResources)).To(Equal(1)) + Expect(err).ToNot(HaveOccurred()) + Expect(aResources[0].A).To(Equal([4]byte{192, 168, 0, 1})) + }) + }) + When("the domain name does not have an embedded IP", func() { + It("returns the default trinity of nameservers", func() { + randomDomain := "_acme-challenge." + testhelper.Random8ByteString() + ".com." + ns := x.NSResources(randomDomain) + Expect(len(ns)).To(Equal(3)) + }) + }) + }) + When("we delegate domains to other nameservers", func() { + When(`we don't use the "=" in the arguments`, func() { + It("returns an informative log message", func() { + var _, logs = xip.NewXip("file://etc/blocklist-test.txt", []string{"ns-hetzner.sslip.io.", "ns-ovh.sslip.io.", "ns-do-sg.sslip.io."}, []string{}, []string{"noEquals"}) + Expect(strings.Join(logs, "")).To(MatchRegexp(`"-delegates: arguments should be in the format "delegatedDomain=nameserver", not "noEquals"`)) + }) + }) + When(`there's no "." at the end of the delegated domain or nameserver`, func() { + It(`helpfully adds the "."`, func() { + var x, logs = xip.NewXip("file://etc/blocklist-test.txt", []string{"ns-hetzner.sslip.io.", "ns-ovh.sslip.io.", "ns-do-sg.sslip.io."}, []string{}, []string{"a=b"}) + Expect(strings.Join(logs, "")).To(MatchRegexp(`Adding delegated NS record "a\.=b\."`)) + ns := x.NSResources("a.") + Expect(len(ns)).To(Equal(1)) + }) + }) + }) + }) + When("we override the default nameservers", func() { + var x, _ = xip.NewXip("file:///", []string{"mickey", "minn.ie.", "goo.fy"}, []string{}, []string{}) + It("returns the configured servers", func() { + randomDomain := testhelper.Random8ByteString() + ".com." + ns := x.NSResources(randomDomain) + Expect(len(ns)).To(Equal(3)) + Expect(ns[0].NS.String()).To(Equal("mickey.")) + Expect(ns[1].NS.String()).To(Equal("minn.ie.")) + Expect(ns[2].NS.String()).To(Equal("goo.fy.")) + }) + + }) + }) + + Describe("SOAResource()", func() { + It("returns the SOA resource for the domain in question", func() { + randomDomain := testhelper.Random8ByteString() + ".com." + randomDomainName := dnsmessage.MustNewName(randomDomain) + soa := xip.SOAResource(randomDomainName) + Expect(soa.NS.Data).To(Equal(randomDomainName.Data)) + }) + }) + + Describe("TXTResources()", func() { + var x xip.Xip + It("returns an empty array for a random domain", func() { + randomDomain := testhelper.Random8ByteString() + ".com." + txts, err := x.TXTResources(randomDomain, nil) + Expect(err).To(Not(HaveOccurred())) + Expect(len(txts)).To(Equal(0)) + }) + When("queried for the sslip.io domain", func() { + It("returns mail-related TXT resources for the sslip.io domain", func() { + domain := "ssLip.iO." + txts, err := x.TXTResources(domain, nil) + Expect(err).To(Not(HaveOccurred())) + Expect(len(txts)).To(Equal(2)) + Expect(txts[0].TXT[0]).To(MatchRegexp("protonmail-verification=")) + Expect(txts[1].TXT[0]).To(MatchRegexp("v=spf1")) + }) + }) + When("a random domain has been customized w/out any TXT defaults", func() { // Unnecessary, but confirms Golang's behavior for me, a doubting Thomas + customizedDomain := testhelper.Random8ByteString() + ".com." + xip.Customizations[customizedDomain] = xip.DomainCustomization{} + It("returns no TXT resources", func() { + txts, err := x.TXTResources(customizedDomain, nil) + Expect(err).To(Not(HaveOccurred())) + Expect(len(txts)).To(Equal(0)) + }) + delete(xip.Customizations, customizedDomain) // clean-up + }) + When(`the domain "ip.sslip.io" is queried`, func() { + It("returns the IP address of the querier", func() { + txts, err := x.TXTResources("ip.sslip.io.", net.IP{1, 1, 1, 1}) + Expect(err).To(Not(HaveOccurred())) + Expect(len(txts)).To(Equal(1)) + Expect(txts[0].TXT[0]).To(MatchRegexp("^1.1.1.1$")) + }) + }) + When(`a customized domain without a TXT entry is queried`, func() { + It("returns no records (and doesn't panic, either)", func() { + txts, err := x.TXTResources("ns.sslip.io.", nil) + Expect(err).To(Not(HaveOccurred())) + Expect(len(txts)).To(Equal(0)) + }) + }) + }) + + Describe("NameToA()", func() { + xip.Customizations["custom.record."] = xip.DomainCustomization{A: []dnsmessage.AResource{ + {A: [4]byte{78, 46, 204, 247}}, + }} + DescribeTable("when it succeeds", + func(fqdn string, expectedA dnsmessage.AResource) { + ipv4Answers := xip.NameToA(fqdn, true) + Expect(ipv4Answers[0]).To(Equal(expectedA)) + Expect(len(ipv4Answers)).To(Equal(1)) + }, + Entry("custom record", "CusTom.RecOrd.", dnsmessage.AResource{A: [4]byte{78, 46, 204, 247}}), + // dots + Entry("loopback", "127.0.0.1", dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}), + Entry("255 with domain", "255.254.253.252.com", dnsmessage.AResource{A: [4]byte{255, 254, 253, 252}}), + Entry(`"This" network, pre-and-post`, "nono.io.0.1.2.3.ssLIp.IO", dnsmessage.AResource{A: [4]byte{0, 1, 2, 3}}), + Entry("private network, two IPs, grabs the leftmost", "nono.io.172.16.0.30.172.31.255.255.sslip.io", dnsmessage.AResource{A: [4]byte{172, 16, 0, 30}}), + // dashes + Entry("shared address with dashes", "100-64-1-2", dnsmessage.AResource{A: [4]byte{100, 64, 1, 2}}), + Entry("link-local with domain", "169-254-168-253-com", dnsmessage.AResource{A: [4]byte{169, 254, 168, 253}}), + Entry("IETF protocol assignments with domain and www", "www-192-0-0-1-com", dnsmessage.AResource{A: [4]byte{192, 0, 0, 1}}), + // dots-and-dashes, mix-and-matches + Entry("Pandaxin's paradox", "minio-01.192-168-1-100.sslip.io", dnsmessage.AResource{A: [4]byte{192, 168, 1, 100}}), + Entry("Hexadecimal #0", "filer.7f000001.sslip.io", dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}), + Entry("Hexadecimal #1, TLD", "0A09091E", dnsmessage.AResource{A: [4]byte{10, 9, 9, 30}}), + Entry("Hexadecimal #1, TLD #2", "0A09091E.", dnsmessage.AResource{A: [4]byte{10, 9, 9, 30}}), + Entry("Hexadecimal #1, TLD #3", ".0A09091E.", dnsmessage.AResource{A: [4]byte{10, 9, 9, 30}}), + Entry("Hexadecimal #1, TLD #4", "www.0A09091E.", dnsmessage.AResource{A: [4]byte{10, 9, 9, 30}}), + Entry("Hexadecimal #2, mixed case", "ffffFFFF.nip.io", dnsmessage.AResource{A: [4]byte{255, 255, 255, 255}}), + Entry("Hexadecimal #3, different numbers", "www.fedcba98.nip.io", dnsmessage.AResource{A: [4]byte{254, 220, 186, 152}}), + Entry("Hexadecimal #3, different numbers #2", "www.76543210.nip.io", dnsmessage.AResource{A: [4]byte{118, 84, 50, 16}}), + Entry("Hexadecimal #4, dashes trump hex", "www.127-0-0-53.76543210.nip.io", dnsmessage.AResource{A: [4]byte{127, 0, 0, 53}}), + Entry("Hexadecimal #4, dashes trump hex #2", "www.76543210.127-0-0-53.nip.io", dnsmessage.AResource{A: [4]byte{127, 0, 0, 53}}), + Entry("Hexadecimal #4, dots trump hex", "www.127.0.0.53.76543210.nip.io", dnsmessage.AResource{A: [4]byte{127, 0, 0, 53}}), + Entry("Hexadecimal #4, dots trump hex #2", "www.76543210.127.0.0.53.nip.io", dnsmessage.AResource{A: [4]byte{127, 0, 0, 53}}), + ) + DescribeTable("when it does NOT match an IP address", + func(fqdn string) { + ipv4Answers := xip.NameToA(fqdn, true) + Expect(len(ipv4Answers)).To(Equal(0)) + }, + Entry("empty string", ""), + Entry("bare domain", "nono.io"), + Entry("canonical domain", "sslip.io"), + Entry("www", "www.sslip.io"), + Entry("a lone number", "538.sslip.io"), + Entry("too big", "256.254.253.252"), + Entry("NS but no dot", "ns-hetzner.sslip.io"), + Entry("NS + cruft at beginning", "p-ns-hetzner.sslip.io"), + Entry("test-net address with dots-and-dashes mixed", "www-192.0-2.3.example-me.com"), + Entry("Hexadecimal with too many digits (9 instead of 8)", "www.0A09091E0.com"), + Entry("Hexadecimal with too few digits (7 instead of 8)", "www.0A09091.com"), + Entry("Hexadecimal with a dash instead of a .", "www-0A09091E.com"), + Entry("Hexadecimal with a dash instead of a . #2", "www.0A09091E-com"), + ) + When("There is more than one A record", func() { + It("returns them all", func() { + fqdn := testhelper.Random8ByteString() + xip.Customizations[strings.ToLower(fqdn)] = xip.DomainCustomization{ + A: []dnsmessage.AResource{ + {A: [4]byte{1}}, + {A: [4]byte{2}}, + }, + } + ipv4Answers := xip.NameToA(fqdn, true) + Expect(err).ToNot(HaveOccurred()) + Expect(len(ipv4Answers)).To(Equal(2)) + Expect(ipv4Answers[0].A).To(Equal([4]byte{1})) + Expect(ipv4Answers[1].A).To(Equal([4]byte{2})) + delete(xip.Customizations, fqdn) + }) + }) + When("There are multiple matches", func() { + It("returns the leftmost one", func() { + ipv4Answers := xip.NameToA("nono.io.127.0.0.1.192.168.0.1.sslip.io", true) + Expect(len(ipv4Answers)).To(Equal(1)) + Expect(ipv4Answers[0]). + To(Equal(dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}})) + }) + }) + When("There are matches with dashes and dots", func() { + It("returns the one with dashes", func() { + ipv4Answers := xip.NameToA("nono.io.127.0.0.1.192-168-0-1.sslip.io", true) + Expect(len(ipv4Answers)).To(Equal(1)) + Expect(ipv4Answers[0]). + To(Equal(dnsmessage.AResource{A: [4]byte{192, 168, 0, 1}})) + }) + }) + }) + + Describe("IsAcmeChallenge()", func() { + When("the domain doesn't have '_acme-challenge.' in it", func() { + It("returns false", func() { + randomDomain := testhelper.Random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeFalse()) + }) + It("returns false even when there are embedded IPs", func() { + randomDomain := "127.0.0.1." + testhelper.Random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeFalse()) + }) + }) + When("it has '_acme-challenge.' in it", func() { + When("it does NOT have any embedded IPs", func() { + It("returns false", func() { + randomDomain := "_acme-challenge." + testhelper.Random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeFalse()) + }) + }) + When("it has embedded IPs", func() { + It("returns true", func() { + randomDomain := "_acme-challenge.127.0.0.1." + testhelper.Random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeTrue()) + randomDomain = "_acme-challenge.fe80--1." + testhelper.Random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeTrue()) + }) + When("it has random capitalization", func() { + It("returns true", func() { + randomDomain := "_AcMe-ChAlLeNgE.127.0.0.1." + testhelper.Random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeTrue()) + randomDomain = "_aCMe-cHAllENge.fe80--1." + testhelper.Random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeTrue()) + }) + }) + }) + }) + }) + Describe("IsDelegated()", func() { + var nsName dnsmessage.Name + nsName, err = dnsmessage.NewName("1.com") + Expect(err).ToNot(HaveOccurred()) + xip.Customizations["a.com"] = xip.DomainCustomization{NS: []dnsmessage.NSResource{{NS: nsName}}} + xip.Customizations["b.com"] = xip.DomainCustomization{} + + When("the domain is delegated", func() { + When("the fqdn exactly matches the domain", func() { + It("returns true", func() { + Expect(xip.IsDelegated("A.com")).To(BeTrue()) + }) + }) + When("the fqdn is a subdomain of the domain", func() { + It("returns true", func() { + Expect(xip.IsDelegated("b.a.COM")).To(BeTrue()) + }) + }) + When("the fqdn doesn't match the domain", func() { + It("returns false", func() { + Expect(xip.IsDelegated("Aa.com")).To(BeFalse()) + }) + }) + }) + When("the domain is customized but not delegated", func() { + It("returns false", func() { + Expect(xip.IsDelegated("b.COM")).To(BeFalse()) + }) + }) + }) + + Describe("NameToAAAA()", func() { + DescribeTable("when it succeeds", + func(fqdn string, expectedAAAA dnsmessage.AAAAResource) { + ipv6Answers := xip.NameToAAAA(fqdn, true) + Expect(ipv6Answers[0]).To(Equal(expectedAAAA)) + Expect(len(ipv6Answers)).To(Equal(1)) + }, + // dashes only + Entry("loopback", "--1", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("ff with domain", "fffe-fdfc-fbfa-f9f8-f7f6-f5f4-f3f2-f1f0.com", dnsmessage.AAAAResource{AAAA: [16]byte{255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240}}), + Entry("ff with domain and pre", "www.fffe-fdfc-fbfa-f9f8-f7f6-f5f4-f3f2-f1f0.com", dnsmessage.AAAAResource{AAAA: [16]byte{255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240}}), + Entry("ff with domain dashes", "1.www-fffe-fdfc-fbfa-f9f8-f7f6-f5f4-f3f2-f1f0-1.com", dnsmessage.AAAAResource{AAAA: [16]byte{255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240}}), + Entry("Browsing the logs", "2006-41d0-2-e01e--56dB-3598.sSLIP.io.", dnsmessage.AAAAResource{AAAA: [16]byte{32, 6, 65, 208, 0, 2, 224, 30, 0, 0, 0, 0, 86, 219, 53, 152}}), + Entry("Browsing the logs", "1-2-3--4-5-6.sSLIP.io.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 4, 0, 5, 0, 6}}), + Entry("Browsing the logs", "1--2-3-4-5-6.sSLIP.io.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 1, 0, 0, 0, 0, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6}}), + Entry("Hexadecimal #0", "filer.00000000000000000000000000000001.sslip.io", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #1, TLD", "00000000000000000000000000000001", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #1, TLD #2", "00000000000000000000000000000001.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #1, TLD #3", ".00000000000000000000000000000001.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #1, TLD #4", "www.00000000000000000000000000000001.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #2, mixed case", "89abcdef0000000089ABCDEF00000000.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{137, 171, 205, 239, 0, 0, 0, 0, 137, 171, 205, 239, 0, 0, 0, 0}}), + Entry("Hexadecimal #3, different numbers", "www.0123456789abcdef0123456789abcdef.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{1, 35, 69, 103, 137, 171, 205, 239, 1, 35, 69, 103, 137, 171, 205, 239}}), + Entry("Hexadecimal #3, different numbers #2", "www.00000000000000000000000000000001.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}), + Entry("Hexadecimal #4, dashes trump hex", "www.2600--.00000000000000000000000000000001.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}), + Entry("Hexadecimal #4, dashes trump hex #2", "www.00000000000000000000000000000001.--2.nip.io", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}}), + ) + DescribeTable("when it does not match an IP address", + func(fqdn string) { + ipv6Answers := xip.NameToAAAA(fqdn, true) + Expect(len(ipv6Answers)).To(Equal(0)) + }, + Entry("empty string", ""), + Entry("bare domain", "nono.io"), + Entry("canonical domain", "sslip.io"), + Entry("www", "www.sslip.io"), + Entry("a 1 without double-dash", "-1"), + Entry("too big", "--g"), + Entry("Hexadecimal with too many digits (33 instead of 32)", "www.0123456789abcdef0123456789abcdef0.com"), + Entry("Hexadecimal with too few digits (31 instead of 32)", "www.0123456789abcdef0123456789abcde.com"), + Entry("Hexadecimal with a dash instead of a .", "www-0123456789abcdef0123456789abcdef.com"), + Entry("Hexadecimal with a dash instead of a . #2", "www.0123456789abcdef0123456789abcdef-com"), + ) + When("using randomly generated IPv6 addresses (fuzz testing)", func() { + It("should succeed every time", func() { + for i := 0; i < 10000; i++ { + addr := testhelper.RandomIPv6Address() + ipv6Answers := xip.NameToAAAA(strings.ReplaceAll(addr.String(), ":", "-"), true) + Expect(err).ToNot(HaveOccurred()) + Expect(ipv6Answers[0].AAAA[:]).To(Equal([]uint8(addr))) + } + }) + }) + When("There is more than one AAAA record", func() { + It("returns them all", func() { + fqdn := testhelper.Random8ByteString() + xip.Customizations[strings.ToLower(fqdn)] = xip.DomainCustomization{ + AAAA: []dnsmessage.AAAAResource{ + {AAAA: [16]byte{1}}, + {AAAA: [16]byte{2}}, + }, + } + ipv6Addrs := xip.NameToAAAA(fqdn, true) + Expect(len(ipv6Addrs)).To(Equal(2)) + Expect(ipv6Addrs[0].AAAA).To(Equal([16]byte{1})) + Expect(ipv6Addrs[1].AAAA).To(Equal([16]byte{2})) + delete(xip.Customizations, fqdn) + }) + }) + }) + + Describe("ReadBlocklist()", func() { + It("strips comments", func() { + input := strings.NewReader("# a comment\n#another comment\nno-comments\n") + bls, blIPs, err := xip.ReadBlocklist(input) + Expect(err).ToNot(HaveOccurred()) + Expect(bls).To(Equal([]string{"no-comments"})) + Expect(blIPs).To(BeNil()) + }) + It("strips blank lines", func() { + input := strings.NewReader("\n\n\nno-blank-lines") + bls, blIPs, err := xip.ReadBlocklist(input) + Expect(err).ToNot(HaveOccurred()) + Expect(bls).To(Equal([]string{"no-blank-lines"})) + Expect(blIPs).To(BeNil()) + }) + It("lowercases names for comparison", func() { + input := strings.NewReader("NO-YELLING") + bls, blIPs, err := xip.ReadBlocklist(input) + Expect(err).ToNot(HaveOccurred()) + Expect(bls).To(Equal([]string{"no-yelling"})) + Expect(blIPs).To(BeNil()) + }) + It("removes all non-allowable characters", func() { + input := strings.NewReader("\nalpha #comment # comment\nåß∂ # comment # comment\ndelta∆\n ... GAMMA∑µ®† ...#asdfasdf#asdfasdf") + bls, blIPs, err := xip.ReadBlocklist(input) + Expect(err).ToNot(HaveOccurred()) + Expect(bls).To(Equal([]string{"alpha", "delta", "gamma"})) + Expect(blIPs).To(BeNil()) + }) + It("reads in IPv4 CIDRs", func() { + input := strings.NewReader("\n43.134.66.67/24 #asdfasdf") + bls, blIPs, err := xip.ReadBlocklist(input) + Expect(err).ToNot(HaveOccurred()) + Expect(bls).To(BeNil()) + Expect(blIPs).To(Equal([]net.IPNet{{IP: net.IP{43, 134, 66, 0}, Mask: net.IPMask{255, 255, 255, 0}}})) + }) + It("reads in IPv6 CIDRs", func() { + input := strings.NewReader("\n 2600::/64 #asdfasdf") + bls, blIPs, err := xip.ReadBlocklist(input) + Expect(err).ToNot(HaveOccurred()) + Expect(bls).To(BeNil()) + Expect(blIPs).To(Equal([]net.IPNet{ + {IP: net.IP{38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Mask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0}}})) + }) + }) + + Describe("IsPublic()", func() { + DescribeTable("when determining whether an IP is public or private", + func(ip net.IP, expectedPublic bool) { + Expect(xip.IsPublic(ip)).To(Equal(expectedPublic)) + }, + Entry("Google Nameserver IPv4", net.ParseIP("8.8.8.8"), true), + Entry("Google Nameserver IPv6", net.ParseIP("2001:4860:4860::8888"), true), + Entry("Apple Studio morgoth.nono.io", net.ParseIP("2601:646:100:69f0:7d:9069:ea74:e3a"), true), + Entry("External interface home.nono.io", net.ParseIP("2001:558:6045:109:892f:2df3:15e3:3184"), true), + Entry("RFC 1918 Section 3 10/8", net.ParseIP("10.9.9.30"), false), + Entry("RFC 1918 Section 3 172.16/12", net.ParseIP("172.31.255.255"), false), + Entry("RFC 1918 Section 3 192.168/16", net.ParseIP("192.168.0.1"), false), + Entry("RFC 4193 Section 8 fc00::/7", net.ParseIP("fdff::"), false), + Entry("CG-NAT 100.64/10", net.ParseIP("100.127.255.255"), false), + Entry("CG-NAT 100.64/10", net.ParseIP("100.128.0.0"), true), + Entry("link-local IPv4", net.ParseIP("169.254.169.254"), false), + Entry("not link-local IPv4", net.ParseIP("169.255.255.255"), true), + Entry("link-local IPv6", net.ParseIP("fe80::"), false), + Entry("loopback IPv4 127/8", net.ParseIP("127.127.127.127"), false), + Entry("loopback IPv6 ::1/128", net.ParseIP("::1"), false), + Entry("IPv4/IPv6 Translation internet", net.ParseIP("64:ff9b::"), true), + Entry("IPv4/IPv6 Translation private internet", net.ParseIP("64:ff9b:1::"), false), + Entry("IPv4/IPv6 Translation internet", net.ParseIP("64:ff9b::"), true), + Entry("Teredo Tunneling", net.ParseIP("2001::"), true), + Entry("ORCHIDv2 (?)", net.ParseIP("2001:20::"), false), + Entry("Documentation", net.ParseIP("2001:db8::"), false), + Entry("Private internets", net.ParseIP("fc00::"), false), + ) + }) +})