diff --git a/Dockerfile b/Dockerfile index 9b9ea54..dea5495 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:stretch as builder +FROM debian:buster as builder RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get install -q -y golang git-core && \ apt-get clean @@ -8,7 +8,7 @@ RUN mkdir -p /root/go/src COPY rest-api /root/go/src/dyndns RUN cd /root/go/src/dyndns && go get && go test -v -FROM debian:stretch +FROM debian:buster-slim MAINTAINER David Prandzioch RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ diff --git a/rest-api/request_handler.go b/rest-api/request_handler.go index 4bb6d37..2199454 100644 --- a/rest-api/request_handler.go +++ b/rest-api/request_handler.go @@ -1,6 +1,8 @@ package main import ( + "bytes" + "errors" "fmt" "log" "net" @@ -56,7 +58,13 @@ func BuildWebserviceResponseFromRequest(r *http.Request, appConfig *Config, extr } else if ipparser.ValidIP6(response.Address) { response.AddrType = "AAAA" } else { - ip, _, err := net.SplitHostPort(r.RemoteAddr) + var ip string + var err error + + ip, err = getUserIP(r) + if ip == "" { + ip, _, err = net.SplitHostPort(r.RemoteAddr) + } if err != nil { response.Success = false @@ -68,19 +76,96 @@ func BuildWebserviceResponseFromRequest(r *http.Request, appConfig *Config, extr // @todo refactor this code to remove duplication if ipparser.ValidIP4(ip) { response.AddrType = "A" - response.Address = ip } else if ipparser.ValidIP6(ip) { response.AddrType = "AAAA" - response.Address = ip } else { response.Success = false response.Message = fmt.Sprintf("%s is neither a valid IPv4 nor IPv6 address", response.Address) log.Println(fmt.Sprintf("Invalid address: %s", response.Address)) return response } + + response.Address = ip } response.Success = true return response } + +func getUserIP(r *http.Request) (string, error) { + for _, h := range []string{"X-Real-Ip", "X-Forwarded-For"} { + addresses := strings.Split(r.Header.Get(h), ",") + // march from right to left until we get a public address + // that will be the address right before our proxy. + for i := len(addresses) - 1; i >= 0; i-- { + ip := strings.TrimSpace(addresses[i]) + // header can contain spaces too, strip those out. + realIP := net.ParseIP(ip) + if !realIP.IsGlobalUnicast() || isPrivateSubnet(realIP) { + // bad address, go to next + continue + } + return ip, nil + } + } + return "", errors.New("no match") +} + +//ipRange - a structure that holds the start and end of a range of ip addresses +type ipRange struct { + start net.IP + end net.IP +} + +// inRange - check to see if a given ip address is within a range given +func inRange(r ipRange, ipAddress net.IP) bool { + // strcmp type byte comparison + if bytes.Compare(ipAddress, r.start) >= 0 && bytes.Compare(ipAddress, r.end) < 0 { + return true + } + return false +} + +var privateRanges = []ipRange{ + ipRange{ + start: net.ParseIP("10.0.0.0"), + end: net.ParseIP("10.255.255.255"), + }, + ipRange{ + start: net.ParseIP("100.64.0.0"), + end: net.ParseIP("100.127.255.255"), + }, + ipRange{ + start: net.ParseIP("172.16.0.0"), + end: net.ParseIP("172.31.255.255"), + }, + ipRange{ + start: net.ParseIP("192.0.0.0"), + end: net.ParseIP("192.0.0.255"), + }, + ipRange{ + start: net.ParseIP("192.168.0.0"), + end: net.ParseIP("192.168.255.255"), + }, + ipRange{ + start: net.ParseIP("198.18.0.0"), + end: net.ParseIP("198.19.255.255"), + }, +} + +// isPrivateSubnet - check to see if this ip is in a private subnet +func isPrivateSubnet(ipAddress net.IP) bool { + // my use case is only concerned with ipv4 atm + if ipCheck := ipAddress.To4(); ipCheck != nil { + // iterate over all our ranges + for _, r := range privateRanges { + // check if this ip is in a private range + if inRange(r, ipAddress) { + return true + } + } + } + return false +} + diff --git a/rest-api/request_handler_test.go b/rest-api/request_handler_test.go index c2b9c91..ba6681a 100644 --- a/rest-api/request_handler_test.go +++ b/rest-api/request_handler_test.go @@ -48,6 +48,56 @@ func TestBuildWebserviceResponseFromRequestToReturnValidObject(t *testing.T) { } } +func TestBuildWebserviceResponseFromRequestWithXRealIPHeaderToReturnValidObject(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("GET", "/update?secret=changeme&domain=foo", nil) + req.Header.Add("X-Real-Ip", "1.2.3.4") + result := BuildWebserviceResponseFromRequest(req, appConfig, defaultExtractor) + + if result.Success != true { + t.Fatalf("Expected WebserviceResponse.Success to be true") + } + + if result.Domain != "foo" { + t.Fatalf("Expected WebserviceResponse.Domain to be foo") + } + + if result.Address != "1.2.3.4" { + t.Fatalf("Expected WebserviceResponse.Address to be 1.2.3.4") + } + + if result.AddrType != "A" { + t.Fatalf("Expected WebserviceResponse.AddrType to be A") + } +} + +func TestBuildWebserviceResponseFromRequestWithXForwardedForHeaderToReturnValidObject(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("GET", "/update?secret=changeme&domain=foo", nil) + req.Header.Add("X-Forwarded-For", "1.2.3.4") + result := BuildWebserviceResponseFromRequest(req, appConfig, defaultExtractor) + + if result.Success != true { + t.Fatalf("Expected WebserviceResponse.Success to be true") + } + + if result.Domain != "foo" { + t.Fatalf("Expected WebserviceResponse.Domain to be foo") + } + + if result.Address != "1.2.3.4" { + t.Fatalf("Expected WebserviceResponse.Address to be 1.2.3.4") + } + + if result.AddrType != "A" { + t.Fatalf("Expected WebserviceResponse.AddrType to be A") + } +} + func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenNoSecretIsGiven(t *testing.T) { var appConfig = &Config{} appConfig.SharedSecret = "changeme"