89 Commits
1.0.6 ... main

Author SHA1 Message Date
f3216cdf21 Update dependencies 2025-06-07 19:04:25 +02:00
f70f4c6b65 Bump golang.org/x/net from 0.37.0 to 0.38.0 (#42)
* Bump golang.org/x/net from 0.37.0 to 0.38.0

Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update packages

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Daniel Carrillo <daniel.carrillo@gmail.com>
2025-04-17 10:53:14 +02:00
d3849cec02 [ci] Migrate golangci conf to v2 2025-04-17 10:30:41 +02:00
00b1661ef9 Update dependencies and fix CVE-2025-22870, CVE-2024-40635 2025-03-18 20:25:24 +01:00
10c199109a Bump go to 1.24 2025-02-21 19:37:35 +01:00
b5fe362183 Make geo database usage optional (#39) 2025-01-02 20:13:41 +01:00
95e7742c56 Fix CVE-2024-45338 in golang.org/x/net 2024-12-19 17:52:04 +01:00
680aeefeab Fix CVE-2024-45337 in golang.org/x/crypto 2024-12-13 09:26:24 +01:00
15136359ae Bump github.com/quic-go/quic-go from 0.47.0 to 0.48.2 (#38)
* Bump github.com/quic-go/quic-go from 0.47.0 to 0.48.2

Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.47.0 to 0.48.2.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Changelog](https://github.com/quic-go/quic-go/blob/master/Changelog.md)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.47.0...v0.48.2)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix linter errors

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Daniel Carrillo <daniel.carrillo@gmail.com>
2024-12-02 19:14:32 +01:00
64011f9e99 Update dependencies 2024-09-15 20:01:33 +02:00
f020abc228 Update dependencies and bump Go version to 1.23 2024-08-26 19:51:37 +02:00
24b05c0015 Bump github.com/docker/docker (#36)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 26.1.4+incompatible to 26.1.5+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.1.4...v26.1.5)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-10 12:08:29 +02:00
f2da841307 Update dependencies 2024-07-30 12:50:27 +02:00
5bb5c974dd Bump github.com/docker/docker (#35)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 26.1.3+incompatible to 26.1.4+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.1.3...v26.1.4)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-30 12:50:27 +02:00
159c30f2f0 Update dependecies 2024-07-30 12:50:27 +02:00
1539ba1987 Update dependencies 2024-07-30 12:50:11 +02:00
4492f77d87 Remove docker compose from integration tests because of the testcontainers dependency nightmare (#34)
* Remove docker compose from integration tests because of the testcontainers dependency nightmare
2024-06-01 19:12:14 +02:00
aaf8a3b163 Revert update dependencies because of testcontainers 2024-05-25 16:14:46 +02:00
c37642c6c1 Revert "Update dependencies"
This reverts commit f3a6f27e99.
2024-05-25 16:10:48 +02:00
f3a6f27e99 Update dependencies 2024-05-25 16:07:52 +02:00
f167424e4f chore: Add concurrency to workflow 2024-05-12 19:26:00 +02:00
789cc6939e Return 404 when the dns domain has any path different than / 2024-05-12 19:24:10 +02:00
b57beded8f chore: Fix typo 2024-05-09 20:01:40 +02:00
d29e238beb chore: Split unit/integration tests in CI workflow 2024-05-09 19:48:06 +02:00
5d3dcb4b8e Bump gin-gonic to v1.10.0 2024-05-09 19:40:14 +02:00
71a0f37abb Update LICENSE date 2024-05-02 13:32:03 +02:00
c8d6da5ebd Update README.md 2024-04-12 19:58:14 +02:00
7caf4ad4a8 Bump github.com/docker/docker (#30)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 25.0.3+incompatible to 25.0.5+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v25.0.3...v25.0.5)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-12 19:32:09 +02:00
d13ea29071 New whatismydns feature (#29) 2024-04-12 19:26:48 +02:00
b11f15ecfe Bump github.com/quic-go/quic-go from 0.40.1 to 0.42.0 (#28)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.40.1 to 0.42.0.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Changelog](https://github.com/quic-go/quic-go/blob/master/Changelog.md)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.40.1...v0.42.0)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 17:40:05 +02:00
454f65f087 Move models initial setup to server handler (not ideal) 2024-03-23 20:24:19 +01:00
1988241b98 chore: Bump workflows dependencies 2024-03-23 18:05:13 +01:00
901345a337 [chore] Update workflows dependecies 2024-03-23 17:47:39 +01:00
0c14419e7e Server handling refactor (#27) 2024-03-23 17:41:34 +01:00
db111642d2 Bump github.com/docker/docker (#26)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.7+incompatible to 24.0.9+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v24.0.7...v24.0.9)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 19:49:19 +01:00
d5b1373e17 Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#25)
Bumps google.golang.org/protobuf from 1.32.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-14 15:40:42 +01:00
ba8a2ec494 Bump github.com/opencontainers/runc from 1.1.9 to 1.1.12 (#24)
Bumps [github.com/opencontainers/runc](https://github.com/opencontainers/runc) from 1.1.9 to 1.1.12.
- [Release notes](https://github.com/opencontainers/runc/releases)
- [Changelog](https://github.com/opencontainers/runc/blob/v1.1.12/CHANGELOG.md)
- [Commits](https://github.com/opencontainers/runc/compare/v1.1.9...v1.1.12)

---
updated-dependencies:
- dependency-name: github.com/opencontainers/runc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 17:27:37 +01:00
f8e27bef56 Add endopoint to check is a given port is open on the client (#22) 2023-12-31 12:52:08 +01:00
2bbeeb34c5 Bump github.com/containerd/containerd from 1.7.7 to 1.7.11 (#20)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.7 to 1.7.11.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.7...v1.7.11)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-20 20:08:35 +01:00
0090b794ee Bump golang.org/x/crypto from 0.14.0 to 0.17.0 (#19)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 09:10:37 +01:00
93f561d6ef Bump github.com/docker/docker from 20.10.24+incompatible to 24.0.7+incompatible (#18)
* Bump github.com/docker/docker

Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.24+incompatible to 24.0.7+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v20.10.24...v24.0.7)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update dependencies

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Daniel Carrillo <daniel.carrillo@gmail.com>
2023-10-30 19:17:40 +01:00
9da6d2fec5 Bump google.golang.org/grpc from 1.53.0 to 1.56.3 (#17)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.53.0 to 1.56.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.53.0...v1.56.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-26 09:25:00 +02:00
8e3d731719 [ci] Remove unused lines from codeql-analysis.yml 2023-10-12 12:52:44 +02:00
d5b244dc5f Bump go version to 1.21.3 (it fix HTTP/2 Stream Resets issue) 2023-10-12 12:14:49 +02:00
d767afd658 Update dependencies 2023-10-11 19:39:33 +02:00
f4fd79737e Bump golang version to 1.21 2023-08-25 18:34:21 +02:00
2ab6b29ed5 Update dependencies 2023-08-12 18:38:01 +02:00
55e6cd4816 Update test to latest quic version 2023-07-22 13:20:17 +02:00
a490d5f10e Update dependencies 2023-07-22 13:14:33 +02:00
994a12da5a Bump google.golang.org/grpc from 1.48.0 to 1.53.0 (#15)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.48.0 to 1.53.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.48.0...v1.53.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-06 08:07:54 +02:00
91deff4a14 chore: bump Go action to v4 (#13) 2023-06-02 17:23:48 +02:00
81c3a4fbb0 Bump github.com/gin-gonic/gin from 1.9.0 to 1.9.1 (#14)
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.9.0...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 17:13:53 +02:00
5b85eef7eb Bump github.com/docker/distribution (#12)
Bumps [github.com/docker/distribution](https://github.com/docker/distribution) from 2.8.1+incompatible to 2.8.2+incompatible.
- [Release notes](https://github.com/docker/distribution/releases)
- [Commits](https://github.com/docker/distribution/compare/v2.8.1...v2.8.2)

---
updated-dependencies:
- dependency-name: github.com/docker/distribution
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 17:00:06 +02:00
c54cf5a456 Add upx compression to Dockerfile 2023-04-29 18:49:21 +02:00
7dfa0a2e6d Update dependencies 2023-04-27 18:21:27 +02:00
68ef680439 Bump github.com/docker/docker (#11)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.17+incompatible to 20.10.24+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v20.10.17...v20.10.24)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-05 19:50:22 +02:00
bd42f712ea Bump github.com/opencontainers/runc from 1.1.3 to 1.1.5 (#10)
Bumps [github.com/opencontainers/runc](https://github.com/opencontainers/runc) from 1.1.3 to 1.1.5.
- [Release notes](https://github.com/opencontainers/runc/releases)
- [Changelog](https://github.com/opencontainers/runc/blob/v1.1.5/CHANGELOG.md)
- [Commits](https://github.com/opencontainers/runc/compare/v1.1.3...v1.1.5)

---
updated-dependencies:
- dependency-name: github.com/opencontainers/runc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-29 18:23:38 +02:00
0b31633309 Remove useless function 2023-03-21 20:19:11 +01:00
b5fa3be506 Move logs about stopping servers to the proper place 2023-03-20 19:27:17 +01:00
8783db018b Use pointers to proper server initializing handling 2023-03-20 17:43:06 +01:00
e60d1ae5b7 Initial server handling refactor (#9) 2023-03-20 16:36:55 +01:00
84a767ade0 chore: Fix README.md typos 2023-03-18 21:14:08 +01:00
19c72f94a5 Add experimental support for HTTP/3 (#8)
* Wait for service in integration tests instead of watching for a string

* Add http3 experimental support
2023-03-18 21:06:51 +01:00
de78dcdf52 Bump dependencies due to minor security issues 2023-03-16 19:59:39 +01:00
eb200ddd81 Fix minor linting issues 2023-03-16 19:59:04 +01:00
5c4ac4a3ee [skip ci] bump action-gh-release to v1 2023-03-10 20:03:32 +01:00
ee328892d6 Bump gin to v1.9.0 2023-03-10 19:30:03 +01:00
04d983d671 Remove redundant ifs 2023-03-10 19:27:10 +01:00
202518d547 Merge branch 'main' of github.com:dcarrillo/whatismyip 2023-02-16 19:27:07 +01:00
52d14fe78f Update dependencies 2023-02-16 19:26:53 +01:00
8aadc4fbb6 Bump golang to 1.20 2023-02-16 19:24:16 +01:00
b6391d8fd1 Bump github.com/containerd/containerd from 1.6.12 to 1.6.18 (#6)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.6.12 to 1.6.18.
2023-02-16 19:21:04 +01:00
35fac1bd57 chore: update linters and go action 2023-01-26 20:47:03 +01:00
b13a30c354 chore: disable go cache 2023-01-26 20:43:13 +01:00
5982683cdd chore: use cache in CI and bump checkout action to v3 2023-01-26 20:38:31 +01:00
1a986a029f Update gin to v1.8.2 (GO-2022-1144) and containerd (CVE-2022-27664) 2022-12-26 19:30:02 +01:00
ed0ddccab5 Update golang.org/x/net (CVE-2022-32149) 2022-11-05 13:37:05 +01:00
20ae50c115 Update golang.org/x/net (CVE-2022-27664) 2022-09-13 20:38:24 +02:00
9763ed0e29 Update to golang 1.19 2022-08-11 20:02:27 +02:00
88691a5149 Update README.md
Add CodeQL badge
2022-07-03 11:57:58 +02:00
6b7fc0bc6a Bump gin and testify to latest versions 2022-07-03 11:39:51 +02:00
c5a659ff64 Create codeql-analysis.yml 2022-07-03 11:29:23 +02:00
bd06da7b2b Bump gin-gonic to 1.8.0 2022-06-03 18:37:22 +02:00
b61d64a755 Moving secure again, this time to github.com/gin-contrib/secure 2022-05-07 15:56:21 +02:00
3df794ecc4 Bump testcontainers-go tp 0.13.0 2022-05-07 13:06:19 +02:00
ca1d002974 Use gin-gonic/contrib/secure instead of deprecated module 2022-05-07 12:51:13 +02:00
1ee7256506 Remove headers set by a trusted proxy from outputs 2022-05-02 18:07:40 +02:00
7c70abf07f Add feature to get the right client port when using a trusted proxy 2022-05-02 17:56:57 +02:00
9070e9a2c2 Use Accept request header instead of user agent to figure out non-browser clients. Funny fact, I borrowed the idea from a fork ('8a3e142cf3/router/generic.go (L33)') 2022-04-30 17:35:12 +02:00
41 changed files with 2383 additions and 1698 deletions

52
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,52 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '21 21 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: install go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
- run: |
echo "Build"
make build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@ -8,22 +8,24 @@ on:
- '*' - '*'
pull_request: pull_request:
concurrency: ${{ github.ref_name }}
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
make: ["lint", "unit-test", "integration-test"]
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v4
- name: install go - name: install go
uses: actions/setup-go@v2 uses: actions/setup-go@v5
with: with:
go-version: "^1.18" go-version-file: go.mod
- name: Lint - name: ${{ matrix.make }}
run: make lint run: make ${{ matrix.make }}
- name: Tests
run: make test
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -33,9 +35,14 @@ jobs:
matrix: matrix:
goosarch: [linux-amd64] goosarch: [linux-amd64]
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: install go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Set env - name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
@ -56,7 +63,7 @@ jobs:
sha256sum whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz > whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz.sha256 sha256sum whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz > whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz.sha256
- name: Release - name: Release
uses: softprops/action-gh-release@v0.1.14 uses: softprops/action-gh-release@v2
with: with:
body_path: changelog.txt body_path: changelog.txt
files: | files: |

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
whatismyip

64
.golangci.yaml Normal file
View File

@ -0,0 +1,64 @@
version: "2"
linters:
default: none
enable:
- ineffassign
- nakedret
- revive
- staticcheck
- unconvert
- unparam
- unused
settings:
revive:
confidence: 0.8
severity: warning
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
- name: unreachable-code
- name: redefines-builtin-id
staticcheck:
checks:
- all
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
issues:
max-same-issues: 0
formatters:
enable:
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@ -1,21 +1,29 @@
FROM golang:1.18-alpine as builder FROM golang:1.24-alpine AS builder
ARG ARG_VERSION ARG ARG_VERSION
ENV VERSION $ARG_VERSION ENV VERSION=$ARG_VERSION
WORKDIR /app WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN --mount=type=cache,target=/go/pkg/mod/ go mod download -x
COPY . . COPY . .
RUN apk add make git && make build VERSION=$VERSION FROM builder AS build-dev-app
# hadolint ignore=DL3018
RUN --mount=type=cache,target=/go/pkg/mod/ apk --no-cache add make && make build
# Build final image FROM builder AS build-prod-app
FROM scratch # hadolint ignore=DL3018
RUN apk --no-cache update && apk add --no-cache ca-certificates make upx \
WORKDIR /app && update-ca-certificates \
&& make build \
COPY --from=builder /app/whatismyip /usr/bin/ && upx --best --lzma whatismyip
EXPOSE 8080
FROM scratch AS dev
COPY --from=build-dev-app /app/whatismyip /usr/bin/
ENTRYPOINT ["whatismyip"]
FROM scratch AS prod
COPY --from=build-prod-app /app/whatismyip /usr/bin/
ENTRYPOINT ["whatismyip"] ENTRYPOINT ["whatismyip"]

View File

@ -176,7 +176,7 @@
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS
Copyright 2021 Daniel Carrillo Copyright 2024 Daniel Carrillo
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -5,44 +5,40 @@ DOCKER_URL ?= dcarrillo/whatismyip
.PHONY: test .PHONY: test
test: unit-test integration-test test: unit-test integration-test
.PHONY: unit-test
unit-test: unit-test:
go test -race -short -cover ./... go test -count=1 -race -short -cover ./...
.PHONY: integration-test
integration-test: integration-test:
go test ./integration-tests -v go test -count=1 -v ./integration-tests
.PHONY: install-tools
install-tools: install-tools:
@command golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @command golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.45.0; \ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin; \
fi
@command $(GOPATH)/revive > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/mgechev/revive; \
fi fi
@command $(GOPATH)/shadow > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @command $(GOPATH)/shadow > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@v0.1.10; \ go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest; \
fi fi
@command $(GOPATH)/golines > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install github.com/segmentio/golines@latest; \
fi
.PHONY: lint
lint: install-tools lint: install-tools
gofmt -l . && test -z $$(gofmt -l .) gofmt -l . && test -z $$(gofmt -l .)
golines -l . && test -z $$(golines -l .)
golangci-lint run golangci-lint run
shadow ./... shadow ./...
.PHONY: build
build: build:
CGO_ENABLED=0 go build -ldflags="-s -w -X 'github.com/dcarrillo/whatismyip/internal/core.Version=${VERSION}'" -o whatismyip ./cmd CGO_ENABLED=0 go build -ldflags="-s -w -X 'github.com/dcarrillo/whatismyip/internal/core.Version=${VERSION}'" -o whatismyip ./cmd
.PHONY: docker-build docker-build-dev:
docker-build: docker build --target=dev --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} .
docker build --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} .
.PHONY: docker-push docker-build-prod:
docker-push: docker-build docker build --target=prod --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} .
docker-push: docker-build-prod
ifneq (,$(findstring devel-,$(VERSION))) ifneq (,$(findstring devel-,$(VERSION)))
@echo "VERSION is set to ${VERSION}, I can't push devel builds" @echo "VERSION is set to ${VERSION}, I can't push devel builds"
exit 1 exit 1
@ -52,12 +48,17 @@ else
docker push ${DOCKER_URL}:latest docker push ${DOCKER_URL}:latest
endif endif
.PHONY: docker-run docker-run: docker-build-dev
docker-run: docker-build
docker run --tty --interactive --rm \ docker run --tty --interactive --rm \
-v ${PWD}/test/GeoIP2-City-Test.mmdb:/tmp/GeoIP2-City-Test.mmdb:ro \ --publish 8080:8080/tcp \
-v ${PWD}/test/GeoLite2-ASN-Test.mmdb:/tmp/GeoLite2-ASN-Test.mmdb:ro -p 8080:8080 \ --publish 8081:8081/tcp \
${DOCKER_URL}:${VERSION} \ --publish 8081:8081/udp \
-geoip2-city /tmp/GeoIP2-City-Test.mmdb \ --volume ${PWD}/test:/test \
-geoip2-asn /tmp/GeoLite2-ASN-Test.mmdb \ ${DOCKER_URL}:${VERSION} \
-trusted-header X-Real-IP -geoip2-city /test/GeoIP2-City-Test.mmdb \
-geoip2-asn /test/GeoLite2-ASN-Test.mmdb \
-trusted-header X-Real-PortReal-IP \
-tls-bind :8081 \
-tls-crt /test/server.pem \
-tls-key /test/server.key \
-enable-http3

122
README.md
View File

@ -1,6 +1,7 @@
# What is my IP address # What is my IP address
[![CI](https://github.com/dcarrillo/whatismyip/workflows/CI/badge.svg)](https://github.com/dcarrillo/whatismyip/actions) [![CI](https://github.com/dcarrillo/whatismyip/workflows/CI/badge.svg)](https://github.com/dcarrillo/whatismyip/actions)
[![CodeQL](https://github.com/dcarrillo/whatismyip/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/dcarrillo/whatismyip/actions/workflows/codeql-analysis.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/dcarrillo/whatismyip)](https://goreportcard.com/report/github.com/dcarrillo/whatismyip) [![Go Report Card](https://goreportcard.com/badge/github.com/dcarrillo/whatismyip)](https://goreportcard.com/report/github.com/dcarrillo/whatismyip)
[![GitHub release](https://img.shields.io/github/release/dcarrillo/whatismyip.svg)](https://github.com/dcarrillo/whatismyip/releases/) [![GitHub release](https://img.shields.io/github/release/dcarrillo/whatismyip.svg)](https://github.com/dcarrillo/whatismyip/releases/)
[![License Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](./LICENSE) [![License Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](./LICENSE)
@ -8,20 +9,28 @@
- [What is my IP address](#what-is-my-ip-address) - [What is my IP address](#what-is-my-ip-address)
- [Features](#features) - [Features](#features)
- [Endpoints](#endpoints) - [Endpoints](#endpoints)
- [DNS discovery](#dns-discovery)
- [Build](#build) - [Build](#build)
- [Usage](#usage) - [Usage](#usage)
- [Examples](#examples) - [Examples](#examples)
- [Run a default TCP server](#run-a-default-tcp-server) - [Run a default TCP server](#run-a-default-tcp-server)
- [Run a TLS (HTTP/2) server only](#run-a-tls-http2-server-only) - [Run a default TCP server with geo information enabled](#run-a-default-tco-server-with-geo-information-enabled)
- [Run a default TCP server with a custom template and trust a custom header set by an upstream proxy](#run-a-default-tcp-server-with-a-custom-template-and-trust-a-custom-header-set-by-an-upstream-proxy) - [Run a TLS (HTTP/2) and enable "what is my DNS" with geo information](#run-a-tls-http2-and-enable-what-is-my-dns-with-geo-information)
- [Run an HTTP/3 server](#run-an-http3-server)
- [Run a default TCP server with a custom template and trust a pair of custom headers set by an upstream proxy](#run-a-default-tcp-server-with-a-custom-template-and-trust-a-pair-of-custom-headers-set-by-an-upstream-proxy)
- [Download](#download) - [Download](#download)
- [Docker](#docker) - [Docker](#docker)
- [Run a container locally using test databases](#run-a-container-locally-using-test-databases) - [Run a container locally using test databases](#run-a-container-locally-using-test-databases)
- [From Docker Hub](#from-docker-hub) - [From Docker Hub](#from-docker-hub)
Just another "what is my IP address" service, including geolocation and headers information, written in go with high performance in mind, it uses [gin](https://github.com/gin-gonic/gin) which uses [httprouter](https://github.com/julienschmidt/httprouter) a lightweight high performance HTTP multiplexer. > [!NOTE]
> Since version 3.0.0, geodb database is not mandatory, not adding the flags will disable the geo feature.
> Since version 2.3.0, the application includes an optional client [DNS discovery](#dns-discovery)
Take a look at [ifconfig.es](https://ifconfig.es) a live site using `whatismyip` Just another "what is my IP address" service, including geolocation, TCP open port checking, and headers information. Written in go with high performance in mind,
it uses [gin](https://github.com/gin-gonic/gin) which uses [httprouter](https://github.com/julienschmidt/httprouter) a lightweight high performance HTTP multiplexer.
Take a look at [ifconfig.es](https://ifconfig.es) a live site using `whatismyip` and the `DNS discovery` enabled.
Get your public IP easily from the command line: Get your public IP easily from the command line:
@ -33,21 +42,32 @@ curl -6 ifconfig.es
::1 ::1
``` ```
Get the IP of your DNS provider:
```bash
curl -L dns.ifconfig.es
2a04:e4c0:47::67 (Spain / OPENDNS)
```
## Features ## Features
- TLS and HTTP/2. - TLS and HTTP/2.
- Can run behind a proxy by trusting a custom header (usually `X-Real-IP`) to figure out the source IP address. - Experimental HTTP/3 support. HTTP/3 requires a TLS server running (`-tls-bind`), as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server.
- Beta DNS discovery: A best-effort approach to discovering the DNS server that is resolving the client's requests.
- Can run behind a proxy by trusting a custom header (usually `X-Real-IP`) to figure out the source IP address. It also supports a custom header to resolve the client port, if the proxy can only add a header for the IP (for example a fixed header from CDNs) the client port is shown as unknown.
- IPv4 and IPv6. - IPv4 and IPv6.
- Geolocation info including ASN. This feature is possible thanks to [maxmind](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data?lang=en) GeoLite2 databases. In order to use these databases, a license key is needed. Please visit Maxmind site for further instructions and get a free license. - Geolocation info including ASN. This feature is possible thanks to [maxmind](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data?lang=en) GeoLite2 databases. In order to use these databases, a license key is needed. Please visit Maxmind site for further instructions and get a free license.
- Checking TCP open ports.
- High performance. - High performance.
- Self-contained server what can reload GeoLite2 databases and/or SSL certificates without stop/start. The `hup` signal is honored. - Self-contained server that can reload GeoLite2 databases and/or SSL certificates without stop/start. The `hup` signal is honored.
- HTML templates for the landing page. - HTML templates for the landing page.
- Text plain and JSON output. - Text plain and JSON output.
## Endpoints ## Endpoints
- https://ifconfig.es/ - https://ifconfig.es/
- https://ifconfig.es/json - https://ifconfig.es/client-port
- https://ifconfig.es/json (this is the same as `curl -H "Accept: application/json" https://ifconfig.es/`)
- https://ifconfig.es/geo - https://ifconfig.es/geo
- https://ifconfig.es/geo/city - https://ifconfig.es/geo/city
- https://ifconfig.es/geo/country - https://ifconfig.es/geo/country
@ -62,66 +82,120 @@ curl -6 ifconfig.es
- https://ifconfig.es/all - https://ifconfig.es/all
- https://ifconfig.es/headers - https://ifconfig.es/headers
- https://ifconfig.es/<header_name> - https://ifconfig.es/<header_name>
- https://ifconfig.es/scan/tcp/<port_number>
- https://dns.ifconfig.es
## DNS discovery
The DNS discovery works by forcing the client to make a request to `<uuid>.dns.ifconfig.es` this DNS request is handled by a microdns server
included in the `whatismyip` binary. In order to run the discovery server, a configuration file in the following form has to be created:
```yaml
---
domain: dns.example.com
redirect_port: ":8000"
resource_records:
- "1800 IN SOA xns.example.com. hostmaster.example.com. 1 10000 2400 604800 1800"
- "3600 IN NS xns.example.com."
ipv4:
- "127.0.0.2"
ipv6:
- "aaa:aaa:aaa:aaaa::1"
```
The DNS authority for example.com has delegated the subdomain zone `dns.example.com` to the server running the `whatismyip` service.
The client can request the URL `dns.example.com` by following the redirection `curl -L dns.example.com`.
To avoid the redirection, you can provide a valid URL, for example, for the real [ifconfig.es](https://ifconfig.es):
```bash
curl $(uuidgen).dns.ifconfig.es
curl $(cat /proc/sys/kernel/random/uuid).dns.ifconfig.es
```
## Build ## Build
Golang >= 1.17 is required. Previous versions may work. Golang >= 1.22 is required.
`make build` `make build`
## Usage ## Usage
```text ```text
Usage of ./whatismyip: Usage of whatismyip:
-bind string -bind string
Listening address (see https://pkg.go.dev/net?#Listen) (default ":8080") Listening address (see https://pkg.go.dev/net?#Listen) (default ":8080")
-enable-http3
Enable HTTP/3 protocol. HTTP/3 requires --tls-bind set, as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server.
-enable-secure-headers
Add sane security-related headers to every response
-geoip2-asn string -geoip2-asn string
Path to GeoIP2 ASN database Path to GeoIP2 ASN database. Enables ASN information. (--geoip2-city becomes mandatory)
-geoip2-city string -geoip2-city string
Path to GeoIP2 city database Path to GeoIP2 city database. Enables geo information (--geoip2-asn becomes mandatory)
-resolver string
Path to the resolver configuration. It actually enables the resolver for DNS client discovery.
-template string -template string
Path to template file Path to the template file
-tls-bind string -tls-bind string
Listening address for TLS (see https://pkg.go.dev/net?#Listen) Listening address for TLS (see https://pkg.go.dev/net?#Listen)
-tls-crt string -tls-crt string
When using TLS, path to certificate file When using TLS, path to certificate file
-tls-key string -tls-key string
When using TLS, path to private key file When using TLS, path to private key file
-trusted-header string -trusted-header string
Trusted request header for remote IP (e.g. X-Real-IP) Trusted request header for remote IP (e.g. X-Real-IP). When using this feature if -trusted-port-header is not set the client port is shown as 'unknown'
-trusted-port-header string
Trusted request header for remote client port (e.g. X-Real-Port). When this parameter is set -trusted-header becomes mandatory
-version -version
Output version information and exit Output version information and exit
``` ```
## Examples ## Examples
### Run a default TCP server ### Run a default TCP server
```bash
./whatismyip
```
### Run a default TCP server with geo information enabled
```bash ```bash
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb ./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb
``` ```
### Run a TLS (HTTP/2) server only ### Run a TLS (HTTP/2) and enable "what is my DNS" with geo information
```bash ```bash
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb \ ./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb \
-bind "" -tls-bind :8081 -tls-crt ./test/server.pem -tls-key ./test/server.key -bind "" -tls-bind :8081 -tls-crt ./test/server.pem -tls-key ./test/server.key \
-resolver ./test/resolver.yml
``` ```
### Run a default TCP server with a custom template and trust a custom header set by an upstream proxy ### Run an HTTP/3 server
```bash ```bash
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb \ ./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb \
-trusted-header X-Real-IP -template mytemplate.tmpl -bind "" -tls-bind :8081 -tls-crt ./test/server.pem -tls-key ./test/server.key -enable-http3
```
### Run a default TCP server with a custom template and trust a pair of custom headers set by an upstream proxy
```bash
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb \
-trusted-header X-Real-IP -trusted-port-header X-Real-Port -template mytemplate.tmpl
``` ```
## Download ## Download
Download latest version from https://github.com/dcarrillo/whatismyip/releases Download the latest version from [github](https://github.com/dcarrillo/whatismyip/releases)
## Docker ## Docker
An ultra-light (~9MB) image is available. An ultra-light (~4MB) image is available on [docker hub](https://hub.docker.com/r/dcarrillo/whatismyip). Since version `2.1.2`, the binary is compressed using [upx](https://github.com/upx/upx).
### Run a container locally using test databases ### Run a container locally using test databases

View File

@ -2,165 +2,98 @@ package main
import ( import (
"context" "context"
"errors"
"flag" "flag"
"fmt" "fmt"
"log"
"net/http" "net/http"
"os" "os"
"os/signal" "slices"
"syscall" "time"
"github.com/dcarrillo/whatismyip/internal/httputils" "github.com/dcarrillo/whatismyip/internal/httputils"
"github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/internal/setting"
"github.com/dcarrillo/whatismyip/models" "github.com/dcarrillo/whatismyip/resolver"
"github.com/dcarrillo/whatismyip/server"
"github.com/dcarrillo/whatismyip/service"
"github.com/gin-contrib/secure"
"github.com/patrickmn/go-cache"
"github.com/dcarrillo/whatismyip/router" "github.com/dcarrillo/whatismyip/router"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/unrolled/secure"
)
var (
tcpServer *http.Server
tlsServer *http.Server
engine *gin.Engine
) )
func main() { func main() {
o, err := setting.Setup(os.Args[1:]) o, err := setting.Setup(os.Args[1:])
if err == flag.ErrHelp || err == setting.ErrVersion { if err != nil {
fmt.Print(o) if err == flag.ErrHelp || err == setting.ErrVersion {
os.Exit(0) fmt.Print(o)
} else if err != nil { os.Exit(0)
fmt.Print(err) }
fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN) servers := []server.Server{}
setupEngine() engine := setupEngine()
if setting.App.Resolver.Domain != "" {
store := cache.New(1*time.Minute, 10*time.Minute)
dnsEngine := resolver.Setup(store)
nameServer := server.NewDNSServer(context.Background(), dnsEngine.Handler())
servers = append(servers, nameServer)
engine.Use(router.GetDNSDiscoveryHandler(store, setting.App.Resolver.Domain, setting.App.Resolver.RedirectPort))
}
var geoSvc *service.Geo
if setting.App.GeodbPath.City != "" || setting.App.GeodbPath.ASN != "" {
if geoSvc, err = service.NewGeo(context.Background(), setting.App.GeodbPath.City, setting.App.GeodbPath.ASN); err != nil {
panic(err)
}
}
router.SetupTemplate(engine) router.SetupTemplate(engine)
router.Setup(engine) router.Setup(engine, geoSvc)
servers = slices.Concat(servers, setupHTTPServers(context.Background(), engine.Handler()))
if setting.App.BindAddress != "" { whatismyip := server.Setup(servers, geoSvc)
runTCPServer() whatismyip.Run()
}
if setting.App.TLSAddress != "" {
runTLSServer()
}
runHandler()
} }
func runHandler() { func setupEngine() *gin.Engine {
signalChan := make(chan os.Signal, 3)
signal.Notify(signalChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
ctx := context.Background()
var s os.Signal
for {
s = <-signalChan
if s == syscall.SIGHUP {
models.CloseDBs()
models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN)
router.SetupTemplate(engine)
if setting.App.BindAddress != "" {
if err := tcpServer.Shutdown(ctx); err != nil {
log.Printf("TCP server forced to shutdown: %s", err)
}
runTCPServer()
}
if setting.App.TLSAddress != "" {
if err := tlsServer.Shutdown(ctx); err != nil {
log.Printf("TLS server forced to shutdown: %s", err)
}
runTLSServer()
}
} else {
log.Printf("Shutting down...")
if setting.App.BindAddress != "" {
if err := tcpServer.Shutdown(ctx); err != nil {
log.Printf("TCP server forced to shutdown: %s", err)
}
}
if setting.App.TLSAddress != "" {
if err := tlsServer.Shutdown(ctx); err != nil {
log.Printf("TLS server forced to shutdown: %s", err)
}
}
models.CloseDBs()
break
}
}
}
func runTCPServer() {
tcpServer = &http.Server{
Addr: setting.App.BindAddress,
Handler: engine,
ReadTimeout: setting.App.Server.ReadTimeout,
WriteTimeout: setting.App.Server.WriteTimeout,
}
go func() {
log.Printf("Starting TCP server listening on %s", setting.App.BindAddress)
if err := tcpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
log.Printf("Stopping TCP server...")
}()
}
func runTLSServer() {
tlsServer = &http.Server{
Addr: setting.App.TLSAddress,
Handler: engine,
ReadTimeout: setting.App.Server.ReadTimeout,
WriteTimeout: setting.App.Server.WriteTimeout,
}
go func() {
log.Printf("Starting TLS server listening on %s", setting.App.TLSAddress)
if err := tlsServer.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
log.Printf("Stopping TLS server...")
}()
}
func setupEngine() {
gin.DisableConsoleColor() gin.DisableConsoleColor()
if os.Getenv(gin.EnvGinMode) == "" { if os.Getenv(gin.EnvGinMode) == "" {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
engine = gin.New() engine := gin.New()
engine.Use(gin.LoggerWithFormatter(httputils.GetLogFormatter)) engine.Use(gin.LoggerWithFormatter(httputils.GetLogFormatter), gin.Recovery())
engine.Use(gin.Recovery())
if setting.App.EnableSecureHeaders { if setting.App.EnableSecureHeaders {
engine.Use(addSecureHeaders()) engine.Use(secure.New(secure.Config{
}
_ = engine.SetTrustedProxies(nil)
engine.TrustedPlatform = setting.App.TrustedHeader
}
func addSecureHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
err := secure.New(secure.Options{
BrowserXssFilter: true, BrowserXssFilter: true,
ContentTypeNosniff: true, ContentTypeNosniff: true,
FrameDeny: true, FrameDeny: true,
}).Process(c.Writer, c.Request) }))
if err != nil { }
c.Abort() _ = engine.SetTrustedProxies(nil)
return engine.TrustedPlatform = setting.App.TrustedHeader
}
// Avoid header rewrite if response is a redirection. return engine
if status := c.Writer.Status(); status > 300 && status < 399 { }
c.Abort()
func setupHTTPServers(ctx context.Context, handler http.Handler) []server.Server {
var servers []server.Server
if setting.App.BindAddress != "" {
tcpServer := server.NewTCPServer(ctx, &handler)
servers = append(servers, tcpServer)
}
if setting.App.TLSAddress != "" {
tlsServer := server.NewTLSServer(ctx, &handler)
servers = append(servers, tlsServer)
if setting.App.EnableHTTP3 {
quicServer := server.NewQuicServer(ctx, tlsServer)
servers = append(servers, quicServer)
} }
} }
return servers
} }

124
go.mod
View File

@ -1,60 +1,100 @@
module github.com/dcarrillo/whatismyip module github.com/dcarrillo/whatismyip
go 1.18 go 1.24
require ( require (
github.com/gin-gonic/gin v1.7.7 github.com/docker/docker v28.0.4+incompatible
github.com/oschwald/maxminddb-golang v1.9.0 github.com/gin-contrib/secure v1.1.2
github.com/stretchr/testify v1.7.1 github.com/gin-gonic/gin v1.10.1
github.com/testcontainers/testcontainers-go v0.12.0 github.com/google/uuid v1.6.0
github.com/unrolled/secure v1.10.0 github.com/miekg/dns v1.1.66
github.com/oschwald/maxminddb-golang v1.13.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/quic-go/quic-go v0.52.0
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.36.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.9.2 // indirect github.com/bytedance/sonic v1.13.3 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/containerd/cgroups v1.0.3 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/containerd v1.6.2 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v20.10.14+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/ebitengine/purego v0.8.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/validator/v10 v10.10.1 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/klauspost/compress v1.17.4 // indirect
github.com/magiconair/properties v1.8.6 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/moby/sys/mount v0.3.1 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/moby/sys/mountinfo v0.6.0 // indirect github.com/magiconair/properties v1.8.9 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runc v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/quic-go/qpack v0.5.1 // indirect
go.opencensus.io v0.23.0 // indirect github.com/shirou/gopsutil/v4 v4.25.1 // indirect
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect github.com/tklauser/numcpus v0.6.1 // indirect
golang.org/x/text v0.3.7 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de // indirect github.com/ugorji/go/codec v1.2.14 // indirect
google.golang.org/grpc v1.45.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
google.golang.org/protobuf v1.28.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/protobuf v1.36.6 // indirect
) )

1340
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -4,49 +4,77 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"io/ioutil" "fmt"
"log" "io"
"net"
"net/http" "net/http"
"path/filepath" "strings"
"runtime"
"testing" "testing"
validator "github.com/dcarrillo/whatismyip/internal/validator/uuid"
"github.com/dcarrillo/whatismyip/router" "github.com/dcarrillo/whatismyip/router"
"github.com/docker/docker/api/types"
"github.com/quic-go/quic-go/http3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go" "github.com/stretchr/testify/require"
tc "github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait" "github.com/testcontainers/testcontainers-go/wait"
) )
func buildContainer() testcontainers.ContainerRequest { func customDialContext() func(ctx context.Context, network, addr string) (net.Conn, error) {
_, filename, _, _ := runtime.Caller(0) return func(ctx context.Context, network, addr string) (net.Conn, error) {
dir := filepath.Dir(filename) dialer := &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, "udp", "127.0.0.1:53531")
},
},
}
req := testcontainers.ContainerRequest{ return dialer.DialContext(ctx, network, addr)
FromDockerfile: testcontainers.FromDockerfile{
Context: "../",
Dockerfile: "Dockerfile",
},
Cmd: []string{
"-geoip2-city", "/tmp/GeoIP2-City-Test.mmdb",
"-geoip2-asn", "/tmp/GeoLite2-ASN-Test.mmdb",
"-bind", ":8000",
"-tls-bind", ":8001",
"-tls-crt", "/tmp/server.pem",
"-tls-key", "/tmp/server.key",
"-trusted-header", "X-Real-IP",
"-enable-secure-headers",
},
ExposedPorts: []string{"8000:8000", "8001:8001"},
WaitingFor: wait.ForLog("Starting TLS server listening on :8001"),
BindMounts: map[string]string{
"/tmp/GeoIP2-City-Test.mmdb": filepath.Join(dir, "/../test/GeoIP2-City-Test.mmdb"),
"/tmp/GeoLite2-ASN-Test.mmdb": filepath.Join(dir, "/../test/GeoLite2-ASN-Test.mmdb"),
"/tmp/server.pem": filepath.Join(dir, "/../test/server.pem"),
"/tmp/server.key": filepath.Join(dir, "/../test/server.key"),
},
} }
}
return req func testWhatIsMyDNS(t *testing.T) {
t.Run("RequestDNSDiscovery", func(t *testing.T) {
http.DefaultTransport.(*http.Transport).DialContext = customDialContext()
req, err := http.NewRequest("GET", "http://localhost:8000", nil)
assert.NoError(t, err)
req.Host = "dns.example.com"
client := &http.Client{
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Do(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusFound, resp.StatusCode)
u, err := resp.Location()
assert.NoError(t, err)
assert.True(t, validator.IsValid(strings.Split(u.Hostname(), ".")[0]))
for _, accept := range []string{"application/json", "*/*", "text/html"} {
req, err = http.NewRequest("GET", u.String(), nil)
req.Host = u.Hostname()
req.Header.Set("Accept", accept)
assert.NoError(t, err)
resp, err = client.Do(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
if accept == "application/json" {
assert.NoError(t, json.Unmarshal(body, &router.DNSJSONResponse{}))
} else {
ip := strings.Split(string(body), " ")[0]
assert.True(t, net.ParseIP(ip) != nil)
}
}
})
} }
func TestContainerIntegration(t *testing.T) { func TestContainerIntegration(t *testing.T) {
@ -55,31 +83,177 @@ func TestContainerIntegration(t *testing.T) {
} }
ctx := context.Background() ctx := context.Background()
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ c, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{
ContainerRequest: buildContainer(), ContainerRequest: tc.ContainerRequest{
Started: true, FromDockerfile: tc.FromDockerfile{
Context: "../",
Dockerfile: "./test/Dockerfile",
PrintBuildLog: true,
KeepImage: false,
BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) {
buildOptions.Target = "test"
},
},
ExposedPorts: []string{
"8000:8000",
"8001:8001",
"8001:8001/udp",
"53531:53/udp",
},
Cmd: []string{
"-geoip2-city", "/GeoIP2-City-Test.mmdb",
"-geoip2-asn", "/GeoLite2-ASN-Test.mmdb",
"-bind", ":8000",
"-tls-bind", ":8001",
"-tls-crt", "/server.pem",
"-tls-key", "/server.key",
"-trusted-header", "X-Real-IP",
"-enable-secure-headers",
"-enable-http3",
"-resolver", "/resolver.yml",
},
Files: []tc.ContainerFile{
{
HostFilePath: "./../test/GeoIP2-City-Test.mmdb",
ContainerFilePath: "/GeoIP2-City-Test.mmdb",
},
{
HostFilePath: "./../test/GeoLite2-ASN-Test.mmdb",
ContainerFilePath: "/GeoLite2-ASN-Test.mmdb",
},
{
HostFilePath: "./../test/server.pem",
ContainerFilePath: "/server.pem",
},
{
HostFilePath: "./../test/server.key",
ContainerFilePath: "/server.key",
},
{
HostFilePath: "./../test/resolver.yml",
ContainerFilePath: "/resolver.yml",
},
},
WaitingFor: wait.ForLog("Starting QUIC server"),
AutoRemove: true,
},
Started: true,
}) })
if err != nil { require.NoError(t, err)
log.Fatal(err) t.Cleanup(func() { c.Terminate(ctx) })
}
defer func() {
err := container.Terminate(ctx)
if err != nil {
log.Fatal(err)
}
}()
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
for _, url := range []string{"http://localhost:8000/json", "https://localhost:8001/json"} { tests := []struct {
resp, _ := http.Get(url) name string
assert.Equal(t, 200, resp.StatusCode) url string
quic bool
var dat router.JSONResponse }{
body, err := ioutil.ReadAll(resp.Body) {
if err != nil { name: "RequestOverHTTP",
log.Fatal(err) url: "http://localhost:8000",
} quic: false,
},
assert.NoError(t, json.Unmarshal(body, &dat)) {
name: "RequestOverHTTPs",
url: "https://localhost:8001",
quic: false,
},
{
name: "RequestOverUDPWithQuic",
url: "https://localhost:8001",
quic: true,
},
} }
testsPortScan := []struct {
name string
port int
want bool
}{
{
name: "RequestOpenPortScan",
port: 8000,
want: true,
},
{
name: "RequestClosedPortScan",
port: 65533,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", tt.url, nil)
assert.NoError(t, err)
req.Header.Set("Accept", "application/json")
var resp *http.Response
var body []byte
if tt.quic {
resp, body, err = doQuicRequest(req)
} else {
client := &http.Client{}
resp, err = client.Do(req)
assert.NoError(t, err)
body, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
if strings.Contains(tt.url, "https://") {
assert.Equal(t, `h3=":8001"; ma=2592000`, resp.Header.Get("Alt-Svc"))
}
}
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
assert.NoError(t, json.Unmarshal(body, &router.JSONResponse{}))
assert.Equal(t, "DENY", resp.Header.Get("X-Frame-Options"))
assert.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options"))
assert.Equal(t, "1; mode=block", resp.Header.Get("X-Xss-Protection"))
})
}
for _, tt := range testsPortScan {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:8000/scan/tcp/%d", tt.port), nil)
assert.NoError(t, err)
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Real-IP", "127.0.0.1")
client := &http.Client{}
resp, err := client.Do(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
j := router.JSONScanResponse{}
assert.NoError(t, json.Unmarshal(body, &j))
assert.Equal(t, tt.want, j.Reachable)
})
}
testWhatIsMyDNS(t)
}
func doQuicRequest(req *http.Request) (*http.Response, []byte, error) {
roundTripper := &http3.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
defer roundTripper.Close()
client := &http.Client{
Transport: roundTripper,
}
resp, err := client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return resp, body, nil
} }

View File

@ -3,9 +3,11 @@ package httputils
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/textproto"
"sort" "sort"
"strings" "strings"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -32,6 +34,17 @@ func HeadersToSortedString(headers http.Header) string {
return output return output
} }
// GetHeadersWithoutTrustedHeaders return a http.Heade object with the original headers except trusted headers
func GetHeadersWithoutTrustedHeaders(ctx *gin.Context) http.Header {
h := ctx.Request.Header
for _, k := range []string{setting.App.TrustedHeader, setting.App.TrustedPortHeader} {
delete(h, textproto.CanonicalMIMEHeaderKey(k))
}
return h
}
// GetLogFormatter returns our custom log format // GetLogFormatter returns our custom log format
func GetLogFormatter(param gin.LogFormatterParams) string { func GetLogFormatter(param gin.LogFormatterParams) string {
return fmt.Sprintf("%s - [%s] \"%s %s %s\" %d %d %d %s \"%s\" \"%s\" \"%s\"\n", return fmt.Sprintf("%s - [%s] \"%s %s %s\" %d %d %d %s \"%s\" \"%s\" \"%s\"\n",
@ -50,7 +63,7 @@ func GetLogFormatter(param gin.LogFormatterParams) string {
) )
} }
func normalizeLog(log interface{}) interface{} { func normalizeLog(log any) any {
switch v := log.(type) { switch v := log.(type) {
case string: case string:
if v == "" { if v == "" {

View File

@ -9,35 +9,47 @@ import (
"time" "time"
"github.com/dcarrillo/whatismyip/internal/core" "github.com/dcarrillo/whatismyip/internal/core"
"gopkg.in/yaml.v3"
) )
type geodbPath struct { type geodbConf struct {
City string City string
ASN string ASN string
Token *string
} }
type serverSettings struct { type serverSettings struct {
ReadTimeout time.Duration ReadTimeout time.Duration
WriteTimeout time.Duration WriteTimeout time.Duration
} }
type resolver struct {
Domain string `yaml:"domain"`
ResourceRecords []string `yaml:"resource_records"`
RedirectPort string `yaml:"redirect_port,omitempty"`
Ipv4 []string `yaml:"ipv4,omitempty"`
Ipv6 []string `yaml:"ipv6,omitempty"`
}
type settings struct { type settings struct {
GeodbPath geodbPath GeodbPath geodbConf
TemplatePath string TemplatePath string
BindAddress string BindAddress string
TLSAddress string TLSAddress string
TLSCrtPath string TLSCrtPath string
TLSKeyPath string TLSKeyPath string
TrustedHeader string TrustedHeader string
TrustedPortHeader string
EnableSecureHeaders bool EnableSecureHeaders bool
EnableHTTP3 bool
Server serverSettings Server serverSettings
Resolver resolver
version bool version bool
} }
const defaultAddress = ":8080" const defaultAddress = ":8080"
// ErrVersion is the custom error triggered when -version flag is passed
var ErrVersion = errors.New("setting: version requested") var ErrVersion = errors.New("setting: version requested")
// App is the var with the parsed settings
var App = settings{ var App = settings{
// hard-coded for the time being // hard-coded for the time being
Server: serverSettings{ Server: serverSettings{
@ -46,15 +58,20 @@ var App = settings{
}, },
} }
// Setup initializes the App object parsing the flags
func Setup(args []string) (output string, err error) { func Setup(args []string) (output string, err error) {
flags := flag.NewFlagSet("whatismyip", flag.ContinueOnError) flags := flag.NewFlagSet("whatismyip", flag.ContinueOnError)
var buf bytes.Buffer var buf bytes.Buffer
var resolverConf string
flags.SetOutput(&buf) flags.SetOutput(&buf)
flags.StringVar(&App.GeodbPath.City, "geoip2-city", "", "Path to GeoIP2 city database") flags.StringVar(&App.GeodbPath.City, "geoip2-city", "", "Path to GeoIP2 city database. Enables geo information (--geoip2-asn becomes mandatory)")
flags.StringVar(&App.GeodbPath.ASN, "geoip2-asn", "", "Path to GeoIP2 ASN database") flags.StringVar(&App.GeodbPath.ASN, "geoip2-asn", "", "Path to GeoIP2 ASN database. Enables ASN information. (--geoip2-city becomes mandatory)")
flags.StringVar(&App.TemplatePath, "template", "", "Path to template file") flags.StringVar(&App.TemplatePath, "template", "", "Path to the template file")
flags.StringVar(
&resolverConf,
"resolver",
"",
"Path to the resolver configuration. It actually enables the resolver for DNS client discovery.")
flags.StringVar( flags.StringVar(
&App.BindAddress, &App.BindAddress,
"bind", "bind",
@ -69,10 +86,17 @@ func Setup(args []string) (output string, err error) {
) )
flags.StringVar(&App.TLSCrtPath, "tls-crt", "", "When using TLS, path to certificate file") flags.StringVar(&App.TLSCrtPath, "tls-crt", "", "When using TLS, path to certificate file")
flags.StringVar(&App.TLSKeyPath, "tls-key", "", "When using TLS, path to private key file") flags.StringVar(&App.TLSKeyPath, "tls-key", "", "When using TLS, path to private key file")
flags.StringVar(&App.TrustedHeader, flags.StringVar(
&App.TrustedHeader,
"trusted-header", "trusted-header",
"", "",
"Trusted request header for remote IP (e.g. X-Real-IP)", "Trusted request header for remote IP (e.g. X-Real-IP). When using this feature if -trusted-port-header is not set the client port is shown as 'unknown'",
)
flags.StringVar(
&App.TrustedPortHeader,
"trusted-port-header",
"",
"Trusted request header for remote client port (e.g. X-Real-Port). When this parameter is set -trusted-header becomes mandatory",
) )
flags.BoolVar(&App.version, "version", false, "Output version information and exit") flags.BoolVar(&App.version, "version", false, "Output version information and exit")
flags.BoolVar( flags.BoolVar(
@ -81,6 +105,12 @@ func Setup(args []string) (output string, err error) {
false, false,
"Add sane security-related headers to every response", "Add sane security-related headers to every response",
) )
flags.BoolVar(
&App.EnableHTTP3,
"enable-http3",
false,
"Enable HTTP/3 protocol. HTTP/3 requires --tls-bind set, as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server.",
)
err = flags.Parse(args) err = flags.Parse(args)
if err != nil { if err != nil {
@ -91,12 +121,20 @@ func Setup(args []string) (output string, err error) {
return fmt.Sprintf("whatismyip version %s", core.Version), ErrVersion return fmt.Sprintf("whatismyip version %s", core.Version), ErrVersion
} }
if App.GeodbPath.City == "" || App.GeodbPath.ASN == "" { if (App.GeodbPath.City != "" && App.GeodbPath.ASN == "") || (App.GeodbPath.City == "" && App.GeodbPath.ASN != "") {
return "", fmt.Errorf("geoip2-city and geoip2-asn parameters are mandatory") return "", fmt.Errorf("both --geoip2-city and --geoip2-asn are mandatory to enable geo information")
}
if App.TrustedPortHeader != "" && App.TrustedHeader == "" {
return "", fmt.Errorf("truster-header is mandatory when truster-port-header is set")
} }
if (App.TLSAddress != "") && (App.TLSCrtPath == "" || App.TLSKeyPath == "") { if (App.TLSAddress != "") && (App.TLSCrtPath == "" || App.TLSKeyPath == "") {
return "", fmt.Errorf("In order to use TLS -tls-crt and -tls-key flags are mandatory") return "", fmt.Errorf("in order to use TLS, the -tls-crt and -tls-key flags are mandatory")
}
if App.EnableHTTP3 && App.TLSAddress == "" {
return "", fmt.Errorf("in order to use HTTP3, the -tls-bind is mandatory")
} }
if App.TemplatePath != "" { if App.TemplatePath != "" {
@ -109,5 +147,21 @@ func Setup(args []string) (output string, err error) {
} }
} }
if resolverConf != "" {
var err error
App.Resolver, err = readYAML(resolverConf)
if err != nil {
return "", fmt.Errorf("error reading resolver configuration %w", err)
}
}
return buf.String(), nil return buf.String(), nil
} }
func readYAML(path string) (resolver resolver, err error) {
yamlFile, err := os.ReadFile(path)
if err != nil {
return resolver, err
}
return resolver, yaml.Unmarshal(yamlFile, &resolver)
}

View File

@ -8,75 +8,69 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestParseMandatoryFlags(t *testing.T) { func TestParseMandatoryFlags(t *testing.T) {
var mandatoryFlags = []struct { mandatoryFlags := []struct {
args []string args []string
conf settings
}{ }{
{ {
[]string{}, []string{
settings{}, "-geoip2-city", "my-city-path",
}, },
{
[]string{"-geoip2-city", "/city-path"},
settings{},
},
{
[]string{"-geoip2-asn", "/asn-path"},
settings{},
}, },
{ {
[]string{ []string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000", "-geoip2-asn", "my-asn-path",
},
},
{
[]string{
"-tls-bind", ":9000",
}, },
settings{},
}, },
{ {
[]string{ []string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000", "-tls-bind", ":9000", "-tls-crt", "/crt-path",
"-tls-crt", "/crt-path",
}, },
settings{},
}, },
{ {
[]string{ []string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000", "-tls-bind", ":9000", "-tls-key", "/key-path",
"-tls-key", "/key-path", },
},
{
[]string{
"-enable-http3",
},
},
{
[]string{
"-bind", ":8000", "-trusted-port-header", "port-header",
}, },
settings{},
}, },
} }
for _, tt := range mandatoryFlags { for _, tt := range mandatoryFlags {
t.Run(strings.Join(tt.args, " "), func(t *testing.T) { t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
_, err := Setup(tt.args) _, err := Setup(tt.args)
assert.NotNil(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "mandatory") assert.Contains(t, err.Error(), "mandatory")
}) })
} }
} }
func TestParseFlags(t *testing.T) { func TestParseFlags(t *testing.T) {
var flags = []struct { flags := []struct {
args []string args []string
conf settings conf settings
}{ }{
{ {
[]string{"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path"}, []string{},
settings{ settings{
GeodbPath: geodbPath{ BindAddress: ":8080",
City: "/city-path",
ASN: "/asn-path",
},
TemplatePath: "",
BindAddress: ":8080",
TLSAddress: "",
TLSCrtPath: "",
TLSKeyPath: "",
TrustedHeader: "",
EnableSecureHeaders: false,
Server: serverSettings{ Server: serverSettings{
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,
@ -86,17 +80,11 @@ func TestParseFlags(t *testing.T) {
{ {
[]string{"-bind", ":8001", "-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path"}, []string{"-bind", ":8001", "-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path"},
settings{ settings{
GeodbPath: geodbPath{ GeodbPath: geodbConf{
City: "/city-path", City: "/city-path",
ASN: "/asn-path", ASN: "/asn-path",
}, },
TemplatePath: "", BindAddress: ":8001",
BindAddress: ":8001",
TLSAddress: "",
TLSCrtPath: "",
TLSKeyPath: "",
TrustedHeader: "",
EnableSecureHeaders: false,
Server: serverSettings{ Server: serverSettings{
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,
@ -109,17 +97,33 @@ func TestParseFlags(t *testing.T) {
"-tls-crt", "/crt-path", "-tls-key", "/key-path", "-tls-crt", "/crt-path", "-tls-key", "/key-path",
}, },
settings{ settings{
GeodbPath: geodbPath{ GeodbPath: geodbConf{
City: "/city-path", City: "/city-path",
ASN: "/asn-path", ASN: "/asn-path",
}, },
TemplatePath: "", BindAddress: ":8080",
BindAddress: ":8080", TLSAddress: ":9000",
TLSAddress: ":9000", TLSCrtPath: "/crt-path",
TLSCrtPath: "/crt-path", TLSKeyPath: "/key-path",
TLSKeyPath: "/key-path", Server: serverSettings{
TrustedHeader: "", ReadTimeout: 10 * time.Second,
EnableSecureHeaders: false, WriteTimeout: 10 * time.Second,
},
},
},
{
[]string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path",
"-trusted-header", "header", "-trusted-port-header", "port-header",
},
settings{
GeodbPath: geodbConf{
City: "/city-path",
ASN: "/asn-path",
},
BindAddress: ":8080",
TrustedHeader: "header",
TrustedPortHeader: "port-header",
Server: serverSettings{ Server: serverSettings{
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,
@ -132,15 +136,11 @@ func TestParseFlags(t *testing.T) {
"-trusted-header", "header", "-enable-secure-headers", "-trusted-header", "header", "-enable-secure-headers",
}, },
settings{ settings{
GeodbPath: geodbPath{ GeodbPath: geodbConf{
City: "/city-path", City: "/city-path",
ASN: "/asn-path", ASN: "/asn-path",
}, },
TemplatePath: "",
BindAddress: ":8080", BindAddress: ":8080",
TLSAddress: "",
TLSCrtPath: "",
TLSKeyPath: "",
TrustedHeader: "header", TrustedHeader: "header",
EnableSecureHeaders: true, EnableSecureHeaders: true,
Server: serverSettings{ Server: serverSettings{
@ -154,14 +154,14 @@ func TestParseFlags(t *testing.T) {
for _, tt := range flags { for _, tt := range flags {
t.Run(strings.Join(tt.args, " "), func(t *testing.T) { t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
_, err := Setup(tt.args) _, err := Setup(tt.args)
assert.Nil(t, err) require.Nil(t, err)
assert.True(t, reflect.DeepEqual(App, tt.conf)) assert.True(t, reflect.DeepEqual(App, tt.conf))
}) })
} }
} }
func TestParseFlagsUsage(t *testing.T) { func TestParseFlagsUsage(t *testing.T) {
var usageArgs = []string{"-help", "-h", "--help"} usageArgs := []string{"-help", "-h", "--help"}
for _, arg := range usageArgs { for _, arg := range usageArgs {
t.Run(arg, func(t *testing.T) { t.Run(arg, func(t *testing.T) {
@ -179,19 +179,28 @@ func TestParseFlagVersion(t *testing.T) {
} }
func TestParseFlagTemplate(t *testing.T) { func TestParseFlagTemplate(t *testing.T) {
flags := []string{ testCases := []struct {
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", name string
"-template", "/template-path", flags []string
errMsg string
}{
{
name: "Invalid template path",
flags: []string{"-template", "/template-path"},
errMsg: "no such file or directory",
},
{
name: "Template path is a directory",
flags: []string{"-template", "/"},
errMsg: "must be a file",
},
} }
_, err := Setup(flags)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no such file or directory")
flags = []string{ for _, tc := range testCases {
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", t.Run(tc.name, func(t *testing.T) {
"-template", "/", _, err := Setup(tc.flags)
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errMsg)
})
} }
_, err = Setup(flags)
assert.Error(t, err)
assert.Contains(t, err.Error(), "must be a file")
} }

View File

@ -0,0 +1,10 @@
package uuid
import (
"github.com/google/uuid"
)
func IsValid(u string) bool {
_, err := uuid.Parse(u)
return err == nil
}

View File

@ -0,0 +1,37 @@
package uuid
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsValid(t *testing.T) {
tests := []struct {
name string
u string
want bool
}{
{
name: "Valid UUID",
u: "3b241101-e2bb-4255-8caf-4136c566a964",
want: true,
},
{
name: "Invalid UUID",
u: "invalid-uuid",
want: false,
},
{
name: "Empty string",
u: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.True(t, IsValid(tt.u) == tt.want)
})
}
}

View File

@ -1,13 +1,13 @@
package models package models
import ( import (
"fmt"
"log" "log"
"net" "net"
"github.com/oschwald/maxminddb-golang" "github.com/oschwald/maxminddb-golang"
) )
// GeoRecord is the model for City database
type GeoRecord struct { type GeoRecord struct {
Country struct { Country struct {
ISOCode string `maxminddb:"iso_code"` ISOCode string `maxminddb:"iso_code"`
@ -26,60 +26,107 @@ type GeoRecord struct {
} `maxminddb:"postal"` } `maxminddb:"postal"`
} }
// ASNRecord is the model for ASN database
type ASNRecord struct { type ASNRecord struct {
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"` AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
} }
type geodb struct { type GeoDB struct {
city *maxminddb.Reader cityPath string
asn *maxminddb.Reader asnPath string
City *maxminddb.Reader
ASN *maxminddb.Reader
} }
var db geodb func Setup(cityPath string, asnPath string) (*GeoDB, error) {
city, asn, err := openDatabases(cityPath, asnPath)
if err != nil {
return nil, err
}
func openMMDB(path string) *maxminddb.Reader { return &GeoDB{
cityPath: cityPath,
asnPath: asnPath,
City: city,
ASN: asn,
}, nil
}
func (db *GeoDB) CloseDBs() error {
var errs []error
if db.City != nil {
if err := db.City.Close(); err != nil {
errs = append(errs, fmt.Errorf("closing city db: %w", err))
}
}
if db.ASN != nil {
if err := db.ASN.Close(); err != nil {
errs = append(errs, fmt.Errorf("closing ASN db: %w", err))
}
}
if len(errs) > 0 {
return fmt.Errorf("errors closing databases: %s", errs)
}
return nil
}
func (db *GeoDB) Reload() error {
if err := db.CloseDBs(); err != nil {
return fmt.Errorf("closing existing connections: %w", err)
}
city, asn, err := openDatabases(db.cityPath, db.asnPath)
if err != nil {
return fmt.Errorf("opening new connections: %w", err)
}
db.City = city
db.ASN = asn
return nil
}
func (db *GeoDB) LookupCity(ip net.IP) (*GeoRecord, error) {
record := &GeoRecord{}
err := db.City.Lookup(ip, record)
if err != nil {
return nil, err
}
return record, nil
}
func (db *GeoDB) LookupASN(ip net.IP) (*ASNRecord, error) {
record := &ASNRecord{}
err := db.ASN.Lookup(ip, record)
if err != nil {
return nil, err
}
return record, nil
}
func openDatabases(cityPath, asnPath string) (*maxminddb.Reader, *maxminddb.Reader, error) {
city, err := openMMDB(cityPath)
if err != nil {
return nil, nil, err
}
asn, err := openMMDB(asnPath)
if err != nil {
return nil, nil, err
}
return city, asn, nil
}
func openMMDB(path string) (*maxminddb.Reader, error) {
db, err := maxminddb.Open(path) db, err := maxminddb.Open(path)
if err != nil { if err != nil {
log.Fatal(err) return nil, err
} }
log.Printf("Database %s has been loaded\n", path) log.Printf("Database %s has been loaded\n", path)
return db return db, nil
}
// Setup opens all Geolite2 databases
func Setup(cityPath string, asnPath string) {
db.city = openMMDB(cityPath)
db.asn = openMMDB(asnPath)
}
// CloseDBs unmaps from memory and frees resources to the filesystem
func CloseDBs() {
log.Printf("Closing dbs...")
if err := db.city.Close(); err != nil {
log.Printf("Error closing city db: %s", err)
}
if err := db.asn.Close(); err != nil {
log.Printf("Error closing ASN db: %s", err)
}
}
// LookUp an IP and get city data
func (record *GeoRecord) LookUp(ip net.IP) error {
if err := db.city.Lookup(ip, record); err != nil {
return err
}
return nil
}
// LookUp an IP and get ASN data
func (record *ASNRecord) LookUp(ip net.IP) error {
if err := db.asn.Lookup(ip, record); err != nil {
return err
}
return nil
} }

View File

@ -1,10 +1,12 @@
package models package models
import ( import (
"fmt"
"net" "net"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestModels(t *testing.T) { func TestModels(t *testing.T) {
@ -59,19 +61,21 @@ func TestModels(t *testing.T) {
AutonomousSystemOrganization: "IP-Only", AutonomousSystemOrganization: "IP-Only",
} }
Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb") db, err := Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
defer CloseDBs() require.NoError(t, err, fmt.Sprintf("Error setting up db: %s", err))
defer db.CloseDBs()
assert.NotNil(t, db.ASN)
assert.NotNil(t, db.City)
assert.NotNil(t, db.asn) cityRecord, err := db.LookupCity(net.ParseIP("81.2.69.192"))
assert.NotNil(t, db.city) require.NoError(t, err, fmt.Sprintf("Error looking up city: %s", err))
cityRecord := &GeoRecord{}
assert.Nil(t, cityRecord.LookUp(net.ParseIP("81.2.69.192")))
assert.Equal(t, expectedCity, cityRecord) assert.Equal(t, expectedCity, cityRecord)
assert.Error(t, cityRecord.LookUp(net.ParseIP("error"))) _, err = db.LookupCity(net.ParseIP("error"))
assert.Error(t, err)
asnRecord := &ASNRecord{} asnRecord, err := db.LookupASN(net.ParseIP("82.99.17.64"))
assert.Nil(t, asnRecord.LookUp(net.ParseIP("82.99.17.64"))) require.NoError(t, err, fmt.Sprintf("Error looking up asn: %s", err))
assert.Equal(t, expectedASN, asnRecord) assert.Equal(t, expectedASN, asnRecord)
assert.Error(t, asnRecord.LookUp(net.ParseIP("error"))) _, err = db.LookupASN(net.ParseIP("error"))
assert.Error(t, err)
} }

165
resolver/setup.go Normal file
View File

@ -0,0 +1,165 @@
package resolver
import (
"log"
"net"
"strings"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/dcarrillo/whatismyip/internal/validator/uuid"
"github.com/miekg/dns"
"github.com/patrickmn/go-cache"
)
type Resolver struct {
handler *dns.ServeMux
store *cache.Cache
domain string
rr []string
ipv4 []net.IP
ipv6 []net.IP
}
func ensureDotSuffix(s string) string {
if !strings.HasSuffix(s, ".") {
return s + "."
}
return s
}
func Setup(store *cache.Cache) *Resolver {
var ipv4, ipv6 []net.IP
for _, ip := range setting.App.Resolver.Ipv4 {
ipv4 = append(ipv4, net.ParseIP(ip))
}
for _, ip := range setting.App.Resolver.Ipv6 {
ipv6 = append(ipv6, net.ParseIP(ip))
}
resolver := &Resolver{
handler: dns.NewServeMux(),
store: store,
domain: ensureDotSuffix(setting.App.Resolver.Domain),
rr: setting.App.Resolver.ResourceRecords,
ipv4: ipv4,
ipv6: ipv6,
}
resolver.handler.HandleFunc(resolver.domain, resolver.resolve)
resolver.handler.HandleFunc(".", resolver.blackHole)
return resolver
}
func (rsv *Resolver) Handler() *dns.ServeMux {
return rsv.handler
}
func (rsv *Resolver) blackHole(w dns.ResponseWriter, r *dns.Msg) {
msg := startReply(r)
msg.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(msg)
logger(w, r.Question[0], msg.Rcode)
}
func (rsv *Resolver) resolve(w dns.ResponseWriter, r *dns.Msg) {
msg := startReply(r)
q := r.Question[0]
ip, _, _ := net.SplitHostPort(w.RemoteAddr().String())
for _, res := range rsv.rr {
t := strings.Split(res, " ")[2]
if q.Qtype == dns.StringToType[t] {
brr, err := buildRR(rsv.domain + " " + res)
if err != nil {
msg.SetRcode(r, dns.RcodeServerFailure)
logger(w, q, msg.Rcode, err.Error())
} else {
msg.Answer = append(msg.Answer, brr)
logger(w, q, msg.Rcode)
}
w.WriteMsg(msg)
return
}
}
lowerName := strings.ToLower(q.Name) // lowercase because of dns-0x20
subDomain := strings.Split(lowerName, ".")[0]
switch {
case uuid.IsValid(subDomain):
msg.SetRcode(r, rsv.getIP(q, msg))
rsv.store.Add(subDomain, ip, cache.DefaultExpiration)
case lowerName == rsv.domain:
msg.SetRcode(r, rsv.getIP(q, msg))
default:
msg.SetRcode(r, dns.RcodeRefused)
}
w.WriteMsg(msg)
logger(w, q, msg.Rcode)
}
func (rsv *Resolver) getIP(question dns.Question, msg *dns.Msg) int {
if question.Qtype == dns.TypeA && len(rsv.ipv4) > 0 {
for _, ip := range rsv.ipv4 {
msg.Answer = append(msg.Answer, &dns.A{
Hdr: setHdr(question),
A: ip,
})
}
return dns.RcodeSuccess
}
if question.Qtype == dns.TypeAAAA && len(rsv.ipv6) > 0 {
for _, ip := range rsv.ipv6 {
msg.Answer = append(msg.Answer, &dns.AAAA{
Hdr: setHdr(question),
AAAA: ip,
})
}
return dns.RcodeSuccess
}
return dns.RcodeRefused
}
func buildRR(rrs string) (dns.RR, error) {
rr, err := dns.NewRR(rrs)
if err != nil {
return nil, err
}
return rr, nil
}
func setHdr(q dns.Question) dns.RR_Header {
return dns.RR_Header{
Name: q.Name,
Rrtype: q.Qtype,
Class: dns.ClassINET,
Ttl: 60,
}
}
func startReply(r *dns.Msg) *dns.Msg {
msg := new(dns.Msg)
msg.SetReply(r)
msg.Authoritative = true
return msg
}
func logger(w dns.ResponseWriter, q dns.Question, code int, err ...string) {
emsg := ""
if len(err) > 0 {
emsg = " - " + strings.Join(err, " ")
}
ip, _, _ := net.SplitHostPort(w.RemoteAddr().String())
log.Printf(
"DNS %s - %s - %s - %s%s",
ip,
dns.TypeToString[q.Qtype],
q.Name,
dns.RcodeToString[code],
emsg,
)
}

97
router/dns.go Normal file
View File

@ -0,0 +1,97 @@
package router
import (
"fmt"
"net"
"net/http"
"strings"
validator "github.com/dcarrillo/whatismyip/internal/validator/uuid"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/patrickmn/go-cache"
)
type DNSJSONResponse struct {
DNS dnsData `json:"dns"`
}
type dnsGeoData struct {
Country string `json:"country,omitempty"`
AsnOrganization string `json:"provider,omitempty"`
}
type dnsData struct {
IP string `json:"ip"`
dnsGeoData
}
// TODO
// Implement a proper vhost manager instead of using a middleware
func GetDNSDiscoveryHandler(store *cache.Cache, domain string, redirectPort string) gin.HandlerFunc {
return func(ctx *gin.Context) {
if !strings.HasSuffix(ctx.Request.Host, domain) {
ctx.Next()
return
}
if ctx.Request.Host == domain && ctx.Request.URL.Path == "/" {
ctx.Redirect(http.StatusFound, fmt.Sprintf("http://%s.%s%s", uuid.New().String(), domain, redirectPort))
ctx.Abort()
return
}
handleDNS(ctx, store)
ctx.Abort()
}
}
func handleDNS(ctx *gin.Context, store *cache.Cache) {
d := strings.Split(ctx.Request.Host, ".")[0]
if !validator.IsValid(d) {
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
return
}
v, found := store.Get(d)
if !found {
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
return
}
ipStr, ok := v.(string)
if !ok {
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
return
}
ip := net.ParseIP(ipStr)
if ip == nil {
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
return
}
geoResp := dnsGeoData{}
if geoSvc != nil {
cityRecord := geoSvc.LookUpCity(ip)
asnRecord := geoSvc.LookUpASN(ip)
geoResp = dnsGeoData{
Country: cityRecord.Country.Names["en"],
AsnOrganization: asnRecord.AutonomousSystemOrganization,
}
}
j := DNSJSONResponse{
DNS: dnsData{
IP: ipStr,
dnsGeoData: geoResp,
},
}
switch ctx.NegotiateFormat(gin.MIMEPlain, gin.MIMEHTML, gin.MIMEJSON) {
case gin.MIMEJSON:
ctx.JSON(http.StatusOK, j)
default:
ctx.String(http.StatusOK, fmt.Sprintf("%s (%s / %s)\n", j.DNS.IP, j.DNS.Country, j.DNS.AsnOrganization))
}
}

153
router/dns_test.go Normal file
View File

@ -0,0 +1,153 @@
package router
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
validator "github.com/dcarrillo/whatismyip/internal/validator/uuid"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/patrickmn/go-cache"
"github.com/stretchr/testify/assert"
)
func TestGetDNSDiscoveryHandler(t *testing.T) {
store := cache.New(cache.NoExpiration, cache.NoExpiration)
handler := GetDNSDiscoveryHandler(store, domain, "")
t.Run("calls next if host does not have domain suffix", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set(trustedHeader, testIP.ipv4)
req.Host = "example.com"
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
handler(c)
app.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, testIP.ipv4+"\n", w.Body.String())
})
t.Run("return 404 if there is a path", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/path", nil)
req.Host = domain
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
handler(c)
app.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
})
t.Run("redirects if host is domain", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Host = domain
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
handler(c)
assert.Equal(t, http.StatusFound, w.Code)
r, err := url.Parse(w.Header().Get("Location"))
assert.NoError(t, err)
assert.True(t, validator.IsValid(strings.Split(r.Host, ".")[0]))
assert.Equal(t, domain, strings.Join(strings.Split(r.Host, ".")[1:], "."))
})
}
func TestHandleDNS(t *testing.T) {
store := cache.New(cache.NoExpiration, cache.NoExpiration)
u := uuid.New().String()
tests := []struct {
name string
subDomain string
stored any
}{
{
name: "not found if the subdomain is not a valid uuid",
subDomain: "not-uuid",
stored: "",
},
{
name: "not found if the ip is not found in the store",
subDomain: u,
stored: "",
},
{
name: "not found if the ip is in store but is not valid",
subDomain: u,
stored: "bogus",
},
{
name: "not found if the store contains no string",
subDomain: u,
stored: 20,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Host = tt.subDomain + "." + domain
if tt.stored != "" {
store.Add(tt.subDomain, tt.stored, cache.DefaultExpiration)
}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
handleDNS(c, store)
assert.Equal(t, http.StatusNotFound, w.Code)
})
}
}
func TestAcceptDNSRequest(t *testing.T) {
store := cache.New(cache.NoExpiration, cache.NoExpiration)
tests := []struct {
name string
accept string
want string
}{
{
name: "returns json dns data",
accept: "application/json",
want: jsonDNSIPv4,
},
{
name: "return plan text dns data",
accept: "text/plain",
want: plainDNSIPv4,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
u := uuid.New().String()
req.Host = u + "." + domain
req.Header.Add("Accept", tt.accept)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
store.Add(u, testIP.ipv4, cache.DefaultExpiration)
handleDNS(c, store)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, tt.want, w.Body.String())
})
}
}

View File

@ -4,68 +4,83 @@ import (
"net" "net"
"net/http" "net/http"
"path/filepath" "path/filepath"
"regexp"
"github.com/dcarrillo/whatismyip/internal/httputils" "github.com/dcarrillo/whatismyip/internal/httputils"
"github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/internal/setting"
"github.com/dcarrillo/whatismyip/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
const userAgentPattern = `curl|wget|libwww-perl|python|ansible-httpget|HTTPie|WindowsPowerShell|http_request|Go-http-client|^$` type GeoResponse struct {
Country string `json:"country,omitempty"`
CountryCode string `json:"country_code,omitempty"`
City string `json:"city,omitempty"`
Latitude float64 `json:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty"`
PostalCode string `json:"postal_code,omitempty"`
TimeZone string `json:"time_zone,omitempty"`
ASN uint `json:"asn,omitempty"`
ASNOrganization string `json:"asn_organization,omitempty"`
}
// JSONResponse maps data as json
type JSONResponse struct { type JSONResponse struct {
IP string `json:"ip"` IP string `json:"ip"`
IPVersion byte `json:"ip_version"` IPVersion byte `json:"ip_version"`
ClientPort string `json:"client_port"` ClientPort string `json:"client_port"`
Country string `json:"country"` Host string `json:"host"`
CountryCode string `json:"country_code"` Headers http.Header `json:"headers"`
City string `json:"city"` GeoResponse
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
PostalCode string `json:"postal_code"`
TimeZone string `json:"time_zone"`
ASN uint `json:"asn"`
ASNOrganization string `json:"asn_organization"`
Host string `json:"host"`
Headers http.Header `json:"headers"`
} }
func getRoot(ctx *gin.Context) { func getRoot(ctx *gin.Context) {
reg := regexp.MustCompile(userAgentPattern) switch ctx.NegotiateFormat(gin.MIMEPlain, gin.MIMEHTML, gin.MIMEJSON) {
if reg.Match([]byte(ctx.Request.UserAgent())) { case gin.MIMEHTML:
ctx.String(http.StatusOK, ctx.ClientIP())
} else {
name := "home" name := "home"
if setting.App.TemplatePath != "" { if setting.App.TemplatePath != "" {
name = filepath.Base(setting.App.TemplatePath) name = filepath.Base(setting.App.TemplatePath)
} }
ctx.HTML(http.StatusOK, name, jsonOutput(ctx)) ctx.HTML(http.StatusOK, name, jsonOutput(ctx))
case gin.MIMEJSON:
getJSON(ctx)
default:
ctx.String(http.StatusOK, ctx.ClientIP()+"\n")
} }
} }
func getClientPort(ctx *gin.Context) string {
var port string
if setting.App.TrustedPortHeader == "" {
if setting.App.TrustedHeader != "" {
port = "unknown"
} else {
_, port, _ = net.SplitHostPort(ctx.Request.RemoteAddr)
}
} else {
port = ctx.GetHeader(setting.App.TrustedPortHeader)
if port == "" {
port = "unknown"
}
}
return port
}
func getClientPortAsString(ctx *gin.Context) { func getClientPortAsString(ctx *gin.Context) {
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) ctx.String(http.StatusOK, getClientPort(ctx)+"\n")
ctx.String(http.StatusOK, port+"\n")
} }
func getAllAsString(ctx *gin.Context) { func getAllAsString(ctx *gin.Context) {
output := "IP: " + ctx.ClientIP() + "\n" ip := net.ParseIP(ctx.ClientIP())
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
output += "Client Port: " + port + "\n"
r := service.Geo{IP: net.ParseIP(ctx.ClientIP())} output := "IP: " + ip.String() + "\n"
if record := r.LookUpCity(); record != nil { output += "Client Port: " + getClientPort(ctx) + "\n"
output += geoCityRecordToString(record) + "\n"
if geoSvc != nil {
output += geoCityRecordToString(geoSvc.LookUpCity(ip)) + "\n"
output += geoASNRecordToString(geoSvc.LookUpASN(ip)) + "\n"
} }
if record := r.LookUpASN(); record != nil { h := httputils.GetHeadersWithoutTrustedHeaders(ctx)
output += geoASNRecordToString(record) + "\n" h.Set("Host", ctx.Request.Host)
}
h := ctx.Request.Header
h["Host"] = []string{ctx.Request.Host}
output += httputils.HeadersToSortedString(h) output += httputils.HeadersToSortedString(h)
ctx.String(http.StatusOK, output) ctx.String(http.StatusOK, output)
@ -76,29 +91,37 @@ func getJSON(ctx *gin.Context) {
} }
func jsonOutput(ctx *gin.Context) JSONResponse { func jsonOutput(ctx *gin.Context) JSONResponse {
ip := service.Geo{IP: net.ParseIP(ctx.ClientIP())} ip := net.ParseIP(ctx.ClientIP())
asnRecord := ip.LookUpASN()
cityRecord := ip.LookUpCity()
var version byte = 4 var version byte = 4
if p := net.ParseIP(ctx.ClientIP()).To4(); p == nil { if p := ip.To4(); p == nil {
version = 6 version = 6
} }
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) geoResp := GeoResponse{}
if geoSvc != nil {
cityRecord := geoSvc.LookUpCity(ip)
asnRecord := geoSvc.LookUpASN(ip)
geoResp = GeoResponse{
Country: cityRecord.Country.Names["en"],
CountryCode: cityRecord.Country.ISOCode,
City: cityRecord.City.Names["en"],
Latitude: cityRecord.Location.Latitude,
Longitude: cityRecord.Location.Longitude,
PostalCode: cityRecord.Postal.Code,
TimeZone: cityRecord.Location.TimeZone,
ASN: asnRecord.AutonomousSystemNumber,
ASNOrganization: asnRecord.AutonomousSystemOrganization,
}
}
return JSONResponse{ return JSONResponse{
IP: ctx.ClientIP(), IP: ip.String(),
IPVersion: version, IPVersion: version,
ClientPort: port, ClientPort: getClientPort(ctx),
Country: cityRecord.Country.Names["en"], Host: ctx.Request.Host,
CountryCode: cityRecord.Country.ISOCode, Headers: httputils.GetHeadersWithoutTrustedHeaders(ctx),
City: cityRecord.City.Names["en"], GeoResponse: geoResp,
Latitude: cityRecord.Location.Latitude,
Longitude: cityRecord.Location.Longitude,
PostalCode: cityRecord.Postal.Code,
TimeZone: cityRecord.Location.TimeZone,
ASN: asnRecord.AutonomousSystemNumber,
ASNOrganization: asnRecord.AutonomousSystemOrganization,
Host: ctx.Request.Host,
Headers: ctx.Request.Header,
} }
} }

View File

@ -6,34 +6,84 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestIP4RootFromCli(t *testing.T) { func TestRootContentType(t *testing.T) {
uas := []string{ tests := []struct {
"", name string
"curl", accepted string
"wget", expected string
"libwww-perl", }{
"python", {
"ansible-httpget", name: "Accept wildcard",
"HTTPie", accepted: "*/*",
"WindowsPowerShell", expected: contentType.text,
"http_request", },
"Go-http-client", {
name: "Bogus accept",
accepted: "bogus/plain",
expected: contentType.text,
},
{
name: "Accept plain text",
accepted: "text/plain",
expected: contentType.text,
},
{
name: "Accept json",
accepted: "application/json",
expected: contentType.json,
},
} }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set(trustedHeader, testIP.ipv4)
req.Header.Set("Accept", tt.accepted)
req, _ := http.NewRequest("GET", "/", nil) w := httptest.NewRecorder()
req.Header.Set("X-Real-IP", testIP.ipv4) app.ServeHTTP(w, req)
for _, ua := range uas { assert.Equal(t, 200, w.Code)
req.Header.Set("User-Agent", ua) assert.Equal(t, tt.expected, w.Header().Get("Content-Type"))
})
}
}
w := httptest.NewRecorder() func TestGetIP(t *testing.T) {
app.ServeHTTP(w, req) expected := testIP.ipv4 + "\n"
tests := []struct {
name string
accepted string
}{
{
name: "No browser",
accepted: "*/*",
},
{
name: "Bogus accept",
accepted: "bogus/plain",
},
{
name: "Plain accept",
accepted: "text/plain",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set(trustedHeader, testIP.ipv4)
req.Header.Set("Accept", tt.accepted)
assert.Equal(t, 200, w.Code) w := httptest.NewRecorder()
assert.Equal(t, testIP.ipv4, w.Body.String()) app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, expected, w.Body.String())
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
})
} }
} }
@ -48,16 +98,76 @@ func TestHost(t *testing.T) {
} }
func TestClientPort(t *testing.T) { func TestClientPort(t *testing.T) {
req, _ := http.NewRequest("GET", "/client-port", nil) type args struct {
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000") params []string
req.Header.Set("X-Real-IP", testIP.ipv4) headers map[string][]string
}
tests := []struct {
name string
args args
expected string
}{
{
name: "No trusted headers set",
expected: "1000\n",
},
{
name: "Trusted header only set",
args: args{
params: []string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
},
},
expected: "unknown\n",
},
{
name: "Trusted and port header set but not included in headers",
args: args{
params: []string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
},
},
expected: "unknown\n",
},
{
name: "Trusted and port header set and included in headers",
args: args{
params: []string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
},
headers: map[string][]string{
trustedHeader: {testIP.ipv4},
trustedPortHeader: {"1001"},
},
},
expected: "1001\n",
},
}
w := httptest.NewRecorder() for _, tt := range tests {
app.ServeHTTP(w, req) _, _ = setting.Setup(tt.args.params)
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/client-port", nil)
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
req.Header = tt.args.headers
assert.Equal(t, 200, w.Code) w := httptest.NewRecorder()
assert.Equal(t, contentType.text, w.Header().Get("Content-Type")) app.ServeHTTP(w, req)
assert.Equal(t, "1000\n", w.Body.String())
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, tt.expected, w.Body.String())
t.Log(w.Header())
})
}
} }
func TestNotFound(t *testing.T) { func TestNotFound(t *testing.T) {
@ -71,36 +181,59 @@ func TestNotFound(t *testing.T) {
} }
func TestJSON(t *testing.T) { func TestJSON(t *testing.T) {
expectedIPv4 := `{"client_port":"1000","ip":"81.2.69.192","ip_version":4,"country":"United Kingdom","country_code":"GB","city":"London","latitude":51.5142,"longitude":-0.0931,"postal_code":"","time_zone":"Europe/London","asn":0,"asn_organization":"","host":"test","headers":{"X-Real-Ip":["81.2.69.192"]}}` _, _ = setting.Setup(
expectedIPv6 := `{"asn":3352, "asn_organization":"TELEFONICA DE ESPANA", "city":"", "client_port":"1000", "country":"", "country_code":"", "headers":{"X-Real-Ip":["2a02:9000::1"]}, "host":"test", "ip":"2a02:9000::1", "ip_version":6, "latitude":0, "longitude":0, "postal_code":"", "time_zone":""}` []string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
},
)
req, _ := http.NewRequest("GET", "/json", nil) type args struct {
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000") ip string
req.Host = "test" }
req.Header.Set("X-Real-IP", testIP.ipv4) tests := []struct {
name string
args args
expected string
}{
{
name: "IPv4",
args: args{
ip: testIP.ipv4,
},
expected: jsonIPv4,
},
{
name: "IPv6",
args: args{
ip: testIP.ipv6,
},
expected: jsonIPv6,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/json", nil)
req.RemoteAddr = net.JoinHostPort(tt.args.ip, "1000")
req.Host = "test"
req.Header.Set(trustedHeader, tt.args.ip)
req.Header.Set(trustedPortHeader, "1001")
w := httptest.NewRecorder() w := httptest.NewRecorder()
app.ServeHTTP(w, req) app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.json, w.Header().Get("Content-Type")) assert.Equal(t, contentType.json, w.Header().Get("Content-Type"))
assert.JSONEq(t, expectedIPv4, w.Body.String()) assert.JSONEq(t, tt.expected, w.Body.String())
})
req.RemoteAddr = net.JoinHostPort(testIP.ipv6, "1000") }
req.Host = "test"
req.Header.Set("X-Real-IP", testIP.ipv6)
w = httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.json, w.Header().Get("Content-Type"))
assert.JSONEq(t, expectedIPv6, w.Body.String())
} }
func TestAll(t *testing.T) { func TestAll(t *testing.T) {
expected := `IP: 81.2.69.192 expected := `IP: 81.2.69.192
Client Port: 1000 Client Port: 1001
City: London City: London
Country: United Kingdom Country: United Kingdom
Country Code: GB Country Code: GB
@ -114,13 +247,21 @@ ASN Organization:
Header1: one Header1: one
Host: test Host: test
X-Real-Ip: 81.2.69.192
` `
_, _ = setting.Setup(
[]string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
},
)
req, _ := http.NewRequest("GET", "/all", nil) req, _ := http.NewRequest("GET", "/all", nil)
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000") req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
req.Host = "test" req.Host = "test"
req.Header.Set("X-Real-IP", testIP.ipv4) req.Header.Set(trustedHeader, testIP.ipv4)
req.Header.Set(trustedPortHeader, "1001")
req.Header.Set("Header1", "one") req.Header.Set("Header1", "one")
w := httptest.NewRecorder() w := httptest.NewRecorder()

View File

@ -8,7 +8,6 @@ import (
"strings" "strings"
"github.com/dcarrillo/whatismyip/models" "github.com/dcarrillo/whatismyip/models"
"github.com/dcarrillo/whatismyip/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -83,10 +82,13 @@ var asnOutput = map[string]asnDataFormatter{
} }
func getGeoAsString(ctx *gin.Context) { func getGeoAsString(ctx *gin.Context) {
field := strings.ToLower(ctx.Params.ByName("field")) if geoSvc == nil {
ip := service.Geo{IP: net.ParseIP(ctx.ClientIP())} ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
record := ip.LookUpCity() return
}
field := strings.ToLower(ctx.Params.ByName("field"))
record := geoSvc.LookUpCity(net.ParseIP(ctx.ClientIP()))
if field == "" { if field == "" {
ctx.String(http.StatusOK, geoCityRecordToString(record)) ctx.String(http.StatusOK, geoCityRecordToString(record))
} else if g, ok := geoOutput[field]; ok { } else if g, ok := geoOutput[field]; ok {
@ -97,10 +99,12 @@ func getGeoAsString(ctx *gin.Context) {
} }
func getASNAsString(ctx *gin.Context) { func getASNAsString(ctx *gin.Context) {
if geoSvc == nil {
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
return
}
field := strings.ToLower(ctx.Params.ByName("field")) field := strings.ToLower(ctx.Params.ByName("field"))
ip := service.Geo{IP: net.ParseIP(ctx.ClientIP())} record := geoSvc.LookUpASN(net.ParseIP(ctx.ClientIP()))
record := ip.LookUpASN()
if field == "" { if field == "" {
ctx.String(http.StatusOK, geoASNRecordToString(record)) ctx.String(http.StatusOK, geoASNRecordToString(record))
} else if g, ok := asnOutput[field]; ok { } else if g, ok := asnOutput[field]; ok {

View File

@ -10,15 +10,17 @@ import (
) )
func getHeadersAsSortedString(ctx *gin.Context) { func getHeadersAsSortedString(ctx *gin.Context) {
h := ctx.Request.Header h := httputils.GetHeadersWithoutTrustedHeaders(ctx)
h["Host"] = []string{ctx.Request.Host} h.Set("Host", ctx.Request.Host)
ctx.String(http.StatusOK, httputils.HeadersToSortedString(h)) ctx.String(http.StatusOK, httputils.HeadersToSortedString(h))
} }
func getHeaderAsString(ctx *gin.Context) { func getHeaderAsString(ctx *gin.Context) {
headers := httputils.GetHeadersWithoutTrustedHeaders(ctx)
h := ctx.Params.ByName("header") h := ctx.Params.ByName("header")
if v := ctx.GetHeader(h); v != "" { if v := headers.Get(ctx.Params.ByName("header")); v != "" {
ctx.String(http.StatusOK, template.HTMLEscapeString(v)) ctx.String(http.StatusOK, template.HTMLEscapeString(v))
} else if strings.ToLower(h) == "host" { } else if strings.ToLower(h) == "host" {
ctx.String(http.StatusOK, template.HTMLEscapeString(ctx.Request.Host)) ctx.String(http.StatusOK, template.HTMLEscapeString(ctx.Request.Host))

View File

@ -5,6 +5,7 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -26,13 +27,20 @@ Header2: value22
Header3: value3 Header3: value3
Host: Host:
` `
_, _ = setting.Setup([]string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
})
req, _ := http.NewRequest("GET", "/headers", nil) req, _ := http.NewRequest("GET", "/headers", nil)
req.Header = map[string][]string{ req.Header = map[string][]string{
"Header1": {"value1"}, "Header1": {"value1"},
"Header2": {"value21", "value22"}, "Header2": {"value21", "value22"},
"Header3": {"value3"}, "Header3": {"value3"},
} }
req.Header.Set(trustedHeader, "1.1.1.1")
req.Header.Set(trustedPortHeader, "1025")
w := httptest.NewRecorder() w := httptest.NewRecorder()
app.ServeHTTP(w, req) app.ServeHTTP(w, req)

54
router/port_scanner.go Normal file
View File

@ -0,0 +1,54 @@
package router
import (
"fmt"
"net"
"net/http"
"strconv"
"github.com/dcarrillo/whatismyip/service"
"github.com/gin-gonic/gin"
)
type JSONScanResponse struct {
IP string `json:"ip"`
Port int `json:"port"`
Reachable bool `json:"reachable"`
Reason string `json:"reason"`
}
func scanTCPPort(ctx *gin.Context) {
port, err := strconv.Atoi(ctx.Params.ByName("port"))
if err == nil && (port < 1 || port > 65535) {
err = fmt.Errorf("%d is not a valid port number", port)
}
if err != nil {
ctx.JSON(http.StatusBadRequest, JSONScanResponse{
Reason: err.Error(),
})
return
}
add := net.TCPAddr{
IP: net.ParseIP(ctx.ClientIP()),
Port: port,
}
scan := service.PortScanner{
Address: &add,
}
isOpen, err := scan.IsPortOpen()
reason := ""
if err != nil {
reason = err.Error()
}
response := JSONScanResponse{
IP: ctx.ClientIP(),
Port: port,
Reachable: isOpen,
Reason: reason,
}
ctx.JSON(http.StatusOK, response)
}

View File

@ -5,10 +5,12 @@ import (
"log" "log"
"github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/internal/setting"
"github.com/dcarrillo/whatismyip/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// SetupTemplate reads and parses a template from file var geoSvc *service.Geo
func SetupTemplate(r *gin.Engine) { func SetupTemplate(r *gin.Engine) {
if setting.App.TemplatePath == "" { if setting.App.TemplatePath == "" {
t, _ := template.New("home").Parse(home) t, _ := template.New("home").Parse(home)
@ -19,9 +21,10 @@ func SetupTemplate(r *gin.Engine) {
} }
} }
// Setup defines the endpoints func Setup(r *gin.Engine, geo *service.Geo) {
func Setup(r *gin.Engine) { geoSvc = geo
r.GET("/", getRoot) r.GET("/", getRoot)
r.GET("/scan/tcp/:port", scanTCPPort)
r.GET("/client-port", getClientPortAsString) r.GET("/client-port", getClientPortAsString)
r.GET("/geo", getGeoAsString) r.GET("/geo", getGeoAsString)
r.GET("/geo/:field", getGeoAsString) r.GET("/geo/:field", getGeoAsString)

View File

@ -1,10 +1,11 @@
package router package router
import ( import (
"context"
"os" "os"
"testing" "testing"
"github.com/dcarrillo/whatismyip/models" "github.com/dcarrillo/whatismyip/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -16,6 +17,7 @@ type testIPs struct {
} }
type contentTypes struct { type contentTypes struct {
html string
text string text string
json string json string
} }
@ -29,19 +31,27 @@ var (
ipv6ASN: "2a02:a800::1", ipv6ASN: "2a02:a800::1",
} }
contentType = contentTypes{ contentType = contentTypes{
html: "content-type: text/html; charset=utf-8",
text: "text/plain; charset=utf-8", text: "text/plain; charset=utf-8",
json: "application/json; charset=utf-8", json: "application/json; charset=utf-8",
} }
jsonIPv4 = `{"client_port":"1001","ip":"81.2.69.192","ip_version":4,"country":"United Kingdom","country_code":"GB","city":"London","latitude":51.5142,"longitude":-0.0931,"time_zone":"Europe/London","host":"test", "headers": {}}`
jsonIPv6 = `{"asn":3352,"asn_organization":"TELEFONICA DE ESPANA","client_port":"1001","host":"test","ip":"2a02:9000::1","ip_version":6,"headers": {}}`
jsonDNSIPv4 = `{"dns":{"ip":"81.2.69.192","country":"United Kingdom"}}`
plainDNSIPv4 = "81.2.69.192 (United Kingdom / )\n"
) )
const trustedHeader = "X-Real-IP" const (
trustedHeader = "X-Real-IP"
trustedPortHeader = "X-Real-Port"
domain = "dns.example.com"
)
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
app = gin.Default() app = gin.Default()
app.TrustedPlatform = trustedHeader app.TrustedPlatform = trustedHeader
models.Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb") svc, _ := service.NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
Setup(app) Setup(app, svc)
defer models.CloseDBs()
os.Exit(m.Run()) os.Exit(m.Run())
} }

View File

@ -62,20 +62,22 @@ func TestDefaultTemplate(t *testing.T) {
tmpl, _ := template.New("home").Parse(home) tmpl, _ := template.New("home").Parse(home)
response := JSONResponse{ response := JSONResponse{
IP: "127.0.0.1", IP: "127.0.0.1",
IPVersion: 4, IPVersion: 4,
ClientPort: "1000", ClientPort: "1000",
Country: "A Country", Host: "localhost",
CountryCode: "XX", Headers: req.Header,
City: "A City", GeoResponse: GeoResponse{
Latitude: 100, Country: "A Country",
Longitude: -100, CountryCode: "XX",
PostalCode: "00000", City: "A City",
TimeZone: "My/Timezone", Latitude: 100,
ASN: 0, Longitude: -100,
ASNOrganization: "My ISP", PostalCode: "00000",
Host: "localhost", TimeZone: "My/Timezone",
Headers: req.Header, ASN: 0,
ASNOrganization: "My ISP",
},
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}

48
server/dns.go Normal file
View File

@ -0,0 +1,48 @@
package server
import (
"context"
"log"
"strconv"
"github.com/miekg/dns"
)
const port = 53
type DNS struct {
server *dns.Server
handler *dns.Handler
ctx context.Context
}
func NewDNSServer(ctx context.Context, handler dns.Handler) *DNS {
return &DNS{
handler: &handler,
ctx: ctx,
}
}
func (d *DNS) Start() {
d.server = &dns.Server{
Addr: ":" + strconv.Itoa(port),
Net: "udp",
Handler: *d.handler,
// UDPSize: 65535,
// ReusePort: true,
}
log.Printf("Starting DNS server listening on :%d (udp)", port)
go func() {
if err := d.server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
}
func (d *DNS) Stop() {
log.Print("Stopping DNS server...")
if err := d.server.Shutdown(); err != nil {
log.Printf("DNS server forced to shutdown: %s", err)
}
}

55
server/quic.go Normal file
View File

@ -0,0 +1,55 @@
package server
import (
"context"
"errors"
"log"
"net/http"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/quic-go/quic-go/http3"
)
type Quic struct {
server *http3.Server
tlsServer *TLS
ctx context.Context
}
func NewQuicServer(ctx context.Context, tlsServer *TLS) *Quic {
return &Quic{
tlsServer: tlsServer,
ctx: ctx,
}
}
func (q *Quic) Start() {
q.server = &http3.Server{
Addr: setting.App.TLSAddress,
Handler: q.tlsServer.server.Handler,
}
parentHandler := q.tlsServer.server.Handler
q.tlsServer.server.Handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if err := q.server.SetQUICHeaders(rw.Header()); err != nil {
log.Fatal(err)
}
parentHandler.ServeHTTP(rw, req)
})
log.Printf("Starting QUIC server listening on %s (udp)", setting.App.TLSAddress)
go func() {
if err := q.server.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
}
func (q *Quic) Stop() {
log.Print("Stopping QUIC server...")
if err := q.server.Close(); err != nil {
log.Print("QUIC server forced to shutdown")
}
}

65
server/server.go Normal file
View File

@ -0,0 +1,65 @@
package server
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/dcarrillo/whatismyip/service"
)
type Server interface {
Start()
Stop()
}
type Manager struct {
servers []Server
geoSvc *service.Geo
}
func Setup(servers []Server, geoSvc *service.Geo) *Manager {
return &Manager{
servers: servers,
geoSvc: geoSvc,
}
}
func (m *Manager) Run() {
m.start()
signalChan := make(chan os.Signal, len(m.servers))
signal.Notify(signalChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
var s os.Signal
for {
s = <-signalChan
if s == syscall.SIGHUP {
m.stop()
if m.geoSvc != nil {
m.geoSvc.Reload()
}
m.start()
} else {
log.Print("Shutting down...")
if m.geoSvc != nil {
m.geoSvc.Shutdown()
}
m.stop()
break
}
}
}
func (m *Manager) start() {
for _, s := range m.servers {
s.Start()
}
}
func (m *Manager) stop() {
for _, s := range m.servers {
s.Stop()
}
}

46
server/tcp.go Normal file
View File

@ -0,0 +1,46 @@
package server
import (
"context"
"errors"
"log"
"net/http"
"github.com/dcarrillo/whatismyip/internal/setting"
)
type TCP struct {
server *http.Server
handler *http.Handler
ctx context.Context
}
func NewTCPServer(ctx context.Context, handler *http.Handler) *TCP {
return &TCP{
handler: handler,
ctx: ctx,
}
}
func (t *TCP) Start() {
t.server = &http.Server{
Addr: setting.App.BindAddress,
Handler: *t.handler,
ReadTimeout: setting.App.Server.ReadTimeout,
WriteTimeout: setting.App.Server.WriteTimeout,
}
log.Printf("Starting TCP server listening on %s", setting.App.BindAddress)
go func() {
if err := t.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
}
func (t *TCP) Stop() {
log.Print("Stopping TCP server...")
if err := t.server.Shutdown(t.ctx); err != nil {
log.Printf("TCP server forced to shutdown: %s", err)
}
}

47
server/tls.go Normal file
View File

@ -0,0 +1,47 @@
package server
import (
"context"
"errors"
"log"
"net/http"
"github.com/dcarrillo/whatismyip/internal/setting"
)
type TLS struct {
server *http.Server
handler *http.Handler
ctx context.Context
}
func NewTLSServer(ctx context.Context, handler *http.Handler) *TLS {
return &TLS{
handler: handler,
ctx: ctx,
}
}
func (t *TLS) Start() {
t.server = &http.Server{
Addr: setting.App.TLSAddress,
Handler: *t.handler,
ReadTimeout: setting.App.Server.ReadTimeout,
WriteTimeout: setting.App.Server.WriteTimeout,
}
log.Printf("Starting TLS server listening on %s", setting.App.TLSAddress)
go func() {
if err := t.server.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
}
func (t *TLS) Stop() {
log.Print("Stopping TLS server...")
if err := t.server.Shutdown(t.ctx); err != nil {
log.Printf("TLS server forced to shutdown: %s", err)
}
}

View File

@ -1,37 +1,73 @@
package service package service
import ( import (
"context"
"log" "log"
"net" "net"
"sync"
"github.com/dcarrillo/whatismyip/models" "github.com/dcarrillo/whatismyip/models"
) )
// Geo defines a base type for lookups
type Geo struct { type Geo struct {
IP net.IP ctx context.Context
cancel context.CancelFunc
db *models.GeoDB
mu sync.RWMutex
} }
// LookUpCity queries the database for city data related to the given IP func NewGeo(ctx context.Context, cityPath string, asnPath string) (*Geo, error) {
func (g *Geo) LookUpCity() *models.GeoRecord { ctx, cancel := context.WithCancel(ctx)
record := &models.GeoRecord{}
err := record.LookUp(g.IP) db, err := models.Setup(cityPath, asnPath)
if err != nil { if err != nil {
log.Println(err) cancel()
return nil, err
}
geo := &Geo{
ctx: ctx,
cancel: cancel,
db: db,
}
return geo, nil
}
func (g *Geo) LookUpCity(ip net.IP) *models.GeoRecord {
record, err := g.db.LookupCity(ip)
if err != nil {
log.Print(err)
return nil return nil
} }
return record return record
} }
// LookUpASN queries the database for ASN data related to the given IP func (g *Geo) LookUpASN(ip net.IP) *models.ASNRecord {
func (g *Geo) LookUpASN() *models.ASNRecord { record, err := g.db.LookupASN(ip)
record := &models.ASNRecord{}
err := record.LookUp(g.IP)
if err != nil { if err != nil {
log.Println(err) log.Print(err)
return nil return nil
} }
return record return record
} }
func (g *Geo) Shutdown() {
g.cancel()
g.db.CloseDBs()
}
func (g *Geo) Reload() {
if err := g.ctx.Err(); err != nil {
log.Printf("Skipping reload, service is shutting down: %v", err)
return
}
g.mu.Lock()
defer g.mu.Unlock()
g.db.Reload()
log.Print("Geo database reloaded")
}

View File

@ -1,36 +1,33 @@
package service package service
import ( import (
"context"
"net" "net"
"os" "os"
"testing" "testing"
"github.com/dcarrillo/whatismyip/models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var geoSvc *Geo
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
models.Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb") geoSvc, _ = NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
defer models.CloseDBs()
os.Exit(m.Run()) os.Exit(m.Run())
} }
func TestCityLookup(t *testing.T) { func TestCityLookup(t *testing.T) {
ip := Geo{IP: net.ParseIP("error")} c := geoSvc.LookUpCity(net.ParseIP("error"))
c := ip.LookUpCity()
assert.Nil(t, c) assert.Nil(t, c)
ip = Geo{IP: net.ParseIP("1.1.1.1")} c = geoSvc.LookUpCity(net.ParseIP("1.1.1.1"))
c = ip.LookUpCity()
assert.NotNil(t, c) assert.NotNil(t, c)
} }
func TestASNLookup(t *testing.T) { func TestASNLookup(t *testing.T) {
ip := Geo{IP: net.ParseIP("error")} a := geoSvc.LookUpASN(net.ParseIP("error"))
a := ip.LookUpASN()
assert.Nil(t, a) assert.Nil(t, a)
ip = Geo{IP: net.ParseIP("1.1.1.1")} a = geoSvc.LookUpASN(net.ParseIP("1.1.1.1"))
a = ip.LookUpASN()
assert.NotNil(t, a) assert.NotNil(t, a)
} }

24
service/port_scanner.go Normal file
View File

@ -0,0 +1,24 @@
package service
import (
"net"
"time"
)
const scannerTimeOut = 3 * time.Second
type PortScanner struct {
Address net.Addr
}
func (p *PortScanner) IsPortOpen() (bool, error) {
conn, err := net.DialTimeout(p.Address.Network(), p.Address.String(), scannerTimeOut)
if err != nil {
return false, err
}
if conn != nil {
defer conn.Close()
}
return true, nil
}

15
test/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM golang:1.24-alpine AS builder
ARG ARG_VERSION
ENV VERSION=$ARG_VERSION
WORKDIR /app
COPY . .
FROM builder AS build-test-app
RUN CGO_ENABLED=0 \
go build -ldflags="-s -w" -o whatismyip ./cmd
FROM scratch AS test
COPY --from=build-test-app /app/whatismyip /usr/bin/
ENTRYPOINT ["whatismyip"]

10
test/resolver.yml Normal file
View File

@ -0,0 +1,10 @@
---
domain: dns.example.com
redirect_port: ":8000"
resource_records:
- "1800 IN SOA xns.example.com. hostmaster.example.com. 1 10000 2400 604800 1800"
- "3600 IN NS xns.example.com."
ipv4:
- "127.0.0.2"
ipv6:
- "aaa:aaa:aaa:aaaa::1"