Skip to content

Commit

Permalink
Add trusted proxies back as optional: (#366)
Browse files Browse the repository at this point in the history
## Description


There is an existing use-case where a user doesn't use Smee for DHCP and interpolating the mac address into the URL for the auto.ipxe script is either not possible or very difficult in their DHCP server. This PR will allow using trusted proxies (XFF) as a fallback if the MAC address is not found in the URL path.

## Why is this needed



Fixes: #

## How Has This Been Tested?





## How are existing users impacted? What migration steps/scripts do we need?





## Checklist:

I have:

- [ ] updated the documentation and/or roadmap (if required)
- [ ] added unit or e2e tests
- [ ] provided instructions on how to upgrade
  • Loading branch information
mergify[bot] authored Nov 3, 2023
2 parents ace8a71 + 2699d5d commit be5dd4a
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 26 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ FLAGS
-osie-url [http] URL where OSIE (HookOS) images are located
-tink-server [http] IP:Port for the Tink server
-tink-server-tls [http] use TLS for Tink server (default "false")
-trusted-proxies [http] comma separated list of trusted proxies in CIDR notation
-syslog-addr [syslog] local IP:Port to listen on for Syslog messages (default "172.17.0.2:514")
-syslog-enabled [syslog] enable Syslog server(receiver) (default "true")
-ipxe-script-patch [tftp/http] iPXE script fragment to patch into served iPXE binaries served via TFTP or HTTP
Expand Down
1 change: 1 addition & 0 deletions cmd/smee/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func ipxeHTTPScriptFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.ipxeHTTPScript.enabled, "http-ipxe-script-enabled", true, "[http] enable iPXE HTTP script server")
fs.StringVar(&c.ipxeHTTPScript.bindAddr, "http-addr", detectPublicIPv4(":80"), "[http] local IP:Port to listen on for iPXE HTTP script requests")
fs.StringVar(&c.ipxeHTTPScript.extraKernelArgs, "extra-kernel-args", "", "[http] extra set of kernel args (k=v k=v) that are appended to the kernel cmdline iPXE script")
fs.StringVar(&c.ipxeHTTPScript.trustedProxies, "trusted-proxies", "", "[http] comma separated list of trusted proxies in CIDR notation")
fs.StringVar(&c.ipxeHTTPScript.hookURL, "osie-url", "", "[http] URL where OSIE (HookOS) images are located")
fs.StringVar(&c.ipxeHTTPScript.tinkServer, "tink-server", "", "[http] IP:Port for the Tink server")
fs.BoolVar(&c.ipxeHTTPScript.tinkServerUseTLS, "tink-server-tls", false, "[http] use TLS for Tink server")
Expand Down
1 change: 1 addition & 0 deletions cmd/smee/flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ FLAGS
-osie-url [http] URL where OSIE (HookOS) images are located
-tink-server [http] IP:Port for the Tink server
-tink-server-tls [http] use TLS for Tink server (default "false")
-trusted-proxies [http] comma separated list of trusted proxies in CIDR notation
-syslog-addr [syslog] local IP:Port to listen on for Syslog messages (default "%[1]v:514")
-syslog-enabled [syslog] enable Syslog server(receiver) (default "true")
-ipxe-script-patch [tftp/http] iPXE script fragment to patch into served iPXE binaries served via TFTP or HTTP
Expand Down
42 changes: 35 additions & 7 deletions cmd/smee/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type ipxeHTTPScript struct {
hookURL string
tinkServer string
tinkServerUseTLS bool
trustedProxies string
}

type dhcpConfig struct {
Expand Down Expand Up @@ -141,10 +142,7 @@ func main() {
EnableTFTPSinglePort: true,
}
tftpServer.EnableTFTPSinglePort = true
if ip, err := netip.ParseAddrPort(cfg.tftp.bindAddr); err != nil {
log.Error(err, "invalid bind address")
panic(fmt.Errorf("invalid bind address: %w", err))
} else {
if ip, err := netip.ParseAddrPort(cfg.tftp.bindAddr); err == nil {
tftpServer.TFTP = ipxedust.ServerSpec{
Disabled: false,
Addr: ip,
Expand All @@ -156,6 +154,9 @@ func main() {
g.Go(func() error {
return tftpServer.ListenAndServe(ctx)
})
} else {
log.Error(err, "invalid bind address")
panic(fmt.Errorf("invalid bind address: %w", err))
}
}

Expand Down Expand Up @@ -205,9 +206,10 @@ func main() {
if len(handlers) > 0 {
// start the http server for ipxe binaries and scripts
httpServer := &http.Config{
GitRev: GitRev,
StartTime: startTime,
Logger: log,
GitRev: GitRev,
StartTime: startTime,
Logger: log,
TrustedProxies: parseTrustedProxies(cfg.ipxeHTTPScript.trustedProxies),
}
log.Info("serving http", "addr", cfg.ipxeHTTPScript.bindAddr)
g.Go(func() error {
Expand Down Expand Up @@ -331,3 +333,29 @@ func defaultLogger(level string) logr.Logger {

return zapr.NewLogger(zapLogger)
}

func parseTrustedProxies(trustedProxies string) (result []string) {
for _, cidr := range strings.Split(trustedProxies, ",") {
cidr = strings.TrimSpace(cidr)
if cidr == "" {
continue
}
_, _, err := net.ParseCIDR(cidr)
if err != nil {
// Its not a cidr, but maybe its an IP
if ip := net.ParseIP(cidr); ip != nil {
if ip.To4() != nil {
cidr += "/32"
} else {
cidr += "/128"
}
} else {
// not an IP, panic
panic("invalid ip cidr in TRUSTED_PROXIES cidr=" + cidr)
}
}
result = append(result, cidr)
}

return result
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/go-logr/zapr v1.3.0
github.com/google/go-cmp v0.6.0
github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a
github.com/packethost/xff v0.0.0-20190305172552-d3e9190c41b3
github.com/peterbourgon/ff/v3 v3.4.0
github.com/prometheus/client_golang v1.17.0
github.com/tinkerbell/dhcp v0.0.0-20231102180731-28d9c2fedcbf
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
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
36 changes: 29 additions & 7 deletions ipxe/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"runtime"
"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"
)

// Config is the configuration for the http server.
type Config struct {
GitRev string
StartTime time.Time
Logger logr.Logger
GitRev string
StartTime time.Time
Logger logr.Logger
TrustedProxies []string
}

// HandlerMapping is a map of routes to http.HandlerFuncs.
Expand All @@ -38,12 +41,31 @@ func (s *Config) ServeHTTP(ctx context.Context, addr string, handlers HandlerMap
// wrap the mux with an OpenTelemetry interceptor
otelHandler := otelhttp.NewHandler(mux, "smee-http")

server := http.Server{
Addr: addr,
Handler: &loggingMiddleware{
// 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{
AllowedSubnets: s.TrustedProxies,
})
if err != nil {
s.Logger.Error(err, "failed to create new xff object")
panic(fmt.Errorf("failed to create new xff object: %v", err))
}

xffHandler = xffmw.Handler(&loggingMiddleware{
handler: otelHandler,
log: s.Logger,
})
} else {
xffHandler = &loggingMiddleware{
handler: otelHandler,
log: s.Logger,
},
}
}

server := http.Server{
Addr: addr,
Handler: xffHandler,

// Mitigate Slowloris attacks. 30 seconds is based on Apache's recommended 20-40
// recommendation. Smee doesn't really have many headers so 20s should be plenty of time.
Expand Down
72 changes: 61 additions & 11 deletions ipxe/script/ipxe.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ func getByMac(ctx context.Context, mac net.HardwareAddr, br handler.BackendReade
}, nil
}

func getByIP(ctx context.Context, ip net.IP, br handler.BackendReader) (data, error) {
d, n, err := br.GetByIP(ctx, ip)
if err != nil {
return data{}, err
}

return data{
AllowNetboot: n.AllowNetboot,
Console: "",
MACAddress: d.MACAddress,
Arch: d.Arch,
VLANID: d.VLANID,
WorkflowID: d.MACAddress.String(),
Facility: n.Facility,
IPXEScript: n.IPXEScript,
IPXEScriptURL: n.IPXEScriptURL,
}, nil
}

// HandlerFunc returns a http.HandlerFunc that serves the ipxe script.
// It is expected that the request path is /<mac address>/auto.ipxe.
func (h *Handler) HandlerFunc() http.HandlerFunc {
Expand All @@ -86,24 +105,55 @@ func (h *Handler) HandlerFunc() http.HandlerFunc {
// 2. the network.interfaces[].netboot.allow_pxe value, in the tink server hardware record, equal to true
// This allows serving custom ipxe scripts, starting up into OSIE or other installation environments
// without a tink workflow present.
mac := path.Base(path.Dir(r.URL.Path))
ha, err := net.ParseMAC(mac)
if err != nil {
w.WriteHeader(http.StatusNotFound)
h.Logger.Info("URL path not supported, the second to last element in the URL path must be a valid mac address", "error", err)

// Try to get the MAC address from the URL path, if not available get the source IP address.
if ha, err := getMAC(r.URL.Path); err == nil {
hw, err := getByMac(ctx, ha, h.Backend)
if err != nil || !hw.AllowNetboot {
w.WriteHeader(http.StatusNotFound)
h.Logger.Info("the hardware data for this machine, or lack there of, does not allow it to pxe", "client", ha, "error", err)

return
}
h.serveBootScript(ctx, w, path.Base(r.URL.Path), hw)
return
}
hw, err := getByMac(ctx, ha, h.Backend)
if err != nil || !hw.AllowNetboot {
w.WriteHeader(http.StatusNotFound)
h.Logger.Info("the hardware data for this machine, or lack there of, does not allow it to pxe", "client", r.RemoteAddr, "error", err)

if ip, err := getIP(r.RemoteAddr); err == nil {
hw, err := getByIP(ctx, ip, h.Backend)
if err != nil || !hw.AllowNetboot {
w.WriteHeader(http.StatusNotFound)
h.Logger.Info("the hardware data for this machine, or lack there of, does not allow it to pxe", "client", r.RemoteAddr, "error", err)

return
}
h.serveBootScript(ctx, w, path.Base(r.URL.Path), hw)
return
}

h.serveBootScript(ctx, w, path.Base(r.URL.Path), hw)
// If we get here, we were unable to get the MAC address from the URL path or the source IP address.
w.WriteHeader(http.StatusNotFound)
h.Logger.Info("unable to get the MAC address from the URL path or the source IP address", "client", r.RemoteAddr, "urlPath", r.URL.Path)
}
}

func getIP(remoteAddr string) (net.IP, error) {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return net.IP{}, fmt.Errorf("error parsing client address: %w: client: %v", err, remoteAddr)
}
ip := net.ParseIP(host)

return ip, nil
}

func getMAC(urlPath string) (net.HardwareAddr, error) {
mac := path.Base(path.Dir(urlPath))
ha, err := net.ParseMAC(mac)
if err != nil {
return net.HardwareAddr{}, fmt.Errorf("URL path not supported, the second to last element in the URL path must be a valid mac address, err: %w", err)
}

return ha, nil
}

func (h *Handler) serveBootScript(ctx context.Context, w http.ResponseWriter, name string, hw data) {
Expand Down
2 changes: 1 addition & 1 deletion lint.mk
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ LINTERS :=
FIXERS :=

GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml
GOLANGCI_LINT_VERSION ?= v1.53.3
GOLANGCI_LINT_VERSION ?= v1.55.2
GOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH)
$(GOLANGCI_LINT_BIN):
mkdir -p $(LINT_ROOT)/out/linters
Expand Down

0 comments on commit be5dd4a

Please sign in to comment.