Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove github.com/packethost/xff dependency #451

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ require (
github.com/go-logr/stdr v1.2.2
github.com/google/go-cmp v0.6.0
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475
github.com/packethost/xff v0.0.0-20190305172552-d3e9190c41b3
github.com/peterbourgon/ff/v3 v3.4.0
github.com/prometheus/client_golang v1.20.5
github.com/stretchr/testify v1.9.0
github.com/tinkerbell/ipxedust v0.0.0-20241108174245-aa0c0298057d
github.com/tinkerbell/tink v0.12.1
github.com/vishvananda/netlink v1.3.0
Expand Down Expand Up @@ -78,6 +78,7 @@ require (
github.com/pin/tftp/v3 v3.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/xattr v0.4.9 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,6 @@ github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
github.com/packethost/xff v0.0.0-20190305172552-d3e9190c41b3 h1:QcUVLV3NdkCVv4DxQkhgkxTsRvuXn+ZuSqD93mQYouc=
github.com/packethost/xff v0.0.0-20190305172552-d3e9190c41b3/go.mod h1:nt3WBqCaQsbnxYVBoB4pF+F584z9PjdSVm29iu4gIBg=
github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
Expand Down
3 changes: 1 addition & 2 deletions internal/ipxe/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"time"

"github.com/go-logr/logr"
"github.com/packethost/xff"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
Expand Down Expand Up @@ -44,7 +43,7 @@
// add X-Forwarded-For support if trusted proxies are configured
var xffHandler http.Handler
if len(s.TrustedProxies) > 0 {
xffmw, err := xff.New(xff.Options{
xffmw, err := newXFF(xffOptions{

Check warning on line 46 in internal/ipxe/http/http.go

View check run for this annotation

Codecov / codecov/patch

internal/ipxe/http/http.go#L46

Added line #L46 was not covered by tests
AllowedSubnets: s.TrustedProxies,
})
if err != nil {
Expand Down
151 changes: 151 additions & 0 deletions internal/ipxe/http/xff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
https://github.com/sebest/xff
Copyright (c) 2015 Sebastien Estienne (sebastien.estienne@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package http

import (
"net"
"net/http"
"strings"
)

// xffOptions is a configuration container to setup the XFF middleware.
type xffOptions struct {
// AllowedSubnets is a list of Subnets from which we will accept the
// X-Forwarded-For header.
// If this list is empty we will accept every Subnets (default).
AllowedSubnets []string
// Debugging flag adds additional output to debug server side XFF issues.
Debug bool
}

// xff http handler.
type xff struct {
// Set to true if all IPs or Subnets are allowed.
allowAll bool
// List of IP subnets that are allowed.
allowedMasks []net.IPNet
}

// New creates a new XFF handler with the provided options.
func newXFF(options xffOptions) (*xff, error) {
allowedMasks, err := toMasks(options.AllowedSubnets)
if err != nil {
return nil, err
}

Check warning on line 55 in internal/ipxe/http/xff.go

View check run for this annotation

Codecov / codecov/patch

internal/ipxe/http/xff.go#L54-L55

Added lines #L54 - L55 were not covered by tests
xff := &xff{
allowAll: len(options.AllowedSubnets) == 0,
allowedMasks: allowedMasks,
}

return xff, nil
}

// Handler updates RemoteAdd from X-Fowarded-For Headers.
func (xff *xff) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.RemoteAddr = getRemoteAddrIfAllowed(r, xff.allowed)
h.ServeHTTP(w, r)
})

Check warning on line 69 in internal/ipxe/http/xff.go

View check run for this annotation

Codecov / codecov/patch

internal/ipxe/http/xff.go#L65-L69

Added lines #L65 - L69 were not covered by tests
}

// getRemoteAddrIfAllowed parses the given request, resolves the X-Forwarded-For header
// and returns the resolved remote address if allowed.
func getRemoteAddrIfAllowed(r *http.Request, allowed func(sip string) bool) string {
if xffh := r.Header.Get("X-Forwarded-For"); xffh != "" {
if sip, sport, err := net.SplitHostPort(r.RemoteAddr); err == nil && sip != "" {
if allowed(sip) {
if xip := parse(xffh, allowed); xip != "" {
return net.JoinHostPort(xip, sport)
}

Check warning on line 80 in internal/ipxe/http/xff.go

View check run for this annotation

Codecov / codecov/patch

internal/ipxe/http/xff.go#L74-L80

Added lines #L74 - L80 were not covered by tests
}
}
}
return r.RemoteAddr

Check warning on line 84 in internal/ipxe/http/xff.go

View check run for this annotation

Codecov / codecov/patch

internal/ipxe/http/xff.go#L84

Added line #L84 was not covered by tests
}

// parse parses the value of the X-Forwarded-For Header and returns the IP address.
func parse(ipList string, allowed func(string) bool) string {
ips := strings.Split(ipList, ",")
if len(ips) == 0 {
return ""
}

Check warning on line 92 in internal/ipxe/http/xff.go

View check run for this annotation

Codecov / codecov/patch

internal/ipxe/http/xff.go#L91-L92

Added lines #L91 - L92 were not covered by tests

// simple case of only 1 proxy
if len(ips) == 1 {
ip := strings.TrimSpace(ips[0])
if net.ParseIP(ip) != nil {
return ip
}
return ""
}

// multiple proxies
// common form of X-F-F is: client, proxy1, proxy2, ... proxyN-1
// so we verify backwards and return the first unallowed/untrusted proxy
lastIP := ""
for i := len(ips) - 1; i >= 0; i-- {
ip := strings.TrimSpace(ips[i])
if net.ParseIP(ip) == nil {
break
}
lastIP = ip
if !allowed(ip) {
break
}
}
return lastIP
}

// converts a list of subnets' string to a list of net.IPNet.
func toMasks(ips []string) (masks []net.IPNet, err error) {
for _, cidr := range ips {
var network *net.IPNet
_, network, err = net.ParseCIDR(cidr)
if err != nil {
return
}
masks = append(masks, *network)
}
return
}

// checks that the IP is allowed.
func (xff *xff) allowed(sip string) bool {
if xff.allowAll {
return true
} else if ip := net.ParseIP(sip); ip != nil && ipInMasks(ip, xff.allowedMasks) {
return true
}
return false
}

// checks if a net.IP is in a list of net.IPNet.
func ipInMasks(ip net.IP, masks []net.IPNet) bool {
for _, mask := range masks {
if mask.Contains(ip) {
return true
}
}
return false
}
158 changes: 158 additions & 0 deletions internal/ipxe/http/xff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
https://github.com/sebest/xff
Copyright (c) 2015 Sebastien Estienne (sebastien.estienne@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package http

import (
"net"
"testing"

"github.com/stretchr/testify/assert"
)

func TestParse_none(t *testing.T) {
res := parse("", nil)
assert.Equal(t, "", res)
}

func allowAll(string) bool { return true }

func TestParse_localhost(t *testing.T) {
res := parse("127.0.0.1", allowAll)
assert.Equal(t, "127.0.0.1", res)
}

func TestParse_invalid(t *testing.T) {
res := parse("invalid", allowAll)
assert.Equal(t, "", res)
}

func TestParse_invalid_sioux(t *testing.T) {
res := parse("123#1#2#3", allowAll)
assert.Equal(t, "", res)
}

func TestParse_invalid_private_lookalike(t *testing.T) {
res := parse("102.3.2.1", allowAll)
assert.Equal(t, "102.3.2.1", res)
}

func TestParse_valid(t *testing.T) {
res := parse("68.45.152.220", allowAll)
assert.Equal(t, "68.45.152.220", res)
}

func TestParse_multi_first(t *testing.T) {
res := parse("12.13.14.15, 68.45.152.220", allowAll)
assert.Equal(t, "12.13.14.15", res)
}

func TestParse_multi_with_invalid(t *testing.T) {
res := parse("invalid, 190.57.149.90", allowAll)
assert.Equal(t, "190.57.149.90", res)
}

func TestParse_multi_with_invalid2(t *testing.T) {
res := parse("190.57.149.90, invalid", allowAll)
assert.Equal(t, "", res)
}

func TestParse_multi_with_invalid_sioux(t *testing.T) {
res := parse("190.57.149.90, 123#1#2#3", allowAll)
assert.Equal(t, "", res)
}

func TestParse_ipv6_with_port(t *testing.T) {
res := parse("2604:2000:71a9:bf00:f178:a500:9a2d:670d", allowAll)
assert.Equal(t, "2604:2000:71a9:bf00:f178:a500:9a2d:670d", res)
}

func TestToMasks_empty(t *testing.T) {
ips := []string{}
masks, err := toMasks(ips)
assert.Empty(t, masks)
assert.Nil(t, err)
}

func TestToMasks(t *testing.T) {
ips := []string{"127.0.0.1/32", "10.0.0.0/8"}
masks, err := toMasks(ips)
_, ipnet1, _ := net.ParseCIDR("127.0.0.1/32")
_, ipnet2, _ := net.ParseCIDR("10.0.0.0/8")
assert.Equal(t, []net.IPNet{*ipnet1, *ipnet2}, masks)
assert.Nil(t, err)
}

func TestToMasks_error(t *testing.T) {
ips := []string{"error"}
masks, err := toMasks(ips)
assert.Empty(t, masks)
assert.Equal(t, &net.ParseError{Type: "CIDR address", Text: "error"}, err)
}

func TestAllowed_all(t *testing.T) {
m, _ := newXFF(xffOptions{
AllowedSubnets: []string{},
})
assert.True(t, m.allowed("127.0.0.1"))
}

func TestAllowed_yes(t *testing.T) {
m, _ := newXFF(xffOptions{
AllowedSubnets: []string{"127.0.0.0/16"},
})
assert.True(t, m.allowed("127.0.0.1"))

m, _ = newXFF(xffOptions{
AllowedSubnets: []string{"127.0.0.1/32"},
})
assert.True(t, m.allowed("127.0.0.1"))
}

func TestAllowed_no(t *testing.T) {
m, _ := newXFF(xffOptions{
AllowedSubnets: []string{"127.0.0.0/16"},
})
assert.False(t, m.allowed("127.1.0.1"))

m, _ = newXFF(xffOptions{
AllowedSubnets: []string{"127.0.0.1/32"},
})
assert.False(t, m.allowed("127.0.0.2"))
}

func TestParseUnallowedMidway(t *testing.T) {
m, _ := newXFF(xffOptions{
AllowedSubnets: []string{"127.0.0.0/16"},
})
res := parse("1.1.1.1, 8.8.8.8, 127.0.0.1, 127.0.0.2", m.allowed)
assert.Equal(t, "8.8.8.8", res)
}

func TestParseMany(t *testing.T) {
m, _ := newXFF(xffOptions{
AllowedSubnets: []string{"127.0.0.0/16"},
})
res := parse("1.1.1.1, 127.0.0.1, 127.0.0.2, 127.0.0.3", m.allowed)
assert.Equal(t, "1.1.1.1", res)
}
16 changes: 16 additions & 0 deletions internal/otel/otel.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
https://github.com/equinix-labs/otel-init-go
Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package otel

import (
Expand Down
Loading