├── .ci └── docker │ ├── Dockerfile │ ├── build.sh │ ├── publish.sh │ └── run.sh ├── .github ├── dependabot.yml └── workflows │ ├── coverity.yml │ └── tests.yml ├── .gitignore ├── .gitmodules ├── .stickler.yml ├── CMakeLists.txt ├── CNAME ├── COPYING ├── DEPENDENCIES.md ├── Dockerfile.integ ├── Doxyfile ├── README.md ├── _config.yml ├── contributors.txt ├── docker-compose.yml ├── docs ├── CNAME ├── README.md ├── TODO.md ├── _config.yml ├── dublin-traceroute.repo ├── examples.md ├── tr01.dot ├── tr01.png ├── tr02.dot ├── tr02.png ├── tr03.dot ├── tr03.png └── traceroute_8.8.8.8.png ├── go.mod ├── go.sum ├── go └── dublintraceroute │ ├── cmd │ ├── dublin-traceroute │ │ └── main.go │ ├── routest │ │ ├── README.md │ │ ├── config.json │ │ ├── main.go │ │ ├── reply.go │ │ ├── send.go │ │ └── start.sh │ └── todot │ │ └── main.go │ ├── dublin-traceroute.go │ ├── net │ ├── icmp.go │ ├── icmp_test.go │ ├── icmpv6.go │ ├── ipv4.go │ ├── ipv4_test.go │ ├── ipv6.go │ ├── ipv6_test.go │ ├── layer.go │ ├── localaddr.go │ ├── protocols.go │ ├── raw.go │ ├── udp.go │ └── udp_test.go │ ├── probes │ ├── probe.go │ ├── probev4 │ │ ├── udpv4.go │ │ └── udpv4probe.go │ └── probev6 │ │ ├── udpv6.go │ │ └── udpv6probe.go │ └── results │ ├── results.go │ └── results_test.go ├── homebrew └── dublin-traceroute.rb ├── include └── dublintraceroute │ ├── common.h │ ├── dublin_traceroute.h │ ├── exceptions.h │ ├── hop.h │ ├── hops.h │ ├── icmp_messages.h │ ├── traceroute_results.h │ └── udpv4probe.h ├── integ ├── integ_test.go └── test_data │ ├── config_8.8.8.8_one_path.json │ └── want_8.8.8.8_one_path.json ├── scripts └── to_graphviz.py ├── src ├── common.cc ├── dublin_traceroute.cc ├── hop.cc ├── main.cc ├── traceroute_results.cc └── udpv4probe.cc └── tests ├── CMakeLists.txt └── src ├── CMakeLists.txt ├── hop.cxx ├── hops.cxx └── udpv4.cxx /.ci/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # {docker build -t insomniacslk/dublin-traceroute -f Dockerfile .} 2 | FROM debian:stable 3 | 4 | # Install dependencies 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends \ 7 | git \ 8 | ca-certificates \ 9 | build-essential \ 10 | pkg-config \ 11 | cmake \ 12 | libjsoncpp-dev \ 13 | libtins-dev \ 14 | libpcap-dev \ 15 | `# dependencies for the python module` \ 16 | python3-dev \ 17 | python3-setuptools \ 18 | libgraphviz-dev \ 19 | python3-pygraphviz \ 20 | python3-tabulate \ 21 | `# dependencies for debugging` \ 22 | strace \ 23 | && \ 24 | rm -rf /var/lib/apt/lists/* 25 | 26 | RUN set -x; \ 27 | git clone https://github.com/insomniacslk/dublin-traceroute.git && \ 28 | cd dublin-traceroute && \ 29 | mkdir build && \ 30 | cd build && \ 31 | cmake .. && \ 32 | make && \ 33 | make install && \ 34 | ldconfig 35 | 36 | # also install the python module 37 | RUN set -x; \ 38 | git clone https://github.com/insomniacslk/python-dublin-traceroute.git && \ 39 | cd python-dublin-traceroute && \ 40 | python3 setup.py build && \ 41 | python3 setup.py install 42 | 43 | CMD dublin-traceroute -o trace.json 8.8.8.8 && \ 44 | python3 -m dublintraceroute plot trace.json && \ 45 | mv trace.json.png /output/ 46 | -------------------------------------------------------------------------------- /.ci/docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -exu 3 | 4 | if [ $UID -ne 0 ] 5 | then 6 | sudo $0 $@ 7 | exit $? 8 | fi 9 | 10 | docker build -t insomniacslk/dublin-traceroute -f Dockerfile . $@ 11 | 12 | -------------------------------------------------------------------------------- /.ci/docker/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -exu 3 | 4 | echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin 5 | docker push "${TRAVIS_REPO_SLUG}" 6 | if [ "${TRAVIS_BRANCH}" != "master" ]; then 7 | docker tag "${TRAVIS_REPO_SLUG}" "${TRAVIS_REPO_SLUG}:${TRAVIS_BRANCH}" 8 | docker push "${TRAVIS_REPO_SLUG}:${TRAVIS_BRANCH}" 9 | fi 10 | -------------------------------------------------------------------------------- /.ci/docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -exu 3 | 4 | if [ $UID -ne 0 ] 5 | then 6 | sudo $0 $@ 7 | exit $? 8 | fi 9 | 10 | if [ ! -d output ] 11 | then 12 | mkdir output 13 | fi 14 | docker run --rm -v "${PWD}/output:/output" -it insomniacslk/dublin-traceroute "$@" 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/go" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/coverity.yml: -------------------------------------------------------------------------------- 1 | name: coverity-scan 2 | on: 3 | schedule: 4 | - cron: '30 4 * * 0' # Every Sunday at 4:30 UTC 5 | 6 | jobs: 7 | latest: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Download Coverity Build Tool 11 | run: | 12 | wget -q https://scan.coverity.com/download/cxx/linux64 --post-data "token=$TOKEN&project=insomniacslk%2Fdublin-traceroute" -O cov-analysis-linux64.tar.gz 13 | mkdir cov-analysis-linux64 14 | tar xzf cov-analysis-linux64.tar.gz --strip 1 -C cov-analysis-linux64 15 | env: 16 | TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }} 17 | 18 | - name: Fixed world writable dirs 19 | run: | 20 | chmod go-w $HOME 21 | sudo chmod -R go-w /usr/share 22 | 23 | - name: Build with cov-build 24 | run: | 25 | mkdir build ;\ 26 | cd build ;\ 27 | cmake .. ;\ 28 | export PATH=$PWD/cov-analysis-linux64/bin:$PATH ;\ 29 | cov-build --dir cov-int make 30 | 31 | - name: Submit the result to Coverity Scan 32 | run: | 33 | tar czvf dublin-traceroute.tgz cov-int 34 | curl \ 35 | --form project=dublin-traceroute \ 36 | --form token=$TOKEN \ 37 | --form email=insomniacslk@users.noreply.github.com \ 38 | --form file=@dublin-traceroute.tgz \ 39 | --form version=trunk \ 40 | --form description="`dublin-traceroute -v`" \ 41 | https://scan.coverity.com/builds?project=insomniacslk%2Fdublin-traceroute 42 | env: 43 | TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | 6 | jobs: 7 | build_cpp: 8 | runs-on: ${{ matrix.cfg.os }} 9 | strategy: 10 | matrix: 11 | cfg: 12 | - { 13 | os: ubuntu-latest, 14 | cpp-version: g++ 15 | } 16 | - { 17 | os: ubuntu-latest, 18 | cpp-version: clang++ 19 | } 20 | - { 21 | os: macos-latest, 22 | cpp-version: clang++ 23 | } 24 | steps: 25 | - uses: actions/checkout@v2 26 | - if: matrix.cfg.os == 'ubuntu-latest' 27 | name: Build on Ubuntu 28 | env: 29 | CXX: ${{ matrix.cfg.cpp-version}} 30 | run: | 31 | sudo apt install libtins-dev libjsoncpp-dev libpcap-dev # pcap required for libtins 32 | mkdir build 33 | cd build 34 | cmake .. 35 | make VERBOSE=1 36 | ./dublin-traceroute -v 37 | - if: matrix.cfg.os == 'macos-latest' 38 | name: Build on macOS 39 | env: 40 | CXX: ${{ matrix.cfg.cpp-version}} 41 | run: | 42 | brew update 43 | brew install libtins jsoncpp 44 | mkdir build 45 | cd build 46 | cmake .. 47 | make 48 | ./dublin-traceroute -v 49 | build_go: 50 | runs-on: ${{ matrix.os }} 51 | strategy: 52 | matrix: 53 | go: ['1.21', '1.22'] 54 | os: ['ubuntu-latest', 'macos-latest'] # , 'windows-latest'] 55 | steps: 56 | - uses: actions/checkout@v2 57 | with: 58 | # clone in the gopath 59 | path: src/github.com/${{ github.repository }} 60 | - if: github.event_name == 'pull_request' 61 | # this is for debugging. pull_request has a different hash from push, 62 | # but the original hash is exposed in github.event.pull_request.head.sha 63 | run: | 64 | echo "Building SHA ${{ github.event.pull_request.head.sha }}" 65 | - uses: actions/setup-go@v2 66 | with: 67 | stable: false 68 | go-version: ${{ matrix.go }} 69 | - run: | 70 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV 71 | - if: matrix.os != 'windows-latest' 72 | run: | 73 | cd src/github.com/${{ github.repository }}/go/dublintraceroute/cmd/dublin-traceroute 74 | go get -v ./... 75 | go build 76 | ./dublin-traceroute --version 77 | - if: matrix.os == 'windows-latest' 78 | run: | 79 | cd src/github.com/${{ github.repository }}/go/dublintraceroute/cmd/dublin-traceroute 80 | go get -v ./... 81 | go build 82 | .\dublin-traceroute.exe --version 83 | test_go: 84 | runs-on: ${{ matrix.os }} 85 | strategy: 86 | matrix: 87 | go: ['1.21', '1.22'] 88 | os: ['ubuntu-latest', 'macos-latest'] # , 'windows-latest'] 89 | steps: 90 | - uses: actions/checkout@v2 91 | with: 92 | # clone in the gopath 93 | path: src/github.com/${{ github.repository }} 94 | - uses: actions/setup-go@v2 95 | with: 96 | stable: false 97 | go-version: ${{ matrix.go }} 98 | - run: | 99 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV 100 | - if: matrix.os != 'windows-latest' 101 | run: | 102 | cd src/github.com/${{ github.repository }}/go/dublintraceroute/cmd/dublin-traceroute 103 | go get -v -t ./... 104 | echo "" > coverage.txt 105 | for d in $(go list ./...); do 106 | go test -v -race -coverprofile=profile.out -covermode=atomic "${d}" 107 | if [ -f profile.out ]; then 108 | cat profile.out >> coverage.txt 109 | rm profile.out 110 | fi 111 | done 112 | bash <(curl -s https://codecov.io/bash) -c -f coverage.txt -F unittest 113 | test_cpp: 114 | runs-on: ubuntu-latest 115 | steps: 116 | - uses: actions/checkout@v2 117 | - uses: actions/setup-go@v2 118 | with: 119 | stable: "false" 120 | go-version: "1.22" 121 | - name: Run Tests 122 | run: | 123 | sudo apt install libtins-dev libjsoncpp-dev libpcap-dev googletest 124 | git submodule init 125 | git submodule update 126 | mkdir build 127 | cd build 128 | cmake .. 129 | make tests 130 | ctest 131 | integ: 132 | runs-on: ubuntu-latest 133 | strategy: 134 | matrix: 135 | go: ['1.22'] 136 | env: 137 | DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }} 138 | DOCKER_PASS: ${{ secrets.DOCKERHUB_PASSWORD }} 139 | steps: 140 | - run: | 141 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV 142 | echo "COMPOSE_FILE=$GITHUB_WORKSPACE/src/github.com/${{ github.repository }}/docker-compose.yml" >> $GITHUB_ENV 143 | - uses: actions/checkout@v2 144 | with: 145 | # clone in the gopath 146 | path: "src/github.com/${{ github.repository }}" 147 | - if: github.event_name == 'pull_request' 148 | # this is for debugging. pull_request has a different hash from push, 149 | # but the original hash is exposed in github.event.pull_request.head.sha 150 | run: | 151 | echo "Building SHA ${{ github.event.pull_request.head.sha }}" 152 | - uses: actions/setup-go@v2 153 | with: 154 | stable: false 155 | go-version: ${{ matrix.go }} 156 | - name: build routest 157 | run: | 158 | set -exu 159 | cd "src/github.com/${{ github.repository }}/go/dublintraceroute/cmd/routest" 160 | go get -v ./... 161 | go build 162 | - name: Login to Docker hub 163 | run: docker login -u $DOCKER_USER -p $DOCKER_PASS 164 | - name: Build docker image 165 | run: docker compose -f $COMPOSE_FILE build 166 | - name: Run integ tests 167 | run: docker compose -f $COMPOSE_FILE up --abort-on-container-exit 168 | - name: publish coverage data 169 | run: | 170 | bash <(curl -s https://codecov.io/bash) -c -f coverage_integ.txt -F integ 171 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | .*.swp 3 | *.so 4 | *.dylib 5 | build 6 | go/dublintraceroute/cmd/dublin-traceroute/dublin-traceroute 7 | go/dublintraceroute/cmd/routest/routest 8 | profile.out 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dependencies/jsoncpp"] 2 | path = external/jsoncpp 3 | url = https://github.com/open-source-parsers/jsoncpp.git 4 | [submodule "dependencies/libtins"] 5 | path = external/libtins 6 | url = https://github.com/mfontanini/libtins.git 7 | [submodule "tests/googletest"] 8 | path = googletest 9 | url = https://github.com/google/googletest.git 10 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | golint: 3 | fixer: true 4 | flake8: 5 | fixer: true 6 | rubocop: 7 | fixer: true 8 | shellcheck: 9 | shell: bash 10 | fixers: 11 | enable: true 12 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | cmake_minimum_required (VERSION 3.16) 4 | project (dublin-traceroute) 5 | 6 | IF(APPLE) 7 | # macOS Mojave is not using /usr/include anymore, see https://github.com/neovim/neovim/issues/9050 8 | if(NOT DEFINED ENV{MACOSX_DEPLOYMENT_TARGET} AND NOT DEFINED ENV{SDKROOT}) 9 | set(CMAKE_THREAD_LIBS_INIT "-lpthread") 10 | set(CMAKE_OSX_DEPLOYMENT_TARGET ${CMAKE_SYSTEM_VERSION}) 11 | ENDIF() 12 | # assume built-in pthreads on MacOS 13 | set(CMAKE_HAVE_THREADS_LIBRARY 1) 14 | set(CMAKE_USE_WIN32_THREADS_INIT 0) 15 | set(CMAKE_USE_PTHREADS_INIT 1) 16 | set(THREADS_PREFER_PTHREAD_FLAG ON) 17 | ENDIF() 18 | 19 | # TODO sync this with VERSION in include/dublintraceroute/common.h 20 | set (dublin-traceroute_VERSION_MAJOR_0) 21 | set (dublin-traceroute_VERSION_MINOR_5) 22 | set (dublin-traceroute_VERSION_PATCH_0) 23 | 24 | # ensure that /usr/local is used to find dependencies. This is especially 25 | # necessary for brew on OSX and for libraries installed manually under 26 | # /usr/local 27 | list(APPEND CMAKE_PREFIX_PATH /usr/local) 28 | 29 | include_directories( 30 | "${PROJECT_SOURCE_DIR}/include" 31 | ) 32 | 33 | add_library(dublintraceroute SHARED 34 | src/common.cc 35 | src/dublin_traceroute.cc 36 | src/hop.cc 37 | src/udpv4probe.cc 38 | src/traceroute_results.cc 39 | ) 40 | 41 | # Set the shared library version 42 | set_target_properties(dublintraceroute 43 | PROPERTIES 44 | SOVERSION 0.2.0 45 | ) 46 | 47 | find_package(PkgConfig) 48 | find_package(Threads REQUIRED) 49 | find_package(libtins 3.4) 50 | if (${libtins_FOUND}) 51 | MESSAGE(STATUS "libtins found via CMake") 52 | else (${libtins_FOUND}) 53 | MESSAGE(STATUS "libtins not found via CMake, trying pkg-config") 54 | pkg_search_module(libtins REQUIRED libtins) 55 | endif (${libtins_FOUND}) 56 | 57 | if (${jsoncpp_FOUND}) 58 | MESSAGE(STATUS "jsoncpp found via CMake") 59 | else (${jsoncpp_FOUND}) 60 | MESSAGE(STATUS "jsoncpp not found via CMake, trying pkg-config") 61 | pkg_search_module(JSONCPP REQUIRED jsoncpp) 62 | endif (${jsoncpp_FOUND}) 63 | 64 | add_executable(dublin-traceroute src/main.cc) 65 | target_link_libraries(dublintraceroute ${CMAKE_THREAD_LIBS_INIT}) 66 | target_link_libraries(dublin-traceroute dublintraceroute) 67 | target_link_libraries(dublintraceroute tins) 68 | 69 | target_link_libraries(dublintraceroute ${JSONCPP_LIBRARIES} jsoncpp) 70 | target_include_directories(dublintraceroute PUBLIC ${JSONCPP_INCLUDE_DIRS} ${TINS_INCLUDE_DIRS}) 71 | 72 | if(APPLE) 73 | # with macOS Mojave /usr/local/{include,lib} have to be specified explicitly 74 | target_include_directories(dublintraceroute PUBLIC /usr/local/include) 75 | target_include_directories(dublintraceroute PUBLIC /opt/homebrew/include) 76 | target_link_directories(dublintraceroute PUBLIC /usr/local/lib) 77 | target_link_directories(dublintraceroute PUBLIC /opt/homebrew/lib) 78 | endif(APPLE) 79 | 80 | #set_property(TARGET dublintraceroute PROPERTY CXX_STANDARD 11) 81 | #set_property(TARGET dublintraceroute PROPERTY CXX_STANDARD_REQUIRED ON) 82 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") 83 | 84 | add_dependencies(dublin-traceroute dublintraceroute) 85 | 86 | if (NOT CMAKE_INSTALL_BINDIR) 87 | set(CMAKE_INSTALL_BINDIR "bin") 88 | endif() 89 | 90 | if (NOT CMAKE_INSTALL_LIBDIR) 91 | set(CMAKE_INSTALL_LIBDIR "lib") 92 | endif() 93 | 94 | install(TARGETS dublin-traceroute dublintraceroute 95 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 96 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 97 | ) 98 | install(DIRECTORY include/dublintraceroute DESTINATION include) 99 | 100 | # find setcap 101 | find_program(SETCAP_EXECUTABLE 102 | NAMES 103 | setcap 104 | PATHS 105 | /bin 106 | /usr/bin 107 | /usr/local/bin 108 | /sbin 109 | ) 110 | 111 | if (SETCAP_EXECUTABLE) 112 | install(CODE "execute_process( 113 | COMMAND 114 | ${SETCAP_EXECUTABLE} 115 | cap_net_raw+ep 116 | ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/dublin-traceroute 117 | RESULT_VARIABLE 118 | SETCAP_RESULT 119 | ) 120 | if (SETCAP_RESULT) 121 | message(WARNING \"setcap failed (${SETCAP_RESULT})\") 122 | endif()" 123 | ) 124 | endif() 125 | 126 | 127 | # Testing 128 | include(FetchContent) 129 | FetchContent_Declare( 130 | googletest 131 | URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.zip 132 | ) 133 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 134 | FetchContent_MakeAvailable(googletest) 135 | 136 | # Make sure we build googletest before anything else 137 | ENABLE_TESTING() 138 | ADD_SUBDIRECTORY(tests) 139 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | dublin-traceroute.net -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-present Andrea Barberio 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | cmake 2 | a recent enough C++11 compiler (gcc >= 4.6 or clang >= 3.3) 3 | libtins >= 3.4 4 | -------------------------------------------------------------------------------- /Dockerfile.integ: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | LABEL BUILD="docker build -t insomniacslk/dublin-traceroute-integ -f Dockerfile.integ ." 4 | # --privileged required for nfqueue 5 | LABEL RUN="docker run --rm --privileged -it insomniacslk/dublin-traceroute-integ" 6 | 7 | # Install dependencies 8 | RUN apt-get update && \ 9 | apt-get install -y --no-install-recommends \ 10 | sudo \ 11 | # to fetch the Go toolchain 12 | ca-certificates \ 13 | wget \ 14 | # for go get 15 | git \ 16 | # for routest 17 | iptables \ 18 | # for dublin-traceroute 19 | build-essential cmake \ 20 | pkg-config libtins-dev libjsoncpp-dev \ 21 | && \ 22 | rm -rf /var/lib/apt/lists/* 23 | 24 | # install Go 25 | WORKDIR /tmp 26 | RUN set -exu; \ 27 | wget https://golang.org/dl/go1.21.13.linux-amd64.tar.gz ;\ 28 | tar -C / -xvzf go1.21.13.linux-amd64.tar.gz 29 | ENV PATH="$PATH:/go/bin:/build/bin" 30 | ENV GOPATH=/go:/build 31 | 32 | ENV PROJDIR=/build/src/github.com/insomniacslk/dublin-traceroute 33 | RUN mkdir -p $PROJDIR 34 | COPY . $PROJDIR 35 | 36 | # build and install routest 37 | WORKDIR /build/src 38 | RUN set -exu; \ 39 | cd $PROJDIR/go/dublintraceroute/cmd/routest ;\ 40 | go get -v ./... ;\ 41 | go build ;\ 42 | go install . 43 | 44 | # build dublin-traceroute (CPP) 45 | RUN set -exu ;\ 46 | cd $PROJDIR ;\ 47 | rm -rf build; mkdir build; cd build ;\ 48 | cmake .. ;\ 49 | make ;\ 50 | make install 51 | 52 | # build dublin-traceroute (Go) 53 | RUN set -exu ;\ 54 | cd $PROJDIR/go/dublintraceroute/cmd/dublin-traceroute ;\ 55 | go get -v ./... ;\ 56 | go build 57 | 58 | CMD set -exu ;\ 59 | cd $PROJDIR ;\ 60 | timeout 60 go test -v -race -coverprofile=/build/coverage/profile.out -covermode=atomic github.com/insomniacslk/dublin-traceroute/integ 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | C++ library and CLI: [![Build Status](https://www.travis-ci.org/insomniacslk/dublin-traceroute.svg?branch=master)](https://www.travis-ci.org/insomniacslk/dublin-traceroute) 2 | [![Coverity Scan Build Status](https://scan.coverity.com/projects/7935/badge.svg)](https://scan.coverity.com/projects/insomniacslk-dublin-traceroute) 3 | 4 | Python module: [![Build Status](https://www.travis-ci.org/insomniacslk/python-dublin-traceroute.svg?branch=master)](https://www.travis-ci.org/insomniacslk/python-dublin-traceroute) [![Version](https://img.shields.io/pypi/v/dublintraceroute.svg)](https://pypi.python.org/pypi/dublintraceroute) 5 | 6 | Debian packages: 7 | 8 | ![Debian Unstable](https://badges.debian.net/badges/debian/unstable/dublin-traceroute/version.svg) 9 | ![Debian Testing](https://badges.debian.net/badges/debian/testing/dublin-traceroute/version.svg) 10 | 11 | Dublin Traceroute is a NAT-aware multipath traceroute tool. 12 | 13 | And this page is just informational. **Read more at https://dublin-traceroute.net** . 14 | 15 | Dublin Traceroute has a blog, with articles on how to make the best out of it. Check it out at [blog.dublin-traceroute.net](https://blog.dublin-traceroute.net). 16 | 17 | Dublin Traceroute has also Python bindings, that will let you: 18 | * use Dublin Traceroute from Python code 19 | * generate graphical visualizations of your multipath traceroutes 20 | * do statistical analysis using Pandas 21 | 22 | See [python-dublin-traceroute](https://github.com/insomniacslk/python-dublin-traceroute) for more information. 23 | 24 | Feedback is very welcome! You can [open a new issue](https://github.com/insomniacslk/dublin-traceroute/issues/new/choose), or contact me directly, see https://insomniac.slackware.it for contact details. 25 | 26 | But, in a few words, you can run traceroutes in multi-path networks (i.e. with ECMP load-balancing enabled), recognize NATs, have nice diagrams like the one below, export to JSON, and do this with a command-line tool, a C++ library or a Python library. 27 | 28 | ![dublin-traceroute example](docs/traceroute_8.8.8.8.png) 29 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /contributors.txt: -------------------------------------------------------------------------------- 1 | insomniac 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | integ: 3 | privileged: true 4 | build: 5 | context: . 6 | dockerfile: Dockerfile.integ 7 | volumes: 8 | - .:/build/coverage 9 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | dublin-traceroute.net -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | * Implement UDP-DNS probes 2 | * Implement TCP-80/443 3 | * sniff traffic and react to failing probes 4 | * ~~Support IPv6~~ (currently only in the Go implementation) 5 | * implement ICMP-paris 6 | * testing! 7 | * ~~add min-ttl option~~ done in [commit 6b6be67](https://github.com/insomniacslk/dublin-traceroute/commit/6b6be67df1ea17c7de20b8ee1e4fab664ee177eb) 8 | * Implement merge of ICMP-paris, UDP-paris and TCP-paris probes 9 | * Implement Bellovin's technique using response's IP ID. 10 | [paper](https://www.cs.columbia.edu/~smb/papers/fnat.pdf) and 11 | [slides](https://www.cs.columbia.edu/~smb/talks/findnat.pdf) 12 | * use RocketFuel's technique to identify different network interfaces on a 13 | router using IP ID 14 | * ~~support MPLS, https://tools.ietf.org/html/rfc4950~~, done in https://github.com/insomniacslk/dublin-traceroute/issues/6 15 | * MPLS reconnaisance techniques, http://www.montefiore.ulg.ac.be/~bdonnet/mpls/ 16 | * break on destination unreachable 17 | * improve documentation 18 | * put everything under a namespace 19 | * ~~implement command line parser in main.cc~~ done in [commit 8a3ae75](https://github.com/insomniacslk/dublin-traceroute/commit/8a3ae7513645afdad5eabd8d6f368383dff98c8b) 20 | * Add an uninstall target in the Makefile for the python extension 21 | * Fix the memory leak where TracerouteResults is not freed 22 | * IP_ID_MATCHING must become a constructor parameter 23 | * SNIFFER_TIMEOUT must become a constructor parameter 24 | * Handle open UDP dst port responses 25 | * Explore the use of eBPF as a backend 26 | * Send 3 packets per hop 27 | * Convert the traceroute internal representation into a graph (use https://graph-tool.skewed.de/ maybe?) 28 | * ~~Use both src and dst ports for tracerouting~~ 29 | * Add path MTU discovery and measure fragmentation-induced latency 30 | * Use [pyasn](https://github.com/hadiasghari/pyasn) to look up AS by IP 31 | * Integrate the ASN graph with a world map (e.g. openstreetmap or google maps) 32 | * Add --webserver to the python CLI to expose a SimpleHTTPServer that serves a PNG with the traceroute diagram 33 | * heat map/flame graph of the network latencies over time (links history) 34 | * ~~improve the build system (there is just a static Makefile now)~~ done in [commit ffa9d3c](https://github.com/insomniacslk/dublin-traceroute/commit/ffa9d3c306fb772e2c95963a94cdc386b0126206), using CMake 35 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /docs/dublin-traceroute.repo: -------------------------------------------------------------------------------- 1 | [dublintraceroute] 2 | name=Dublin Traceroute and dependencies 3 | baseurl=https://dublin-traceroute.net/repo 4 | enabled=1 5 | gpgcheck=0 6 | #gpgkey= 7 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | You can invoke Dublin-traceroute in multiple ways. So far there are: 4 | 5 | * a command-line tool, ```dublin-traceroute```, 6 | * a C++ library, ```libdublintraceroute.so```, 7 | * a Python module, ```dublintraceroute```, which now lives in a separate repository, see [python-dublin-traceroute](https://github.com/insomniacslk/python-dublin-traceroute). 8 | 9 | ### Using the command-line tool 10 | 11 | NOTE: the CLI tool is still in development and lacks many features. 12 | 13 | The basic usage of ```dublin-traceroute ``` is the following: 14 | 15 | ```bashed 16 | $ dublin-traceroute 17 | ``` 18 | 19 | For example, you can traceroute Google's public DNS server as follows: 20 | 21 | ```bash 22 | $ dublin-traceroute 8.8.8.8 23 | ``` 24 | 25 | The current default is to probe 20 different paths with a maximum TTL set to 30. 26 | 27 | You will see the output on the terminal, but ```dublin-traceroute``` will also 28 | create a file named ```trace.json``` in the current directory. You can run the 29 | script located at ```scripts/to_graphviz.py``` to generate an image showing graphically 30 | the traceroute. The image file is named ```trace.json.png```. For example: 31 | 32 | ```bash 33 | $ dublin-traceroute 8.8.8.8 34 | ... 35 | $ python scripts/to_graphviz.py trace.json 36 | $ eog trace.json.png # or open it with your favourite viewer 37 | ``` 38 | 39 | The image will look something like this: 40 | 41 | ![traceroute to 8.8.8.8](traceroute_8.8.8.8.png) 42 | 43 | You can view the content of ```trace.json``` of course, and use it as needed. 44 | 45 | ### Using Python 46 | 47 | The Python bindings have been split into a separate package. See 48 | [python-dublin-traceroute](https://github.com/insomniacslk/python-dublin-traceroute) . 49 | 50 | 51 | ### Using the C++ library 52 | 53 | TODO 54 | 55 | Meanwhile, you can look at [the implementation of the command-line tool](../../src/main.cc) 56 | -------------------------------------------------------------------------------- /docs/tr01.dot: -------------------------------------------------------------------------------- 1 | DiGraph { 2 | rankdir=LR; 3 | A, F [shape=rectangle]; 4 | A -> B -> D -> F; 5 | A -> C -> E -> F; 6 | } 7 | -------------------------------------------------------------------------------- /docs/tr01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insomniacslk/dublin-traceroute/ea3a974f50c7465bf79d7adab83799332f864b40/docs/tr01.png -------------------------------------------------------------------------------- /docs/tr02.dot: -------------------------------------------------------------------------------- 1 | DiGraph { 2 | rankdir=LR; 3 | A, F [shape=rectangle]; 4 | A -> B -> D -> F; 5 | A -> C -> E -> F; 6 | A -> B -> E -> F [color=red]; 7 | A -> C -> D -> F [style=invis]; 8 | } 9 | -------------------------------------------------------------------------------- /docs/tr02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insomniacslk/dublin-traceroute/ea3a974f50c7465bf79d7adab83799332f864b40/docs/tr02.png -------------------------------------------------------------------------------- /docs/tr03.dot: -------------------------------------------------------------------------------- 1 | DiGraph { 2 | rankdir=LR; 3 | A, G [shape=rectangle]; 4 | A -> B -> D -> F -> G; 5 | A -> C -> E -> G; 6 | A -> C -> D -> G [color=red]; 7 | } 8 | -------------------------------------------------------------------------------- /docs/tr03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insomniacslk/dublin-traceroute/ea3a974f50c7465bf79d7adab83799332f864b40/docs/tr03.png -------------------------------------------------------------------------------- /docs/traceroute_8.8.8.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insomniacslk/dublin-traceroute/ea3a974f50c7465bf79d7adab83799332f864b40/docs/traceroute_8.8.8.8.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/insomniacslk/dublin-traceroute 2 | 3 | go 1.22 4 | toolchain go1.24.1 5 | 6 | require ( 7 | github.com/florianl/go-nfqueue v1.3.1 8 | github.com/goccy/go-graphviz v0.1.2 9 | github.com/sirupsen/logrus v1.9.3 10 | github.com/spf13/pflag v1.0.5 11 | github.com/stretchr/testify v1.8.2 12 | golang.org/x/net v0.38.0 13 | golang.org/x/sys v0.31.0 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/fogleman/gg v1.3.0 // indirect 19 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 20 | github.com/google/go-cmp v0.6.0 // indirect 21 | github.com/josharian/native v1.1.0 // indirect 22 | github.com/mdlayher/netlink v1.7.2 // indirect 23 | github.com/mdlayher/socket v0.5.0 // indirect 24 | github.com/pkg/errors v0.9.1 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | golang.org/x/image v0.23.0 // indirect 27 | golang.org/x/sync v0.6.0 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/corona10/goimagehash v1.0.2 h1:pUfB0LnsJASMPGEZLj7tGY251vF+qLGqOgEP4rUs6kA= 2 | github.com/corona10/goimagehash v1.0.2/go.mod h1:/l9umBhvcHQXVtQO1V6Gp1yD20STawkhRnnX0D1bvVI= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/florianl/go-nfqueue v1.3.1 h1:khQ9fYCrjbu5CF8dZF55G2RTIEIQRI0Aj5k3msJR6Gw= 7 | github.com/florianl/go-nfqueue v1.3.1/go.mod h1:aHWbgkhryJxF5XxYvJ3oRZpdD4JP74Zu/hP1zuhja+M= 8 | github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= 9 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 10 | github.com/goccy/go-graphviz v0.1.2 h1:sWSJ6w13BCm/ZOUTHDVrdvbsxqN8yyzaFcHrH/hQ9Yg= 11 | github.com/goccy/go-graphviz v0.1.2/go.mod h1:pMYpbAqJT10V8dzV1JN/g/wUlG/0imKPzn3ZsrchGCI= 12 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 13 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 14 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 15 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 16 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 17 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 18 | github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 19 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 20 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 21 | github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= 22 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 23 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 24 | github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= 25 | github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= 26 | github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= 27 | github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY= 28 | github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 29 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 30 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 34 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 35 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 36 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 39 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 40 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 41 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 42 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 43 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 44 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 45 | golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= 46 | golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= 47 | golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 48 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 49 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 50 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 51 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 53 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 54 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 62 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 63 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 64 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 65 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 66 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 67 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 68 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 69 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 70 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 72 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 73 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 74 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 75 | -------------------------------------------------------------------------------- /go/dublintraceroute/cmd/dublin-traceroute/main.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "go/build" 9 | "log" 10 | "net" 11 | "os" 12 | "runtime" 13 | "time" 14 | 15 | flag "github.com/spf13/pflag" 16 | 17 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute" 18 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/probes/probev4" 19 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/probes/probev6" 20 | ) 21 | 22 | // Program constants and default values 23 | const ( 24 | ProgramName = "Dublin Traceroute" 25 | ProgramVersion = "v0.2" 26 | ProgramAuthorName = "Andrea Barberio" 27 | ProgramAuthorInfo = "https://insomniac.slackware.it" 28 | DefaultSourcePort = 12345 29 | DefaultDestPort = 33434 30 | DefaultNumPaths = 10 31 | DefaultMinTTL = 1 32 | DefaultMaxTTL = 30 33 | DefaultDelay = 50 //msec 34 | DefaultReadTimeout = 3 * time.Second 35 | DefaultOutputFormat = "json" 36 | ) 37 | 38 | // used to hold flags 39 | type args struct { 40 | version bool 41 | target string 42 | sport int 43 | useSrcport bool 44 | dport int 45 | npaths int 46 | minTTL int 47 | maxTTL int 48 | delay int 49 | brokenNAT bool 50 | outputFile string 51 | outputFormat string 52 | v4 bool 53 | } 54 | 55 | // Args will hold the program arguments 56 | var Args args 57 | 58 | // resolve returns the first IP address for the given host. If `wantV6` is true, 59 | // it will return the first IPv6 address, or nil if none. Similarly for IPv4 60 | // when `wantV6` is false. 61 | // If the host is already an IP address, such IP address will be returned. If 62 | // `wantV6` is true but no IPv6 address is found, it will return an error. 63 | // Similarly for IPv4 when `wantV6` is false. 64 | func resolve(host string, wantV6 bool) (net.IP, error) { 65 | if ip := net.ParseIP(host); ip != nil { 66 | if wantV6 && ip.To4() != nil { 67 | return nil, errors.New("Wanted an IPv6 address but got an IPv4 address") 68 | } else if !wantV6 && ip.To4() == nil { 69 | return nil, errors.New("Wanted an IPv4 address but got an IPv6 address") 70 | } 71 | return ip, nil 72 | } 73 | ipaddrs, err := net.LookupIP(host) 74 | if err != nil { 75 | return nil, err 76 | } 77 | var ret net.IP 78 | for _, ipaddr := range ipaddrs { 79 | if wantV6 && ipaddr.To4() == nil { 80 | ret = ipaddr 81 | break 82 | } else if !wantV6 && ipaddr.To4() != nil { 83 | ret = ipaddr 84 | } 85 | } 86 | if ret == nil { 87 | return nil, errors.New("No IP address of the requested type was found") 88 | } 89 | return ret, nil 90 | } 91 | 92 | func init() { 93 | // Ensure that CGO is disabled 94 | var ctx build.Context 95 | if ctx.CgoEnabled { 96 | fmt.Println("Disabling CGo") 97 | ctx.CgoEnabled = false 98 | } 99 | 100 | // handle flags 101 | flag.Usage = func() { 102 | fmt.Fprintf(os.Stderr, "Dublin Traceroute (Go implementation) %s\n", ProgramVersion) 103 | fmt.Fprintf(os.Stderr, "Written by %s - %s\n", ProgramAuthorName, ProgramAuthorInfo) 104 | fmt.Fprintf(os.Stderr, "\n") 105 | flag.PrintDefaults() 106 | } 107 | // Args holds the program's arguments as parsed by `flag` 108 | flag.BoolVarP(&Args.version, "version", "v", false, "print the version of Dublin Traceroute") 109 | flag.IntVarP(&Args.sport, "sport", "s", DefaultSourcePort, "the source port to send packets from") 110 | flag.IntVarP(&Args.dport, "dport", "d", DefaultDestPort, "the base destination port to send packets to") 111 | flag.IntVarP(&Args.npaths, "npaths", "n", DefaultNumPaths, "the number of paths to probe") 112 | flag.IntVarP(&Args.minTTL, "min-ttl", "t", DefaultMinTTL, "the minimum TTL to probe") 113 | flag.IntVarP(&Args.maxTTL, "max-ttl", "T", DefaultMaxTTL, "the maximum TTL to probe") 114 | flag.IntVarP(&Args.delay, "delay", "D", DefaultDelay, "the inter-packet delay in milliseconds") 115 | flag.BoolVarP(&Args.brokenNAT, "broken-nat", "b", false, "the network has a broken NAT configuration (e.g. no payload fixup). Try this if you see fewer hops than expected") 116 | flag.BoolVarP(&Args.useSrcport, "use-srcport", "i", false, "generate paths using source port instead of destination port") 117 | flag.StringVarP(&Args.outputFile, "output-file", "o", "", "the output file name. If unspecified, or \"-\", print to stdout") 118 | flag.StringVarP(&Args.outputFormat, "output-format", "f", DefaultOutputFormat, "the output file format, either \"json\" or \"dot\"") 119 | flag.BoolVarP(&Args.v4, "force-ipv4", "4", false, "Force the use of the legacy IPv4 protocol") 120 | flag.CommandLine.SortFlags = false 121 | } 122 | 123 | func main() { 124 | SetColourPurple := "\x1b[0;35m" 125 | UnsetColour := "\x1b[0m" 126 | if os.Geteuid() == 0 { 127 | if runtime.GOOS == "linux" { 128 | fmt.Fprintf(os.Stderr, "%sWARNING: you are running this program as root. Consider setting the CAP_NET_RAW capability and running as non-root user as a more secure alternative%s\n", SetColourPurple, UnsetColour) 129 | } 130 | } 131 | 132 | flag.Parse() 133 | if Args.version { 134 | fmt.Printf("%v %v\n", ProgramName, ProgramVersion) 135 | os.Exit(0) 136 | } 137 | 138 | if len(flag.Args()) != 1 { 139 | log.Fatal("Exactly one target is required") 140 | } 141 | 142 | Args.target = flag.Arg(0) 143 | target, err := resolve(Args.target, !Args.v4) 144 | if err != nil { 145 | log.Fatalf("Cannot resolve %s: %v", flag.Arg(0), err) 146 | } 147 | fmt.Fprintf(os.Stderr, "Traceroute configuration:\n") 148 | fmt.Fprintf(os.Stderr, "Target : %v\n", target) 149 | fmt.Fprintf(os.Stderr, "Base source port : %v\n", Args.sport) 150 | fmt.Fprintf(os.Stderr, "Base destination port : %v\n", Args.dport) 151 | fmt.Fprintf(os.Stderr, "Use srcport for paths : %v\n", Args.useSrcport) 152 | fmt.Fprintf(os.Stderr, "Number of paths : %v\n", Args.npaths) 153 | fmt.Fprintf(os.Stderr, "Minimum TTL : %v\n", Args.minTTL) 154 | fmt.Fprintf(os.Stderr, "Maximum TTL : %v\n", Args.maxTTL) 155 | fmt.Fprintf(os.Stderr, "Inter-packet delay : %v\n", Args.delay) 156 | fmt.Fprintf(os.Stderr, "Timeout : %v\n", time.Duration(Args.delay)*time.Millisecond) 157 | fmt.Fprintf(os.Stderr, "Treat as broken NAT : %v\n", Args.brokenNAT) 158 | 159 | var dt dublintraceroute.DublinTraceroute 160 | if Args.v4 { 161 | dt = &probev4.UDPv4{ 162 | Target: target, 163 | SrcPort: uint16(Args.sport), 164 | DstPort: uint16(Args.dport), 165 | UseSrcPort: Args.useSrcport, 166 | NumPaths: uint16(Args.npaths), 167 | MinTTL: uint8(Args.minTTL), 168 | MaxTTL: uint8(Args.maxTTL), 169 | Delay: time.Duration(Args.delay) * time.Millisecond, 170 | Timeout: DefaultReadTimeout, 171 | BrokenNAT: Args.brokenNAT, 172 | } 173 | } else { 174 | dt = &probev6.UDPv6{ 175 | Target: target, 176 | SrcPort: uint16(Args.sport), 177 | DstPort: uint16(Args.dport), 178 | UseSrcPort: Args.useSrcport, 179 | NumPaths: uint16(Args.npaths), 180 | MinHopLimit: uint8(Args.minTTL), 181 | MaxHopLimit: uint8(Args.maxTTL), 182 | Delay: time.Duration(Args.delay) * time.Millisecond, 183 | Timeout: DefaultReadTimeout, 184 | BrokenNAT: Args.brokenNAT, 185 | } 186 | } 187 | results, err := dt.Traceroute() 188 | if err != nil { 189 | log.Fatalf("Traceroute() failed: %v", err) 190 | } 191 | var ( 192 | output string 193 | ) 194 | switch Args.outputFormat { 195 | case "json": 196 | output, err = results.ToJSON(true, " ") 197 | case "dot": 198 | output, err = results.ToDOT() 199 | default: 200 | log.Fatalf("Unknown output format \"%s\"", Args.outputFormat) 201 | } 202 | if err != nil { 203 | log.Fatalf("Failed to generate output in format \"%s\": %v", Args.outputFormat, err) 204 | } 205 | if Args.outputFile == "-" || Args.outputFile == "" { 206 | fmt.Println(output) 207 | } else { 208 | err := os.WriteFile(Args.outputFile, []byte(output), 0644) 209 | if err != nil { 210 | log.Fatalf("Failed to write to file: %v", err) 211 | } 212 | log.Printf("Saved results to to \"%s\"", Args.outputFile) 213 | if Args.outputFormat == "json" { 214 | log.Printf("You can convert it to DOT by running `todot \"%s\" -o \"%s.dot\"`", Args.outputFile, Args.outputFile) 215 | } 216 | log.Printf("You can convert the DOT file to PNG by running `dot -Tpng \"%s.dot\" -o \"%s.png\"`", Args.outputFile, Args.outputFile) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /go/dublintraceroute/cmd/routest/README.md: -------------------------------------------------------------------------------- 1 | # routest 2 | 3 | RouTest is a tool to help end-to-end testing of tracerouting tools, by crafting pre-defined responses to traceroute packets. 4 | 5 | It works using NFQUEUE (hence iptables, hence it only works on Linux). The user needs to: 6 | * set up an NFQUEUE rule, for example `iptables -A OUTPUT -p udp --dport 33434:33634 -d 8.8.8.8 -j NFQUEUE --queue-num 101`. This will instruct the kernel to pass any UDP packet with IP destination 8.8.8.8 and destination port in the range 33434-33634 to the user space 7 | * create a JSON config file that contains a description of the packets and the expected responses. A sample `config.json` is provided. 8 | * run `routest -c -q 101 -i lo` (where 101 is the number of the NFQUEUE specified in the iptables command above). This will receive the packets from the kernel, and craft a response according to the config file. 9 | 10 | Once this is done, just run your traceroute, e.g.: 11 | 12 | ``` 13 | $ sudo dublin-traceroute -n1 8.8.8.8 14 | WARNING: you are running this program as root. Consider setting the CAP_NET_RAW 15 | capability and running as non-root user as a more secure alternative. 16 | Starting dublin-traceroute 17 | Traceroute from 0.0.0.0:12345 to 8.8.8.8:33434~33434 (probing 1 path, min TTL is 1, max TTL is 30, delay is 10 ms) 18 | == Flow ID 33434 == 19 | 1 1.2.3.4 (1.2.3.4), IP ID: 17022 RTT 4.729 ms ICMP (type=11, code=0) 'TTL expired in transit', NAT ID: 0, flow hash: 52376 20 | 2 1.2.3.5 (1.2.3.5), IP ID: 30935 RTT 4.443 ms ICMP (type=11, code=0) 'TTL expired in transit', NAT ID: 0, flow hash: 52376 21 | 3 1.2.3.6 (1.2.3.6), IP ID: 35202 RTT 4.117 ms ICMP (type=11, code=0) 'TTL expired in transit', NAT ID: 0, flow hash: 52376 22 | 4 8.8.8.8 (dns.google), IP ID: 35204 RTT 3.545 ms ICMP (type=3, code=3) 'Destination port unreachable', NAT ID: 0, flow hash: 52376 23 | Saved JSON file to trace.json . 24 | You can convert it to DOT by running python3 -m dublintraceroute plot trace.json 25 | ``` 26 | 27 | 28 | On the `routest` side, the output is: 29 | ``` 30 | $ sudo ./routest -c config.json -q 101 -i lo 31 | INFO[0000] Loaded configuration: 32 | [ 33 | { 34 | "dst": "8.8.8.8", 35 | "dst_port": 33434, 36 | "ttl": 1, 37 | "reply": { 38 | "src": "1.2.3.4", 39 | "icmp_type": 11, 40 | "icmp_code": 0 41 | } 42 | }, 43 | { 44 | "dst": "8.8.8.8", 45 | "dst_port": 33434, 46 | "ttl": 2, 47 | "reply": { 48 | "src": "1.2.3.5", 49 | "icmp_type": 11, 50 | "icmp_code": 0 51 | } 52 | }, 53 | { 54 | "dst": "8.8.8.8", 55 | "dst_port": 33434, 56 | "ttl": 3, 57 | "reply": { 58 | "src": "1.2.3.6", 59 | "icmp_type": 11, 60 | "icmp_code": 0 61 | } 62 | }, 63 | { 64 | "dst": "8.8.8.8", 65 | "dst_port": 33434, 66 | "ttl": 4, 67 | "reply": { 68 | "src": "8.8.8.8", 69 | "icmp_type": 3, 70 | "icmp_code": 3 71 | } 72 | } 73 | ] 74 | DEBU[0001] Matching packet: &{Version:4 HeaderLen:5 DiffServ:0 TotalLen:35 ID:23210 Flags:2 FragOff:0 TTL:1 Proto:17 Checksum:36363 Src:172.17.212.243 Dst:8.8.8.8 Options:[] next:0xc00008e9c0 IPinICMP:false} >> &{Src:12345 Dst:33434 Len:15 Csum:23210 next: prev:} 75 | DEBU[0001] Found match {Src: Dst:8.8.8.8 SrcPort: DstPort:33434 TTL:1 Reply:{Src:1.2.3.4 Dst: IcmpType:11 IcmpCode:0 Payload:[]}} 76 | DEBU[0001] Matching packet: &{Version:4 HeaderLen:5 DiffServ:0 TotalLen:35 ID:23209 Flags:2 FragOff:0 TTL:2 Proto:17 Checksum:36108 Src:172.17.212.243 Dst:8.8.8.8 Options:[] next:0xc0000d01e0 IPinICMP:false} >> &{Src:12345 Dst:33434 Len:15 Csum:23209 next: prev:} 77 | DEBU[0001] Found match {Src: Dst:8.8.8.8 SrcPort: DstPort:33434 TTL:2 Reply:{Src:1.2.3.5 Dst: IcmpType:11 IcmpCode:0 Payload:[]}} 78 | DEBU[0001] Matching packet: &{Version:4 HeaderLen:5 DiffServ:0 TotalLen:35 ID:23208 Flags:2 FragOff:0 TTL:3 Proto:17 Checksum:35853 Src:172.17.212.243 Dst:8.8.8.8 Options:[] next:0xc0000d0450 IPinICMP:false} >> &{Src:12345 Dst:33434 Len:15 Csum:23208 next: prev:} 79 | DEBU[0001] Found match {Src: Dst:8.8.8.8 SrcPort: DstPort:33434 TTL:3 Reply:{Src:1.2.3.6 Dst: IcmpType:11 IcmpCode:0 Payload:[]}} 80 | DEBU[0001] Matching packet: &{Version:4 HeaderLen:5 DiffServ:0 TotalLen:35 ID:23207 Flags:2 FragOff:0 TTL:4 Proto:17 Checksum:35598 Src:172.17.212.243 Dst:8.8.8.8 Options:[] next:0xc00008eba0 IPinICMP:false} >> &{Src:12345 Dst:33434 Len:15 Csum:23207 next: prev:} 81 | DEBU[0001] Found match {Src: Dst:8.8.8.8 SrcPort: DstPort:33434 TTL:4 Reply:{Src:8.8.8.8 Dst: IcmpType:3 IcmpCode:3 Payload:[]}} 82 | DEBU[0001] Matching packet: &{Version:4 HeaderLen:5 DiffServ:0 TotalLen:35 ID:23206 Flags:2 FragOff:0 TTL:5 Proto:17 Checksum:35343 Src:172.17.212.243 Dst:8.8.8.8 Options:[] next:0xc0000d0630 IPinICMP:false} >> &{Src:12345 Dst:33434 Len:15 Csum:23206 next: prev:} 83 | INFO[0001] Packet not matching 84 | DEBU[0001] Matching packet: &{Version:4 HeaderLen:5 DiffServ:0 TotalLen:35 ID:23205 Flags:2 FragOff:0 TTL:6 Proto:17 Checksum:35088 Src:172.17.212.243 Dst:8.8.8.8 Options:[] next:0xc0000fa120 IPinICMP:false} >> &{Src:12345 Dst:33434 Len:15 Csum:23205 next: prev:} 85 | INFO[0001] Packet not matching 86 | ... 87 | ``` 88 | -------------------------------------------------------------------------------- /go/dublintraceroute/cmd/routest/config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "dst": "8.8.8.8", 4 | "dst_port": 33434, 5 | "ttl": 1, 6 | "reply": { 7 | "src": "1.2.3.4", 8 | "icmp_type": 11, 9 | "icmp_code": 0 10 | } 11 | }, 12 | { 13 | "dst": "8.8.8.8", 14 | "dst_port": 33434, 15 | "ttl": 2, 16 | "reply": { 17 | "src": "1.2.3.5", 18 | "icmp_type": 11, 19 | "icmp_code": 0 20 | } 21 | }, 22 | { 23 | "dst": "8.8.8.8", 24 | "dst_port": 33434, 25 | "ttl": 3, 26 | "reply": { 27 | "src": "1.2.3.6", 28 | "icmp_type": 11, 29 | "icmp_code": 0 30 | } 31 | }, 32 | { 33 | "dst": "8.8.8.8", 34 | "dst_port": 33434, 35 | "ttl": 4, 36 | "reply": { 37 | "src": "8.8.8.8", 38 | "icmp_type": 3, 39 | "icmp_code": 3 40 | } 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /go/dublintraceroute/cmd/routest/main.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "flag" 9 | "io/ioutil" 10 | "net" 11 | "time" 12 | 13 | "github.com/florianl/go-nfqueue" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var log = logrus.New() 18 | 19 | var ( 20 | flagConfig = flag.String("c", "", "Config file") 21 | flagQueue = flag.Int("q", -1, "NfQueue ID") 22 | flagIfname = flag.String("i", "", "Interface name to send packets through. If not specified, packets are not forced through a specific interface") 23 | flagNoDefaultDrop = flag.Bool("no-default-drop", false, "Do not drop non-matching packets") 24 | ) 25 | 26 | // Config is a configuration to match probe packets with custom replies. 27 | type Config []Probe 28 | 29 | // Probe is the probe packet we want to match. 30 | type Probe struct { 31 | Src *net.IP `json:"src,omitempty"` 32 | Dst net.IP `json:"dst"` 33 | SrcPort *uint16 `json:"src_port,omitempty"` 34 | DstPort uint16 `json:"dst_port"` 35 | TTL uint8 `json:"ttl"` 36 | Reply Reply `json:"reply"` 37 | } 38 | 39 | // Reply is what we want to reply for a given hop. 40 | type Reply struct { 41 | Src net.IP `json:"src"` 42 | Dst *net.IP `json:"dst,omitempty"` 43 | IcmpType uint8 `json:"icmp_type"` 44 | IcmpCode uint8 `json:"icmp_code"` 45 | Payload []byte `json:"payload,omitempty"` 46 | } 47 | 48 | func loadConfig(buf []byte) (*Config, error) { 49 | var c Config 50 | if err := json.Unmarshal(buf, &c); err != nil { 51 | return nil, err 52 | } 53 | return &c, nil 54 | } 55 | 56 | func main() { 57 | log.Level = logrus.DebugLevel 58 | flag.Parse() 59 | 60 | if *flagQueue < 0 { 61 | log.Fatalf("Must specify a nfqueue ID") 62 | } 63 | if *flagConfig == "" { 64 | log.Fatal("Empty config") 65 | } 66 | data, err := ioutil.ReadFile(*flagConfig) 67 | if err != nil { 68 | log.Fatalf("Cannot read file %s: %v", *flagConfig, err) 69 | } 70 | cfg, err := loadConfig(data) 71 | if err != nil { 72 | log.Fatalf("Cannot load config file %s: %v", *flagConfig, err) 73 | } 74 | log.Infof("Loaded configuration: \n%s", data) 75 | 76 | nfqConfig := nfqueue.Config{ 77 | NfQueue: uint16(*flagQueue), 78 | MaxPacketLen: 0xffff, 79 | MaxQueueLen: 0xff, 80 | Copymode: nfqueue.NfQnlCopyPacket, 81 | ReadTimeout: 5 * time.Second, 82 | } 83 | nfq, err := nfqueue.Open(&nfqConfig) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | defer func() { 88 | if err := nfq.Close(); err != nil { 89 | log.Debugf("Failed to close NfQueue: %v", err) 90 | } 91 | }() 92 | 93 | ctx := context.Background() 94 | fn := func(a nfqueue.Attribute) (ret int) { 95 | // TODO IPv6 96 | verdict := nfqueue.NfDrop 97 | header, payload, err := forgeReplyv4(cfg, *a.Payload) 98 | if err != nil { 99 | if err == ErrNoMatch { 100 | log.Infof("Packet not matching") 101 | if *flagNoDefaultDrop { 102 | verdict = nfqueue.NfAccept 103 | } 104 | } else { 105 | log.Warningf("Failed to forge reply: %v", err) 106 | } 107 | goto end 108 | } 109 | if err := Send4(*flagIfname, header, payload); err != nil { 110 | log.Warningf("send4 failed: %v", err) 111 | goto end 112 | } 113 | end: 114 | if err := nfq.SetVerdict(*a.PacketID, verdict); err != nil { 115 | log.Warningf("SetVerdict failed: %v", err) 116 | } 117 | return 0 118 | } 119 | if err := nfq.Register(ctx, fn); err != nil { 120 | log.Fatal(err) 121 | } 122 | <-ctx.Done() 123 | } 124 | -------------------------------------------------------------------------------- /go/dublintraceroute/cmd/routest/reply.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | 9 | inet "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/net" 10 | "golang.org/x/net/ipv4" 11 | ) 12 | 13 | // ErrNoMatch signals a packet not matching the desired criteria. 14 | var ErrNoMatch = errors.New("packet not matching") 15 | 16 | // forgeReplyv4 forges a reply for the provided input, assuming that this 17 | // is UDP over IPv4. 18 | // If the packet doesn't match in the configuration, an ErrNoMatch is 19 | // returned. 20 | // The function returns the IPv4 header and its serialized payload. 21 | func forgeReplyv4(cfg *Config, payload []byte) (*ipv4.Header, []byte, error) { 22 | p, err := ipv4.ParseHeader(payload) 23 | if err != nil { 24 | return nil, nil, err 25 | } 26 | udp, err := inet.NewUDP(payload[p.Len:]) 27 | if err != nil { 28 | return nil, nil, fmt.Errorf("failed to parse UDP header: %w", err) 29 | } 30 | log.Debugf("Matching packet: %+v >> %+v", p, udp) 31 | var match *Probe 32 | for _, c := range *cfg { 33 | if p.Dst.Equal(c.Dst) && 34 | (c.Src == nil || p.Src.Equal(*c.Src)) && 35 | int(c.TTL) == p.TTL && 36 | c.DstPort == udp.Dst && 37 | (c.SrcPort == nil || *c.SrcPort == udp.Src) { 38 | match = &c 39 | break 40 | } 41 | } 42 | if match == nil { 43 | return nil, nil, ErrNoMatch 44 | } 45 | log.Debugf("Found match %+v", *match) 46 | dst := p.Src 47 | if match.Reply.Dst != nil { 48 | dst = *match.Reply.Dst 49 | } 50 | ip := ipv4.Header{ 51 | Version: 4, 52 | Len: ipv4.HeaderLen, 53 | TotalLen: ipv4.HeaderLen + inet.UDPHeaderLen + len(payload), 54 | TTL: 64, // dummy value, good enough for a reply 55 | Protocol: int(inet.ProtoICMP), 56 | Src: match.Reply.Src, 57 | Dst: dst, 58 | } 59 | if match.Reply.Payload != nil { 60 | payload = match.Reply.Payload 61 | } 62 | icmp := inet.ICMP{ 63 | Type: inet.ICMPType(match.Reply.IcmpType), 64 | Code: inet.ICMPCode(match.Reply.IcmpCode), 65 | Payload: payload, 66 | } 67 | icmpBytes, err := icmp.MarshalBinary() 68 | if err != nil { 69 | return nil, nil, fmt.Errorf("failed to serialize ICMPv4: %w", err) 70 | } 71 | return &ip, icmpBytes, nil 72 | } 73 | -------------------------------------------------------------------------------- /go/dublintraceroute/cmd/routest/send.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package main 4 | 5 | import ( 6 | "net" 7 | "syscall" 8 | 9 | "golang.org/x/net/ipv4" 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | // Send4 sends an IPv4 packet to its destination. 14 | func Send4(ifname string, ip *ipv4.Header, payload []byte) error { 15 | h, err := ip.Marshal() 16 | if err != nil { 17 | return err 18 | } 19 | data := append(h, payload...) 20 | fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW) 21 | if err != nil { 22 | return err 23 | } 24 | if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { 25 | return err 26 | } 27 | if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1); err != nil { 28 | return err 29 | } 30 | if ifname != "" { 31 | if err := unix.BindToDevice(fd, ifname); err != nil { 32 | return err 33 | } 34 | } 35 | 36 | var daddrBytes [net.IPv4len]byte 37 | copy(daddrBytes[:], ip.Src.To4()) 38 | daddr := syscall.SockaddrInet4{ 39 | Addr: daddrBytes, 40 | Port: 33434, 41 | } 42 | return syscall.Sendto(fd, data, 0, &daddr) 43 | } 44 | -------------------------------------------------------------------------------- /go/dublintraceroute/cmd/routest/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # SPDX-License-Identifier: BSD-2-Clause 4 | 5 | set -eux 6 | 7 | if [ $UID -ne 0 ] 8 | then 9 | # shellcheck disable=SC2086,SC2068 10 | sudo $0 $@ 11 | exit $? 12 | fi 13 | 14 | QUEUE=100 15 | iptables -A OUTPUT -p udp --dport 33434:33634 -d 8.8.8.8 -j NFQUEUE --queue-num "$QUEUE" 16 | # then run ./routest -q $QUEUE -c -i 17 | -------------------------------------------------------------------------------- /go/dublintraceroute/cmd/todot/main.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "go/build" 9 | "io" 10 | "log" 11 | "os" 12 | 13 | flag "github.com/spf13/pflag" 14 | 15 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/results" 16 | ) 17 | 18 | func init() { 19 | // Ensure that CGO is disabled 20 | var ctx build.Context 21 | if ctx.CgoEnabled { 22 | fmt.Println("Disabling CGo") 23 | ctx.CgoEnabled = false 24 | } 25 | } 26 | 27 | var ( 28 | flagOutputFile = flag.StringP("output", "o", "-", "Output file. Use \"-\" to print to standard output") 29 | ) 30 | 31 | func main() { 32 | 33 | flag.Parse() 34 | 35 | var ( 36 | buf []byte 37 | err error 38 | ) 39 | if len(flag.Args()) == 0 || flag.Arg(0) == "-" { 40 | fmt.Fprintf(os.Stderr, "Reading from stdin...\n") 41 | buf, err = io.ReadAll(os.Stdin) 42 | } else { 43 | buf, err = os.ReadFile(flag.Arg(0)) 44 | } 45 | if err != nil { 46 | log.Fatalf("Failed to read file '%s': %v", flag.Arg(0), err) 47 | } 48 | 49 | var result results.Results 50 | if err := json.Unmarshal(buf, &result); err != nil { 51 | log.Fatalf("Failed to unmarshal JSON into Results: %v", err) 52 | } 53 | output, err := result.ToDOT() 54 | if err != nil { 55 | log.Fatalf("Failed to convert to DOT: %v", err) 56 | } 57 | if *flagOutputFile == "-" { 58 | fmt.Println(output) 59 | } else { 60 | err := os.WriteFile(*flagOutputFile, []byte(output), 0644) 61 | if err != nil { 62 | log.Fatalf("Failed to write DOT file: %v", err) 63 | } 64 | log.Printf("Saved DOT file to %s", *flagOutputFile) 65 | log.Printf("Run `dot -Tpng \"%s\" -o \"%s.png\"` to convert to PNG", *flagOutputFile, *flagOutputFile) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /go/dublintraceroute/dublin-traceroute.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package dublintraceroute 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/results" 9 | ) 10 | 11 | // default values and constants 12 | const ( 13 | DefaultReadTimeout = time.Millisecond * 3000 14 | ) 15 | 16 | // DublinTraceroute is the common interface that every Dublin Traceroute 17 | // probe type has to implement 18 | type DublinTraceroute interface { 19 | Validate() error 20 | Traceroute() (*results.Results, error) 21 | } 22 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/icmp.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "errors" 9 | ) 10 | 11 | // TODO implement multi-part ICMP, https://tools.ietf.org/html/rfc4884 12 | // and MPLS extensions, https://tools.ietf.org/html/rfc4950 13 | 14 | // ICMP is an ICMPv4 packet 15 | type ICMP struct { 16 | Type ICMPType 17 | Code ICMPCode 18 | Checksum uint16 19 | // See RFC792, RFC4884, RFC4950. 20 | Unused uint32 21 | Payload []byte 22 | } 23 | 24 | // ICMPHeaderLen is the ICMPv4 header length 25 | var ICMPHeaderLen = 8 26 | 27 | // ICMPType defines ICMP types 28 | type ICMPType uint8 29 | 30 | // ICMP types 31 | var ( 32 | ICMPEchoReply ICMPType 33 | ICMPDestUnreachable ICMPType = 3 34 | ICMPSourceQuench ICMPType = 4 35 | ICMPRedirect ICMPType = 5 36 | ICMPAlternateHostAddr ICMPType = 6 37 | ICMPEchoRequest ICMPType = 8 38 | ICMPRouterAdv ICMPType = 9 39 | ICMPRouterSol ICMPType = 10 40 | ICMPTimeExceeded ICMPType = 11 41 | ICMPParamProblem ICMPType = 12 42 | ICMPTimestampReq ICMPType = 13 43 | ICMPTimestampReply ICMPType = 14 44 | ICMPAddrMaskReq ICMPType = 17 45 | ICMPAddrMaskReply ICMPType = 18 46 | ICMPTraceroute ICMPType = 30 47 | ICMPConversionErr ICMPType = 31 48 | ICMPMobileHostRedirect ICMPType = 32 49 | ICMPIPv6WhereAreYou ICMPType = 33 50 | ICMPIPv6IAmHere ICMPType = 34 51 | ICMPMobileRegistrationReq ICMPType = 35 52 | ICMPMobileRegistrationReply ICMPType = 36 53 | ICMPDomainNameReq ICMPType = 37 54 | ICMPDomainNameReply ICMPType = 38 55 | ICMPSkipAlgoDiscoveryProtocol ICMPType = 39 56 | ICMPPhoturis ICMPType = 40 57 | ICMPExperimentalMobilityProtocols ICMPType = 41 58 | ) 59 | 60 | // ICMPCode defines ICMP types 61 | type ICMPCode uint8 62 | 63 | // TODO map ICMP codes, see https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes 64 | 65 | // NewICMP constructs a new ICMP header from a sequence of bytes 66 | func NewICMP(b []byte) (*ICMP, error) { 67 | var i ICMP 68 | if err := i.UnmarshalBinary(b); err != nil { 69 | return nil, err 70 | } 71 | return &i, nil 72 | } 73 | 74 | // ComputeChecksum computes the ICMP checksum. 75 | func (i ICMP) ComputeChecksum() (uint16, error) { 76 | var bc bytes.Buffer 77 | binary.Write(&bc, binary.BigEndian, i.Type) 78 | binary.Write(&bc, binary.BigEndian, i.Code) 79 | binary.Write(&bc, binary.BigEndian, i.Payload) 80 | return Checksum(bc.Bytes()), nil 81 | } 82 | 83 | // MarshalBinary serializes the layer 84 | func (i ICMP) MarshalBinary() ([]byte, error) { 85 | var b bytes.Buffer 86 | binary.Write(&b, binary.BigEndian, i.Type) 87 | binary.Write(&b, binary.BigEndian, i.Code) 88 | csum, err := i.ComputeChecksum() 89 | if err != nil { 90 | return nil, err 91 | } 92 | i.Checksum = csum 93 | binary.Write(&b, binary.BigEndian, i.Checksum) 94 | // TODO implement multipart, RFC4884, RFC4950 95 | binary.Write(&b, binary.BigEndian, i.Unused) 96 | binary.Write(&b, binary.BigEndian, i.Payload) 97 | return b.Bytes(), nil 98 | } 99 | 100 | // UnmarshalBinary deserializes the layer 101 | func (i *ICMP) UnmarshalBinary(b []byte) error { 102 | if len(b) < ICMPHeaderLen { 103 | return errors.New("short icmp header") 104 | } 105 | i.Type = ICMPType(b[0]) 106 | i.Code = ICMPCode(b[1]) 107 | i.Checksum = binary.BigEndian.Uint16(b[2:4]) 108 | // TODO parse ICMP multi-part 109 | payload := b[ICMPHeaderLen:] 110 | if len(payload) > 0 { 111 | i.Payload = payload 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/icmp_test.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestICMPMarshalBinary(t *testing.T) { 13 | want := []byte{ 14 | 11, // ICMP time exceeded 15 | 0, 16 | 0xf4, 0xff, // checksum 17 | 0, 0, 0, 0, // unused 18 | } 19 | icmp := ICMP{ 20 | Type: ICMPTimeExceeded, 21 | Code: 0, 22 | } 23 | b, err := icmp.MarshalBinary() 24 | require.NoError(t, err) 25 | require.Equal(t, want, b) 26 | } 27 | 28 | func TestICMPUnmarshalBinaryBinary(t *testing.T) { 29 | b := []byte{ 30 | 11, // ICMP time exceeded 31 | 0, 32 | 0xf4, 0xff, // checksum 33 | 0, 0, 0, 0, // unused 34 | // payload 35 | 0xde, 0xad, 0xc0, 0xde, 36 | } 37 | var i ICMP 38 | err := i.UnmarshalBinaryBinary(b) 39 | require.NoError(t, err) 40 | assert.Equal(t, ICMPTimeExceeded, i.Type) 41 | assert.Equal(t, ICMPCode(0), i.Code) 42 | } 43 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/icmpv6.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "errors" 9 | ) 10 | 11 | // ICMPv6 is an ICMPv6 packet 12 | type ICMPv6 struct { 13 | Type ICMPv6Type 14 | Code ICMPv6Code 15 | Checksum uint16 16 | // See RFC792, RFC4884, RFC4950. 17 | Unused uint32 18 | next Layer 19 | } 20 | 21 | // ICMPv6HeaderLen is the ICMPv6 header length 22 | var ICMPv6HeaderLen = 8 23 | 24 | // ICMPv6Type defines ICMP types 25 | type ICMPv6Type uint8 26 | 27 | // ICMP types 28 | var ( 29 | ICMPv6TypeDestUnreachable ICMPv6Type = 1 30 | ICMPv6TypePacketTooBig ICMPv6Type = 2 31 | ICMPv6TypeTimeExceeded ICMPv6Type = 3 32 | ICMPv6TypeParameterProblem ICMPv6Type = 4 33 | ICMPv6TypeEchoRequest ICMPv6Type = 128 34 | ICMPv6TypeEchoReply ICMPv6Type = 129 35 | ICMPv6TypeGroupMembershipQuery ICMPv6Type = 130 36 | ICMPv6TypeGroupMembershipReport ICMPv6Type = 131 37 | ICMPv6TypeGroupMembershipReduction ICMPv6Type = 132 38 | ICMPv6TypeRouterSolicitation ICMPv6Type = 133 39 | ICMPv6TypeRouterAdvertisement ICMPv6Type = 134 40 | ICMPv6TypeNeighborAdvertisement ICMPv6Type = 135 41 | ICMPv6TypeNeighborSolicitation ICMPv6Type = 136 42 | ICMPv6TypeRedirect ICMPv6Type = 137 43 | ICMPv6TypeRouterRenumbering ICMPv6Type = 138 44 | ICMPv6TypeICMPNodeInformationQuery ICMPv6Type = 139 45 | ICMPv6TypeICMPNodeInformationResponse ICMPv6Type = 140 46 | ICMPv6TypeInverseNeighborDiscoverySolicitationMessage ICMPv6Type = 141 47 | ICMPv6TypeInverseNeighborDiscoveryAdvertisementMessage ICMPv6Type = 142 48 | ICMPv6TypeMLDv2MulticastListenerReport ICMPv6Type = 143 49 | ICMPv6TypeHomeAgentAddressDiscoveryRequestMessage ICMPv6Type = 144 50 | ICMPv6TypeHomeAgentAddressDiscoveryReplyMessage ICMPv6Type = 145 51 | ICMPv6TypeMobilePrefixSolicitation ICMPv6Type = 146 52 | ICMPv6TypeMobilePrefixAdvertisement ICMPv6Type = 147 53 | ICMPv6TypeCertificationPathSolicitation ICMPv6Type = 148 54 | ICMPv6TypeCertificationPathAdvertisement ICMPv6Type = 149 55 | ICMPv6TypeExperimentalMobilityProtocols ICMPv6Type = 150 56 | ICMPv6TypeMulticastRouterAdvertisement ICMPv6Type = 151 57 | ICMPv6TypeMulticastRouterSolicitation ICMPv6Type = 152 58 | ICMPv6TypeMulticastRouterTermination ICMPv6Type = 153 59 | ICMPv6TypeFMIPv6Messages ICMPv6Type = 154 60 | ) 61 | 62 | // ICMPv6Code defines ICMP types 63 | type ICMPv6Code uint8 64 | 65 | // TODO map ICMP codes, see https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes 66 | var ( 67 | // Destination unreachable 68 | ICMPv6CodeNoRouteToDestination ICMPv6Code 69 | ICMPv6CodeAdministrativelyProhibited ICMPv6Code = 1 70 | ICMPv6CodeAddressUnreachable ICMPv6Code = 2 71 | ICMPv6CodePortUnreachable ICMPv6Code = 4 72 | // Time exceeded 73 | ICMPv6CodeHopLimitExceeded ICMPv6Code 74 | ICMPv6CodeFragmentReassemblyTimeout ICMPv6Code = 1 75 | ) 76 | 77 | // NewICMPv6 constructs a new ICMPv6 header from a sequence of bytes 78 | func NewICMPv6(b []byte) (*ICMPv6, error) { 79 | var i ICMPv6 80 | if err := i.UnmarshalBinary(b); err != nil { 81 | return nil, err 82 | } 83 | return &i, nil 84 | } 85 | 86 | // Next returns the next layer 87 | func (i ICMPv6) Next() Layer { 88 | return i.next 89 | } 90 | 91 | // SetNext sets the next layer 92 | func (i *ICMPv6) SetNext(l Layer) { 93 | i.next = l 94 | } 95 | 96 | // MarshalBinary serializes the layer 97 | func (i ICMPv6) MarshalBinary() ([]byte, error) { 98 | var b bytes.Buffer 99 | binary.Write(&b, binary.BigEndian, i.Type) 100 | binary.Write(&b, binary.BigEndian, i.Code) 101 | var ( 102 | payload []byte 103 | err error 104 | ) 105 | if i.next != nil { 106 | payload, err = i.next.MarshalBinary() 107 | if err != nil { 108 | return nil, err 109 | } 110 | } 111 | // compute checksum 112 | i.Checksum = 0 113 | var bc bytes.Buffer 114 | binary.Write(&bc, binary.BigEndian, i.Type) 115 | binary.Write(&bc, binary.BigEndian, i.Code) 116 | binary.Write(&bc, binary.BigEndian, payload) 117 | i.Checksum = Checksum(bc.Bytes()) 118 | binary.Write(&b, binary.BigEndian, i.Checksum) 119 | binary.Write(&b, binary.BigEndian, i.Unused) 120 | return b.Bytes(), nil 121 | } 122 | 123 | // UnmarshalBinary deserializes the layer 124 | func (i *ICMPv6) UnmarshalBinary(b []byte) error { 125 | if len(b) < ICMPv6HeaderLen { 126 | return errors.New("short icmpv6 header") 127 | } 128 | i.Type = ICMPv6Type(b[0]) 129 | i.Code = ICMPv6Code(b[1]) 130 | i.Checksum = binary.BigEndian.Uint16(b[2:4]) 131 | // TODO parse ICMP extensions 132 | payload := b[ICMPv6HeaderLen:] 133 | if len(payload) > 0 { 134 | i.next = &Raw{Data: payload} 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/ipv4.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | /* 6 | import ( 7 | "bytes" 8 | "encoding/binary" 9 | "errors" 10 | "net" 11 | ) 12 | 13 | // Version4 is IP version 4 14 | var Version4 = 4 15 | 16 | // MinIPv4HeaderLen is the minimum IPv4 header length 17 | var MinIPv4HeaderLen = 20 18 | 19 | // Flag is an IPv4 flag type 20 | type Flag byte 21 | 22 | // IPv4 flags 23 | var ( 24 | DontFragment = 2 25 | MoreFragments = 4 26 | ) 27 | 28 | // Option is an IP option 29 | type Option [4]byte 30 | 31 | // NewIPv4 constructs a new IPv4 header from a sequence of bytes 32 | func NewIPv4(b []byte) (*IPv4, error) { 33 | var h IPv4 34 | if err := h.UnmarshalBinary(b); err != nil { 35 | return nil, err 36 | } 37 | return &h, nil 38 | } 39 | 40 | // IPv4 is the IPv4 header 41 | type IPv4 struct { 42 | Version int 43 | HeaderLen int 44 | DiffServ int 45 | TotalLen int 46 | ID int 47 | Flags int 48 | FragOff int 49 | TTL int 50 | Proto IPProto 51 | Checksum int 52 | Src net.IP 53 | Dst net.IP 54 | Options []Option 55 | next Layer 56 | // IP in ICMP, if set, won't make the parser fail on short packets 57 | IPinICMP bool 58 | } 59 | 60 | // Next returns the next layer 61 | func (h IPv4) Next() Layer { 62 | return h.next 63 | } 64 | 65 | // SetNext sets the next layer 66 | func (h *IPv4) SetNext(l Layer) { 67 | h.next = l 68 | } 69 | 70 | // MarshalBinary serializes the layer 71 | func (h IPv4) MarshalBinary() ([]byte, error) { 72 | var b bytes.Buffer 73 | // Version check 74 | if h.Version == 0 { 75 | h.Version = Version4 76 | } 77 | if h.Version != Version4 { 78 | return nil, errors.New("invalid version") 79 | } 80 | // IHL checks 81 | h.HeaderLen = 5 + len(h.Options) 82 | if h.HeaderLen > 0xff || h.HeaderLen < 5 { 83 | return nil, errors.New("invalid ip header length") 84 | } 85 | binary.Write(&b, binary.BigEndian, byte(h.Version<<4)|byte(h.HeaderLen)) 86 | 87 | // Differentiated Services (DSCP, ECN) 88 | if h.DiffServ < 0 || h.DiffServ > 0x3f { 89 | return nil, errors.New("invalid differentiated services") 90 | } 91 | binary.Write(&b, binary.BigEndian, byte(h.DiffServ)) 92 | 93 | // Total length (header + data) 94 | // marshal the payload to know the length 95 | next := h.Next() 96 | var ( 97 | payload []byte 98 | err error 99 | ) 100 | if next != nil { 101 | payload, err = next.MarshalBinary() 102 | if err != nil { 103 | return nil, err 104 | } 105 | } 106 | h.TotalLen = h.HeaderLen*4 + len(payload) 107 | if h.TotalLen < 0 || h.TotalLen > 0xffff { 108 | return nil, errors.New("invalid total length") 109 | } 110 | binary.Write(&b, binary.BigEndian, uint16(h.TotalLen)) 111 | 112 | // ID 113 | if h.ID < 0 || h.ID > 0xffff { 114 | return nil, errors.New("invalid ID") 115 | } 116 | binary.Write(&b, binary.BigEndian, uint16(h.ID)) 117 | 118 | // Flags 119 | if h.Flags < 0 || h.Flags > 0x7 || h.Flags&0x1 != 0 { 120 | return nil, errors.New("invalid flags") 121 | } 122 | var u16 = uint16(h.Flags << 13) 123 | 124 | // Fragment offset 125 | if h.FragOff < 0 || h.FragOff > 0x1fff { 126 | return nil, errors.New("invalid fragment offset") 127 | } 128 | u16 |= uint16(h.FragOff & 0x1fff) 129 | binary.Write(&b, binary.BigEndian, u16) 130 | 131 | // TTL 132 | if h.TTL < 0 || h.TTL > 0xff { 133 | return nil, errors.New("invalid TTL") 134 | } 135 | binary.Write(&b, binary.BigEndian, uint8(h.TTL)) 136 | 137 | // Protocol 138 | if h.Proto < 0 || h.Proto > 0xff { 139 | return nil, errors.New("invalid protocol") 140 | } 141 | if h.Proto == 0 { 142 | switch next.(type) { 143 | case *UDP: 144 | h.Proto = ProtoUDP 145 | case *ICMP: 146 | h.Proto = ProtoICMP 147 | } 148 | } 149 | binary.Write(&b, binary.BigEndian, uint8(h.Proto)) 150 | 151 | // Checksum - left to 0, filled in by the platform 152 | if h.Checksum < 0 || h.Checksum > 0xffff { 153 | return nil, errors.New("invalid checksum") 154 | } 155 | binary.Write(&b, binary.BigEndian, uint16(h.Checksum)) 156 | 157 | // src and dst addresses 158 | if h.Src == nil { 159 | h.Src = net.IPv4zero 160 | } 161 | if h.Dst == nil { 162 | h.Dst = net.IPv4zero 163 | } 164 | binary.Write(&b, binary.BigEndian, h.Src.To4()) 165 | binary.Write(&b, binary.BigEndian, h.Dst.To4()) 166 | 167 | // Options 168 | for _, opt := range h.Options { 169 | binary.Write(&b, binary.BigEndian, opt) 170 | } 171 | 172 | ret := append(b.Bytes(), payload...) 173 | return ret, nil 174 | } 175 | 176 | // IsFragment returns whether this packet is a fragment of a larger packet. 177 | func (h IPv4) IsFragment() bool { 178 | return h.Flags&MoreFragments != 0 || h.FragOff != 0 179 | } 180 | 181 | // UnmarshalBinary deserializes the raw bytes to an IPv4 header 182 | func (h *IPv4) UnmarshalBinary(b []byte) error { 183 | if len(b) < MinIPv4HeaderLen { 184 | return errors.New("short ipv4 header") 185 | } 186 | var ( 187 | u8 byte 188 | u16 [2]byte 189 | u32 [4]byte 190 | ) 191 | buf := bytes.NewBuffer(b) 192 | u8, _ = buf.ReadByte() 193 | h.Version = int(u8 >> 4) 194 | if h.Version != Version4 { 195 | return errors.New("invalid version") 196 | } 197 | h.HeaderLen = int(u8 & 0xf) 198 | if len(b) < h.HeaderLen*4 { 199 | return errors.New("short ipv4 header") 200 | } 201 | u8, _ = buf.ReadByte() 202 | h.DiffServ = int(u8) 203 | buf.Read(u16[:]) 204 | h.TotalLen = int(binary.BigEndian.Uint16(u16[:])) 205 | buf.Read(u16[:]) 206 | h.ID = int(binary.BigEndian.Uint16(u16[:])) 207 | buf.Read(u16[:]) 208 | tmp := binary.BigEndian.Uint16(u16[:]) 209 | h.Flags = int((tmp >> 13) & 0x1f) 210 | h.FragOff = int(tmp & 0x1fff) 211 | u8, _ = buf.ReadByte() 212 | h.TTL = int(u8) 213 | u8, _ = buf.ReadByte() 214 | h.Proto = IPProto(u8) 215 | buf.Read(u16[:]) 216 | h.Checksum = int(binary.BigEndian.Uint16(u16[:])) 217 | buf.Read(u32[:]) 218 | h.Src = append([]byte{}, u32[:]...) 219 | buf.Read(u32[:]) 220 | h.Dst = append([]byte{}, u32[:]...) 221 | h.Options = make([]Option, 0, h.HeaderLen-5) 222 | for i := 0; i < cap(h.Options); i++ { 223 | buf.Read(u32[:]) 224 | h.Options = append(h.Options, Option(u32)) 225 | } 226 | // payload 227 | if len(b) < h.TotalLen && !h.IPinICMP { 228 | return errors.New("invalid IPv4 packet: payload too short") 229 | } 230 | payload := b[h.HeaderLen*4 : h.TotalLen] 231 | if h.Proto == ProtoUDP && !h.IsFragment() { 232 | u, err := NewUDP(payload) 233 | if err != nil { 234 | return err 235 | } 236 | h.next = u 237 | } else if h.Proto == ProtoICMP { 238 | i, err := NewICMP(payload) 239 | if err != nil { 240 | return err 241 | } 242 | h.next = i 243 | } else { 244 | h.next = &Raw{Data: payload} 245 | } 246 | 247 | return nil 248 | } 249 | */ 250 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/ipv4_test.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | import ( 6 | "net" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestIPv4MarshalBinary(t *testing.T) { 13 | want := []byte{ 14 | 0x47, // Ver, IHL 15 | 0x00, // differentiated services 16 | 0, 28, // total len 17 | 0x12, 0x34, // ID 18 | 0x40, 0x00, // don't fragment, frag offset = 0 19 | 0xa, // TTL 20 | 0x11, // Proto, UDP 21 | 0x00, 0x00, // Checksum 22 | 192, 168, 10, 1, // src 23 | 8, 8, 8, 8, // dst 24 | 0xa, 0xb, 0xc, 0xd, // opt 1 25 | 0x1, 0x2, 0x3, 0x4, // opt 2 26 | } 27 | iph := IPv4{ 28 | Version: 4, 29 | HeaderLen: 7, 30 | TotalLen: 28, 31 | ID: 0x1234, 32 | Flags: DontFragment, 33 | TTL: 10, 34 | Proto: ProtoUDP, 35 | Src: net.ParseIP("192.168.10.1"), 36 | Dst: net.ParseIP("8.8.8.8"), 37 | Options: []Option{ 38 | {0xa, 0xb, 0xc, 0xd}, 39 | {0x1, 0x2, 0x3, 0x4}, 40 | }, 41 | } 42 | b, err := iph.MarshalBinary() 43 | require.NoError(t, err) 44 | require.Equal(t, want, b) 45 | } 46 | 47 | func TestIPv4MarshalBinaryPayload(t *testing.T) { 48 | want := []byte{ 49 | 0x47, // Ver, IHL 50 | 0x00, // differentiated services 51 | 0, 32, // total len 52 | 0x12, 0x34, // ID 53 | 0x40, 0x00, // don't fragment, frag offset = 0 54 | 0xa, // TTL 55 | 0x11, // Proto, UDP 56 | 0x00, 0x00, // Checksum 57 | 192, 168, 10, 1, // src 58 | 8, 8, 8, 8, // dst 59 | 0xa, 0xb, 0xc, 0xd, // opt 1 60 | 0x1, 0x2, 0x3, 0x4, // opt 2 61 | 0xde, 0xad, 0xc0, 0xde, // payload 62 | } 63 | iph := IPv4{ 64 | Version: 4, 65 | HeaderLen: 7, 66 | TotalLen: 28, 67 | ID: 0x1234, 68 | Flags: DontFragment, 69 | TTL: 10, 70 | Proto: ProtoUDP, 71 | Src: net.ParseIP("192.168.10.1"), 72 | Dst: net.ParseIP("8.8.8.8"), 73 | Options: []Option{ 74 | {0xa, 0xb, 0xc, 0xd}, 75 | {0x1, 0x2, 0x3, 0x4}, 76 | }, 77 | } 78 | iph.SetNext(&Raw{Data: []byte{0xde, 0xad, 0xc0, 0xde}}) 79 | b, err := iph.MarshalBinary() 80 | require.NoError(t, err) 81 | require.Equal(t, want, b) 82 | } 83 | 84 | func TestIPv4Unmarshal(t *testing.T) { 85 | data := []byte{ 86 | 0x47, // Ver, IHL 87 | 0x00, // DSCP, ECN 88 | 0, 36, // total len 89 | 0x12, 0x34, // ID 90 | 0x40, 0x00, // don't fragment, frag offset = 0 91 | 0xa, // TTL 92 | 0x11, // Proto, UDP 93 | 0x43, 0x21, // Checksum 94 | 192, 168, 10, 1, // src 95 | 8, 8, 8, 8, // dst 96 | 0xa, 0xb, 0xc, 0xd, // opt 1 97 | 0x1, 0x2, 0x3, 0x4, // opt 2 98 | // UDP 99 | 0x30, 0x39, // src port 1235 100 | 0x01, 0xbb, // dst port 443 101 | 0x00, 0x00, // len 102 | 0x00, 0x00, // checksum 103 | 104 | } 105 | var ip IPv4 106 | err := ip.Unmarshal(data) 107 | require.NoError(t, err) 108 | require.Equal(t, Version4, ip.Version) 109 | require.Equal(t, 0, ip.DiffServ) 110 | require.Equal(t, 36, ip.TotalLen) 111 | require.Equal(t, 0x1234, ip.ID) 112 | require.Equal(t, DontFragment, ip.Flags) 113 | require.Equal(t, 0, ip.FragOff) 114 | require.Equal(t, 10, ip.TTL) 115 | require.Equal(t, ProtoUDP, ip.Proto) 116 | require.Equal(t, 0x4321, ip.Checksum) 117 | require.Equal(t, net.IP{192, 168, 10, 1}, ip.Src) 118 | require.Equal(t, net.IP{8, 8, 8, 8}, ip.Dst) 119 | require.Equal(t, 2, len(ip.Options)) 120 | require.Equal(t, Option{0xa, 0xb, 0xc, 0xd}, ip.Options[0]) 121 | require.Equal(t, Option{0x1, 0x2, 0x3, 0x4}, ip.Options[1]) 122 | } 123 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/ipv6.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "errors" 9 | "net" 10 | ) 11 | 12 | // Version6 is IP version 6 13 | var Version6 = 6 14 | 15 | // IPv6HeaderLen is the length of the IPv6 header 16 | var IPv6HeaderLen = 40 17 | 18 | // NewIPv6 constructs a new IPv6 header from a sequence of bytes 19 | func NewIPv6(b []byte) (*IPv6, error) { 20 | var h IPv6 21 | if err := h.UnmarshalBinary(b); err != nil { 22 | return nil, err 23 | } 24 | return &h, nil 25 | } 26 | 27 | // IPv6 is the IPv6 header 28 | type IPv6 struct { 29 | Version int 30 | TrafficClass int 31 | FlowLabel int 32 | PayloadLen int 33 | NextHeader IPProto 34 | HopLimit int 35 | Src net.IP 36 | Dst net.IP 37 | next Layer 38 | // IP in ICMP, if set, won't make the parser fail on short packets 39 | IPinICMP bool 40 | } 41 | 42 | // Next returns the next layer 43 | func (h IPv6) Next() Layer { 44 | return h.next 45 | } 46 | 47 | // SetNext sets the next Layer 48 | func (h *IPv6) SetNext(l Layer) { 49 | h.next = l 50 | } 51 | 52 | // MarshalBinary serializes the layer 53 | func (h IPv6) MarshalBinary() ([]byte, error) { 54 | var b bytes.Buffer 55 | // Version check 56 | if h.Version == 0 { 57 | h.Version = Version6 58 | } 59 | if h.Version != Version6 { 60 | return nil, errors.New("invalid IPv6 version") 61 | } 62 | // traffic class 63 | if h.TrafficClass < 0 || h.TrafficClass > 0xff { 64 | return nil, errors.New("invalid IPv6 traffic class") 65 | } 66 | // flow label 67 | if h.FlowLabel < 0 || h.FlowLabel > 0x0fffff { 68 | return nil, errors.New("invalid IPv6 flow label") 69 | } 70 | // payload length 71 | var ( 72 | payload []byte 73 | err error 74 | ) 75 | if h.next != nil { 76 | payload, err = h.next.MarshalBinary() 77 | if err != nil { 78 | return nil, err 79 | } 80 | } 81 | if h.PayloadLen == 0 { 82 | h.PayloadLen = len(payload) 83 | } 84 | if h.PayloadLen < 0 || h.PayloadLen > 0xffff { 85 | return nil, errors.New("invalid IPv6 payload length") 86 | } 87 | // next header 88 | if h.NextHeader < 0 || h.NextHeader > 0xff { 89 | return nil, errors.New("invalid IPv6 next header") 90 | } 91 | /* 92 | if h.NextHeader == 0 { 93 | switch h.next.(type) { 94 | case *UDP: 95 | h.NextHeader = ProtoUDP 96 | case *ICMPv6: 97 | h.NextHeader = ProtoICMPv6 98 | } 99 | } 100 | */ 101 | // hop limit 102 | if h.HopLimit < 0 || h.HopLimit > 0xff { 103 | return nil, errors.New("invalid IPv6 hop limit") 104 | } 105 | // src and dst 106 | if h.Src == nil { 107 | h.Src = net.IPv6zero 108 | } 109 | if h.Dst == nil { 110 | h.Dst = net.IPv6zero 111 | } 112 | 113 | binary.Write(&b, binary.BigEndian, uint32(h.Version<<28|h.TrafficClass<<20|h.FlowLabel)) 114 | binary.Write(&b, binary.BigEndian, uint16(h.PayloadLen)) 115 | binary.Write(&b, binary.BigEndian, uint8(h.NextHeader)) 116 | binary.Write(&b, binary.BigEndian, uint8(h.HopLimit)) 117 | binary.Write(&b, binary.BigEndian, []byte(h.Src.To16())) 118 | binary.Write(&b, binary.BigEndian, []byte(h.Dst.To16())) 119 | ret := b.Bytes() 120 | // payload 121 | ret = append(ret, payload...) 122 | return ret, nil 123 | } 124 | 125 | // UnmarshalBinary deserializes the raw bytes to an IPv6 header 126 | func (h *IPv6) UnmarshalBinary(b []byte) error { 127 | if len(b) < IPv6HeaderLen { 128 | return errors.New("short ipv6 header") 129 | } 130 | var ( 131 | u8 byte 132 | u16 [2]byte 133 | u32 [4]byte 134 | u128 [16]byte 135 | ) 136 | buf := bytes.NewBuffer(b) 137 | buf.Read(u32[:]) 138 | h.Version = int(u32[0] >> 4) 139 | h.TrafficClass = int(u32[0]&0xf)<<4 | int(u32[1]>>4) 140 | h.FlowLabel = int(u32[1]&0xf)<<16 | int(u32[2])<<8 | int(u32[3]) 141 | buf.Read(u16[:]) 142 | h.PayloadLen = int(binary.BigEndian.Uint16(u16[:])) 143 | u8, _ = buf.ReadByte() 144 | h.NextHeader = IPProto(u8) 145 | u8, _ = buf.ReadByte() 146 | h.HopLimit = int(u8) 147 | buf.Read(u128[:]) 148 | h.Src = append([]byte{}, u128[:]...) 149 | buf.Read(u128[:]) 150 | h.Dst = append([]byte{}, u128[:]...) 151 | // payload 152 | if len(b) < h.PayloadLen && !h.IPinICMP { 153 | return errors.New("invalid IPv6 packet: payload too short") 154 | } 155 | payload := b[IPv6HeaderLen : IPv6HeaderLen+h.PayloadLen] 156 | if h.NextHeader == ProtoUDP { 157 | /* 158 | u, err := NewUDP(payload) 159 | if err != nil { 160 | return err 161 | } 162 | h.next = u 163 | */ 164 | } else { 165 | h.next = &Raw{Data: payload} 166 | } 167 | return nil 168 | } 169 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/ipv6_test.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | import ( 6 | "net" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestIPv6MarshalBinary(t *testing.T) { 14 | want := []byte{ 15 | 0x60, 0x00, 0x00, 0x00, // version, tclass, flow label 16 | 0, 8, // payload length 17 | 17, // next header 18 | 5, // hop limit 19 | 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // src 2001:db8:1::10 20 | 0x20, 0x01, 0x48, 0x60, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88, // dst 2001:4860:4860::8888 21 | // udp 22 | 0xab, 0xcd, 0xbc, 0xde, 0x00, 0x08, 0x00, 0x00, 23 | } 24 | iph := IPv6{ 25 | Version: 6, 26 | PayloadLen: 8, 27 | NextHeader: ProtoUDP, 28 | HopLimit: 5, 29 | Src: net.ParseIP("2001:db8:1::10"), 30 | Dst: net.ParseIP("2001:4860:4860::8888"), 31 | } 32 | udp := UDP{ 33 | Src: 0xabcd, 34 | Dst: 0xbcde, 35 | } 36 | iph.SetNext(&udp) 37 | b, err := iph.MarshalBinary() 38 | require.NoError(t, err) 39 | require.Equal(t, want, b) 40 | } 41 | 42 | func TestIPv6UnmarshalBinary(t *testing.T) { 43 | data := []byte{ 44 | 0x61, 0x4a, 0xbc, 0xde, // version, tclass, flow label 45 | 0, 8, // payload length 46 | 17, // next header 47 | 5, // hop limit 48 | 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // src 2001:db8:1::10 49 | 0x20, 0x01, 0x48, 0x60, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88, // dst 2001:4860:4860::8888 50 | // udp 51 | 0xab, 0xcd, 0xbc, 0xde, 0x00, 0x08, 0x00, 0x00, 52 | } 53 | var ip IPv6 54 | err := ip.UnmarshalBinary(data) 55 | require.NoError(t, err) 56 | assert.Equal(t, 6, ip.Version) 57 | assert.Equal(t, 0x14, ip.TrafficClass) 58 | assert.Equal(t, 0xabcde, ip.FlowLabel) 59 | assert.Equal(t, 8, ip.PayloadLen) 60 | assert.Equal(t, ProtoUDP, ip.NextHeader) 61 | assert.Equal(t, net.ParseIP("2001:db8:1::10"), ip.Src) 62 | assert.Equal(t, net.ParseIP("2001:4860:4860::8888"), ip.Dst) 63 | require.NotNil(t, ip.Next()) 64 | // TODO check UDP payload 65 | } 66 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/layer.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | // Layer is a serializable interface that support chaining. 6 | type Layer interface { 7 | MarshalBinary() ([]byte, error) 8 | UnmarshalBinary(data []byte) error 9 | Next() Layer 10 | SetNext(Layer) 11 | } 12 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/localaddr.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | import "net" 6 | 7 | // GetLocalAddr returns the local address to reach the IP address with the given network type. 8 | func GetLocalAddr(network string, ip net.IP) (net.Addr, error) { 9 | // ugly porkaround until I find how to get the local address in a better 10 | // way. A port different from 0 is required on darwin, so using udp/53. 11 | conn, err := net.Dial(network, net.JoinHostPort(ip.String(), "53")) 12 | if err != nil { 13 | return nil, err 14 | } 15 | localAddr := conn.LocalAddr() 16 | _ = conn.Close() 17 | return localAddr, nil 18 | } 19 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/protocols.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | // IPProto is the IP protocol type 6 | type IPProto int 7 | 8 | // a few common IANA protocol numbers 9 | var ( 10 | ProtoICMP IPProto = 1 11 | ProtoTCP IPProto = 6 12 | ProtoUDP IPProto = 17 13 | ProtoICMPv6 IPProto = 58 14 | ) 15 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/raw.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | // Raw is a raw payload 6 | type Raw struct { 7 | Data []byte 8 | } 9 | 10 | // NewRaw builds a new Raw layer 11 | func NewRaw(b []byte) (*Raw, error) { 12 | var r Raw 13 | if err := r.UnmarshalBinary(b); err != nil { 14 | return nil, err 15 | } 16 | return &r, nil 17 | } 18 | 19 | // Next returns the next layer. For Raw the next layer is always nil 20 | func (r Raw) Next() Layer { 21 | return nil 22 | } 23 | 24 | // SetNext sets the next layer. For Raw this is a no op 25 | func (r Raw) SetNext(Layer) {} 26 | 27 | // MarshalBinary serializes the layer 28 | func (r Raw) MarshalBinary() ([]byte, error) { 29 | return r.Data, nil 30 | } 31 | 32 | // UnmarshalBinary deserializes the layer 33 | func (r *Raw) UnmarshalBinary(b []byte) error { 34 | r.Data = b 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/udp.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "errors" 9 | 10 | "golang.org/x/net/ipv4" 11 | "golang.org/x/net/ipv6" 12 | ) 13 | 14 | // UDPHeaderLen is the UDP header length 15 | var UDPHeaderLen = 8 16 | 17 | // UDP is the UDP header 18 | type UDP struct { 19 | Src uint16 20 | Dst uint16 21 | Len uint16 22 | Csum uint16 23 | Payload []byte 24 | // PseudoHeader is used for checksum computation. The caller is responsible 25 | // for passing a valid pseudoheader as a byte slice. 26 | PseudoHeader []byte 27 | } 28 | 29 | // NewUDP constructs a new UDP header from a sequence of bytes. 30 | func NewUDP(b []byte) (*UDP, error) { 31 | var h UDP 32 | if err := h.UnmarshalBinary(b); err != nil { 33 | return nil, err 34 | } 35 | return &h, nil 36 | } 37 | 38 | // Checksum computes the UDP checksum. See RFC768 and RFC1071. 39 | func Checksum(b []byte) uint16 { 40 | var sum uint32 41 | 42 | for ; len(b) >= 2; b = b[2:] { 43 | sum += uint32(b[0])<<8 | uint32(b[1]) 44 | } 45 | if len(b) > 0 { 46 | sum += uint32(b[0]) << 8 47 | } 48 | for sum > 0xffff { 49 | sum = (sum >> 16) + (sum & 0xffff) 50 | } 51 | csum := ^uint16(sum) 52 | if csum == 0 { 53 | csum = 0xffff 54 | } 55 | return csum 56 | } 57 | 58 | // IPv4HeaderToPseudoHeader returns a byte slice usable as IPv4 pseudoheader 59 | // for UDP checksum calculation. 60 | func IPv4HeaderToPseudoHeader(hdr *ipv4.Header, udplen int) ([]byte, error) { 61 | if hdr == nil { 62 | return nil, errors.New("got nil IPv4 header") 63 | } 64 | var pseudoheader [12]byte 65 | copy(pseudoheader[0:4], hdr.Src.To4()) 66 | copy(pseudoheader[4:8], hdr.Dst.To4()) 67 | pseudoheader[8] = 0 68 | pseudoheader[9] = byte(hdr.Protocol) 69 | binary.BigEndian.PutUint16(pseudoheader[10:12], uint16(udplen)) 70 | 71 | return pseudoheader[:], nil 72 | } 73 | 74 | // IPv6HeaderToPseudoHeader returns a byte slice usable as IPv6 pseudoheader 75 | // for UDP checksum calculation. 76 | func IPv6HeaderToPseudoHeader(hdr *ipv6.Header) ([]byte, error) { 77 | if hdr == nil { 78 | return nil, errors.New("got nil IPv4 header") 79 | } 80 | var pseudoheader [40]byte 81 | copy(pseudoheader[0:16], hdr.Src.To16()) 82 | copy(pseudoheader[16:32], hdr.Dst.To16()) 83 | binary.BigEndian.PutUint32(pseudoheader[32:36], uint32(hdr.PayloadLen)) 84 | // three zero-ed bytes 85 | pseudoheader[39] = byte(hdr.NextHeader) 86 | 87 | return pseudoheader[:], nil 88 | } 89 | 90 | // MarshalBinary serializes the layer. If the checksum is zero and the IP 91 | // header is not nil, checksum is computed using the pseudoheader, otherwise 92 | // it is left to zero. 93 | func (h *UDP) MarshalBinary() ([]byte, error) { 94 | var buf bytes.Buffer 95 | if err := binary.Write(&buf, binary.BigEndian, h.Src); err != nil { 96 | return nil, err 97 | } 98 | if err := binary.Write(&buf, binary.BigEndian, h.Dst); err != nil { 99 | return nil, err 100 | } 101 | if h.Len == 0 { 102 | h.Len = uint16(UDPHeaderLen + len(h.Payload)) 103 | } 104 | if h.Len < 8 || h.Len > 0xffff-20 { 105 | return nil, errors.New("invalid udp header len") 106 | } 107 | if err := binary.Write(&buf, binary.BigEndian, h.Len); err != nil { 108 | return nil, err 109 | } 110 | if h.Csum == 0 && h.PseudoHeader != nil { 111 | var b bytes.Buffer 112 | if err := binary.Write(&b, binary.BigEndian, h.PseudoHeader); err != nil { 113 | return nil, err 114 | } 115 | if err := binary.Write(&b, binary.BigEndian, buf.Bytes()); err != nil { 116 | return nil, err 117 | } 118 | if err := binary.Write(&b, binary.BigEndian, h.Payload); err != nil { 119 | return nil, err 120 | } 121 | h.Csum = Checksum(b.Bytes()) 122 | } 123 | if err := binary.Write(&buf, binary.BigEndian, h.Csum); err != nil { 124 | return nil, err 125 | } 126 | ret := append(buf.Bytes(), h.Payload...) 127 | return ret, nil 128 | } 129 | 130 | // UnmarshalBinary deserializes the raw bytes to an UDP header 131 | func (h *UDP) UnmarshalBinary(b []byte) error { 132 | if len(b) < UDPHeaderLen { 133 | return errors.New("short udp header") 134 | } 135 | h.Src = binary.BigEndian.Uint16(b[:2]) 136 | h.Dst = binary.BigEndian.Uint16(b[2:4]) 137 | h.Len = binary.BigEndian.Uint16(b[4:6]) 138 | h.Csum = binary.BigEndian.Uint16(b[6:8]) 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /go/dublintraceroute/net/udp_test.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package net 4 | 5 | import ( 6 | "net" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestUDPMarshalBinary(t *testing.T) { 14 | want := []byte{ 15 | 0x12, 0x34, // src port 16 | 0x23, 0x45, // dst port 17 | 0x00, 0x08, // len 18 | 0x00, 0x00, // csum 19 | } 20 | udp := UDP{ 21 | Src: 0x1234, 22 | Dst: 0x2345, 23 | Len: 8, 24 | Csum: 0, 25 | } 26 | b, err := udp.MarshalBinary() 27 | require.NoError(t, err) 28 | require.Equal(t, want, b) 29 | } 30 | 31 | func TestMarshalBinaryIPv4(t *testing.T) { 32 | want := []byte{ 33 | 0x12, 0x34, // src port 34 | 0x23, 0x45, // dst port 35 | 0x00, 0x08, // len 36 | 0xef, 0xab, // csum 37 | } 38 | ip := IPv4{ 39 | Src: net.IP{192, 168, 10, 1}, 40 | Dst: net.IP{8, 8, 8, 8}, 41 | Proto: ProtoUDP, 42 | } 43 | udp := UDP{ 44 | Src: 0x1234, 45 | Dst: 0x2345, 46 | Len: 8, 47 | Csum: 0, 48 | } 49 | udp.SetPrev(&ip) 50 | b, err := udp.MarshalBinary() 51 | require.NoError(t, err) 52 | require.Equal(t, want, b) 53 | } 54 | 55 | func TestUDPUnmarshalBinary(t *testing.T) { 56 | b := []byte{ 57 | 0x12, 0x34, // src port 58 | 0x23, 0x45, // dst port 59 | 0x00, 0x08, // len 60 | 0xff, 0x35, // csum 61 | } 62 | var u UDP 63 | err := u.UnmarshalBinary(b) 64 | require.NoError(t, err) 65 | assert.Equal(t, uint16(0x1234), u.Src) 66 | assert.Equal(t, uint16(0x2345), u.Dst) 67 | assert.Equal(t, uint16(8), u.Len) 68 | assert.Equal(t, uint16(0xff35), u.Csum) 69 | } 70 | -------------------------------------------------------------------------------- /go/dublintraceroute/probes/probe.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package probes 4 | 5 | // Probe represents a sent probe. Every protocol-specific probe has to implement 6 | // this interface 7 | type Probe interface { 8 | Validate() error 9 | } 10 | 11 | // ProbeResponse represents a response to a sent probe. Every protocol-specific 12 | // probe response has to implement this interface 13 | type ProbeResponse interface { 14 | Validate() error 15 | Matches(Probe) bool 16 | } 17 | -------------------------------------------------------------------------------- /go/dublintraceroute/probes/probev4/udpv4probe.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package probev4 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "net" 9 | "time" 10 | 11 | inet "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/net" 12 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/probes" 13 | "golang.org/x/net/ipv4" 14 | ) 15 | 16 | // ProbeUDPv4 represents a sent probe packet with its metadata 17 | type ProbeUDPv4 struct { 18 | Data []byte 19 | ip *ipv4.Header 20 | udp *inet.UDP 21 | payload []byte 22 | // time the packet is sent at 23 | Timestamp time.Time 24 | // local address of the packet sender 25 | LocalAddr net.IP 26 | } 27 | 28 | // Validate verifies that the probe has the expected structure, and returns an error if not 29 | func (p *ProbeUDPv4) Validate() error { 30 | if p.ip == nil { 31 | // decode packet 32 | hdr, err := ipv4.ParseHeader(p.Data) 33 | if err != nil { 34 | return err 35 | } 36 | p.ip = hdr 37 | } 38 | if p.ip.Protocol != int(inet.ProtoUDP) { 39 | return fmt.Errorf("IP payload is not UDP, expected type %d, got %d", inet.ProtoUDP, p.ip.Protocol) 40 | } 41 | p.payload = p.Data[p.ip.Len:] 42 | if len(p.payload) == 0 { 43 | return errors.New("IP layer has no payload") 44 | } 45 | udp, err := inet.NewUDP(p.Data[p.ip.Len:]) 46 | if err != nil { 47 | return fmt.Errorf("failed to parse UDP header: %w", err) 48 | } 49 | p.udp = udp 50 | p.payload = p.Data[p.ip.Len+inet.UDPHeaderLen:] 51 | return nil 52 | } 53 | 54 | // IP returns the IP header of the probe. If not decoded yet, will return nil. 55 | func (p ProbeUDPv4) IP() *ipv4.Header { 56 | return p.ip 57 | } 58 | 59 | // UDP returns the payload of the IP header of the probe. If not decoded yet, 60 | // will return nil. 61 | func (p ProbeUDPv4) UDP() *inet.UDP { 62 | return p.udp 63 | } 64 | 65 | // ProbeResponseUDPv4 represents a received probe response with its metadata 66 | type ProbeResponseUDPv4 struct { 67 | // header, payload and timestamp are expected to be passed at object creation 68 | Header *ipv4.Header 69 | // the IPv4 payload (expected ICMP -> IP -> UDP) 70 | Payload []byte 71 | // Addr is the IP address of the response sender 72 | Addr net.IP 73 | // time the packet is received at 74 | Timestamp time.Time 75 | 76 | // the following are computed, internal fields instead 77 | icmp *inet.ICMP 78 | innerIP *ipv4.Header 79 | innerUDP *inet.UDP 80 | innerPayload []byte 81 | } 82 | 83 | // Validate verifies that the probe response has the expected structure, and returns an error if not 84 | func (pr *ProbeResponseUDPv4) Validate() error { 85 | if pr.icmp == nil { 86 | // decode packet 87 | icmp, err := inet.NewICMP(pr.Payload) 88 | if err != nil { 89 | return err 90 | } 91 | pr.icmp = icmp 92 | } 93 | if len(pr.icmp.Payload) == 0 { 94 | return errors.New("ICMP layer has no payload") 95 | } 96 | ip, err := ipv4.ParseHeader(pr.icmp.Payload) 97 | if err != nil { 98 | return fmt.Errorf("failed to parse inner IPv4 header: %w", err) 99 | } 100 | pr.innerIP = ip 101 | payload := pr.icmp.Payload[ip.Len:] 102 | if len(payload) == 0 { 103 | return errors.New("inner IP layer has no payload") 104 | } 105 | if ip.Protocol != int(inet.ProtoUDP) { 106 | return fmt.Errorf("inner IP payload is not UDP, want protocol %d, got %d", inet.ProtoUDP, ip.Protocol) 107 | } 108 | udp, err := inet.NewUDP(payload) 109 | if err != nil { 110 | return fmt.Errorf("failed to decode inner UDP header: %w", err) 111 | } 112 | pr.innerUDP = udp 113 | pr.innerPayload = payload[inet.UDPHeaderLen:] 114 | return nil 115 | } 116 | 117 | // ICMP returns the ICMP layer of the probe response. If not decoded yet, will return nil. 118 | func (pr *ProbeResponseUDPv4) ICMP() *inet.ICMP { 119 | return pr.icmp 120 | } 121 | 122 | // InnerIP returns the inner IP layer of the probe response. If not decoded yet, will return nil. 123 | func (pr *ProbeResponseUDPv4) InnerIP() *ipv4.Header { 124 | return pr.innerIP 125 | } 126 | 127 | // InnerUDP returns the UDP layer of the probe. If not decoded yet, will return nil. 128 | func (pr *ProbeResponseUDPv4) InnerUDP() *inet.UDP { 129 | return pr.innerUDP 130 | } 131 | 132 | // Matches returns true if this probe response matches the given probe. Both 133 | // probes must have been already validated with Validate, this function may 134 | // panic otherwise. 135 | func (pr *ProbeResponseUDPv4) Matches(pi probes.Probe) bool { 136 | p := pi.(*ProbeUDPv4) 137 | if p == nil { 138 | return false 139 | } 140 | if pr.icmp.Type != inet.ICMPTimeExceeded && !(pr.icmp.Type == inet.ICMPDestUnreachable && pr.icmp.Code == 3) { 141 | // we want time-exceeded or port-unreachable 142 | return false 143 | } 144 | if !pr.InnerIP().Dst.To4().Equal(p.ip.Dst.To4()) { 145 | return false 146 | } 147 | if p.UDP().Src != pr.InnerUDP().Src || p.UDP().Dst != pr.InnerUDP().Dst { 148 | // source and destination ports do not match 149 | return false 150 | } 151 | if pr.InnerIP().ID != p.ip.ID { 152 | // the two packets do not belong to the same flow 153 | return false 154 | } 155 | return true 156 | } 157 | -------------------------------------------------------------------------------- /go/dublintraceroute/probes/probev6/udpv6.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package probev6 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net" 11 | "time" 12 | 13 | "golang.org/x/net/ipv6" 14 | 15 | inet "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/net" 16 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/probes" 17 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/results" 18 | ) 19 | 20 | // UDPv6 is a probe type based on IPv6 and UDP 21 | type UDPv6 struct { 22 | Target net.IP 23 | SrcPort uint16 24 | DstPort uint16 25 | UseSrcPort bool 26 | NumPaths uint16 27 | MinHopLimit uint8 28 | MaxHopLimit uint8 29 | Delay time.Duration 30 | Timeout time.Duration 31 | BrokenNAT bool 32 | } 33 | 34 | // Validate checks that the probe is configured correctly and it is safe to 35 | // subsequently run the Traceroute() method 36 | func (d *UDPv6) Validate() error { 37 | if d.Target.To16() == nil { 38 | return errors.New("Invalid IPv6 address") 39 | } 40 | if d.UseSrcPort { 41 | if d.SrcPort+d.NumPaths > 0xffff { 42 | return errors.New("Source port plus number of paths cannot exceed 65535") 43 | } 44 | } else { 45 | if d.DstPort+d.NumPaths > 0xffff { 46 | return errors.New("Destination port plus number of paths cannot exceed 65535") 47 | } 48 | } 49 | if d.MaxHopLimit < d.MinHopLimit { 50 | return errors.New("Invalid maximum Hop Limit, must be greater or equal than minimum Hop Limit") 51 | } 52 | if d.Delay < 1 { 53 | return errors.New("Invalid delay, must be positive") 54 | } 55 | return nil 56 | } 57 | 58 | // packet generates a probe packet and returns its bytes 59 | func (d UDPv6) packet(hl uint8, src, dst net.IP, srcport, dstport uint16) ([]byte, []byte, error) { 60 | // Forge the payload so that it can be used for path tracking and 61 | // NAT detection. 62 | // 63 | // The payload length does the trick here, in a similar manner to 64 | // how the IP ID is used for the IPv4 probes. 65 | // In order to uniquely track a probe packet we need a unique field 66 | // that: 67 | // * is part of the first 1280 bytes including the ICMPv6 packet. 68 | // * is not used by the ECMP hashing algorithm. 69 | // 70 | // The Payload Length in the IPv6 header is used for this purpose, 71 | // and the payload size is tuned to represent a unique ID that 72 | // will be used to identify the original probe packet carried by the 73 | // ICMP response. 74 | 75 | // Length is 13 for the first flow, 14 for the second, etc. 76 | // 13 is given by 8 (udp header length) + 5 (magic string 77 | // length, "NSMNC") 78 | plen := 10 + int(hl) 79 | magic := []byte("NSMNC") 80 | // FIXME this is wrong. Or maybe not. 81 | payload := bytes.Repeat(magic, plen/len(magic)+1)[:plen] 82 | 83 | udph := inet.UDP{ 84 | Src: srcport, 85 | Dst: dstport, 86 | Len: uint16(inet.UDPHeaderLen + len(payload)), 87 | } 88 | udpb, err := udph.MarshalBinary() 89 | if err != nil { 90 | return nil, nil, err 91 | } 92 | return udpb, payload, nil 93 | } 94 | 95 | type pkt struct { 96 | UDPHeader, Payload []byte 97 | Src net.IP 98 | SrcPort, DstPort int 99 | HopLimit int 100 | } 101 | 102 | // packets returns a channel of packets that will be sent as probes 103 | func (d UDPv6) packets(src, dst net.IP) <-chan pkt { 104 | numPackets := int(d.NumPaths) * int(d.MaxHopLimit-d.MinHopLimit) 105 | ret := make(chan pkt, numPackets) 106 | 107 | go func() { 108 | var srcPort, dstPort, basePort uint16 109 | if d.UseSrcPort { 110 | basePort = d.SrcPort 111 | } else { 112 | basePort = d.DstPort 113 | } 114 | for hl := d.MinHopLimit; hl <= d.MaxHopLimit; hl++ { 115 | for port := basePort; port < basePort+d.NumPaths; port++ { 116 | if d.UseSrcPort { 117 | srcPort = port 118 | dstPort = d.DstPort 119 | } else { 120 | srcPort = d.SrcPort 121 | dstPort = port 122 | } 123 | udpb, payload, err := d.packet(hl, src, dst, srcPort, dstPort) 124 | if err != nil { 125 | log.Printf("Warning: cannot generate packet for hop limit=%d srcport=%d dstport=%d: %v", hl, srcPort, dstPort, err) 126 | } else { 127 | ret <- pkt{UDPHeader: udpb, Payload: payload, Src: src, SrcPort: int(srcPort), DstPort: int(dstPort), HopLimit: int(hl)} 128 | } 129 | } 130 | } 131 | close(ret) 132 | }() 133 | return ret 134 | } 135 | 136 | // SendReceive sends all the packets to the target address, respecting the 137 | // configured inter-packet delay 138 | func (d UDPv6) SendReceive() ([]probes.Probe, []probes.ProbeResponse, error) { 139 | localAddr, err := inet.GetLocalAddr("udp6", d.Target) 140 | if err != nil { 141 | return nil, nil, fmt.Errorf("failed to get local address for target %s with network type 'udp4': %w", d.Target, err) 142 | } 143 | localUDPAddr, ok := localAddr.(*net.UDPAddr) 144 | if !ok { 145 | return nil, nil, fmt.Errorf("invalid address type for %s: want %T, got %T", localAddr, localUDPAddr, localAddr) 146 | } 147 | // UDPv6 connection, not used to listen but to send probes 148 | // TODO this should be unnecessary, but I couldn't find how to avoid it in 149 | // the net/ipv6 API. 150 | conn, err := net.ListenPacket("udp6", net.JoinHostPort(localUDPAddr.IP.String(), "0")) 151 | if err != nil { 152 | return nil, nil, fmt.Errorf("failed to create UDPv6 packet listener: %w", err) 153 | } 154 | defer conn.Close() 155 | pconn := ipv6.NewPacketConn(conn) 156 | 157 | // Listen for IPv6/ICMP traffic back 158 | iconn, err := net.ListenPacket("ip6:ipv6-icmp", localUDPAddr.IP.String()) 159 | if err != nil { 160 | return nil, nil, fmt.Errorf("failed to create ICMPv6 packet listener: %w", err) 161 | } 162 | defer iconn.Close() 163 | 164 | numPackets := int(d.NumPaths) * int(d.MaxHopLimit-d.MinHopLimit) 165 | 166 | // spawn the listener 167 | recvErrors := make(chan error) 168 | recvChan := make(chan []probes.ProbeResponse, 1) 169 | go func(errch chan error, rc chan []probes.ProbeResponse) { 170 | howLong := d.Delay*time.Duration(numPackets) + d.Timeout 171 | received, err := d.ListenFor(ipv6.NewPacketConn(iconn), howLong) 172 | errch <- err 173 | // TODO pass the rp chan to ListenFor and let it feed packets there 174 | rc <- received 175 | }(recvErrors, recvChan) 176 | 177 | // send the packets 178 | sent := make([]probes.Probe, 0, numPackets) 179 | for p := range d.packets(localUDPAddr.IP, d.Target) { 180 | cm := ipv6.ControlMessage{ 181 | HopLimit: p.HopLimit, 182 | Src: localUDPAddr.IP, 183 | } 184 | if _, err := pconn.WriteTo(append(p.UDPHeader, p.Payload...), &cm, &net.UDPAddr{IP: d.Target, Port: p.DstPort}); err != nil { 185 | return nil, nil, fmt.Errorf("WriteTo failed: %w", err) 186 | } 187 | // get timestamp as soon as possible after the packet is sent 188 | ts := time.Now() 189 | probe := ProbeUDPv6{ 190 | Payload: append(p.UDPHeader, p.Payload...), 191 | HopLimit: p.HopLimit, 192 | LocalAddr: p.Src, 193 | RemoteAddr: d.Target, 194 | Timestamp: ts, 195 | } 196 | if err := probe.Validate(); err != nil { 197 | return nil, nil, err 198 | } 199 | sent = append(sent, &probe) 200 | time.Sleep(d.Delay) 201 | } 202 | if err = <-recvErrors; err != nil { 203 | return nil, nil, err 204 | } 205 | received := <-recvChan 206 | return sent, received, nil 207 | } 208 | 209 | // ListenFor waits for ICMP packets until the timeout expires 210 | func (d UDPv6) ListenFor(conn *ipv6.PacketConn, howLong time.Duration) ([]probes.ProbeResponse, error) { 211 | packets := make([]probes.ProbeResponse, 0) 212 | deadline := time.Now().Add(howLong) 213 | for { 214 | if deadline.Sub(time.Now()) <= 0 { 215 | break 216 | } 217 | select { 218 | default: 219 | // TODO tune data size 220 | data := make([]byte, 4096) 221 | now := time.Now() 222 | conn.SetReadDeadline(now.Add(time.Millisecond * 100)) 223 | n, _, addr, err := conn.ReadFrom(data) 224 | receivedAt := time.Now() 225 | if err != nil { 226 | if nerr, ok := err.(*net.OpError); ok { 227 | if nerr.Timeout() { 228 | continue 229 | } 230 | return nil, err 231 | } 232 | } 233 | packets = append(packets, &ProbeResponseUDPv6{ 234 | Data: data[:n], 235 | Addr: (*(addr).(*net.IPAddr)).IP, 236 | Timestamp: receivedAt, 237 | }) 238 | } 239 | } 240 | return packets, nil 241 | } 242 | 243 | // Match compares the sent and received packets and finds the matching ones. It 244 | // returns a Results structure 245 | func (d UDPv6) Match(sent []probes.Probe, received []probes.ProbeResponse) results.Results { 246 | res := results.Results{ 247 | Flows: make(map[uint16][]results.Probe), 248 | } 249 | for _, sp := range sent { 250 | spu := sp.(*ProbeUDPv6) 251 | if err := spu.Validate(); err != nil { 252 | log.Printf("Invalid probe: %w", err) 253 | continue 254 | } 255 | sentUDP := spu.UDP() 256 | probe := results.Probe{ 257 | Sent: results.Packet{ 258 | Timestamp: results.UnixUsec(spu.Timestamp), 259 | IP: results.IP{ 260 | SrcIP: spu.LocalAddr, 261 | DstIP: spu.RemoteAddr, 262 | TTL: uint8(spu.HopLimit), // TTL should be really renamed to something better.. 263 | }, 264 | UDP: &results.UDP{ 265 | SrcPort: uint16(sentUDP.Src), 266 | DstPort: uint16(sentUDP.Dst), 267 | }, 268 | }, 269 | } 270 | var flowID uint16 271 | if d.UseSrcPort { 272 | flowID = uint16(sentUDP.Src) 273 | } else { 274 | flowID = uint16(sentUDP.Dst) 275 | } 276 | for _, rp := range received { 277 | rpu := rp.(*ProbeResponseUDPv6) 278 | if err := rpu.Validate(); err != nil { 279 | log.Printf("Invalid probe response: %v", err) 280 | continue 281 | } 282 | if !rpu.Matches(spu) { 283 | continue 284 | } 285 | // source port may be mangled by a NAT 286 | if sentUDP.Src != rpu.InnerUDP().Src { 287 | // source ports do not match - it's not for this packet 288 | probe.NATID = uint16(rpu.InnerUDP().Src) 289 | } 290 | // TODO 291 | // Here, in IPv4, we would check for innerIP.ID != sentIP.Id but 292 | // for IPv6 we need something different. See the comment above 293 | // about Payload Length, and line 278 in probes/probev4/udpv4.go 294 | 295 | // at this point, we know that the sent and received packet 296 | // belong to the same flow. 297 | // TODO in IPv4, at this point we can detect a NAT using the 298 | // checksum. Implement a similar technique for v6 299 | 300 | // TODO implement computeFlowHash also for IPv6. The function 301 | // can be generalized for both v4 and v6 302 | // flowhash, err := computeFlowHash(spu.Packet) 303 | icmp := rpu.ICMPv6() 304 | description := "Unknown" 305 | if icmp.Type == inet.ICMPv6TypeDestUnreachable && icmp.Code == inet.ICMPv6CodePortUnreachable { 306 | description = "Destination port unreachable" 307 | } else if icmp.Type == inet.ICMPv6TypeTimeExceeded && icmp.Code == inet.ICMPv6CodeHopLimitExceeded { 308 | description = "Hop limit exceeded" 309 | } 310 | // this is our packet. Let's fill the probe data up 311 | // probe.Flowhash = flowhash 312 | // TODO check if To16() is the right thing to do here 313 | probe.IsLast = bytes.Equal(rpu.Addr.To16(), d.Target.To16()) 314 | probe.Name = rpu.Addr.String() 315 | probe.RttUsec = uint64(rpu.Timestamp.Sub(spu.Timestamp)) / 1000 316 | probe.ZeroTTLForwardingBug = (rpu.InnerIPv6().HopLimit == 0) 317 | probe.Received = &results.Packet{ 318 | Timestamp: results.UnixUsec(rpu.Timestamp), 319 | ICMP: &results.ICMP{ 320 | Type: uint8(icmp.Type), 321 | Code: uint8(icmp.Code), 322 | Description: description, 323 | }, 324 | IP: results.IP{ 325 | SrcIP: rpu.Addr, 326 | DstIP: spu.LocalAddr, 327 | }, 328 | UDP: &results.UDP{ 329 | SrcPort: uint16(rpu.InnerUDP().Src), 330 | DstPort: uint16(rpu.InnerUDP().Dst), 331 | }, 332 | } 333 | // break, since this is a response to the sent probe 334 | break 335 | } 336 | res.Flows[flowID] = append(res.Flows[flowID], probe) 337 | } 338 | return res 339 | } 340 | 341 | // Traceroute sends the probes and returns a Results structure or an error 342 | func (d UDPv6) Traceroute() (*results.Results, error) { 343 | if err := d.Validate(); err != nil { 344 | return nil, err 345 | } 346 | sent, received, err := d.SendReceive() 347 | if err != nil { 348 | return nil, err 349 | } 350 | results := d.Match(sent, received) 351 | 352 | return &results, nil 353 | } 354 | -------------------------------------------------------------------------------- /go/dublintraceroute/probes/probev6/udpv6probe.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package probev6 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "time" 9 | 10 | inet "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/net" 11 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/probes" 12 | "golang.org/x/net/ipv6" 13 | ) 14 | 15 | // ProbeUDPv6 represents a sent probe packet with its metadata 16 | type ProbeUDPv6 struct { 17 | // Payload of the sent IPv6 packet 18 | Payload []byte 19 | // HopLimit value when the packet was sent 20 | HopLimit int 21 | // time the packet is set at 22 | Timestamp time.Time 23 | // local address of the packet sender 24 | LocalAddr, RemoteAddr net.IP 25 | // internal fields 26 | udp *inet.UDP 27 | } 28 | 29 | // Validate verifies that the probe has the expected structure, and returns an error if not 30 | func (p *ProbeUDPv6) Validate() error { 31 | if p.udp == nil { 32 | // decode packet 33 | udp, err := inet.NewUDP(p.Payload) 34 | if err != nil { 35 | return err 36 | } 37 | p.udp = udp 38 | } 39 | return nil 40 | } 41 | 42 | // UDP returns the UDP layer of the probe. If not decoded yet, will return nil. 43 | func (p ProbeUDPv6) UDP() *inet.UDP { 44 | return p.udp 45 | } 46 | 47 | // ProbeResponseUDPv6 represents a received probe response with its metadata 48 | type ProbeResponseUDPv6 struct { 49 | // payload of the received IPv6 packet (expected ICMPv6 -> IPv6 -> UDP) 50 | Data []byte 51 | // time the packet is received at 52 | Timestamp time.Time 53 | // sender IP address 54 | Addr net.IP 55 | // internal fields 56 | icmp *inet.ICMPv6 57 | innerIPv6 *ipv6.Header 58 | innerUDP *inet.UDP 59 | payload []byte 60 | } 61 | 62 | // Validate verifies that the probe response has the expected structure, and 63 | // returns an error if not 64 | func (pr *ProbeResponseUDPv6) Validate() error { 65 | if pr.icmp != nil && pr.innerIPv6 != nil && pr.innerUDP != nil { 66 | return nil 67 | } 68 | // decode ICMPv6 layer 69 | icmp, err := inet.NewICMPv6(pr.Data) 70 | if err != nil { 71 | return fmt.Errorf("failed to decode ICMPv6: %w", err) 72 | } 73 | pr.icmp = icmp 74 | // decode inner IPv6 layer 75 | ip, err := ipv6.ParseHeader(pr.Data[inet.ICMPv6HeaderLen:]) 76 | if err != nil { 77 | return fmt.Errorf("failed to decode inner IPv6: %w", err) 78 | } 79 | pr.innerIPv6 = ip 80 | // decode inner UDP layer 81 | udp, err := inet.NewUDP(pr.Data[inet.ICMPv6HeaderLen+inet.IPv6HeaderLen:]) 82 | if err != nil { 83 | return fmt.Errorf("failed to decode inner UDP: %w", err) 84 | } 85 | pr.innerUDP = udp 86 | pr.payload = pr.Data[inet.ICMPv6HeaderLen+inet.IPv6HeaderLen+inet.UDPHeaderLen:] 87 | return nil 88 | } 89 | 90 | // Matches returns true if this probe response matches the given probe. Both 91 | // probes must have been already validated with Validate, this function may 92 | // panic otherwise. 93 | func (pr ProbeResponseUDPv6) Matches(pi probes.Probe) bool { 94 | p, ok := pi.(*ProbeUDPv6) 95 | if !ok || p == nil { 96 | return false 97 | } 98 | icmp := pr.ICMPv6() 99 | if icmp.Type != inet.ICMPv6TypeTimeExceeded && 100 | !(icmp.Type == inet.ICMPv6TypeDestUnreachable && icmp.Code == inet.ICMPv6CodePortUnreachable) { 101 | // we want time-exceeded or port-unreachable 102 | return false 103 | } 104 | // TODO check that To16() is the right thing to call here 105 | if !pr.InnerIPv6().Dst.To16().Equal(p.RemoteAddr.To16()) { 106 | // this is not a response to any of our probes, discard it 107 | return false 108 | } 109 | innerUDP := pr.InnerUDP() 110 | if p.UDP().Dst != innerUDP.Dst { 111 | // this is not our packet 112 | return false 113 | } 114 | if pr.InnerIPv6().PayloadLen != len(p.Payload)+inet.UDPHeaderLen { 115 | // different payload length, not our packet 116 | // NOTE: here I am using pr.InnerIPv6().PayloadLen instead of len(pr.payload) 117 | // because the responding hop might use an RFC4884 multi-part ICMPv6 message, 118 | // which has extra data at the end of time-exceeded and destination-unreachable 119 | // messages 120 | return false 121 | } 122 | return true 123 | } 124 | 125 | // ICMPv6 returns the ICMPv6 layer of the probe. 126 | func (pr *ProbeResponseUDPv6) ICMPv6() *inet.ICMPv6 { 127 | return pr.icmp 128 | } 129 | 130 | // InnerIPv6 returns the IP layer of the inner packet of the probe. 131 | func (pr *ProbeResponseUDPv6) InnerIPv6() *ipv6.Header { 132 | return pr.innerIPv6 133 | } 134 | 135 | // InnerUDP returns the UDP layer of the inner packet of the probe. 136 | func (pr *ProbeResponseUDPv6) InnerUDP() *inet.UDP { 137 | return pr.innerUDP 138 | } 139 | 140 | // InnerPayload returns the payload of the inner UDP packet 141 | func (pr *ProbeResponseUDPv6) InnerPayload() []byte { 142 | return pr.payload 143 | } 144 | -------------------------------------------------------------------------------- /go/dublintraceroute/results/results.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package results 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "fmt" 9 | "log" 10 | "math/rand" 11 | "net" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/goccy/go-graphviz" 18 | "github.com/goccy/go-graphviz/cgraph" 19 | ) 20 | 21 | // IP represents some information from the IP header. 22 | type IP struct { 23 | SrcIP net.IP `json:"src"` 24 | DstIP net.IP `json:"dst"` 25 | ID uint16 `json:"id"` 26 | TTL uint8 `json:"ttl"` 27 | } 28 | 29 | // UDP represents some information from the UDP header. 30 | type UDP struct { 31 | SrcPort uint16 `json:"sport"` 32 | DstPort uint16 `json:"dport"` 33 | } 34 | 35 | // ICMP represents some information from the ICMP header. 36 | type ICMP struct { 37 | Code uint8 `json:"code"` 38 | Type uint8 `json:"type"` 39 | Description string `json:"description"` 40 | Extensions []ICMPExtension `json:"extensions"` 41 | MPLSLabels []MPLSLabel `json:"mpls_labels"` 42 | } 43 | 44 | // ICMPExtension represents the ICMP extension header. 45 | type ICMPExtension struct { 46 | Class uint8 `json:"class"` 47 | Type uint8 `json:"type"` 48 | Payload []byte `json:"payload"` 49 | Size uint8 `json:"size"` 50 | } 51 | 52 | // MPLSLabel represents an MPLS label in an ICMP header. 53 | type MPLSLabel struct { 54 | BottomOfStack uint8 `json:"bottom_of_stack"` 55 | Experimental uint8 `json:"experimental"` 56 | Label uint32 `json:"label"` 57 | TTL uint8 `json:"ttl"` 58 | } 59 | 60 | // UnixUsec is UNIX time in the form sec.usec 61 | type UnixUsec time.Time 62 | 63 | // UnmarshalJSON deserializes a seconds.microseconds timestamp into an UnixUsec 64 | // object. The timestamp can be optionally surrounded by double quotes. 65 | func (um *UnixUsec) UnmarshalJSON(b []byte) error { 66 | s := string(b) 67 | // strip quotes, if any 68 | if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' { 69 | s = s[1 : len(s)-1] 70 | } 71 | split := strings.Split(string(s), ".") 72 | if len(split) != 2 { 73 | return fmt.Errorf("invalid timestamp %s", s) 74 | } 75 | sec, err := strconv.ParseInt(split[0], 10, 64) 76 | if err != nil { 77 | return fmt.Errorf("invalid seconds in timestamp %s: %v", s, err) 78 | } 79 | if len(split[1]) > 6 { 80 | // truncate string, we only want down to microseconds 81 | split[1] = split[1][:6] 82 | } 83 | dec, err := strconv.ParseInt(split[1], 10, 64) 84 | if err != nil { 85 | return fmt.Errorf("invalid decimal string in timestamp %s: %v", s, err) 86 | } 87 | if dec > 999999 { 88 | return fmt.Errorf("invalid decimal string in timestamp %s: too large", s) 89 | } 90 | // right-pad decimal string with zeros 91 | decStr := split[1] 92 | if len(split[1]) < 6 { 93 | decStr += strings.Repeat("0", 6-len(split[1])) 94 | } 95 | // now that it's correctly padded, parse it again 96 | usec, err := strconv.ParseInt(strings.TrimRight(decStr, " "), 10, 64) 97 | if err != nil { 98 | return fmt.Errorf("invalid microseconds in timestamp %s: %v", s, err) 99 | } 100 | 101 | if usec > 999999 { 102 | return fmt.Errorf("invalid microseconds in timestamp %s: too large", s) 103 | } 104 | *um = UnixUsec(time.Unix(sec, usec*1000)) 105 | return nil 106 | } 107 | 108 | // MarshalJSON serializes an UnixUsec object into a seconds.microseconds 109 | // representation. 110 | func (um UnixUsec) MarshalJSON() ([]byte, error) { 111 | u := time.Time(um).UnixNano() / 1000 112 | return []byte(fmt.Sprintf("%d.%06d", u/1e6, u%1e6)), nil 113 | } 114 | 115 | // Packet represents some information of a sent or received packet. 116 | type Packet struct { 117 | Timestamp UnixUsec `json:"timestamp"` 118 | IP IP `json:"ip"` 119 | UDP *UDP `json:"udp,omitempty"` 120 | ICMP *ICMP `json:"icmp,omitempty"` 121 | // TODO add TCP, HTTP, DNS 122 | } 123 | 124 | // Probe holds information about a dublin-traceroute probe. 125 | type Probe struct { 126 | Flowhash uint16 `json:"flowhash"` 127 | IsLast bool `json:"is_last"` 128 | Name string `json:"name"` 129 | NATID uint16 `json:"nat_id"` 130 | RttUsec uint64 `json:"rtt_usec"` 131 | Sent Packet `json:"sent"` 132 | Received *Packet `json:"received"` 133 | ZeroTTLForwardingBug bool `json:"zerottl_forwarding_bug"` 134 | } 135 | 136 | // Results is the main container type for a dublin-traceroute set of results. 137 | type Results struct { 138 | Flows map[uint16][]Probe `json:"flows"` 139 | compressed bool 140 | } 141 | 142 | func (r *Results) compress() { 143 | if r.compressed { 144 | return 145 | } 146 | for k, v := range r.Flows { 147 | for idx, e := range v { 148 | if e.IsLast { 149 | r.Flows[k] = r.Flows[k][:idx+1] 150 | break 151 | } 152 | } 153 | } 154 | r.compressed = true 155 | } 156 | 157 | // ToJSON encodes a Results object to a JSON string. 158 | func (r *Results) ToJSON(compress bool, indent string) (string, error) { 159 | if compress { 160 | if !r.compressed { 161 | r.compress() 162 | } 163 | } 164 | b, err := json.MarshalIndent(r, "", indent) 165 | if err != nil { 166 | return "", fmt.Errorf("failed to marshal JSON: %w", err) 167 | } 168 | return string(b), nil 169 | } 170 | 171 | // ToDOT encodes a Results object to a DOT file suitable for GraphViz 172 | func (r *Results) ToDOT() (string, error) { 173 | type node struct { 174 | node *cgraph.Node 175 | probe *Probe 176 | } 177 | gv := graphviz.New() 178 | graph, err := gv.Graph() 179 | if err != nil { 180 | return "", fmt.Errorf("failed to create graph: %w", err) 181 | } 182 | graph.SetRankDir(cgraph.BTRank) 183 | 184 | flowIDs := make([]int, 0, len(r.Flows)) 185 | for flowID := range r.Flows { 186 | flowIDs = append(flowIDs, int(flowID)) 187 | } 188 | sort.Ints(flowIDs) 189 | 190 | for _, flowID := range flowIDs { 191 | hops := r.Flows[uint16(flowID)] 192 | if len(hops) == 0 { 193 | log.Printf("No hops for flow ID %d", flowID) 194 | continue 195 | } 196 | var nodes []node 197 | // add first hop 198 | firstNodeName := hops[0].Sent.IP.SrcIP.String() 199 | firstHop, err := graph.CreateNode(firstNodeName) 200 | if err != nil { 201 | return "", fmt.Errorf("failed to create first node: %w", err) 202 | } 203 | firstHop.SetShape(cgraph.RectShape) 204 | nodes = append(nodes, node{node: firstHop, probe: &hops[0]}) 205 | 206 | // then add all the other hops 207 | for idx, hop := range hops { 208 | hop := hop 209 | nodename := fmt.Sprintf("NULL - %d", idx) 210 | label := "*" 211 | hostname := "" 212 | if hop.Received != nil { 213 | nodename = hop.Received.IP.SrcIP.String() 214 | if hop.Name != nodename { 215 | hostname = "\n" + hop.Name 216 | } 217 | // MPLS labels 218 | mpls := "" 219 | if len(hop.Received.ICMP.MPLSLabels) > 0 { 220 | mpls = "MPLS labels: \n" 221 | for _, mplsLabel := range hop.Received.ICMP.MPLSLabels { 222 | mpls += fmt.Sprintf(" - %d, ttl: %d\n", mplsLabel.Label, mplsLabel.TTL) 223 | } 224 | } 225 | label = fmt.Sprintf("%s%s\n%s\n%s", nodename, hostname, hop.Received.ICMP.Description, mpls) 226 | } 227 | n, err := graph.CreateNode(nodename) 228 | if err != nil { 229 | return "", fmt.Errorf("failed to create node '%s': %w", nodename, err) 230 | } 231 | if hop.IsLast { 232 | n.SetShape(cgraph.RectShape) 233 | } 234 | n.SetLabel(label) 235 | nodes = append(nodes, node{node: n, probe: &hop}) 236 | 237 | if hop.IsLast { 238 | break 239 | } 240 | } 241 | // add edges 242 | if len(nodes) <= 1 { 243 | // no edges to add if there is only one node 244 | continue 245 | } 246 | color := rand.Intn(0xffffff) 247 | // start at node 1. Each node back-references the previous one 248 | for idx := 1; idx < len(nodes); idx++ { 249 | if idx >= len(nodes) { 250 | // we are at the second-to-last node 251 | break 252 | } 253 | prev := nodes[idx-1] 254 | cur := nodes[idx] 255 | edgeName := fmt.Sprintf("%s - %s - %d - %d", prev.node.Name(), cur.node.Name(), idx, flowID) 256 | edgeLabel := "" 257 | if idx == 1 { 258 | edgeLabel += fmt.Sprintf( 259 | "srcport %d\ndstport %d", 260 | cur.probe.Sent.UDP.SrcPort, 261 | cur.probe.Sent.UDP.DstPort, 262 | ) 263 | } 264 | if prev.probe.NATID != cur.probe.NATID { 265 | edgeLabel += "\nNAT detected" 266 | } 267 | edgeLabel += fmt.Sprintf("\n%d.%d ms", int(cur.probe.RttUsec/1000), int(cur.probe.RttUsec%1000)) 268 | 269 | edge, err := graph.CreateEdge(edgeName, prev.node, cur.node) 270 | if err != nil { 271 | return "", fmt.Errorf("failed to create edge '%s': %w", edgeName, err) 272 | } 273 | edge.SetLabel(edgeLabel) 274 | edge.SetColor(fmt.Sprintf("#%06x", color)) 275 | } 276 | } 277 | var buf bytes.Buffer 278 | if err := gv.Render(graph, "dot", &buf); err != nil { 279 | return "", fmt.Errorf("failed to render graph: %w", err) 280 | } 281 | if err := graph.Close(); err != nil { 282 | return "", fmt.Errorf("failed to close graph: %w", err) 283 | } 284 | gv.Close() 285 | return buf.String(), nil 286 | } 287 | -------------------------------------------------------------------------------- /go/dublintraceroute/results/results_test.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package results 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestUnixUsecMarshalJSON(t *testing.T) { 14 | // 10 seconds 15 | um := UnixUsec(time.Unix(10, 0)) 16 | b, err := um.MarshalJSON() 17 | require.NoError(t, err) 18 | assert.Equal(t, string("10.000000"), string(b)) 19 | // 10.3 second 20 | um = UnixUsec(time.Unix(10, 300_000_000)) 21 | b, err = um.MarshalJSON() 22 | require.NoError(t, err) 23 | assert.Equal(t, string("10.300000"), string(b)) 24 | // 10.0003 second 25 | um = UnixUsec(time.Unix(10, 300_000)) 26 | b, err = um.MarshalJSON() 27 | require.NoError(t, err) 28 | assert.Equal(t, string("10.000300"), string(b)) 29 | } 30 | 31 | func TestUnixUsecUnmarshalJSON(t *testing.T) { 32 | // 10.3 seconds 33 | var um UnixUsec 34 | err := um.UnmarshalJSON([]byte("10.3")) 35 | require.NoError(t, err) 36 | assert.Equal(t, UnixUsec(time.Unix(10, 300_000_000)), um) 37 | // 10.0003 seconds 38 | err = um.UnmarshalJSON([]byte("10.0003")) 39 | require.NoError(t, err) 40 | assert.Equal(t, UnixUsec(time.Unix(10, 300_000)), um) 41 | // 10.0000003, expect truncation of the digits below microsec 42 | err = um.UnmarshalJSON([]byte("10.0000003")) 43 | require.NoError(t, err) 44 | assert.Equal(t, UnixUsec(time.Unix(10, 0)), um) 45 | } 46 | -------------------------------------------------------------------------------- /homebrew/dublin-traceroute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # You need the latest XCode for this formula to build 4 | class DublinTraceroute < Formula 5 | desc 'NAT-aware multipath tracerouting tool' 6 | homepage 'https://dublin-traceroute.net' 7 | url 'https://github.com/insomniacslk/dublin-traceroute/archive/v0.4.2.tar.gz' 8 | sha256 '255980a630fb0d8b1cac270a656d6236bfddc5ba253bda4b898302918caf76d1' 9 | head 'https://github.com/insomniacslk/dublin-traceroute.git' 10 | 11 | depends_on 'cmake' => :build 12 | depends_on 'pkg-config' => :build 13 | # this should be in the libtins formula 14 | depends_on 'libpcap' 15 | depends_on 'libtins' 16 | depends_on 'jsoncpp' 17 | 18 | def install 19 | system 'cmake', '.', *std_cmake_args 20 | system 'make', 'install' 21 | end 22 | 23 | test do 24 | system "#{bin}/dublin-traceroute" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /include/dublintraceroute/common.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file common.h 5 | * \author Andrea Barberio 6 | * \copyright 2-clause BSD 7 | * \date October 2015 8 | * \brief Common utilities for dublin-traceroute 9 | * 10 | * This file contains the common utilities for dublin-traceroute. 11 | * 12 | * This module currently offers the set-up of the logging facilities and IP ID 13 | * matching algorithm switch. 14 | * 15 | * \sa common.cc 16 | */ 17 | 18 | #ifndef _COMMON_H 19 | #define _COMMON_H 20 | 21 | #define VERSION "0.5.0" 22 | 23 | #include 24 | 25 | #include 26 | #include 27 | 28 | #include "hops.h" 29 | 30 | 31 | /* Define/undefine USE_IP_ID_MATCHING to enable the IP ID packet matching, that 32 | * enables multipath traceroutes to work through NAT 33 | */ 34 | #define USE_IP_ID_MATCHING 35 | 36 | #define PROGNAME "dublin-traceroute" 37 | 38 | typedef uint16_t flow_id_t; 39 | typedef std::map> flow_map_t; 40 | 41 | void setupLogging(); 42 | void shutDownLogging(); 43 | 44 | #endif /* _COMMON_H */ 45 | 46 | -------------------------------------------------------------------------------- /include/dublintraceroute/dublin_traceroute.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file dublin_traceroute.h 5 | * \author Andrea Barberio 6 | * \copyright 2-clause BSD 7 | * \date October 2015 8 | * \brief Main class for the NAT-aware multipath traceroute 9 | * 10 | * This module contains the implementation of the NAT-aware multipath 11 | * traceroute known as dublin-traceroute. 12 | * 13 | * \sa dublin_traceroute.cc 14 | */ 15 | 16 | #ifndef _Dublin_TRACEROUTE_H 17 | #define _Dublin_TRACEROUTE_H 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "common.h" 25 | #include "exceptions.h" 26 | #include "traceroute_results.h" 27 | 28 | // TODO add copyright and author 29 | 30 | /* TODO put everything into a namespace, e.g. 31 | * namespace DublinTraceroute { // or DublinTraceroute 32 | * class Traceroute { 33 | * TracerouteResults run(); 34 | * ... 35 | * } 36 | * } 37 | */ 38 | 39 | enum probe_type { 40 | // valid probe types must be within `min` and `max`, which are used for 41 | // argument checking. 42 | min, 43 | // UDP over IPv4 44 | UDPv4, 45 | max, 46 | }; 47 | 48 | inline 49 | const std::string 50 | probe_type_name(probe_type type) { 51 | switch (type) { 52 | case probe_type::UDPv4: 53 | return "IPv4/UDP"; 54 | default: 55 | return ""; 56 | } 57 | } 58 | 59 | class DublinTraceroute { 60 | 61 | private: 62 | const uint16_t srcport_, 63 | dstport_; 64 | const std::string dst_; 65 | const probe_type type_; 66 | Tins::IPv4Address target_; 67 | const uint8_t npaths_, 68 | min_ttl_, 69 | max_ttl_; 70 | const uint16_t delay_; 71 | const bool broken_nat_, 72 | use_srcport_for_path_generation_, 73 | no_dns_; 74 | std::mutex mutex_tracerouting, 75 | mutex_sniffed_packets; 76 | Tins::IPv4Address my_address; 77 | std::vector> sniffed_packets; 78 | const void validate_arguments(); 79 | 80 | public: 81 | static const probe_type default_type = probe_type::UDPv4; 82 | static const uint16_t default_srcport = 12345; 83 | static const uint16_t default_dstport = 33434; 84 | static const uint8_t default_npaths = 20; 85 | static const uint8_t default_min_ttl = 1; 86 | static const uint8_t default_max_ttl = 30; 87 | static const uint16_t default_delay = 10; 88 | static const bool default_broken_nat = false; 89 | static const bool default_use_srcport_for_path_generation = false; 90 | static const bool default_no_dns = false; 91 | DublinTraceroute( 92 | const std::string &dst, 93 | const probe_type type = default_type, 94 | const uint16_t srcport = default_srcport, 95 | const uint16_t dstport = default_dstport, 96 | const uint8_t npaths = default_npaths, 97 | const uint8_t min_ttl = default_min_ttl, 98 | const uint8_t max_ttl = default_max_ttl, 99 | const uint16_t delay = default_delay, 100 | const bool broken_nat = default_broken_nat, 101 | const bool use_srcport_for_path_generation = default_use_srcport_for_path_generation, 102 | const bool no_dns = default_no_dns 103 | ): 104 | dst_(dst), 105 | type_(type), 106 | srcport_(srcport), 107 | dstport_(dstport), 108 | npaths_(npaths), 109 | min_ttl_(min_ttl), 110 | max_ttl_(max_ttl), 111 | delay_(delay), 112 | broken_nat_(broken_nat), 113 | use_srcport_for_path_generation_(use_srcport_for_path_generation), 114 | no_dns_(no_dns) 115 | { validate_arguments(); } 116 | DublinTraceroute( 117 | const char *dst, 118 | const probe_type type = default_type, 119 | const uint16_t srcport = default_srcport, 120 | const uint16_t dstport = default_dstport, 121 | const uint8_t npaths = default_npaths, 122 | const uint8_t min_ttl = default_min_ttl, 123 | const uint8_t max_ttl = default_max_ttl, 124 | const uint16_t delay = default_delay, 125 | const bool broken_nat = default_broken_nat, 126 | const bool use_srcport_for_path_generation = default_use_srcport_for_path_generation, 127 | const bool no_dns = default_no_dns 128 | ): 129 | dst_(std::string(dst)), 130 | type_(type), 131 | srcport_(srcport), 132 | dstport_(dstport), 133 | npaths_(npaths), 134 | min_ttl_(min_ttl), 135 | max_ttl_(max_ttl), 136 | delay_(delay), 137 | broken_nat_(broken_nat), 138 | use_srcport_for_path_generation_(use_srcport_for_path_generation), 139 | no_dns_(no_dns) 140 | { validate_arguments(); } 141 | ~DublinTraceroute() { std::lock_guard lock(mutex_tracerouting); }; 142 | DublinTraceroute(const DublinTraceroute& source): 143 | dst_(source.dst_), 144 | type_(source.type_), 145 | srcport_(source.srcport_), 146 | dstport_(source.dstport_), 147 | npaths_(source.npaths_), 148 | min_ttl_(source.min_ttl_), 149 | max_ttl_(source.max_ttl_), 150 | delay_(source.delay_), 151 | broken_nat_(source.broken_nat_), 152 | use_srcport_for_path_generation_(source.use_srcport_for_path_generation_), 153 | no_dns_(source.no_dns_) 154 | { validate_arguments(); } 155 | 156 | inline const std::string &dst() const { return dst_; } 157 | inline const probe_type type() const { return static_cast(type_); } 158 | inline const uint16_t srcport() const { return srcport_; } 159 | inline const uint16_t dstport() const { return dstport_; } 160 | inline const uint8_t npaths() const { return npaths_; } 161 | inline const uint8_t min_ttl() const { return min_ttl_; } 162 | inline const uint8_t max_ttl() const { return max_ttl_; } 163 | inline const uint16_t delay() const { return delay_; } 164 | inline const bool broken_nat() const { return broken_nat_; } 165 | inline const bool no_dns() const { return no_dns_; } 166 | inline const bool use_srcport_for_path_generation() const { return use_srcport_for_path_generation_; } 167 | inline const Tins::IPv4Address &target() const { return target_; } 168 | void target(const Tins::IPv4Address &addr) { target_ = addr; } 169 | std::shared_ptr traceroute(); 170 | 171 | private: 172 | bool sniffer_callback(Tins::Packet& packet); 173 | void match_sniffed_packets(TracerouteResults &results); 174 | void match_hostnames(TracerouteResults &results, std::shared_ptr flows); 175 | }; 176 | 177 | #endif /* _Dublin_TRACEROUTE_H */ 178 | 179 | -------------------------------------------------------------------------------- /include/dublintraceroute/exceptions.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file exceptions.h 5 | * \author Andrea Barberio 6 | * \copyright 2-clause BSD 7 | * \date October 2015 8 | * \brief Common exceptions 9 | * 10 | * This module contains the common exceptions definitions 11 | */ 12 | 13 | #ifndef _EXCEPTIONS_H 14 | #define _EXCEPTIONS_H 15 | 16 | #include 17 | 18 | class DublinTracerouteException: public std::runtime_error { 19 | public: 20 | DublinTracerouteException(const char *msg): std::runtime_error(msg) {}; 21 | DublinTracerouteException(std::string msg): std::runtime_error(msg.c_str()) {}; 22 | }; 23 | 24 | 25 | class DublinTracerouteInProgressException: public DublinTracerouteException { 26 | public: 27 | DublinTracerouteInProgressException(const char *msg): DublinTracerouteException(msg) {}; 28 | DublinTracerouteInProgressException(std::string msg): DublinTracerouteException(msg.c_str()) {}; 29 | }; 30 | 31 | class DublinTracerouteFailedException: public DublinTracerouteException { 32 | public: 33 | DublinTracerouteFailedException(const char *msg): DublinTracerouteException(msg) {}; 34 | DublinTracerouteFailedException(std::string msg): DublinTracerouteException(msg.c_str()) {}; 35 | }; 36 | 37 | #endif /* _EXCEPTIONS_H */ 38 | 39 | -------------------------------------------------------------------------------- /include/dublintraceroute/hop.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file hop.h 5 | * \Author Andrea Barberio 6 | * \date October 2015 7 | * \brief Definition of the Hop class 8 | * 9 | * This file contains the definition of the Hop class, which represent every 10 | * single hop in a traceroute. A Hop includes the sent packet, the matching 11 | * received packet (if any), NAT information and last-hop information. 12 | * 13 | * This module currently offers the set-up of the logging facilities. 14 | * 15 | * \sa hop.cc 16 | */ 17 | 18 | #ifndef _HOP_H 19 | #define _HOP_H 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | #include 27 | 28 | 29 | class Hop { 30 | private: 31 | std::shared_ptr sent_; 32 | std::shared_ptr received_; 33 | std::shared_ptr sent_timestamp_; 34 | std::shared_ptr received_timestamp_; 35 | std::string name_ = ""; 36 | bool last_hop_; 37 | public: 38 | Hop(): last_hop_(false) { } 39 | std::shared_ptr sent() { return sent_; } 40 | std::shared_ptr received() { return received_; } 41 | std::string name() { return name_; } 42 | std::shared_ptr received_timestamp() { return received_timestamp_; } 43 | std::shared_ptr sent_timestamp() { return sent_timestamp_; } 44 | std::string resolve(); 45 | uint16_t nat_id(); 46 | const bool zerottl_forwarding_bug(); 47 | void sent(Tins::IP &packet); 48 | void received(Tins::IP &packet, const Tins::Timestamp ×tamp); 49 | void name(std::string &name); 50 | void sent_timestamp(const Tins::Timestamp ×tamp); 51 | const bool is_last_hop() const { return last_hop_; } 52 | void is_last_hop(bool is_last) { last_hop_ = is_last; } 53 | unsigned int rtt(); 54 | const uint16_t flowhash(); 55 | operator bool() const { return (bool)received_; } 56 | Json::Value to_json(); 57 | std::string summary(); 58 | }; 59 | 60 | #endif /* _HOP_H */ 61 | 62 | -------------------------------------------------------------------------------- /include/dublintraceroute/hops.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file hops.h 5 | * \Author Andrea Barberio 6 | * \date 2017 7 | * \brief Definition of the Hops class 8 | * 9 | * This file contains a wrapper class around a std::vector of Hop objects. 10 | * 11 | * \sa hops.cc 12 | */ 13 | 14 | #ifndef _HOPS_H 15 | #define _HOPS_H 16 | 17 | #include "dublintraceroute/hop.h" 18 | 19 | 20 | class Hops { 21 | private: 22 | std::vector hops_; 23 | public: 24 | Hops() { } 25 | Hops(const Hops &source): hops_(source.hops_) { } 26 | void push_back(Hop hop) { hops_.push_back(hop); }; 27 | std::vector::iterator begin() { return hops_.begin(); }; 28 | std::vector::iterator end() { return hops_.end(); }; 29 | std::vector::reverse_iterator rbegin() { return hops_.rbegin(); }; 30 | std::vector::reverse_iterator rend() { return hops_.rend(); }; 31 | Hop at(std::vector::size_type pos) { return hops_.at(pos); } 32 | std::vector::size_type size() { return hops_.size(); } 33 | bool operator==(const Hops &rhs) const { return hops_ == rhs.hops_; } 34 | }; 35 | 36 | #endif /* _HOPS_H */ 37 | 38 | -------------------------------------------------------------------------------- /include/dublintraceroute/icmp_messages.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file icmp_messages.h 5 | * \Author Andrea Barberio 6 | * \date October 2015 7 | * \brief ICMP messages definitions 8 | * 9 | * This file contains the ICMP messages definitions 10 | */ 11 | 12 | #ifndef _ICMP_MESSAGES_H 13 | #define _ICMP_MESSAGES_H 14 | 15 | #define ICMP_EXTENSION_MPLS_CLASS 1 16 | #define ICMP_EXTENSION_MPLS_TYPE 1 17 | 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | // defining a map key that wraps ICMP type and code 24 | typedef std::tuple icmpmessage_t; 25 | 26 | 27 | struct icmpmessage_hash { 28 | std::size_t operator()(const icmpmessage_t &key) const { 29 | return std::get<0>(key) ^ std::get<1>(key); 30 | } 31 | }; 32 | 33 | 34 | struct icmpmessage_equals { 35 | bool operator()(const icmpmessage_t &left, const icmpmessage_t &right) const { 36 | return ( 37 | std::get<0>(left) == std::get<0>(right) && 38 | std::get<1>(left) == std::get<1>(right) 39 | ); 40 | } 41 | }; 42 | 43 | 44 | struct data { 45 | std::string x; 46 | }; 47 | 48 | 49 | typedef std::unordered_map icmpmessagemap_t; 50 | 51 | 52 | struct icmpmessages { 53 | private: 54 | icmpmessagemap_t icmp_message_map; 55 | public: 56 | icmpmessages() { 57 | // Codes coming from https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol#Control_messages 58 | // ICMP code 0 (echo reply) 59 | icmp_message_map[std::make_tuple(0, 0)] = {"Echo reply"}; 60 | // ICMP code 1 is reserved 61 | // ICMP code 2 is reserved 62 | // ICMP code 3 (destination unreachable) 63 | icmp_message_map[std::make_tuple(3, 0)] = {"Destination network unreachable"}; 64 | icmp_message_map[std::make_tuple(3, 1)] = {"Destination host unreachable"}; 65 | icmp_message_map[std::make_tuple(3, 2)] = {"Destination protocol unreachable"}; 66 | icmp_message_map[std::make_tuple(3, 3)] = {"Destination port unreachable"}; 67 | icmp_message_map[std::make_tuple(3, 4)] = {"DF set but fragmentation required"}; 68 | icmp_message_map[std::make_tuple(3, 5)] = {"Source route failed"}; 69 | icmp_message_map[std::make_tuple(3, 6)] = {"Destination network unknown"}; 70 | icmp_message_map[std::make_tuple(3, 7)] = {"Destination host unknown"}; 71 | icmp_message_map[std::make_tuple(3, 8)] = {"Source host isolated"}; 72 | icmp_message_map[std::make_tuple(3, 9)] = {"Network administratively prohibited"}; 73 | icmp_message_map[std::make_tuple(3, 10)] = {"Host administratively prohibited"}; 74 | icmp_message_map[std::make_tuple(3, 11)] = {"Network unreachable for TOS"}; 75 | icmp_message_map[std::make_tuple(3, 12)] = {"Host unreachable for TOS"}; 76 | icmp_message_map[std::make_tuple(3, 13)] = {"Communication administratively prohibited"}; 77 | icmp_message_map[std::make_tuple(3, 14)] = {"Host precedence violation"}; 78 | icmp_message_map[std::make_tuple(3, 15)] = {"Precedence cutoff in effect"}; 79 | // ICMP code 4 (source quench) is deprecated 80 | icmp_message_map[std::make_tuple(4, 0)] = {"Source quench (congestion control)"}; 81 | // ICMP code 5 (redirect message) 82 | icmp_message_map[std::make_tuple(5, 0)] = {"Redirect datagram for the network"}; 83 | icmp_message_map[std::make_tuple(5, 1)] = {"Redirect datagram for the host"}; 84 | icmp_message_map[std::make_tuple(5, 2)] = {"Redirect datagram for the TOS and network"}; 85 | icmp_message_map[std::make_tuple(5, 3)] = {"Redirect datagram for the TOS and host"}; 86 | // ICMP code 6 (alternate host address) is deprecated 87 | icmp_message_map[std::make_tuple(6, 0)] = {"Alternate host address"}; 88 | // ICMP code 7 is reserved 89 | // ICMP code 8 (echo request) 90 | icmp_message_map[std::make_tuple(8, 0)] = {"Echo request"}; 91 | // ICMP code 9 (Router advertisement) 92 | icmp_message_map[std::make_tuple(9, 0)] = {"Router advertisement"}; 93 | // ICMP code 10 (Router solicitation) 94 | icmp_message_map[std::make_tuple(10, 0)] = {"Router solicitation"}; 95 | // ICMP code 11 96 | icmp_message_map[std::make_tuple(11, 0)] = {"TTL expired in transit"}; 97 | icmp_message_map[std::make_tuple(11, 1)] = {"Fragment reassembly time exceeded"}; 98 | // ICMP code 12 (parameter problem: bad IP header) 99 | icmp_message_map[std::make_tuple(12, 0)] = {"Pointer indicates the error"}; 100 | icmp_message_map[std::make_tuple(12, 1)] = {"Missing a required option"}; 101 | icmp_message_map[std::make_tuple(12, 2)] = {"Bad length"}; 102 | // ICMP code 13 (timestamp) 103 | icmp_message_map[std::make_tuple(13, 0)] = {"Timestamp"}; 104 | // ICMP code 14 (timestamp reply) 105 | icmp_message_map[std::make_tuple(14, 0)] = {"Timestamp reply"}; 106 | // ICMP code 15 (information request) is deprecated 107 | icmp_message_map[std::make_tuple(15, 0)] = {"Information request"}; 108 | // ICMP code 16 (information reply) is deprecated 109 | icmp_message_map[std::make_tuple(16, 0)] = {"Information reply"}; 110 | // ICMP code 17 (address mask request) is deprecated 111 | icmp_message_map[std::make_tuple(17, 0)] = {"Address mask request"}; 112 | // ICMP code 18 (address mask reply) is deprecated 113 | icmp_message_map[std::make_tuple(18, 0)] = {"Address mask reply"}; 114 | // ICMP code 19 is reserved for security 115 | // ICMP codes 20~29 are reserved for robustness experiment 116 | // ICMP code 30 (traceroute) is deprecated 117 | icmp_message_map[std::make_tuple(30, 0)] = {"Traceroute"}; 118 | // ICMP code 31 is deprecated 119 | icmp_message_map[std::make_tuple(31, 0)] = {"Datagram conversion error"}; 120 | // ICMP code 32 is deprecated 121 | icmp_message_map[std::make_tuple(32, 0)] = {"Mobile host redirect"}; 122 | // ICMP code 33 is deprecated 123 | icmp_message_map[std::make_tuple(33, 0)] = {"Where-are-you (originally for IPv6)"}; 124 | // ICMP code 34 is deprecated 125 | icmp_message_map[std::make_tuple(34, 0)] = {"Here-I-am (originally for IPv6)"}; 126 | // ICMP code 35 is deprecated 127 | icmp_message_map[std::make_tuple(35, 0)] = {"Mobile registration request"}; 128 | // ICMP code 36 is deprecated 129 | icmp_message_map[std::make_tuple(36, 0)] = {"Mobile registration reply"}; 130 | // ICMP code 37 is deprecated 131 | icmp_message_map[std::make_tuple(37, 0)] = {"Domain name request"}; 132 | // ICMP code 38 is deprecated 133 | icmp_message_map[std::make_tuple(38, 0)] = {"Domain name reply"}; 134 | // ICMP code 39 is deprecated 135 | icmp_message_map[std::make_tuple(39, 0)] = {"SKIP algorighm discovery protocol"}; 136 | // ICMP code 40 is deprecated 137 | icmp_message_map[std::make_tuple(40, 0)] = {"Photuris, security failures"}; 138 | // ICMP code 41 is experimental 139 | icmp_message_map[std::make_tuple(41, 0)] = {"Experimental mobility protocols"}; 140 | // ICMP codes 42~252 are reserved 141 | // ICMP code 253 is experimental 142 | icmp_message_map[std::make_tuple(253, 0)] = {"RFC3692-style experiment 1"}; 143 | // ICMP code 254 is experimental 144 | icmp_message_map[std::make_tuple(254, 0)] = {"RFC3692-style experiment 2"}; 145 | // ICMP code 255 is reserved 146 | } 147 | std::string get(uint8_t type, uint8_t code) { 148 | try { 149 | return icmp_message_map.at(std::make_tuple(type, code)).x; 150 | } catch (std::out_of_range) { 151 | std::stringstream ss; 152 | ss << "Unknown message (type=" << static_cast(type) << ", code=" << static_cast(code) << ")"; 153 | return ss.str(); 154 | } 155 | } 156 | }; 157 | 158 | #endif /* _ICMP_MESSAGES_H */ 159 | 160 | -------------------------------------------------------------------------------- /include/dublintraceroute/traceroute_results.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file traceroute_results.h 5 | * \author Andrea Barberio 6 | * \copyright 2-clause BSD 7 | * \date October 2015 8 | * \brief Traceroute results class for dublin-traceroute 9 | * 10 | * This file contains the Traceroute results class for dublin-traceroute. 11 | * 12 | * This class is a container for a per-flow hops representation, and offers 13 | * facilities to print the traceroute output and convert it to a JSON 14 | * representation. 15 | * 16 | * \sa traceroute_results.cc 17 | */ 18 | 19 | #ifndef _TRACEROUTE_RESULTS_H 20 | #define _TRACEROUTE_RESULTS_H 21 | 22 | #include 23 | #include 24 | 25 | #include "common.h" 26 | #include "hop.h" 27 | 28 | 29 | class TracerouteResults { 30 | private: 31 | std::shared_ptr flows_; 32 | uint8_t min_ttl = 1; 33 | bool compressed_; 34 | bool broken_nat_; 35 | bool use_srcport_for_path_generation_; 36 | 37 | public: 38 | TracerouteResults(std::shared_ptr flows, const uint8_t min_ttl /* = 1 */, const bool broken_nat /* = false */, const bool use_srcport_for_path_generation /* = false */); 39 | ~TracerouteResults() { }; 40 | inline flow_map_t &flows() { return *flows_; } 41 | std::shared_ptr match_packet(const Tins::Packet &packet); 42 | void show(std::ostream &stream=std::cerr); 43 | void compress(); 44 | std::string to_json(); 45 | }; 46 | 47 | #endif /* _TRACEROUTE_RESULTS_H */ 48 | 49 | -------------------------------------------------------------------------------- /include/dublintraceroute/udpv4probe.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file udpv4probe.h 5 | * \Author Andrea Barberio 6 | * \date 2017 7 | * \brief Definition of the UDPProbev4 class 8 | * 9 | * This file contains the definition of the UDPv4Probe class, which represents 10 | * an UDP probe that will be sent over IPv4. 11 | * 12 | * \sa udpv4probe.cc 13 | */ 14 | 15 | #ifndef _UDPV4PROBE_H 16 | #define _UDPV4PROBE_H 17 | 18 | #include 19 | 20 | 21 | class UDPv4Probe { 22 | private: 23 | Tins::IPv4Address local_addr_; 24 | Tins::IPv4Address remote_addr_; 25 | uint16_t local_port_; 26 | uint16_t remote_port_; 27 | uint8_t ttl_; 28 | Tins::IP *packet = nullptr; 29 | 30 | public: 31 | const Tins::IPv4Address local_addr() const { return local_addr_; } 32 | const Tins::IPv4Address remote_addr() const { return remote_addr_; } 33 | const uint16_t local_port() const { return local_port_; }; 34 | const uint16_t remote_port() const { return remote_port_; }; 35 | const uint8_t ttl() const { return ttl_; }; 36 | 37 | UDPv4Probe( 38 | Tins::IPv4Address remote_addr, 39 | uint16_t remote_port, 40 | uint16_t local_port, 41 | uint8_t ttl, 42 | Tins::IPv4Address local_addr = 0): 43 | remote_addr_(remote_addr), 44 | remote_port_(remote_port), 45 | local_port_(local_port), 46 | ttl_(ttl), 47 | local_addr_(local_addr) { }; 48 | ~UDPv4Probe(); 49 | Tins::IP* forge(); 50 | Tins::IP& send(); 51 | }; 52 | 53 | #endif /* _UDPV4PROBE_H */ 54 | 55 | -------------------------------------------------------------------------------- /integ/integ_test.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net" 13 | "os" 14 | "os/exec" 15 | "strconv" 16 | "sync/atomic" 17 | "testing" 18 | "time" 19 | 20 | "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/results" 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | // WARNING: this test is meant to run on CI, don't run it on your production 25 | // machines or it will mess up your iptables rules. 26 | 27 | const NfQueueNum int64 = 101 28 | 29 | var ( 30 | one = 1 31 | ) 32 | 33 | var ( 34 | // TODO detect this at start-up 35 | needSudo = false 36 | 37 | defaultDubTrTimeout = 10 * time.Second 38 | goDublinTraceroutePath = "../go/dublintraceroute/cmd/dublin-traceroute/dublin-traceroute" 39 | cppDublinTraceroutePath = "../build/dublin-traceroute" 40 | ) 41 | 42 | func setup() { 43 | cl := []string{"iptables", "-A", "OUTPUT", "-p", "udp", "--dport", "33434:33634", "-d", "8.8.8.8", "-j", "NFQUEUE", "--queue-num", strconv.FormatInt(NfQueueNum, 10)} 44 | if needSudo { 45 | cl = append([]string{"sudo"}, cl...) 46 | } 47 | log.Printf("Running %v", cl) 48 | if err := exec.Command(cl[0], cl[1:]...).Run(); err != nil { 49 | log.Panicf("Failed to run iptables: %v", err) 50 | } 51 | } 52 | 53 | func shutdown() { 54 | // nothing to do here 55 | } 56 | 57 | func TestMain(m *testing.M) { 58 | setup() 59 | code := m.Run() 60 | shutdown() 61 | os.Exit(code) 62 | } 63 | 64 | type testConfig struct { 65 | // timeout for dublin-traceroute 66 | timeout time.Duration 67 | // arguments to routest 68 | configFile string 69 | // arguments to dublin-traceroute 70 | paths *int 71 | minTTL *int 72 | maxTTL *int 73 | srcPort *int 74 | dstPort *int 75 | delay *int 76 | target string 77 | } 78 | 79 | func runWithConfig(useGoImplementation bool, cfg testConfig) ([]byte, []byte, error) { 80 | // validate to config 81 | if cfg.timeout <= 0 { 82 | cfg.timeout = defaultDubTrTimeout 83 | } 84 | 85 | ctx, cancel := context.WithCancel(context.Background()) 86 | defer cancel() 87 | 88 | // run routest 89 | cl := []string{"routest", "-i", "lo", "-c", cfg.configFile, "-q", strconv.FormatInt(NfQueueNum, 10)} 90 | if needSudo { 91 | cl = append([]string{"sudo"}, cl...) 92 | } 93 | log.Printf("Running routest with args %v", cl[1:]) 94 | rCmd := exec.CommandContext(ctx, cl[0], cl[1:]...) 95 | rCmd.Stdout, rCmd.Stderr = os.Stdout, os.Stderr 96 | var routestTerminatedCorrectly int32 97 | var routestPid = -1 98 | defer func() { 99 | if routestPid != -1 { 100 | if err := rCmd.Process.Kill(); err != nil { 101 | log.Panicf("Failed to terminate routest process: %v", err) 102 | } else { 103 | atomic.StoreInt32(&routestTerminatedCorrectly, 1) 104 | } 105 | } else { 106 | atomic.StoreInt32(&routestTerminatedCorrectly, 1) 107 | } 108 | }() 109 | go func() { 110 | err := rCmd.Run() 111 | if err != nil && atomic.LoadInt32(&routestTerminatedCorrectly) != 1 { 112 | log.Panicf("Error returned from command %+v: %v", rCmd, err) 113 | } 114 | routestPid = rCmd.Process.Pid 115 | }() 116 | // wait a second to give routest time to start 117 | // TODO do something better than waiting 118 | time.Sleep(time.Second) 119 | 120 | // run dublin-traceroute 121 | errCh := make(chan error, 1) 122 | 123 | traceFile := "trace.json" 124 | cl = []string{} 125 | if needSudo { 126 | cl = append([]string{"sudo"}, cl...) 127 | } 128 | executable := cppDublinTraceroutePath 129 | if useGoImplementation { 130 | executable = goDublinTraceroutePath 131 | } 132 | cl = append(cl, executable) 133 | if cfg.paths != nil { 134 | cl = append(cl, "-n", strconv.FormatInt(int64(*cfg.paths), 10)) 135 | } 136 | if cfg.minTTL != nil { 137 | cl = append(cl, "-t", strconv.FormatInt(int64(*cfg.minTTL), 10)) 138 | } 139 | if cfg.maxTTL != nil { 140 | cl = append(cl, "-T", strconv.FormatInt(int64(*cfg.maxTTL), 10)) 141 | } 142 | if cfg.srcPort != nil { 143 | cl = append(cl, "-s", strconv.FormatInt(int64(*cfg.srcPort), 10)) 144 | } 145 | if cfg.dstPort != nil { 146 | cl = append(cl, "-d", strconv.FormatInt(int64(*cfg.dstPort), 10)) 147 | } 148 | if cfg.delay != nil { 149 | cl = append(cl, "-D", strconv.FormatInt(int64(*cfg.delay), 10)) 150 | } 151 | cl = append(cl, "-o", traceFile) 152 | if useGoImplementation { 153 | a := net.ParseIP(cfg.target) 154 | if a == nil { 155 | log.Panicf("Invalid IP address: %s", cfg.target) 156 | } 157 | if a.To4() != nil { 158 | cl = append(cl, "--force-ipv4") 159 | } 160 | } 161 | cl = append(cl, cfg.target) 162 | log.Printf("Running %s with args %v", executable, cl[1:]) 163 | dCmd := exec.CommandContext(ctx, cl[0], cl[1:]...) 164 | var outWriter bytes.Buffer 165 | dCmd.Stdout, dCmd.Stderr = &outWriter, os.Stderr 166 | go func() { 167 | errCh <- dCmd.Run() 168 | }() 169 | select { 170 | case err := <-errCh: 171 | if err != nil { 172 | return nil, nil, fmt.Errorf("failed call to dublin-traceroute: %v", err) 173 | } 174 | break 175 | case <-time.After(cfg.timeout): 176 | return nil, nil, fmt.Errorf("dublin-traceroute timed out after %s", cfg.timeout) 177 | } 178 | trace, err := ioutil.ReadFile(traceFile) 179 | if err != nil { 180 | return nil, nil, fmt.Errorf("Cannot read trace file %s: %v", traceFile, err) 181 | } 182 | return outWriter.Bytes(), trace, nil 183 | } 184 | 185 | func requireEqualResults(t *testing.T, got, want *results.Results) { 186 | for wantK, wantV := range want.Flows { 187 | require.Contains(t, got.Flows, wantK) 188 | gotV := got.Flows[wantK] 189 | require.Equal(t, len(wantV), len(gotV)) 190 | for idx := 0; idx < len(wantV); idx++ { 191 | wantReply, gotReply := wantV[idx], gotV[idx] 192 | // skip FlowHash, Name, NatID 193 | require.Equal(t, wantReply.IsLast, gotReply.IsLast) 194 | // accept 20 msec of difference 195 | require.InDelta(t, wantReply.RttUsec, gotReply.RttUsec, 20000.) 196 | 197 | // match Sent packet, ignoring Timestamp, IP.SrcIP 198 | require.NotNil(t, gotReply.Sent, "Sent packet should be not-nil") 199 | require.NotNil(t, gotReply.Sent.IP, "Sent.IP should be not-nil") 200 | require.Equal(t, wantReply.Sent.IP.DstIP, gotReply.Sent.IP.DstIP, "Sent.IP.DstIP does not match") 201 | require.Equal(t, wantReply.Sent.IP.ID, gotReply.Sent.IP.ID, "Sent.IP.ID does not match") 202 | require.Equal(t, wantReply.Sent.IP.TTL, gotReply.Sent.IP.TTL, "Sent.IP.TTL does not match") 203 | require.Equal(t, wantReply.Sent.UDP, gotReply.Sent.UDP, "Sent.UDP does not match") 204 | // sent ICMP 205 | require.Nil(t, gotReply.Sent.ICMP, "Sent.ICMP should be nil") 206 | 207 | // match Received packet, ignoring Timestamp, IP.DstIP, IP.ID, 208 | // received IP must be non-nil 209 | require.NotNil(t, gotReply.Received, "Received should be not-nil") 210 | require.NotNil(t, gotReply.Received.IP, "Received.IP should be not-nil") 211 | require.Equal(t, wantReply.Received.IP.SrcIP, gotReply.Received.IP.SrcIP, "Received.IP.SrcIP does not match") 212 | // received UDP is not guaranteed to be in the response, it is safe 213 | // to skip this check. 214 | // received ICMP 215 | require.NotNil(t, gotReply.Received.ICMP, "Received.ICMP should not be nil") 216 | require.Equal(t, wantReply.Received.ICMP.Code, gotReply.Received.ICMP.Code, "Received.ICMP.Code does not match") 217 | require.Equal(t, wantReply.Received.ICMP.Type, gotReply.Received.ICMP.Type, "Received.ICMP.Type does not match") 218 | // TODO test MPLS extension 219 | 220 | // check for zero-ttl forwarding bug 221 | require.Equal(t, wantReply.ZeroTTLForwardingBug, gotReply.ZeroTTLForwardingBug, "ZeroTTLForwardingBug") 222 | } 223 | } 224 | } 225 | 226 | func testGoogleDNSOnePath(t *testing.T, useGoImplementation bool) { 227 | wantJSON, err := ioutil.ReadFile("test_data/want_8.8.8.8_one_path.json") 228 | require.NoError(t, err) 229 | c := testConfig{ 230 | configFile: "test_data/config_8.8.8.8_one_path.json", 231 | paths: &one, 232 | target: "8.8.8.8", 233 | } 234 | _, gotJSON, err := runWithConfig(useGoImplementation, c) 235 | require.NoError(t, err) 236 | var want, got results.Results 237 | err = json.Unmarshal(wantJSON, &want) 238 | require.NoError(t, err) 239 | err = json.Unmarshal(gotJSON, &got) 240 | require.NoError(t, err) 241 | require.Equal(t, len(want.Flows), len(got.Flows)) 242 | requireEqualResults(t, &got, &want) 243 | } 244 | 245 | func TestGoogleDNSOnePathCPP(t *testing.T) { 246 | testGoogleDNSOnePath(t, false) 247 | } 248 | 249 | func TestGoogleDNSOnePathGo(t *testing.T) { 250 | testGoogleDNSOnePath(t, true) 251 | } 252 | -------------------------------------------------------------------------------- /integ/test_data/config_8.8.8.8_one_path.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "dst": "8.8.8.8", 4 | "dst_port": 33434, 5 | "ttl": 1, 6 | "reply": { 7 | "src": "1.2.3.4", 8 | "icmp_type": 11, 9 | "icmp_code": 0 10 | } 11 | }, 12 | { 13 | "dst": "8.8.8.8", 14 | "dst_port": 33434, 15 | "ttl": 2, 16 | "reply": { 17 | "src": "1.2.3.5", 18 | "icmp_type": 11, 19 | "icmp_code": 0 20 | } 21 | }, 22 | { 23 | "dst": "8.8.8.8", 24 | "dst_port": 33434, 25 | "ttl": 3, 26 | "reply": { 27 | "src": "1.2.3.6", 28 | "icmp_type": 11, 29 | "icmp_code": 0 30 | } 31 | }, 32 | { 33 | "dst": "8.8.8.8", 34 | "dst_port": 33434, 35 | "ttl": 4, 36 | "reply": { 37 | "src": "8.8.8.8", 38 | "icmp_type": 3, 39 | "icmp_code": 3 40 | } 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /integ/test_data/want_8.8.8.8_one_path.json: -------------------------------------------------------------------------------- 1 | { 2 | "flows" : 3 | { 4 | "33434" : 5 | [ 6 | { 7 | "flowhash" : 52120, 8 | "is_last" : false, 9 | "name" : "1.2.3.4", 10 | "nat_id" : 0, 11 | "received" : 12 | { 13 | "icmp" : 14 | { 15 | "code" : 0, 16 | "description" : "TTL expired in transit", 17 | "extensions" : [], 18 | "mpls_labels" : [], 19 | "type" : 11 20 | }, 21 | "ip" : 22 | { 23 | "dst" : "172.16.0.239", 24 | "id" : 14661, 25 | "src" : "1.2.3.4", 26 | "ttl" : 63 27 | }, 28 | "timestamp" : "1565903869.430765" 29 | }, 30 | "rtt_usec" : 10103, 31 | "sent" : 32 | { 33 | "ip" : 34 | { 35 | "dst" : "8.8.8.8", 36 | "src" : "172.16.0.239", 37 | "ttl" : 1 38 | }, 39 | "timestamp" : "1565903869.420662", 40 | "udp" : 41 | { 42 | "dport" : 33434, 43 | "sport" : 12345 44 | } 45 | }, 46 | "zerottl_forwarding_bug" : false 47 | }, 48 | { 49 | "flowhash" : 52120, 50 | "is_last" : false, 51 | "name" : "1.2.3.5", 52 | "nat_id" : 0, 53 | "received" : 54 | { 55 | "icmp" : 56 | { 57 | "code" : 0, 58 | "description" : "TTL expired in transit", 59 | "extensions" : [], 60 | "mpls_labels" : [], 61 | "type" : 11 62 | }, 63 | "ip" : 64 | { 65 | "dst" : "172.16.0.239", 66 | "id" : 57097, 67 | "src" : "1.2.3.5", 68 | "ttl" : 63 69 | }, 70 | "timestamp" : "1565903869.436066" 71 | }, 72 | "rtt_usec" : 3985, 73 | "sent" : 74 | { 75 | "ip" : 76 | { 77 | "dst" : "8.8.8.8", 78 | "src" : "172.16.0.239", 79 | "ttl" : 2 80 | }, 81 | "timestamp" : "1565903869.432081", 82 | "udp" : 83 | { 84 | "dport" : 33434, 85 | "sport" : 12345 86 | } 87 | }, 88 | "zerottl_forwarding_bug" : false 89 | }, 90 | { 91 | "flowhash" : 52120, 92 | "is_last" : false, 93 | "name" : "1.2.3.6", 94 | "nat_id" : 0, 95 | "received" : 96 | { 97 | "icmp" : 98 | { 99 | "code" : 0, 100 | "description" : "TTL expired in transit", 101 | "extensions" : [], 102 | "mpls_labels" : [], 103 | "type" : 11 104 | }, 105 | "ip" : 106 | { 107 | "dst" : "172.16.0.239", 108 | "id" : 6242, 109 | "src" : "1.2.3.6", 110 | "ttl" : 63 111 | }, 112 | "timestamp" : "1565903869.446319" 113 | }, 114 | "rtt_usec" : 3618, 115 | "sent" : 116 | { 117 | "ip" : 118 | { 119 | "dst" : "8.8.8.8", 120 | "src" : "172.16.0.239", 121 | "ttl" : 3 122 | }, 123 | "timestamp" : "1565903869.442701", 124 | "udp" : 125 | { 126 | "dport" : 33434, 127 | "sport" : 12345 128 | } 129 | }, 130 | "zerottl_forwarding_bug" : false 131 | }, 132 | { 133 | "flowhash" : 52120, 134 | "is_last" : true, 135 | "name" : "dns.google", 136 | "nat_id" : 0, 137 | "received" : 138 | { 139 | "icmp" : 140 | { 141 | "code" : 3, 142 | "description" : "Destination port unreachable", 143 | "extensions" : [], 144 | "mpls_labels" : [], 145 | "type" : 3 146 | }, 147 | "ip" : 148 | { 149 | "dst" : "172.16.0.239", 150 | "id" : 61292, 151 | "src" : "8.8.8.8", 152 | "ttl" : 63 153 | }, 154 | "timestamp" : "1565903869.456515" 155 | }, 156 | "rtt_usec" : 3255, 157 | "sent" : 158 | { 159 | "ip" : 160 | { 161 | "dst" : "8.8.8.8", 162 | "src" : "172.16.0.239", 163 | "ttl" : 4 164 | }, 165 | "timestamp" : "1565903869.453260", 166 | "udp" : 167 | { 168 | "dport" : 33434, 169 | "sport" : 12345 170 | } 171 | }, 172 | "zerottl_forwarding_bug" : false 173 | } 174 | ] 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /scripts/to_graphviz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # SPDX-License-Identifier: BSD-2-Clause 4 | 5 | import json 6 | import random 7 | import argparse 8 | 9 | import pygraphviz 10 | 11 | 12 | def parse_args(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument('jsonfile', 15 | type=argparse.FileType('r'), 16 | help='The JSON file generated by dublin-traceroute') 17 | parser.add_argument('--no-rtt', 18 | action='store_true', 19 | default=False, 20 | help='Do not add RTT information to the graph ' 21 | '(default: False)') 22 | parser.add_argument('--generate-dot', 23 | action='store_true', 24 | default=False, 25 | help='Generate the intermediate DOT file') 26 | return parser.parse_args() 27 | 28 | 29 | def json_to_graphviz(traceroute, no_rtt=False): 30 | graph = pygraphviz.AGraph(strict=False, directed=True) 31 | graph.node_attr['shape'] = 'ellipse' 32 | graph.graph_attr['rankdir'] = 'BT' 33 | 34 | # create a dummy first node to add the source host to the graph 35 | # FIXME this approach sucks 36 | for flow, hops in traceroute['flows'].iteritems(): 37 | src_ip = hops[0]['sent']['ip']['src'] 38 | firsthop = {} 39 | hops = [firsthop] + hops 40 | color = random.randrange(0, 0xffffff) 41 | 42 | previous_nat_id = 0 43 | for index, hop in enumerate(hops): 44 | 45 | # add node 46 | if index == 0: 47 | # first hop, the source host 48 | nodename = src_ip 49 | graph.add_node(nodename, shape='rectangle') 50 | else: 51 | # all the other hops 52 | received = hop['received'] 53 | nodeattrs = {} 54 | if received is None: 55 | nodename = 'NULL{idx}'.format(idx=index) 56 | nodeattrs['label'] = '*' 57 | else: 58 | nodename = received['ip']['src'] 59 | if hop['name'] != nodename: 60 | hostname = '\n{h}'.format(h=hop['name']) 61 | else: 62 | hostname = '' 63 | nodeattrs['label'] = '{ip}{name}\n{icmp}'.format( 64 | ip=nodename, 65 | name=hostname, 66 | icmp=received['icmp']['description'] 67 | ) 68 | if index == 0 or hop['is_last']: 69 | nodeattrs['shape'] = 'rectangle' 70 | graph.add_node(nodename) 71 | graph.get_node(nodename).attr.update(nodeattrs) 72 | 73 | # add edge 74 | try: 75 | nexthop = hops[index + 1] 76 | except IndexError: 77 | # This means that we are at the last hop, no further edge 78 | continue 79 | 80 | next_received = nexthop['received'] 81 | edgeattrs = {'color': '#{c:x}'.format(c=color), 'label': ''} 82 | if next_received is None: 83 | next_nodename = 'NULL{idx}'.format(idx=index + 1) 84 | else: 85 | next_nodename = next_received['ip']['src'] 86 | if index == 0: 87 | edgeattrs['label'] = 'dport\n{dp}'.format(dp=flow) 88 | rtt = nexthop['rtt_usec'] 89 | try: 90 | if previous_nat_id != nexthop['nat_id']: 91 | edgeattrs['label'] += '\nNAT detected' 92 | previous_nat_id = hop['nat_id'] 93 | except KeyError: 94 | pass 95 | if not no_rtt: 96 | if rtt is not None: 97 | edgeattrs['label'] += '\n{sec}.{usec} ms'.format( 98 | sec=rtt / 1000, usec=rtt % 1000) 99 | graph.add_edge(nodename, next_nodename) 100 | graph.get_edge(nodename, next_nodename).attr.update(edgeattrs) 101 | 102 | return graph 103 | 104 | 105 | def main(): 106 | args = parse_args() 107 | traceroute = json.load(args.jsonfile) 108 | graph = json_to_graphviz(traceroute, args.no_rtt) 109 | print graph 110 | graph.layout('dot') 111 | 112 | # Save to DOT 113 | if args.generate_dot: 114 | dotfile = '{name}.dot'.format(name=args.jsonfile.name) 115 | graph.write(dotfile) 116 | print('Generated DOT file: {f}'.format(f=dotfile)) 117 | 118 | # Save to PNG 119 | pngfile = '{name}.png'.format(name=args.jsonfile.name) 120 | graph.draw(pngfile) 121 | print('Graph saved to {f}'.format(f=pngfile)) 122 | 123 | if __name__ == '__main__': 124 | main() 125 | -------------------------------------------------------------------------------- /src/common.cc: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file common.cc 5 | * \author Andrea Barberio 6 | * \copyright 2-clause BSD 7 | * \date October 2015 8 | * \brief Common utilities for dublin-traceroute 9 | * 10 | * This file contains the common utilities for dublin-traceroute. 11 | * 12 | * This module currently offers the set-up of the logging facilities and IP ID 13 | * matching algorithm switch. 14 | * 15 | * \sa common.h 16 | */ 17 | 18 | #include "dublintraceroute/common.h" 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/dublin_traceroute.cc: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file dublin_traceroute.cc 5 | * \author Andrea Barberio 6 | * \copyright 2-clause BSD 7 | * \date October 2015 8 | * \brief Main class for the NAT-aware multipath traceroute 9 | * 10 | * This module contains the implementation of the NAT-aware multipath 11 | * traceroute known as dublin-traceroute. 12 | * 13 | * \sa dublin_traceroute.h 14 | */ 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | extern int errno; 27 | 28 | #include 29 | 30 | #include "dublintraceroute/dublin_traceroute.h" 31 | #include "dublintraceroute/hops.h" 32 | #include "dublintraceroute/udpv4probe.h" 33 | 34 | /* 35 | * Dublin Traceroute 36 | * NAT-aware extension of paris-traceroute based on libtins 37 | * 38 | * https://dublin-traceroute.net 39 | * http://paris-traceroute.net 40 | * https://libtins.github.io 41 | */ 42 | 43 | 44 | #define SNIFFER_TIMEOUT_MS 2000 45 | 46 | 47 | Tins::Timestamp extract_timestamp_from_msg(struct msghdr &msg) { 48 | int level, type; 49 | struct cmsghdr *cm; 50 | struct timeval *tvp = NULL, 51 | tv, 52 | now; 53 | // if there's no timestamp in the control message, fall back to 54 | // gettimeofday, and get it early in this function 55 | if (gettimeofday(&now, NULL) == -1) { 56 | std::cerr << strerror(errno) << std::endl; 57 | return Tins::Timestamp(); 58 | } 59 | for (cm = CMSG_FIRSTHDR(&msg); cm != NULL; cm = CMSG_NXTHDR(&msg, cm)) 60 | { 61 | level = cm->cmsg_level; 62 | type = cm->cmsg_type; 63 | if (SOL_SOCKET == level && SO_TIMESTAMP == type) { 64 | tvp = (struct timeval *) CMSG_DATA(cm); 65 | break; 66 | } 67 | } 68 | if (tvp != NULL) { 69 | tv.tv_sec = tvp->tv_sec; 70 | tv.tv_usec = tvp->tv_usec; 71 | return Tins::Timestamp(tv); 72 | } 73 | return Tins::Timestamp(now); 74 | } 75 | 76 | /** \brief Method that validates the arguments passed at the construction 77 | * 78 | * This method checks that the arguments passed at the construction are valid 79 | * and in the expected range. 80 | * 81 | * \sa DublinTraceroute 82 | * 83 | * \return none 84 | */ 85 | const void DublinTraceroute::validate_arguments() { 86 | // it is not necessary to validate srcport, dstport, npaths and 87 | // broken_nat, as they are already constrained by their types. 88 | // Similarly for min_ttl and max_ttl, but the latter must be greater or 89 | // equal than the former. 90 | if (min_ttl_ > max_ttl_) { 91 | throw std::invalid_argument( 92 | "max-ttl must be greater or equal than min-ttl"); 93 | } 94 | if (delay_ > 1000) { 95 | throw std::invalid_argument( 96 | "delay must be between 0 and 1000 milliseconds"); 97 | } 98 | if (type_ <= probe_type::min || type_ >= probe_type::max) { 99 | throw std::invalid_argument( 100 | "invalid probe type"); 101 | } 102 | } 103 | 104 | 105 | /** \brief run the multipath traceroute 106 | * 107 | * This method will execute a multipath traceroute. The way it operates is by 108 | * crafting and sending packets suitable for a multipath traceroute, and 109 | * sniffind the network traffic for the replies. 110 | * 111 | * \sa TracerouteResults 112 | * \returns an instance of TracerouteResults 113 | */ 114 | std::shared_ptr DublinTraceroute::traceroute() { 115 | // avoid running multiple traceroutes 116 | if (mutex_tracerouting.try_lock() == false) 117 | throw DublinTracerouteInProgressException("Traceroute already in progress"); 118 | 119 | validate_arguments(); 120 | 121 | // Resolve the target host 122 | try { 123 | target(Tins::Utils::resolve_domain(dst())); 124 | } catch (std::runtime_error) { 125 | target(Tins::IPv4Address(dst())); 126 | } 127 | 128 | uint16_t num_packets = (max_ttl() - min_ttl() + 1) * npaths(); 129 | std::chrono::steady_clock::time_point deadline = \ 130 | std::chrono::steady_clock::now() + \ 131 | std::chrono::milliseconds(SNIFFER_TIMEOUT_MS) + \ 132 | std::chrono::milliseconds(delay() * num_packets); 133 | // configure the sniffing handler 134 | auto handler = std::bind( 135 | &DublinTraceroute::sniffer_callback, 136 | this, 137 | std::placeholders::_1 138 | ); 139 | 140 | // start the ICMP listener 141 | int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); 142 | if (sock == -1) { 143 | throw std::runtime_error(strerror(errno)); 144 | } 145 | int ts_flag = 1; 146 | int ret; 147 | if ((ret = setsockopt(sock, SOL_SOCKET, SO_TIMESTAMP, (int *)&ts_flag, sizeof(ts_flag))) == -1) { 148 | throw std::runtime_error(strerror(errno)); 149 | } 150 | std::thread listener_thread( 151 | [&]() { 152 | size_t received; 153 | char buf[512]; 154 | struct msghdr msg; 155 | memset(&msg, 0, sizeof(msg)); 156 | struct iovec iov[1]; 157 | iov[0].iov_base = buf; 158 | iov[0].iov_len = sizeof(buf); 159 | msg.msg_iov = iov; 160 | msg.msg_iovlen = sizeof(iov) / sizeof(struct iovec); 161 | struct csmghdr *cmsg; 162 | msg.msg_control = cmsg; 163 | msg.msg_controllen = 0; 164 | while (std::chrono::steady_clock::now() <= deadline) { 165 | received = recvmsg(sock, &msg, MSG_DONTWAIT); 166 | if (received == -1) { 167 | if (errno != EAGAIN && errno != EWOULDBLOCK) { 168 | std::cerr << strerror(errno) << std::endl; 169 | } 170 | } else if (msg.msg_flags & MSG_TRUNC) { 171 | std::cerr << "Warning: received datagram too large for buffer" << std::endl; 172 | } else if (received < 20) { 173 | std::cerr << "Warning: short read, less than 20 bytes" << std::endl; 174 | } else if (buf[0] >> 4 == 4) { 175 | // is it IP version 4? Then enqueue it 176 | // for processing 177 | Tins::IP *ip; 178 | try { 179 | ip = new Tins::IP((const uint8_t *)buf, received); 180 | } catch (Tins::malformed_packet&) { 181 | std::cerr << "Warning: malformed packet" << std::endl; 182 | continue; 183 | } 184 | // Tins::Timestamp is a timeval struct, 185 | // so no monotonic clock anyway.. 186 | auto timestamp = extract_timestamp_from_msg((struct msghdr &)msg); 187 | Tins::Packet packet = Tins::Packet((Tins::PDU *)ip, timestamp); 188 | handler(packet); 189 | delete ip; 190 | } 191 | std::this_thread::sleep_for(std::chrono::milliseconds(5)); 192 | } 193 | close(sock); 194 | } 195 | ); 196 | 197 | std::shared_ptr flows(new flow_map_t); 198 | 199 | uint16_t iterated_port = dstport(); 200 | if(use_srcport_for_path_generation()) iterated_port = srcport(); 201 | uint16_t end_port = iterated_port + npaths(); 202 | 203 | // forge the packets to send 204 | for (iterated_port; iterated_port < end_port; iterated_port++) { 205 | /* Forge the packets to send and append them to the packets 206 | * vector. 207 | * To force a packet through the same network flow, it has to 208 | * maintain several constant fields that will be used for the 209 | * ECMP hashing. These fields are, in the case of IP+UDP: 210 | * 211 | * IPv4.tos 212 | * IPv4.proto 213 | * IPv4.src 214 | * IPv4.dst 215 | * UDP.sport 216 | * UDP.dport 217 | */ 218 | Hops hops; 219 | for (uint8_t ttl = min_ttl_; ttl <= max_ttl_; ttl++) { 220 | /* 221 | * Adjust the payload for each flow to obtain the same UDP 222 | * checksum. The UDP checksum is used to identify the flow. 223 | */ 224 | 225 | UDPv4Probe *probe = NULL; 226 | if(use_srcport_for_path_generation()){ 227 | probe = new UDPv4Probe(target(), dstport(), iterated_port, ttl); 228 | } 229 | else{ 230 | probe = new UDPv4Probe(target(), iterated_port, srcport(), ttl); 231 | //UDPv4Probe probe(target(), dport, srcport(), ttl); 232 | } 233 | Tins::IP *packet; 234 | try { 235 | packet = &probe->send(); 236 | } catch (std::runtime_error &e) { 237 | std::stringstream ss; 238 | ss << "Cannot send packet: " << e.what(); 239 | throw DublinTracerouteException(ss.str()); 240 | } 241 | auto now = Tins::Timestamp::current_time(); 242 | 243 | try { 244 | Hop hop; 245 | hop.sent(*packet); 246 | hop.sent_timestamp(now); 247 | hops.push_back(hop); 248 | } catch (std::runtime_error e) { 249 | std::stringstream ss; 250 | ss << "Cannot find flow: " << iterated_port << ": " << e.what(); 251 | throw DublinTracerouteException(ss.str()); 252 | } 253 | std::this_thread::sleep_for(std::chrono::milliseconds(delay())); 254 | } 255 | flows->insert(std::make_pair(iterated_port, std::make_shared(hops))); 256 | } 257 | 258 | listener_thread.join(); 259 | 260 | TracerouteResults *results = new TracerouteResults(flows, min_ttl_, broken_nat(), use_srcport_for_path_generation()); 261 | 262 | match_sniffed_packets(*results); 263 | if (!no_dns()) { 264 | match_hostnames(*results, flows); 265 | } 266 | 267 | mutex_tracerouting.unlock(); 268 | 269 | return std::make_shared(*results); 270 | } 271 | 272 | 273 | bool DublinTraceroute::sniffer_callback(Tins::Packet &packet) { 274 | std::lock_guard lock(mutex_sniffed_packets); 275 | sniffed_packets.push_back(std::make_shared(packet)); 276 | return true; 277 | } 278 | 279 | 280 | void DublinTraceroute::match_sniffed_packets(TracerouteResults &results) { 281 | for (auto &packet: sniffed_packets) 282 | results.match_packet(*packet); 283 | } 284 | 285 | 286 | void DublinTraceroute::match_hostnames(TracerouteResults &results, std::shared_ptr flows) { 287 | // TODO make this asynchronous 288 | // TODO move this to a proxy method ::resolve() in TracerouteResults 289 | for (auto &iter: *flows) { 290 | auto packets = iter.second; 291 | for (auto &hop: *packets) { 292 | hop.resolve(); 293 | } 294 | } 295 | } 296 | 297 | 298 | -------------------------------------------------------------------------------- /src/hop.cc: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file hop.cc 5 | * \Author Andrea Barberio 6 | * \date October 2015 7 | * \brief Definition of the Hop class 8 | * 9 | * This file contains the definition of the Hop class, which represent every 10 | * single hop in a traceroute. A Hop includes the sent packet, the matching 11 | * received packet (if any), NAT information and last-hop information. 12 | * 13 | * This module currently offers the set-up of the logging facilities. 14 | * 15 | * \sa hop.h 16 | */ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include "dublintraceroute/hop.h" 27 | #include "dublintraceroute/common.h" 28 | #include "dublintraceroute/exceptions.h" 29 | #include "dublintraceroute/icmp_messages.h" 30 | 31 | 32 | /** \brief setter the sent packet 33 | */ 34 | void Hop::sent(Tins::IP &packet) { 35 | sent_ = std::make_shared(packet); 36 | } 37 | 38 | /** \brief setter for the timestamp of the sent packet 39 | */ 40 | void Hop::sent_timestamp(const Tins::Timestamp ×tamp) { 41 | sent_timestamp_ = std::make_shared(timestamp); 42 | } 43 | 44 | /** \brief setter for the host name of the responding IP 45 | */ 46 | void Hop::name(std::string &name) { 47 | name_ = name; 48 | } 49 | 50 | std::string Hop::resolve() { 51 | if (!received()) 52 | return std::string(); 53 | 54 | struct sockaddr_in sa; 55 | memset(&sa, 0, sizeof(sa)); 56 | sa.sin_family = AF_INET; 57 | sa.sin_port = htons(0); 58 | if (inet_pton(AF_INET, received()->src_addr().to_string().c_str(), &sa.sin_addr) != 1) 59 | throw (std::runtime_error("inet_pton failed")); 60 | char host[NI_MAXHOST], service[NI_MAXSERV]; 61 | 62 | std::string name; 63 | if (getnameinfo((struct sockaddr *)&sa, sizeof(sa), host, sizeof(host), service, sizeof(service), NI_NUMERICSERV) == 0) 64 | name = std::string(host); 65 | else 66 | name = received()->src_addr().to_string(); 67 | name_ = name; 68 | 69 | return name; 70 | } 71 | 72 | /** \brief setter for the received packet and its timestamp 73 | */ 74 | void Hop::received(Tins::IP &packet, const Tins::Timestamp ×tamp) { 75 | received_ = std::make_shared(packet); 76 | received_timestamp_ = std::make_shared(timestamp); 77 | } 78 | 79 | /** \brief return the NAT ID of the hop 80 | * 81 | * This method returns the NAT identifier for this hop. The NAT identifier is 82 | * calculated as the difference between the checksum of the inner UDP layer of 83 | * the received packet and the checksum of the sent UDP packet. 84 | */ 85 | uint16_t Hop::nat_id() { 86 | 87 | if (!received()) { 88 | throw DublinTracerouteException( 89 | "Cannot get NAT ID for unmatched packets" 90 | ); 91 | } 92 | // FIXME catch pdu_not_found 93 | uint16_t chk1 = sent_->rfind_pdu().checksum(); 94 | Tins::IP inner_ip = received_->rfind_pdu().to(); 95 | uint16_t chk2 = inner_ip.rfind_pdu().checksum(); 96 | return chk2 - chk1; 97 | } 98 | 99 | 100 | /** \brief return true if the hop suffers from the zero-ttl forwarding bug 101 | * 102 | * This method checks that the hop does not suffer of the zero-ttl forwarding 103 | * bug, and returns true or false accordingly. This is done by checking the TTL 104 | * of the IP-in-ICMP response, i.e. the IP header that was received by the 105 | * hop. 106 | */ 107 | const bool Hop::zerottl_forwarding_bug() { 108 | 109 | if (!received()) { 110 | throw DublinTracerouteException( 111 | "Cannot get zero-TTL forwarding information for unmatched packets" 112 | ); 113 | } 114 | // FIXME catch pdu_not_found 115 | uint16_t chk1 = sent_->rfind_pdu().checksum(); 116 | Tins::IP inner_ip = received_->rfind_pdu().to(); 117 | if (inner_ip.ttl() == 0) { 118 | // TODO handle the interesting case where TTL is neither 0 or 1 119 | return true; 120 | } 121 | return false; 122 | } 123 | 124 | /** \brief return the RTT in microseconds 125 | * 126 | * This method returns the Round-Trip Time in microseconds, if a matching packet 127 | * was received, 0 otherwise. 128 | */ 129 | unsigned int Hop::rtt() { 130 | if (received()) { 131 | unsigned long long ts1 = sent_timestamp()->seconds() * 1000000 + sent_timestamp()->microseconds(); 132 | unsigned long long ts2 = received_timestamp()->seconds() * 1000000 + received_timestamp()->microseconds(); 133 | return ts2 - ts1; 134 | } else { 135 | return 0; 136 | } 137 | } 138 | 139 | /** \brief Method that computes the flow hash of a given packet 140 | * 141 | * This method computes the flow hash of a packet and returns it. Returns 0 if 142 | * no flow hash cannot be computed (e.g. it is not an IP/UDP packet). 143 | * This number is only useful for comparison purposes, and does not necessarily 144 | * match any network device's micro-flow hash implementation. 145 | * 146 | * Two packets with the same flow hash will traverse the same path, so you can 147 | * use this method to compare them. 148 | * 149 | * \sa Hop 150 | * 151 | * \return the packet's flow hash 152 | */ 153 | const uint16_t Hop::flowhash() { 154 | uint16_t flowhash = 0; 155 | Tins::IP ip; 156 | try { 157 | ip = (*sent()).rfind_pdu(); 158 | } catch (Tins::pdu_not_found) { 159 | return 0; 160 | } 161 | flowhash += ip.tos() + ip.protocol(); 162 | flowhash += (uint32_t)(ip.src_addr()); 163 | flowhash += (uint32_t)(ip.dst_addr()); 164 | Tins::UDP udp; 165 | try { 166 | udp = (*sent()).rfind_pdu(); 167 | } catch (Tins::pdu_not_found) { 168 | return 0; 169 | } 170 | flowhash += udp.sport() + udp.dport(); 171 | if (flowhash == 0) 172 | flowhash = 0xffff; 173 | return flowhash; 174 | } 175 | 176 | /** \brief Convert the hop to JSON 177 | * 178 | * This method converts the hop data to a JSON representation. The 179 | * representation is lossy and cannot be used to rebuild the original packet. 180 | */ 181 | Json::Value Hop::to_json() { 182 | icmpmessages icmpm; 183 | Json::Value root; 184 | Json::Value nullvalue; 185 | 186 | // Serialize the sent packet 187 | root["is_last"] = is_last_hop(); 188 | if (received()) 189 | root["zerottl_forwarding_bug"] = zerottl_forwarding_bug(); 190 | root["sent"]["timestamp"] = std::to_string(sent_timestamp()->seconds()) + "." + std::to_string(sent_timestamp()->microseconds()); 191 | 192 | // flow hash 193 | root["flowhash"] = flowhash(); 194 | 195 | // IP layer 196 | root["sent"]["ip"]["src"] = sent()->src_addr().to_string(); 197 | root["sent"]["ip"]["dst"] = sent()->dst_addr().to_string(); 198 | root["sent"]["ip"]["ttl"] = sent()->ttl(); 199 | 200 | // UDP layer 201 | try { 202 | auto udp = sent()->rfind_pdu(); 203 | root["sent"]["udp"]["sport"] = udp.sport(); 204 | root["sent"]["udp"]["dport"] = udp.dport(); 205 | } catch (Tins::pdu_not_found) { 206 | } 207 | 208 | // If present, serialize the received packet 209 | if (received()) { 210 | root["rtt_usec"] = rtt(); 211 | root["received"]["timestamp"] = std::to_string(received_timestamp()->seconds()) + "." + std::to_string(received_timestamp()->microseconds()); 212 | 213 | // IP layer 214 | root["received"]["ip"]["src"] = received()->src_addr().to_string(); 215 | root["received"]["ip"]["dst"] = received()->dst_addr().to_string(); 216 | root["received"]["ip"]["ttl"] = received()->ttl(); 217 | root["received"]["ip"]["id"] = received()->id(); 218 | 219 | // ICMP layer 220 | try { 221 | auto icmp = received()->rfind_pdu(); 222 | root["received"]["icmp"]["type"] = static_cast(icmp.type()); 223 | root["received"]["icmp"]["code"] = static_cast(icmp.code()); 224 | root["received"]["icmp"]["description"] = icmpm.get(icmp.type(), icmp.code()); 225 | root["received"]["icmp"]["extensions"] = Json::Value(Json::arrayValue); 226 | root["received"]["icmp"]["mpls_labels"] = Json::Value(Json::arrayValue); 227 | if (icmp.has_extensions()) { 228 | for (auto &extension : icmp.extensions().extensions()) { 229 | Json::Value ext_node = Json::Value(); 230 | unsigned int size = static_cast(extension.size()); 231 | unsigned int ext_class = static_cast(extension.extension_class()); 232 | unsigned int ext_type = static_cast(extension.extension_type()); 233 | auto &payload = extension.payload(); 234 | // hex-encoding every byte so the JSON file doesn't contain binary sequences 235 | // I could have used base64 or other more efficient encoding, but this is simple and requires no deps 236 | std::stringstream payload_hex; 237 | for (auto &ch : payload) { 238 | payload_hex << std::setfill('0') << std::setw(2) << std::hex << static_cast(ch); 239 | } 240 | ext_node["size"] = size; // 16 bits 241 | ext_node["class"] = ext_class; // 8 bits 242 | ext_node["type"] = ext_type; // 8 bits 243 | ext_node["payload"] = payload_hex.str(); 244 | root["received"]["icmp"]["extensions"].append(ext_node); 245 | 246 | // if MPLS was encountered, also add parsed extension 247 | if (ext_class == ICMP_EXTENSION_MPLS_CLASS && ext_type == ICMP_EXTENSION_MPLS_TYPE) { 248 | // FIXME here I am assuming that size is always a multiple of 4 249 | for (unsigned int idx = 0; idx < payload.size(); idx += 4) { 250 | unsigned int label = (payload[idx] << 12) + (payload[idx + 1] << 4) + (payload[idx + 2] >> 4); 251 | unsigned int experimental = (payload[idx + 2] & 0x0f) >> 1; 252 | unsigned int bottom_of_stack = payload[idx + 2] & 0x01; 253 | unsigned int ttl = payload[idx + 3]; 254 | Json::Value mpls_node = Json::Value(); 255 | mpls_node["label"] = label; 256 | mpls_node["experimental"] = experimental; 257 | mpls_node["bottom_of_stack"] = bottom_of_stack; 258 | mpls_node["ttl"] = ttl; 259 | root["received"]["icmp"]["mpls_labels"].append(mpls_node); 260 | } 261 | } 262 | } 263 | } 264 | } catch (Tins::pdu_not_found) { 265 | } 266 | } else { 267 | root["received"] = nullvalue; 268 | root["rtt_usec"] = nullvalue; 269 | } 270 | 271 | // set the DNS name 272 | root["name"] = name(); 273 | 274 | try { 275 | root["nat_id"] = nat_id(); 276 | } catch (DublinTracerouteException) { 277 | } 278 | 279 | return root; 280 | } 281 | 282 | 283 | std::string Hop::summary() { 284 | std::stringstream stream; 285 | if (sent() == nullptr) { 286 | return std::string(""); 287 | } 288 | Tins::IP ip; 289 | try { 290 | ip = sent()->rfind_pdu(); 291 | } catch (Tins::pdu_not_found) { 292 | return std::string(""); 293 | } 294 | Tins::UDP udp; 295 | try { 296 | udp = sent()->rfind_pdu(); 297 | } catch (Tins::pdu_not_found) { 298 | return std::string(""); 299 | } 300 | stream 301 | << "UDP " 302 | << ip.src_addr() << ":" << udp.sport() 303 | << " -> " 304 | << ip.dst_addr() << ":" << udp.dport() 305 | << " TTL: " << static_cast(ip.ttl()) 306 | << ", Flow hash: " << flowhash(); 307 | return stream.str(); 308 | } 309 | 310 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file main.cc 5 | * \Author Andrea Barberio 6 | * \date October 2015 7 | * \brief entry point for dublin-traceroute 8 | * 9 | * This file contains the main routine for calling the standalone dublin-traceroute 10 | * executable. 11 | */ 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | const char *shortopts = "hvs:d:n:t:T:D:biNo:"; 22 | const struct option longopts[] = { 23 | {"help", no_argument, NULL, 'h'}, 24 | {"version", no_argument, NULL, 'v'}, 25 | {"sport", required_argument, NULL, 's'}, 26 | {"dport", required_argument, NULL, 'd'}, 27 | {"npaths", required_argument, NULL, 'n'}, 28 | {"min-ttl", required_argument, NULL, 't'}, 29 | {"max-ttl", required_argument, NULL, 'T'}, 30 | {"delay", required_argument, NULL, 'D'}, 31 | {"broken-nat", no_argument, NULL, 'b'}, 32 | {"use-srcport", no_argument, NULL, 'i'}, 33 | {"no-dns", no_argument, NULL, 'N'}, 34 | {"output-file", required_argument, NULL, 'o'}, 35 | {NULL, 0, NULL, 0}, 36 | }; 37 | 38 | static void usage() { 39 | std::cerr << 40 | "Dublin Traceroute v" VERSION "\n" 41 | R"(Written by Andrea Barberio - https://insomniac.slackware.it 42 | 43 | Usage: 44 | )" PROGNAME R"( [--sport=src_base_port] 45 | [--dport=dest_base_port] 46 | [--npaths=num_paths] 47 | [--min-ttl=min_ttl] 48 | [--max-ttl=max_ttl] 49 | [--delay=delay_in_ms] 50 | [--broken-nat] 51 | [--use-srcport] 52 | [--no-dns] 53 | [--output-file=file_name] 54 | [--help] 55 | [--version] 56 | 57 | Options: 58 | -h --help this help 59 | -v --version print the version of Dublin Traceroute 60 | -s SRC_PORT --sport=SRC_PORT the source port to send packets from (default: )" << DublinTraceroute::default_srcport << R"() 61 | -d DST_PORT --dport=DST_PORT the base destination port to send packets to (default: )" << DublinTraceroute::default_dstport << R"() 62 | -n NPATHS --npaths=NPATHS the number of paths to probe (default: )" << static_cast(DublinTraceroute::default_npaths) << R"() 63 | -t MIN_TTL --min-ttl=MIN_TTL the minimum TTL to probe (default: )" << static_cast(DublinTraceroute::default_min_ttl) << R"() 64 | -T MAX_TTL --max-ttl=MAX_TTL the maximum TTL to probe. Must be greater or equal than the minimum TTL (default: )" << static_cast(DublinTraceroute::default_max_ttl) << R"() 65 | -D DELAY --delay=DELAY the inter-packet delay in milliseconds (default: )" << DublinTraceroute::default_delay << R"() 66 | -b --broken-nat the network has a broken NAT configuration (e.g. no payload fixup). Try this if you see fewer hops than expected 67 | -i --use-srcport generate paths using source port instead of destination port 68 | -N --no-dns do not attempt to do reverse DNS lookup of the hops 69 | -o --output-file the output file name (default: stdout) 70 | 71 | 72 | See documentation at https://dublin-traceroute.net 73 | Please report bugs at https://github.com/insomniacslk/dublin-traceroute 74 | Additional features in the Python module at https://github.com/insomniacslk/python-dublin-traceroute 75 | )"; 76 | } 77 | 78 | 79 | int 80 | main(int argc, char **argv) { 81 | std::string target; 82 | probe_type type = DublinTraceroute::default_type; 83 | long sport = DublinTraceroute::default_srcport; 84 | long dport = DublinTraceroute::default_dstport; 85 | long npaths = DublinTraceroute::default_npaths; 86 | long min_ttl = DublinTraceroute::default_min_ttl; 87 | long max_ttl = DublinTraceroute::default_max_ttl; 88 | long delay = DublinTraceroute::default_delay; 89 | bool broken_nat = DublinTraceroute::default_broken_nat; 90 | bool use_srcport_for_path_generation = DublinTraceroute::default_use_srcport_for_path_generation; 91 | bool no_dns = DublinTraceroute::default_no_dns; 92 | std::string output_file = ""; 93 | 94 | if (geteuid() == 0) { 95 | std::cerr 96 | << "WARNING: you are running this program as root. Consider setting the CAP_NET_RAW " << std::endl 97 | << " capability and running as non-root user as a more secure alternative." << std::endl; 98 | } 99 | 100 | int index, 101 | iarg = 0; 102 | 103 | #define TO_LONG(name, value) { \ 104 | try { \ 105 | name = std::stol(value); \ 106 | } catch (std::invalid_argument) { \ 107 | std::cerr << "Invalid argument. See --help" << std::endl; \ 108 | std::exit(EXIT_FAILURE); \ 109 | } \ 110 | } 111 | while ((iarg = getopt_long(argc, argv, shortopts, longopts, &index)) != -1) { 112 | switch (iarg) { 113 | case 'h': 114 | usage(); 115 | std::exit(EXIT_SUCCESS); 116 | case 'v': 117 | std::cerr << "Dublin Traceroute " << VERSION << std::endl; 118 | std::exit(EXIT_SUCCESS); 119 | case 's': 120 | TO_LONG(sport, optarg); 121 | break; 122 | case 'd': 123 | TO_LONG(dport, optarg); 124 | break; 125 | case 'n': 126 | TO_LONG(npaths, optarg); 127 | break; 128 | case 't': 129 | TO_LONG(min_ttl, optarg); 130 | break; 131 | case 'T': 132 | TO_LONG(max_ttl, optarg); 133 | break; 134 | case 'D': 135 | TO_LONG(delay, optarg); 136 | break; 137 | case 'b': 138 | broken_nat = true; 139 | break; 140 | case 'i': 141 | use_srcport_for_path_generation = true; 142 | break; 143 | case 'N': 144 | no_dns = true; 145 | break; 146 | case 'o': 147 | output_file.assign(optarg); 148 | break; 149 | default: 150 | std::cerr << "Invalid argument: " << iarg << ". See --help" << std::endl; 151 | std::exit(EXIT_FAILURE); 152 | } 153 | } 154 | #undef TO_LONG 155 | if (optind == argc) { 156 | std::cerr << "Target is required. See --help" << std::endl; 157 | std::exit(EXIT_FAILURE); 158 | } 159 | if (optind + 1 < argc) { 160 | std::cerr << "Exactly one target is required. See --help" << std::endl; 161 | std::exit(EXIT_FAILURE); 162 | } 163 | target = argv[optind]; 164 | 165 | if (sport < 1 || sport > 65535) { 166 | std::cerr << "Source port must be between 1 and 65535" << std::endl; 167 | std::exit(EXIT_FAILURE); 168 | } 169 | 170 | if (dport < 1 || dport > 65535) { 171 | std::cerr << "Destination port must be between 1 and 65535" << std::endl; 172 | std::exit(EXIT_FAILURE); 173 | } 174 | if (npaths < 1 || npaths > 65535) { 175 | std::cerr << "Number of paths must be between 1 and 65535" << std::endl; 176 | std::exit(EXIT_FAILURE); 177 | } 178 | if (min_ttl < 1 || min_ttl > 255) { 179 | std::cerr << "Min TTL must be between 1 and 255" << std::endl; 180 | std::exit(EXIT_FAILURE); 181 | } 182 | if (max_ttl < 1 || max_ttl > 255) { 183 | std::cerr << "Max TTL must be between 1 and 255" << std::endl; 184 | std::exit(EXIT_FAILURE); 185 | } 186 | if (min_ttl > max_ttl) { 187 | std::cerr << "Min TTL must be smaller or equal than max TTL" << std::endl; 188 | std::exit(EXIT_FAILURE); 189 | } 190 | if (use_srcport_for_path_generation) { 191 | if (sport + npaths - 1 > 65535) { 192 | std::cerr << "Source port + number of paths must not exceed 65535" << std::endl; 193 | std::exit(EXIT_FAILURE); 194 | } 195 | } else { 196 | if (dport + npaths - 1 > 65535) { 197 | std::cerr << "Destination port + number of paths must not exceed 65535" << std::endl; 198 | std::exit(EXIT_FAILURE); 199 | } 200 | } 201 | if (delay < 0 || delay > 1000) { 202 | std::cerr << "The inter-packet delay must be a number between 0 and 1000 milliseconds" << std::endl; 203 | std::exit(EXIT_FAILURE); 204 | } 205 | 206 | std::cerr << "Starting " PROGNAME " (probe type: " << probe_type_name(type) << ")" << std::endl; 207 | 208 | DublinTraceroute Dublin( 209 | target, 210 | type, 211 | sport, 212 | dport, 213 | npaths, 214 | min_ttl, 215 | max_ttl, 216 | delay, 217 | broken_nat, 218 | use_srcport_for_path_generation, 219 | no_dns 220 | ); 221 | 222 | std::cerr << "Traceroute from 0.0.0.0:" << Dublin.srcport(); 223 | if(use_srcport_for_path_generation == 1){ 224 | std::cerr << "~" << (Dublin.srcport() + npaths - 1); 225 | } 226 | 227 | std::cerr << " to " << Dublin.dst() << ":" << Dublin.dstport(); 228 | if(use_srcport_for_path_generation == 0){ 229 | std::cerr << "~" << (Dublin.dstport() + npaths - 1); 230 | } 231 | 232 | std::cerr << " (probing " << npaths << " path" << (npaths == 1 ? "" : "s") 233 | << ", min TTL is " << min_ttl 234 | << ", max TTL is " << max_ttl 235 | << ", delay is " << delay << " ms" 236 | << ")" 237 | << std::endl; 238 | 239 | std::shared_ptr results; 240 | try { 241 | results = Dublin.traceroute(); 242 | } catch (DublinTracerouteException &e) { 243 | std::cerr << "Failed: " << e.what() << std::endl; 244 | std::exit(EXIT_FAILURE); 245 | } catch (std::runtime_error &e) { 246 | std::cerr << "Failed: " << e.what() << std::endl; 247 | std::exit(EXIT_FAILURE); 248 | } 249 | 250 | results->show(); 251 | 252 | // Save as JSON 253 | std::streambuf *buf; 254 | std::ofstream of; 255 | if (output_file == "") { 256 | buf = std::cout.rdbuf(); 257 | } else { 258 | of.open(output_file); 259 | buf = of.rdbuf(); 260 | } 261 | std::ostream jsonfile(buf); 262 | jsonfile << results->to_json() << std::endl; 263 | if (output_file != "") { 264 | std::cerr << "Saved JSON file to " << output_file << " ." << std::endl; 265 | std::cerr << "You can convert it to DOT by running python3 -m dublintraceroute plot " << output_file << std::endl; 266 | } 267 | 268 | std::exit(EXIT_SUCCESS); 269 | } 270 | 271 | -------------------------------------------------------------------------------- /src/traceroute_results.cc: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file traceroute_results.cc 5 | * \author Andrea Barberio 6 | * \copyright 2-clause BSD 7 | * \date October 2015 8 | * \brief Traceroute results class for dublin-traceroute 9 | * 10 | * This file contains the Traceroute results class for dublin-traceroute. 11 | * 12 | * This class is a container for a per-flow hops representation, and offers 13 | * facilities to print the traceroute output and convert it to a JSON 14 | * representation. 15 | * 16 | * \sa traceroute_results.h 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #include "dublintraceroute/traceroute_results.h" 23 | #include "dublintraceroute/icmp_messages.h" 24 | 25 | 26 | TracerouteResults::TracerouteResults(std::shared_ptr flows, const uint8_t min_ttl = 1, const bool broken_nat = true, const bool use_srcport_for_path_generation = true): 27 | flows_(flows), min_ttl(min_ttl), compressed_(false), broken_nat_(broken_nat), use_srcport_for_path_generation_(use_srcport_for_path_generation) { 28 | } 29 | 30 | 31 | std::shared_ptr TracerouteResults::match_packet(const Tins::Packet &packet) { 32 | // Is this an IP packet? 33 | Tins::IP ip; 34 | try { 35 | ip = packet.pdu()->rfind_pdu(); 36 | } catch (Tins::pdu_not_found) { 37 | return nullptr; 38 | } 39 | 40 | // Does it contain an ICMP response? 41 | Tins::ICMP icmp; 42 | try { 43 | icmp = ip.rfind_pdu(); 44 | } catch (Tins::pdu_not_found) { 45 | return nullptr; 46 | } 47 | 48 | // does the ICMP contain an inner IP packet sent by us? 49 | Tins::IP inner_ip; 50 | try { 51 | inner_ip = icmp.rfind_pdu().to(); 52 | } catch (Tins::pdu_not_found) { 53 | return nullptr; 54 | } catch (Tins::malformed_packet) { 55 | return nullptr; 56 | } 57 | 58 | // does the inner packet contain our original UDP packet? 59 | Tins::UDP inner_udp; 60 | try { 61 | inner_udp = inner_ip.rfind_pdu(); 62 | } catch (Tins::pdu_not_found) { 63 | return nullptr; 64 | } catch (Tins::malformed_packet) { 65 | return nullptr; 66 | } 67 | 68 | // Try to match the received packet against the sent packets. The flow 69 | // is identified by the UDP destination port in the case of use_srcport_for_path_generation is set to false 70 | // if use_srcport_for_path_generation is set to true the source port is used to identify the flow 71 | auto flow_id = inner_udp.dport(); 72 | if(use_srcport_for_path_generation_){ 73 | flow_id = inner_udp.sport(); 74 | } 75 | 76 | std::shared_ptr hops; 77 | try { 78 | hops = flows().at(flow_id); 79 | } catch (std::out_of_range) { 80 | return nullptr; 81 | } 82 | 83 | unsigned int index = 0; 84 | for (auto &hop: *hops) { 85 | // FIXME catch Tins::pdu_not_found 86 | auto &sent = hop.sent()->rfind_pdu(); 87 | if (!broken_nat_) { 88 | if (sent.src_addr() != inner_ip.src_addr()) 89 | continue; 90 | } 91 | // FIXME catch Tins::pdu_not_found 92 | auto &udp = hop.sent()->rfind_pdu(); 93 | /* 94 | * The original paris-traceroute would match the checksum, but 95 | * this does not work when there is NAT translation. Using the 96 | * IP ID to match the packets works through NAT rewriting 97 | * instead, and only requires the inner IP layer, which is 98 | * guaranteed to return entirely in ICMP ttl-exceeded responses. 99 | * 100 | * To use the paris-traceroute approach, undefine 101 | * USE_IP_ID_MATCHING in common.h . 102 | */ 103 | #ifdef USE_IP_ID_MATCHING 104 | if (udp.checksum() == inner_ip.id()) { 105 | #else /* USE_IP_ID_MATCHING */ 106 | if (udp.checksum() == inner_udp.checksum()) { 107 | #endif /* USE_IP_ID_MATCHING */ 108 | try { 109 | hop.received(ip, packet.timestamp()); 110 | return std::make_shared(ip); 111 | } catch (std::out_of_range) { 112 | // this should never happen 113 | throw; 114 | } 115 | } 116 | index++; 117 | } 118 | return nullptr; 119 | } 120 | 121 | void TracerouteResults::show(std::ostream &stream) { 122 | compress(); 123 | icmpmessages icmpm; 124 | 125 | for (auto &iter: flows()) { 126 | unsigned int hopnum = min_ttl; 127 | unsigned int index = 0; 128 | uint16_t prev_nat_id = 0; 129 | stream << "== Flow ID " << iter.first << " ==" << std::endl; 130 | for (auto &hop: *iter.second) { 131 | stream << hopnum << " "; 132 | if (!hop) { 133 | stream << "*" << std::endl; 134 | } else { 135 | // print the IP address of the hop 136 | stream << hop.received()->src_addr(); 137 | if (hop.name() != "") { 138 | stream << " (" << hop.name() << ")"; 139 | } 140 | 141 | // print the response IP ID, useful to detect 142 | // loops due to NATs, fake hops, etc 143 | stream << ", IP ID: " << hop.received()->id(); 144 | 145 | // print the RTT 146 | std::stringstream rttss; 147 | rttss << (hop.rtt() / 1000) << "." << (hop.rtt() % 1000) << " ms "; 148 | stream << " RTT " << rttss.str(); 149 | 150 | // print the ICMP type and code 151 | Tins::ICMP icmp; 152 | try { 153 | icmp = hop.received()->rfind_pdu(); 154 | stream << " ICMP " 155 | << "(type=" << icmp.type() << ", code=" << static_cast(icmp.code()) << ") '" 156 | << icmpm.get(icmp.type(), icmp.code()) << "'"; 157 | if (icmp.has_extensions()) { 158 | for (auto &extension : icmp.extensions().extensions()) { 159 | unsigned int ext_class = static_cast(extension.extension_class()); 160 | unsigned int ext_type = static_cast(extension.extension_type()); 161 | auto &payload = extension.payload(); 162 | if (ext_class == ICMP_EXTENSION_MPLS_CLASS && ext_type == ICMP_EXTENSION_MPLS_TYPE) { 163 | // expecting size to be a multiple of 4 in valid MPLS label stacks 164 | unsigned int num_labels = (extension.size() - 4) / 4; 165 | for (unsigned int idx = 0; idx < payload.size() ; idx += 4) { 166 | unsigned int label = (payload[idx + 0] << 12) + (payload[idx + 1] << 4) + (payload[idx + 2] >> 4); 167 | unsigned int experimental = (payload[idx + 2] & 0x0f) >> 1; 168 | unsigned int bottom_of_stack = payload[idx + 2] & 0x01; 169 | unsigned int ttl = payload[idx + 3]; 170 | stream << ", MPLS(label=" << label << ", experimental=" << experimental << ", bottom_of_stack=" << bottom_of_stack << ", ttl=" << ttl << ")"; 171 | } 172 | } else { 173 | stream 174 | << ", Extension(" 175 | << "class=" << ext_class 176 | << ", type=" << ext_type 177 | << ", payload_size=" << payload.size() 178 | << ")"; 179 | } 180 | } 181 | } 182 | } catch (Tins::pdu_not_found) { 183 | } 184 | 185 | /* NAT detection. 186 | * Note that if the previous hop was not 187 | * responding, the detected NAT could have been 188 | * started before 189 | */ 190 | auto inner_ip = hop.received()->rfind_pdu().to(); 191 | stream << ", NAT ID: " << hop.nat_id(); 192 | if (hopnum > 1 && hop.nat_id() != prev_nat_id) 193 | stream << " (NAT detected)"; 194 | prev_nat_id = hop.nat_id(); 195 | 196 | // Show the flow hash for the sent packet 197 | stream << ", flow hash: " << hop.flowhash(); 198 | 199 | stream << std::endl; 200 | 201 | } 202 | // Break if we reached the target hop 203 | if (hop.is_last_hop()) 204 | break; 205 | hopnum++; 206 | index++; 207 | } 208 | } 209 | } 210 | 211 | void TracerouteResults::compress() { 212 | /** \brief compress the traceroute graph 213 | * 214 | * Compress the traceroute graph in order to remove repetitions of 215 | * non-responding hops (i.e. the ones that show a "*" in a traceroute). 216 | * 217 | * Implementation note: this is not actually a compression, since the 218 | * traceroute is not implemented (yet) as a graph, but this will come in 219 | * a future release. Currently this method simply marks the first 220 | * non-responding hop at the end of a path as last-hop. 221 | * 222 | * It is safe to call this method multiple times, and there is no 223 | * performance penalty. 224 | */ 225 | if (compressed_) 226 | return; 227 | for (auto &iter: flows()) { 228 | Tins::IPv4Address target = iter.second->at(0).sent()->dst_addr(); 229 | for (auto hop = iter.second->rbegin(); hop != iter.second->rend(); hop++) { 230 | // TODO also check for ICMP type==3 and code==3 231 | if (hop->received()) { 232 | if (hop->received()->src_addr() != target) 233 | break; 234 | } 235 | hop->is_last_hop(true); 236 | } 237 | } 238 | compressed_ = true; 239 | } 240 | 241 | std::string TracerouteResults::to_json() { 242 | compress(); 243 | std::stringstream json; 244 | Json::Value root; 245 | 246 | for (auto &iter: flows()) { 247 | auto flow_id = std::to_string(iter.first); 248 | Json::Value hops(Json::arrayValue); 249 | for (auto &hop: *iter.second) { 250 | hops.append(hop.to_json()); 251 | if (hop.is_last_hop()) 252 | break; 253 | } 254 | root["flows"][flow_id] = hops; 255 | } 256 | 257 | json << root; 258 | return json.str(); 259 | } 260 | -------------------------------------------------------------------------------- /src/udpv4probe.cc: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | /** 4 | * \file udpv4probe.cc 5 | * \Author Andrea Barberio 6 | * \date 2017 7 | * \brief Definition of the UDPv4Probe class 8 | * 9 | * This file contains the definition of the UDPv4Probe class, which represents 10 | * an UDP probe that will be sent over IPv4. 11 | * 12 | * \sa udpv4probe.h 13 | */ 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "dublintraceroute/udpv4probe.h" 24 | #include "dublintraceroute/common.h" 25 | #include "dublintraceroute/exceptions.h" 26 | #include "dublintraceroute/icmp_messages.h" 27 | 28 | 29 | /** \brief method that sends the probe to the specified destination 30 | */ 31 | Tins::IP* UDPv4Probe::forge() { 32 | /* The payload is used to manipulate the UDP checksum, that will be 33 | * used as hop identifier. 34 | * The last two bytes will be adjusted to influence the hop identifier, 35 | * which for UDP traceroutes is the UDP checksum. 36 | */ 37 | unsigned char payload[] = {'N', 'S', 'M', 'N', 'C', 0x00, 0x00}; 38 | 39 | /* The identifier is used to identify and match a response packet to 40 | * the corresponding sent packet 41 | */ 42 | uint16_t identifier = remote_port_ + ttl_; 43 | 44 | payload[5] = ((unsigned char *)&identifier)[0]; 45 | payload[6] = ((unsigned char *)&identifier)[1]; 46 | Tins::IP *packet = new Tins::IP(remote_addr_, local_addr_) / 47 | Tins::UDP(remote_port_, local_port_) / 48 | Tins::RawPDU((char *)payload); 49 | packet->ttl(ttl_); 50 | packet->flags(Tins::IP::DONT_FRAGMENT); 51 | 52 | // serialize the packet so we can extract source IP and checksum 53 | packet->serialize(); 54 | 55 | packet->id(packet->rfind_pdu().checksum()); 56 | return packet; 57 | } 58 | 59 | Tins::IP &UDPv4Probe::send() { 60 | Tins::NetworkInterface iface = Tins::NetworkInterface::default_interface(); 61 | Tins::PacketSender sender; 62 | if (packet == nullptr) { 63 | packet = forge(); 64 | } 65 | sender.send(*packet, iface.name()); 66 | return *packet; 67 | } 68 | 69 | UDPv4Probe::~UDPv4Probe() { 70 | if (packet != nullptr) 71 | delete packet; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | INCLUDE_DIRECTORIES(${gtest_INCLUDE_DIRS}) 4 | ADD_SUBDIRECTORY(src) 5 | -------------------------------------------------------------------------------- /tests/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | include(GoogleTest) 4 | enable_testing() 5 | 6 | # This file is shamelessly copied and modified from libtins. 7 | # Use dublintraceroute's include directories + test include directories 8 | INCLUDE_DIRECTORIES( 9 | ${PROJECT_SOURCE_DIR}/include/dublintraceroute/ 10 | ${GOOGLETEST_INCLUDE} 11 | ) 12 | 13 | # Find pthread library 14 | FIND_PACKAGE(Threads REQUIRED) 15 | 16 | LINK_DIRECTORIES( 17 | ${GOOGLETEST_LIBRARY} 18 | ) 19 | # Link against GoogleTest, libdublintraceroute and pthread. 20 | # Pthread is required by GoogleTest 21 | LINK_LIBRARIES( 22 | gtest 23 | gtest_main 24 | dublintraceroute 25 | ${CMAKE_THREAD_LIBS_INIT} 26 | ) 27 | 28 | # Add tests target 29 | ADD_CUSTOM_TARGET( 30 | tests DEPENDS 31 | UDPv4Test 32 | HopTest 33 | HopsTest 34 | ${OPTIONAL_TEST_TARGETS} 35 | ) 36 | 37 | # Test executables 38 | 39 | ADD_EXECUTABLE(UDPv4Test EXCLUDE_FROM_ALL udpv4.cxx) 40 | gtest_discover_tests(UDPv4Test) 41 | ADD_EXECUTABLE(HopTest EXCLUDE_FROM_ALL hop.cxx) 42 | gtest_discover_tests(HopTest) 43 | ADD_EXECUTABLE(HopsTest EXCLUDE_FROM_ALL hops.cxx) 44 | gtest_discover_tests(HopsTest) 45 | 46 | # Tests 47 | 48 | ADD_TEST(UDPv4 UDPv4Test) 49 | ADD_TEST(Hop HopTest) 50 | ADD_TEST(Hops HopsTest) 51 | 52 | -------------------------------------------------------------------------------- /tests/src/hop.cxx: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | using namespace Tins; 10 | 11 | 12 | namespace { 13 | 14 | class HopTest: public ::testing::Test { 15 | }; 16 | 17 | TEST_F(HopTest, TestHopConstructor) { 18 | Hop h = Hop(); 19 | ASSERT_EQ(h.is_last_hop(), false); 20 | ASSERT_EQ(h.sent(), nullptr); 21 | ASSERT_EQ(h.received(), nullptr); 22 | ASSERT_EQ(h.name(), std::string()); 23 | ASSERT_EQ(h.sent_timestamp(), nullptr); 24 | ASSERT_EQ(h.received_timestamp(), nullptr); 25 | ASSERT_THROW(h.nat_id(), DublinTracerouteException); 26 | ASSERT_THROW(h.zerottl_forwarding_bug(), DublinTracerouteException); 27 | ASSERT_EQ(h.flowhash(), 0); 28 | ASSERT_EQ(h.rtt(), 0); 29 | ASSERT_EQ((bool)h, false); 30 | } 31 | 32 | TEST_F(HopTest, TestHopSentPacketIPOnly) { 33 | Hop h = Hop(); 34 | IP ip = IP("8.8.8.8", "0.0.0.0"); 35 | ip.ttl(32); 36 | h.sent(ip); 37 | ASSERT_EQ(h.sent()->dst_addr(), "8.8.8.8"); 38 | ASSERT_EQ(h.sent()->src_addr(), "0.0.0.0"); 39 | ASSERT_EQ(h.sent()->ttl(), 32); 40 | ASSERT_EQ(h.flowhash(), 0); // need UDP layer to have a flow hash 41 | } 42 | 43 | TEST_F(HopTest, TestHopSentPacketIPUDP) { 44 | Hop h = Hop(); 45 | IP ip = IP("8.8.8.8", "0.0.0.0") / UDP(33435, 12344); 46 | ip.ttl(16); 47 | h.sent(ip); 48 | ASSERT_EQ(h.sent()->dst_addr(), "8.8.8.8"); 49 | ASSERT_EQ(h.sent()->src_addr(), "0.0.0.0"); 50 | ASSERT_EQ(h.sent()->ttl(), 16); 51 | ASSERT_EQ(h.flowhash(), 47835); 52 | } 53 | 54 | TEST_F(HopTest, TestHopReceivedPacketIPOnly) { 55 | Hop h = Hop(); 56 | IP ip = IP("8.8.8.8", "0.0.0.0"); 57 | auto now = Tins::Timestamp::current_time(); 58 | h.sent_timestamp(now); 59 | 60 | struct timeval tv; 61 | tv.tv_sec = now.seconds() + 2; 62 | tv.tv_usec = now.microseconds(); 63 | auto then = Tins::Timestamp(tv); 64 | h.received(ip, then); 65 | ASSERT_THROW(h.nat_id(), Tins::pdu_not_found); 66 | ASSERT_THROW(h.zerottl_forwarding_bug(), Tins::pdu_not_found); 67 | ASSERT_EQ(h.rtt(), 2000000); 68 | } 69 | 70 | // TODO test resolver, summary and JSON representation 71 | 72 | } 73 | 74 | 75 | int main(int argc, char **argv) { 76 | ::testing::InitGoogleTest(&argc, argv); 77 | std::exit(RUN_ALL_TESTS()); 78 | } 79 | 80 | -------------------------------------------------------------------------------- /tests/src/hops.cxx: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | using namespace Tins; 9 | 10 | 11 | namespace { 12 | 13 | class HopsTest: public ::testing::Test { 14 | }; 15 | 16 | TEST_F(HopsTest, TestHopsConstructor) { 17 | Hops h = Hops(); 18 | } 19 | 20 | TEST_F(HopsTest, TestHopsCopyConstructor) { 21 | Hops h1 = Hops(); 22 | Hops h2 = Hops(h1); 23 | ASSERT_EQ(h1, h2); 24 | } 25 | 26 | TEST_F(HopsTest, TestHopsSize) { 27 | Hops hops = Hops(); 28 | ASSERT_EQ(hops.size(), 0); 29 | } 30 | 31 | TEST_F(HopsTest, TestHopsPushBack) { 32 | Hops hops = Hops(); 33 | hops.push_back(Hop()); 34 | ASSERT_EQ(hops.size(), 1); 35 | hops.push_back(Hop()); 36 | ASSERT_EQ(hops.size(), 2); 37 | hops.push_back(Hop()); 38 | ASSERT_EQ(hops.size(), 3); 39 | } 40 | 41 | TEST_F(HopsTest, TestHopsAt) { 42 | Hops hops = Hops(); 43 | Hop h = Hop(); 44 | std::string name = "test"; 45 | h.name(name); 46 | hops.push_back(h); 47 | ASSERT_EQ(hops.at(0), h); 48 | ASSERT_EQ(hops.at(0).name(), name); 49 | } 50 | 51 | TEST_F(HopsTest, TestHopsForwardIterator) { 52 | Hops hops = Hops(); 53 | int i; 54 | for (i = 0; i < 3; i++) { 55 | Hop h = Hop(); 56 | std::string name = std::to_string(i); 57 | h.name(name); 58 | hops.push_back(h); 59 | } 60 | i = 0; 61 | for (auto &h: hops) { 62 | std::string name = std::to_string(i); 63 | ASSERT_EQ(hops.at(i).name(), name); 64 | i++; 65 | } 66 | } 67 | 68 | TEST_F(HopsTest, TestHopsReverseIterator) { 69 | Hops hops = Hops(); 70 | int i; 71 | for (i = 0; i < 3; i++) { 72 | Hop h = Hop(); 73 | std::string name = std::to_string(i); 74 | h.name(name); 75 | hops.push_back(h); 76 | } 77 | i = 2; 78 | for (auto hop = hops.rbegin(); hop != hops.rend(); hop++) { 79 | std::string name = std::to_string(i); 80 | ASSERT_EQ(hops.at(i).name(), name); 81 | i--; 82 | } 83 | } 84 | 85 | } 86 | 87 | 88 | int main(int argc, char **argv) { 89 | ::testing::InitGoogleTest(&argc, argv); 90 | std::exit(RUN_ALL_TESTS()); 91 | } 92 | 93 | -------------------------------------------------------------------------------- /tests/src/udpv4.cxx: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | using namespace Tins; 9 | 10 | 11 | namespace { 12 | 13 | class UDPv4Test: public ::testing::Test { 14 | }; 15 | 16 | TEST_F(UDPv4Test, TestUDPv4Constructor) { 17 | UDPv4Probe p = UDPv4Probe(IPv4Address("8.8.8.8"), 33434, 12345, 64, IPv4Address("127.0.0.2")); 18 | ASSERT_EQ(p.local_port(), 12345); 19 | ASSERT_EQ(p.remote_port(), 33434); 20 | ASSERT_EQ(p.ttl(), 64); 21 | ASSERT_EQ(p.remote_addr().to_string(), std::string("8.8.8.8")); 22 | ASSERT_EQ(p.local_addr().to_string(), std::string("127.0.0.2")); 23 | } 24 | 25 | TEST_F(UDPv4Test, TestUDPv4ConstructorDefaultLocalAddr) { 26 | UDPv4Probe p = UDPv4Probe(IPv4Address("8.8.8.8"), 33434, 12345, 64); 27 | ASSERT_EQ(p.local_port(), 12345); 28 | ASSERT_EQ(p.remote_port(), 33434); 29 | ASSERT_EQ(p.ttl(), 64); 30 | ASSERT_EQ(p.remote_addr().to_string(), std::string("8.8.8.8")); 31 | ASSERT_EQ(p.local_addr().to_string(), std::string("0.0.0.0")); 32 | } 33 | 34 | TEST_F(UDPv4Test, TestUDPv4PacketForging) { 35 | UDPv4Probe p = UDPv4Probe(IPv4Address("127.0.0.3"), 33434, 12345, 64, IPv4Address("127.0.0.2")); 36 | IP* ip = p.forge(); 37 | ASSERT_EQ(ip->tos(), 0); 38 | ASSERT_EQ(ip->id(), 60794); 39 | ASSERT_EQ(ip->flags(), Tins::IP::Flags::DONT_FRAGMENT); 40 | ASSERT_EQ(ip->ttl(), 64); 41 | ASSERT_EQ(ip->dst_addr().to_string(), std::string("127.0.0.3")); 42 | ASSERT_EQ(ip->src_addr().to_string(), std::string("127.0.0.2")); 43 | delete ip; 44 | } 45 | 46 | TEST_F(UDPv4Test, TestUDPv4PacketForgingDefaultLocalAddr) { 47 | UDPv4Probe p = UDPv4Probe(IPv4Address("8.8.8.8"), 33434, 12345, 64); 48 | IP* ip = p.forge(); 49 | ASSERT_EQ(ip->tos(), 0); 50 | ASSERT_EQ(ip->flags(), Tins::IP::Flags::DONT_FRAGMENT); 51 | ASSERT_EQ(ip->ttl(), 64); 52 | ASSERT_EQ(ip->dst_addr().to_string(), std::string("8.8.8.8")); 53 | // not testing src_addr and IP ID because the default addr depends on 54 | // the actual network interface's configuration 55 | delete ip; 56 | } 57 | 58 | } 59 | 60 | 61 | int main(int argc, char **argv) { 62 | ::testing::InitGoogleTest(&argc, argv); 63 | std::exit(RUN_ALL_TESTS()); 64 | } 65 | 66 | --------------------------------------------------------------------------------