mirror of
https://github.com/dcarrillo/whatismyip.git
synced 2024-12-22 07:28:00 +00:00
Add feature to get the right client port when using a trusted proxy
This commit is contained in:
parent
9070e9a2c2
commit
7c70abf07f
2
Makefile
2
Makefile
@ -16,7 +16,7 @@ integration-test:
|
||||
.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 v1.45.0; \
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.45.2; \
|
||||
fi
|
||||
|
||||
@command $(GOPATH)/shadow > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
|
19
README.md
19
README.md
@ -13,7 +13,7 @@
|
||||
- [Examples](#examples)
|
||||
- [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 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 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)
|
||||
- [Docker](#docker)
|
||||
- [Run a container locally using test databases](#run-a-container-locally-using-test-databases)
|
||||
@ -36,7 +36,7 @@ curl -6 ifconfig.es
|
||||
## Features
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- High performance.
|
||||
@ -47,6 +47,7 @@ curl -6 ifconfig.es
|
||||
## Endpoints
|
||||
|
||||
- https://ifconfig.es/
|
||||
- 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/city
|
||||
@ -72,9 +73,11 @@ Golang >= 1.17 is required. Previous versions may work.
|
||||
## Usage
|
||||
|
||||
```text
|
||||
Usage of ./whatismyip:
|
||||
Usage of whatismyip:
|
||||
-bind string
|
||||
Listening address (see https://pkg.go.dev/net?#Listen) (default ":8080")
|
||||
-enable-secure-headers
|
||||
Add sane security-related headers to every response
|
||||
-geoip2-asn string
|
||||
Path to GeoIP2 ASN database
|
||||
-geoip2-city string
|
||||
@ -88,7 +91,9 @@ Usage of ./whatismyip:
|
||||
-tls-key string
|
||||
When using TLS, path to private key file
|
||||
-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
|
||||
Output version information and exit
|
||||
```
|
||||
@ -108,11 +113,11 @@ Usage of ./whatismyip:
|
||||
-bind "" -tls-bind :8081 -tls-crt ./test/server.pem -tls-key ./test/server.key
|
||||
```
|
||||
|
||||
### 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 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 -template mytemplate.tmpl
|
||||
-trusted-header X-Real-IP -trusted-port-header X-Real-Port -template mytemplate.tmpl
|
||||
```
|
||||
|
||||
## Download
|
||||
@ -121,7 +126,7 @@ Download latest version from https://github.com/dcarrillo/whatismyip/releases
|
||||
|
||||
## Docker
|
||||
|
||||
An ultra-light (~9MB) image is available.
|
||||
An ultra-light (~10MB) image is available at [docker hub](https://hub.docker.com/r/dcarrillo/whatismyip).
|
||||
|
||||
### Run a container locally using test databases
|
||||
|
||||
|
@ -27,6 +27,7 @@ type settings struct {
|
||||
TLSCrtPath string
|
||||
TLSKeyPath string
|
||||
TrustedHeader string
|
||||
TrustedPortHeader string
|
||||
EnableSecureHeaders bool
|
||||
Server serverSettings
|
||||
version bool
|
||||
@ -74,6 +75,11 @@ func Setup(args []string) (output string, err error) {
|
||||
"",
|
||||
"Trusted request header for remote IP (e.g. X-Real-IP)",
|
||||
)
|
||||
flags.StringVar(&App.TrustedPortHeader,
|
||||
"trusted-port-header",
|
||||
"",
|
||||
"Trusted request header for remote client port (e.g. X-Real-Port)",
|
||||
)
|
||||
flags.BoolVar(&App.version, "version", false, "Output version information and exit")
|
||||
flags.BoolVar(
|
||||
&App.EnableSecureHeaders,
|
||||
@ -91,21 +97,25 @@ func Setup(args []string) (output string, err error) {
|
||||
return fmt.Sprintf("whatismyip version %s", core.Version), ErrVersion
|
||||
}
|
||||
|
||||
if App.TrustedPortHeader != "" && App.TrustedHeader == "" {
|
||||
return "", fmt.Errorf("truster-header is mandatory when truster-port-header is set\n")
|
||||
}
|
||||
|
||||
if App.GeodbPath.City == "" || App.GeodbPath.ASN == "" {
|
||||
return "", fmt.Errorf("geoip2-city and geoip2-asn parameters are mandatory")
|
||||
return "", fmt.Errorf("geoip2-city and geoip2-asn parameters are mandatory\n")
|
||||
}
|
||||
|
||||
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 -tls-crt and -tls-key flags are mandatory\n")
|
||||
}
|
||||
|
||||
if App.TemplatePath != "" {
|
||||
info, err := os.Stat(App.TemplatePath)
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("%s no such file or directory", App.TemplatePath)
|
||||
return "", fmt.Errorf("%s no such file or directory\n", App.TemplatePath)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return "", fmt.Errorf("%s must be a file", App.TemplatePath)
|
||||
return "", fmt.Errorf("%s must be a file\n", App.TemplatePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,51 +8,51 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseMandatoryFlags(t *testing.T) {
|
||||
var mandatoryFlags = []struct {
|
||||
args []string
|
||||
conf settings
|
||||
}{
|
||||
{
|
||||
[]string{},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{"-geoip2-city", "/city-path"},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{"-geoip2-asn", "/asn-path"},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
|
||||
},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
|
||||
"-tls-crt", "/crt-path",
|
||||
},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
|
||||
"-tls-key", "/key-path",
|
||||
},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-bind", ":8000",
|
||||
"-trusted-port-header", "port-header",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range mandatoryFlags {
|
||||
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||
_, err := Setup(tt.args)
|
||||
assert.NotNil(t, err)
|
||||
require.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "mandatory")
|
||||
})
|
||||
}
|
||||
@ -70,13 +70,7 @@ func TestParseFlags(t *testing.T) {
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
TemplatePath: "",
|
||||
BindAddress: ":8080",
|
||||
TLSAddress: "",
|
||||
TLSCrtPath: "",
|
||||
TLSKeyPath: "",
|
||||
TrustedHeader: "",
|
||||
EnableSecureHeaders: false,
|
||||
BindAddress: ":8080",
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
@ -90,13 +84,7 @@ func TestParseFlags(t *testing.T) {
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
TemplatePath: "",
|
||||
BindAddress: ":8001",
|
||||
TLSAddress: "",
|
||||
TLSCrtPath: "",
|
||||
TLSKeyPath: "",
|
||||
TrustedHeader: "",
|
||||
EnableSecureHeaders: false,
|
||||
BindAddress: ":8001",
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
@ -113,13 +101,29 @@ func TestParseFlags(t *testing.T) {
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
TemplatePath: "",
|
||||
BindAddress: ":8080",
|
||||
TLSAddress: ":9000",
|
||||
TLSCrtPath: "/crt-path",
|
||||
TLSKeyPath: "/key-path",
|
||||
TrustedHeader: "",
|
||||
EnableSecureHeaders: false,
|
||||
BindAddress: ":8080",
|
||||
TLSAddress: ":9000",
|
||||
TLSCrtPath: "/crt-path",
|
||||
TLSKeyPath: "/key-path",
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path",
|
||||
"-trusted-header", "header", "-trusted-port-header", "port-header",
|
||||
},
|
||||
settings{
|
||||
GeodbPath: geodbPath{
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
BindAddress: ":8080",
|
||||
TrustedHeader: "header",
|
||||
TrustedPortHeader: "port-header",
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
@ -136,11 +140,7 @@ func TestParseFlags(t *testing.T) {
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
TemplatePath: "",
|
||||
BindAddress: ":8080",
|
||||
TLSAddress: "",
|
||||
TLSCrtPath: "",
|
||||
TLSKeyPath: "",
|
||||
TrustedHeader: "header",
|
||||
EnableSecureHeaders: true,
|
||||
Server: serverSettings{
|
||||
@ -154,7 +154,7 @@ func TestParseFlags(t *testing.T) {
|
||||
for _, tt := range flags {
|
||||
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||
_, err := Setup(tt.args)
|
||||
assert.Nil(t, err)
|
||||
require.Nil(t, err)
|
||||
assert.True(t, reflect.DeepEqual(App, tt.conf))
|
||||
})
|
||||
}
|
||||
@ -192,6 +192,6 @@ func TestParseFlagTemplate(t *testing.T) {
|
||||
"-template", "/",
|
||||
}
|
||||
_, err = Setup(flags)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "must be a file")
|
||||
}
|
||||
|
@ -44,15 +44,31 @@ func getRoot(ctx *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
|
||||
ctx.String(http.StatusOK, port+"\n")
|
||||
ctx.String(http.StatusOK, getClientPort(ctx)+"\n")
|
||||
}
|
||||
|
||||
func getAllAsString(ctx *gin.Context) {
|
||||
output := "IP: " + ctx.ClientIP() + "\n"
|
||||
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
|
||||
output += "Client Port: " + port + "\n"
|
||||
output += "Client Port: " + getClientPort(ctx) + "\n"
|
||||
|
||||
r := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
|
||||
if record := r.LookUpCity(); record != nil {
|
||||
@ -83,11 +99,10 @@ func jsonOutput(ctx *gin.Context) JSONResponse {
|
||||
version = 6
|
||||
}
|
||||
|
||||
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
|
||||
return JSONResponse{
|
||||
IP: ctx.ClientIP(),
|
||||
IPVersion: version,
|
||||
ClientPort: port,
|
||||
ClientPort: getClientPort(ctx),
|
||||
Country: cityRecord.Country.Names["en"],
|
||||
CountryCode: cityRecord.Country.ISOCode,
|
||||
City: cityRecord.City.Names["en"],
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/internal/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -97,16 +98,78 @@ func TestHost(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientPort(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/client-port", nil)
|
||||
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
|
||||
req.Header.Set(trustedHeader, testIP.ipv4)
|
||||
type args struct {
|
||||
params []string
|
||||
headers map[string][]string
|
||||
}
|
||||
type expected struct {
|
||||
body 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()
|
||||
app.ServeHTTP(w, req)
|
||||
for _, tt := range tests {
|
||||
_, _ = 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)
|
||||
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "1000\n", w.Body.String())
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, tt.expected, w.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
@ -120,6 +183,15 @@ func TestNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
_, _ = setting.Setup(
|
||||
[]string{
|
||||
"-geoip2-city", "city",
|
||||
"-geoip2-asn", "asn",
|
||||
"-trusted-header", trustedHeader,
|
||||
"-trusted-port-header", trustedPortHeader,
|
||||
},
|
||||
)
|
||||
|
||||
type args struct {
|
||||
ip string
|
||||
}
|
||||
@ -149,6 +221,7 @@ func TestJSON(t *testing.T) {
|
||||
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()
|
||||
app.ServeHTTP(w, req)
|
||||
@ -162,7 +235,7 @@ func TestJSON(t *testing.T) {
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
expected := `IP: 81.2.69.192
|
||||
Client Port: 1000
|
||||
Client Port: 1001
|
||||
City: London
|
||||
Country: United Kingdom
|
||||
Country Code: GB
|
||||
@ -177,12 +250,22 @@ ASN Organization:
|
||||
Header1: one
|
||||
Host: test
|
||||
X-Real-Ip: 81.2.69.192
|
||||
X-Real-Port: 1001
|
||||
`
|
||||
_, _ = setting.Setup(
|
||||
[]string{
|
||||
"-geoip2-city", "city",
|
||||
"-geoip2-asn", "asn",
|
||||
"-trusted-header", trustedHeader,
|
||||
"-trusted-port-header", trustedPortHeader,
|
||||
},
|
||||
)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/all", nil)
|
||||
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
|
||||
req.Host = "test"
|
||||
req.Header.Set(trustedHeader, testIP.ipv4)
|
||||
req.Header.Set(trustedPortHeader, "1001")
|
||||
req.Header.Set("Header1", "one")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -34,11 +34,12 @@ var (
|
||||
text: "text/plain; charset=utf-8",
|
||||
json: "application/json; charset=utf-8",
|
||||
}
|
||||
jsonIPv4 = `{"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"]}}`
|
||||
jsonIPv6 = `{"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":""}`
|
||||
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":{"X-Real-Ip":["81.2.69.192"], "X-Real-Port":["1001"]}}`
|
||||
jsonIPv6 = `{"asn":3352, "asn_organization":"TELEFONICA DE ESPANA", "city":"", "client_port":"1001", "country":"", "country_code":"", "headers":{"X-Real-Ip":["2a02:9000::1"], "X-Real-Port":["1001"]}, "host":"test", "ip":"2a02:9000::1", "ip_version":6, "latitude":0, "longitude":0, "postal_code":"", "time_zone":""}`
|
||||
)
|
||||
|
||||
const trustedHeader = "X-Real-IP"
|
||||
const trustedPortHeader = "X-Real-Port"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
app = gin.Default()
|
||||
|
Loading…
Reference in New Issue
Block a user