Compare commits

..

No commits in common. "main" and "2.1.5" have entirely different histories.
main ... 2.1.5

31 changed files with 1420 additions and 1308 deletions

View File

@ -29,18 +29,21 @@ jobs:
contents: read
security-events: write
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: install go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version-file: go.mod
cache: true
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
languages: go
@ -49,4 +52,4 @@ jobs:
make build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v2

View File

@ -8,19 +8,17 @@ on:
- '*'
pull_request:
concurrency: ${{ github.ref_name }}
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
make: ["lint", "unit-test", "integration-test"]
make: ["lint", "test"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: install go
uses: actions/setup-go@v5
uses: actions/setup-go@v4
with:
go-version-file: go.mod
@ -35,11 +33,11 @@ jobs:
matrix:
goosarch: [linux-amd64]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: install go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version-file: go.mod
cache: true
@ -63,7 +61,7 @@ jobs:
sha256sum whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz > whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz.sha256
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
with:
body_path: changelog.txt
files: |

1
.gitignore vendored
View File

@ -1 +0,0 @@
whatismyip

View File

@ -1,49 +0,0 @@
---
run:
timeout: 10m
issues:
max-same-issues: 0
linters:
disable-all: true
enable:
- goimports
- ineffassign
- nakedret
- revive
- staticcheck
- stylecheck
- unconvert
- unparam
- unused
linters-settings:
staticcheck:
checks:
- all
revive:
ignore-generated-header: true
severity: warning
confidence: 0.8
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

View File

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

View File

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

View File

@ -5,40 +5,44 @@ DOCKER_URL ?= dcarrillo/whatismyip
.PHONY: test
test: unit-test integration-test
.PHONY: unit-test
unit-test:
go test -count=1 -race -short -cover ./...
go test -race -short -cover ./...
.PHONY: integration-test
integration-test:
go test -count=1 -v ./integration-tests
go test ./integration-tests -v
.PHONY: install-tools
install-tools:
@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; \
fi
@command $(GOPATH)/revive > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/mgechev/revive; \
fi
@command $(GOPATH)/shadow > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest; \
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
gofmt -l . && test -z $$(gofmt -l .)
golines -l . && test -z $$(golines -l .)
golangci-lint run
shadow ./...
.PHONY: build
build:
CGO_ENABLED=0 go build -ldflags="-s -w -X 'github.com/dcarrillo/whatismyip/internal/core.Version=${VERSION}'" -o whatismyip ./cmd
docker-build-dev:
docker build --target=dev --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} .
.PHONY: docker-build
docker-build:
docker build --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} .
docker-build-prod:
docker build --target=prod --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} .
docker-push: docker-build-prod
.PHONY: docker-push
docker-push: docker-build
ifneq (,$(findstring devel-,$(VERSION)))
@echo "VERSION is set to ${VERSION}, I can't push devel builds"
exit 1
@ -48,7 +52,8 @@ else
docker push ${DOCKER_URL}:latest
endif
docker-run: docker-build-dev
.PHONY: docker-run
docker-run: docker-build
docker run --tty --interactive --rm \
-v ${PWD}/test/GeoIP2-City-Test.mmdb:/tmp/GeoIP2-City-Test.mmdb:ro \
-v ${PWD}/test/GeoLite2-ASN-Test.mmdb:/tmp/GeoLite2-ASN-Test.mmdb:ro -p 8080:8080 \

View File

@ -9,12 +9,11 @@
- [What is my IP address](#what-is-my-ip-address)
- [Features](#features)
- [Endpoints](#endpoints)
- [DNS discovery](#dns-discovery)
- [Build](#build)
- [Usage](#usage)
- [Examples](#examples)
- [Run a default TCP server](#run-a-default-tcp-server)
- [Run a TLS (HTTP/2) and enable What is my DNS](#run-a-tls-http2-and-enable-what-is-my-dns)
- [Run a TLS (HTTP/2) server only](#run-a-tls-http2-server-only)
- [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)
@ -22,13 +21,9 @@
- [Run a container locally using test databases](#run-a-container-locally-using-test-databases)
- [From Docker Hub](#from-docker-hub)
> [!NOTE]
> Since version 2.3.0, the application includes an optional client [DNS discovery](#dns-discovery)
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.
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.
Take a look at [ifconfig.es](https://ifconfig.es) a live site using `whatismyip`
Get your public IP easily from the command line:
@ -40,22 +35,13 @@ curl -6 ifconfig.es
::1
```
Get the IP of your DNS provider:
```bash
curl -L dns.ifconfig.es
2a04:e4c0:47::67 (Spain / OPENDNS)
```
## Features
- TLS and HTTP/2.
- 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.
- 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.
- 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.
@ -80,42 +66,10 @@ curl -L dns.ifconfig.es
- https://ifconfig.es/all
- https://ifconfig.es/headers
- 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
Golang >= 1.22 is required.
Golang >= 1.19 is required.
`make build`
@ -133,8 +87,6 @@ Usage of whatismyip:
Path to GeoIP2 ASN database
-geoip2-city string
Path to GeoIP2 city database
-resolver string
Path to the resolver configuration. It actually enables the resolver for DNS client discovery.
-template string
Path to template file
-tls-bind string
@ -159,12 +111,11 @@ Usage of whatismyip:
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb
```
### Run a TLS (HTTP/2) and enable What is my DNS
### Run a TLS (HTTP/2) server only
```bash
./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 \
-resolver ./test/resolver.yml
-bind "" -tls-bind :8081 -tls-crt ./test/server.pem -tls-key ./test/server.key
```
### Run an HTTP/3 server
@ -183,7 +134,7 @@ Usage of whatismyip:
## Download
Download the latest version from [github](https://github.com/dcarrillo/whatismyip/releases)
Download the latest version from https://github.com/dcarrillo/whatismyip/releases
## Docker

View File

@ -4,18 +4,14 @@ import (
"context"
"flag"
"fmt"
"net/http"
"os"
"slices"
"time"
"github.com/dcarrillo/whatismyip/internal/httputils"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/dcarrillo/whatismyip/resolver"
"github.com/dcarrillo/whatismyip/server"
"github.com/gin-contrib/secure"
"github.com/patrickmn/go-cache"
"github.com/dcarrillo/whatismyip/models"
"github.com/dcarrillo/whatismyip/router"
"github.com/gin-gonic/gin"
)
@ -30,22 +26,12 @@ func main() {
os.Exit(1)
}
servers := []server.Server{}
models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN)
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))
}
router.SetupTemplate(engine)
router.Setup(engine)
servers = slices.Concat(servers, setupHTTPServers(context.Background(), engine.Handler()))
whatismyip := server.Setup(servers)
whatismyip := server.Setup(context.Background(), engine.Handler())
whatismyip.Run()
}
@ -69,23 +55,3 @@ func setupEngine() *gin.Engine {
return engine
}
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
}

135
go.mod
View File

@ -1,100 +1,79 @@
module github.com/dcarrillo/whatismyip
go 1.23
go 1.21.3
require (
github.com/docker/docker v26.1.5+incompatible
github.com/gin-contrib/secure v1.1.0
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/miekg/dns v1.1.62
github.com/oschwald/maxminddb-golang v1.13.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/quic-go/quic-go v0.47.0
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.31.0
gopkg.in/yaml.v3 v3.0.1
github.com/gin-contrib/secure v0.0.1
github.com/gin-gonic/gin v1.9.1
github.com/oschwald/maxminddb-golang v1.12.0
github.com/quic-go/quic-go v0.39.0
github.com/stretchr/testify v1.8.4
github.com/testcontainers/testcontainers-go v0.13.0
golang.org/x/net v0.17.0
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/bytedance/sonic v1.12.2 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/containerd/containerd v1.7.15 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Microsoft/hcsshim v0.9.6 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/containerd/cgroups v1.0.4 // indirect
github.com/containerd/containerd v1.6.18 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v20.10.24+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // 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.22.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/go-playground/validator/v10 v10.15.5 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // 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/term v0.5.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/moby/sys/mount v0.3.3 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
github.com/onsi/ginkgo/v2 v2.13.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/arch v0.10.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/tools v0.25.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/mock v0.3.0 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

1346
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -4,78 +4,61 @@ import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
"log"
"net/http"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
validator "github.com/dcarrillo/whatismyip/internal/validator/uuid"
"github.com/dcarrillo/whatismyip/router"
"github.com/docker/docker/api/types"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tc "github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func customDialContext() func(ctx context.Context, network, addr string) (net.Conn, error) {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
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")
func buildContainer() testcontainers.ContainerRequest {
_, filename, _, _ := runtime.Caller(0)
dir := filepath.Dir(filename)
req := testcontainers.ContainerRequest{
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",
"-enable-http3",
},
ExposedPorts: []string{"8000:8000", "8001:8001", "8001:8001/udp"},
WaitingFor: wait.ForHTTP("/geo").
WithTLS(true, &tls.Config{InsecureSkipVerify: true}).
WithPort("8001"),
Mounts: testcontainers.Mounts(
testcontainers.BindMount(
filepath.Join(dir, "/../test/GeoIP2-City-Test.mmdb"),
"/tmp/GeoIP2-City-Test.mmdb",
),
testcontainers.BindMount(
filepath.Join(dir, "/../test/GeoLite2-ASN-Test.mmdb"),
"/tmp/GeoLite2-ASN-Test.mmdb",
),
testcontainers.BindMount(filepath.Join(dir, "/../test/server.pem"), "/tmp/server.pem"),
testcontainers.BindMount(filepath.Join(dir, "/../test/server.key"), "/tmp/server.key"),
),
}
return dialer.DialContext(ctx, network, addr)
}
}
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)
}
}
})
return req
}
func TestContainerIntegration(t *testing.T) {
@ -84,71 +67,22 @@ func TestContainerIntegration(t *testing.T) {
}
ctx := context.Background()
c, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{
ContainerRequest: tc.ContainerRequest{
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.ForAll(
wait.ForListeningPort("8000/tcp").WithStartupTimeout(5*time.Second),
wait.ForListeningPort("8001/tcp").WithStartupTimeout(5*time.Second),
wait.ForListeningPort("8001/udp").WithStartupTimeout(5*time.Second),
wait.ForListeningPort("53/udp").WithStartupTimeout(5*time.Second),
),
AutoRemove: true,
},
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: buildContainer(),
Started: true,
})
require.NoError(t, err)
t.Cleanup(func() { c.Terminate(ctx) })
if err != nil {
log.Fatal(err)
}
defer func() {
err = container.Terminate(ctx)
if err != nil {
log.Fatal(err)
}
}()
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
tests := []struct {
name string
url string
@ -171,27 +105,9 @@ func TestContainerIntegration(t *testing.T) {
},
}
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, _ := http.NewRequest("GET", tt.url, nil)
req.Header.Set("Accept", "application/json")
var resp *http.Response
@ -215,28 +131,6 @@ func TestContainerIntegration(t *testing.T) {
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) {
@ -244,6 +138,7 @@ func doQuicRequest(req *http.Request) (*http.Response, []byte, error) {
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
QuicConfig: &quic.Config{},
}
defer roundTripper.Close()

View File

@ -63,7 +63,7 @@ func GetLogFormatter(param gin.LogFormatterParams) string {
)
}
func normalizeLog(log any) any {
func normalizeLog(log interface{}) interface{} {
switch v := log.(type) {
case string:
if v == "" {

View File

@ -9,7 +9,6 @@ import (
"time"
"github.com/dcarrillo/whatismyip/internal/core"
"gopkg.in/yaml.v3"
)
type geodbPath struct {
@ -20,15 +19,6 @@ type serverSettings struct {
ReadTimeout 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 {
GeodbPath geodbPath
TemplatePath string
@ -41,14 +31,15 @@ type settings struct {
EnableSecureHeaders bool
EnableHTTP3 bool
Server serverSettings
Resolver resolver
version bool
}
const defaultAddress = ":8080"
// ErrVersion is the custom error triggered when -version flag is passed
var ErrVersion = errors.New("setting: version requested")
// App is the var with the parsed settings
var App = settings{
// hard-coded for the time being
Server: serverSettings{
@ -57,20 +48,15 @@ var App = settings{
},
}
// Setup initializes the App object parsing the flags
func Setup(args []string) (output string, err error) {
flags := flag.NewFlagSet("whatismyip", flag.ContinueOnError)
var buf bytes.Buffer
var resolverConf string
flags.SetOutput(&buf)
flags.StringVar(&App.GeodbPath.City, "geoip2-city", "", "Path to GeoIP2 city database")
flags.StringVar(&App.GeodbPath.ASN, "geoip2-asn", "", "Path to GeoIP2 ASN database")
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(&App.TemplatePath, "template", "", "Path to template file")
flags.StringVar(
&App.BindAddress,
"bind",
@ -146,21 +132,5 @@ 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 %s", err)
}
}
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

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

View File

@ -1,37 +0,0 @@
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,165 +0,0 @@
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,
)
}

View File

@ -1,85 +0,0 @@
package router
import (
"fmt"
"net"
"net/http"
"strings"
validator "github.com/dcarrillo/whatismyip/internal/validator/uuid"
"github.com/dcarrillo/whatismyip/service"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/patrickmn/go-cache"
)
type DNSJSONResponse struct {
DNS dnsData `json:"dns"`
}
type dnsData struct {
IP string `json:"ip"`
Country string `json:"country"`
AsnOrganization string `json:"provider"`
}
// 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
}
geo := service.Geo{IP: ip}
j := DNSJSONResponse{
DNS: dnsData{
IP: ipStr,
Country: geo.LookUpCity().Country.Names["en"],
AsnOrganization: geo.LookUpASN().AutonomousSystemOrganization,
},
}
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))
}
}

View File

@ -1,153 +0,0 @@
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

@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin"
)
// JSONResponse maps data as json
type JSONResponse struct {
IP string `json:"ip"`
IPVersion byte `json:"ip_version"`

View File

@ -1,54 +0,0 @@
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

@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin"
)
// SetupTemplate reads and parses a template from file
func SetupTemplate(r *gin.Engine) {
if setting.App.TemplatePath == "" {
t, _ := template.New("home").Parse(home)
@ -18,9 +19,9 @@ func SetupTemplate(r *gin.Engine) {
}
}
// Setup defines the endpoints
func Setup(r *gin.Engine) {
r.GET("/", getRoot)
r.GET("/scan/tcp/:port", scanTCPPort)
r.GET("/client-port", getClientPortAsString)
r.GET("/geo", getGeoAsString)
r.GET("/geo/:field", getGeoAsString)

View File

@ -36,15 +36,10 @@ var (
}
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,"postal_code":"","time_zone":"Europe/London","asn":0,"asn_organization":"","host":"test", "headers": {}}`
jsonIPv6 = `{"asn":3352, "asn_organization":"TELEFONICA DE ESPANA", "city":"", "client_port":"1001", "country":"", "country_code":"", "host":"test", "ip":"2a02:9000::1", "ip_version":6, "latitude":0, "longitude":0, "postal_code":"", "time_zone":"", "headers": {}}`
jsonDNSIPv4 = `{"dns":{"ip":"81.2.69.192","country":"United Kingdom","provider":""}}`
plainDNSIPv4 = "81.2.69.192 (United Kingdom / )\n"
)
const (
trustedHeader = "X-Real-IP"
trustedPortHeader = "X-Real-Port"
domain = "dns.example.com"
)
const trustedHeader = "X-Real-IP"
const trustedPortHeader = "X-Real-Port"
func TestMain(m *testing.M) {
app = gin.Default()

View File

@ -1,48 +0,0 @@
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.Printf("Stopping DNS server...")
if err := d.server.Shutdown(); err != nil {
log.Printf("DNS server forced to shutdown: %s", err)
}
}

View File

@ -2,7 +2,6 @@ package server
import (
"context"
"errors"
"log"
"net/http"
@ -10,20 +9,20 @@ import (
"github.com/quic-go/quic-go/http3"
)
type Quic struct {
type QuicServer struct {
server *http3.Server
tlsServer *TLS
tlsServer *TLSServer
ctx context.Context
}
func NewQuicServer(ctx context.Context, tlsServer *TLS) *Quic {
return &Quic{
func NewQuicServer(ctx context.Context, tlsServer *TLSServer) *QuicServer {
return &QuicServer{
tlsServer: tlsServer,
ctx: ctx,
}
}
func (q *Quic) Start() {
func (q *QuicServer) Start() {
q.server = &http3.Server{
Addr: setting.App.TLSAddress,
Handler: q.tlsServer.server.Handler,
@ -31,7 +30,7 @@ func (q *Quic) Start() {
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 {
if err := q.server.SetQuicHeaders(rw.Header()); err != nil {
log.Fatal(err)
}
@ -41,13 +40,13 @@ func (q *Quic) Start() {
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) {
err.Error() != "quic: Server closed" {
log.Fatal(err)
}
}()
}
func (q *Quic) Stop() {
func (q *QuicServer) Stop() {
log.Printf("Stopping QUIC server...")
if err := q.server.Close(); err != nil {
log.Printf("QUIC server forced to shutdown")

View File

@ -2,12 +2,14 @@ package server
import (
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/dcarrillo/whatismyip/models"
"golang.org/x/net/context"
)
type Server interface {
@ -15,48 +17,80 @@ type Server interface {
Stop()
}
type Manager struct {
servers []Server
type Factory struct {
tcpServer *TCPServer
tlsServer *TLSServer
quicServer *QuicServer
}
func Setup(servers []Server) *Manager {
return &Manager{
servers: servers,
func Setup(ctx context.Context, handler http.Handler) *Factory {
var tcpServer *TCPServer
var tlsServer *TLSServer
var quicServer *QuicServer
if setting.App.BindAddress != "" {
tcpServer = NewTCPServer(ctx, &handler)
}
if setting.App.TLSAddress != "" {
tlsServer = NewTLSServer(ctx, &handler)
if setting.App.EnableHTTP3 {
quicServer = NewQuicServer(ctx, tlsServer)
}
}
return &Factory{
tcpServer: tcpServer,
tlsServer: tlsServer,
quicServer: quicServer,
}
}
func (m *Manager) Run() {
m.start()
func (f *Factory) Run() {
f.start()
models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN)
signalChan := make(chan os.Signal, len(m.servers))
signalChan := make(chan os.Signal, 3)
signal.Notify(signalChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
var s os.Signal
for {
s = <-signalChan
if s == syscall.SIGHUP {
m.stop()
f.stop()
models.CloseDBs()
models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN)
m.start()
f.start()
} else {
log.Printf("Shutting down...")
m.stop()
f.stop()
models.CloseDBs()
break
}
}
}
func (m *Manager) start() {
for _, s := range m.servers {
s.Start()
func (f *Factory) start() {
if f.tcpServer != nil {
f.tcpServer.Start()
}
if f.tlsServer != nil {
f.tlsServer.Start()
if f.quicServer != nil {
f.quicServer.Start()
}
}
}
func (m *Manager) stop() {
for _, s := range m.servers {
s.Stop()
func (f *Factory) stop() {
if f.tcpServer != nil {
f.tcpServer.Stop()
}
if f.tlsServer != nil {
if f.quicServer != nil {
f.quicServer.Stop()
}
f.tlsServer.Stop()
}
}

View File

@ -9,20 +9,20 @@ import (
"github.com/dcarrillo/whatismyip/internal/setting"
)
type TCP struct {
type TCPServer struct {
server *http.Server
handler *http.Handler
ctx context.Context
}
func NewTCPServer(ctx context.Context, handler *http.Handler) *TCP {
return &TCP{
func NewTCPServer(ctx context.Context, handler *http.Handler) *TCPServer {
return &TCPServer{
handler: handler,
ctx: ctx,
}
}
func (t *TCP) Start() {
func (t *TCPServer) Start() {
t.server = &http.Server{
Addr: setting.App.BindAddress,
Handler: *t.handler,
@ -38,7 +38,7 @@ func (t *TCP) Start() {
}()
}
func (t *TCP) Stop() {
func (t *TCPServer) Stop() {
log.Printf("Stopping TCP server...")
if err := t.server.Shutdown(t.ctx); err != nil {
log.Printf("TCP server forced to shutdown: %s", err)

View File

@ -9,20 +9,20 @@ import (
"github.com/dcarrillo/whatismyip/internal/setting"
)
type TLS struct {
type TLSServer struct {
server *http.Server
handler *http.Handler
ctx context.Context
}
func NewTLSServer(ctx context.Context, handler *http.Handler) *TLS {
return &TLS{
func NewTLSServer(ctx context.Context, handler *http.Handler) *TLSServer {
return &TLSServer{
handler: handler,
ctx: ctx,
}
}
func (t *TLS) Start() {
func (t *TLSServer) Start() {
t.server = &http.Server{
Addr: setting.App.TLSAddress,
Handler: *t.handler,
@ -39,7 +39,7 @@ func (t *TLS) Start() {
}()
}
func (t *TLS) Stop() {
func (t *TLSServer) Stop() {
log.Printf("Stopping TLS server...")
if err := t.server.Shutdown(t.ctx); err != nil {
log.Printf("TLS server forced to shutdown: %s", err)

View File

@ -1,24 +0,0 @@
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
}

View File

@ -1,15 +0,0 @@
FROM golang:1.23-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"]

View File

@ -1,10 +0,0 @@
---
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"