-
Notifications
You must be signed in to change notification settings - Fork 6
feat: add ip allow-list route-service #239
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0d1ef10
feat: add ip allow-list route-service
maxmoehl 4dbd31b
doc: clarify these are two apps
maxmoehl 0d55bb5
feat: add some more logs
maxmoehl 1a05498
fix: make coding more explicit
maxmoehl 360c10a
doc: unify wording to ip-prefix
maxmoehl b5aed68
doc: use example domain instead of real one
maxmoehl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # IP Allow-List Route Service | ||
|
|
||
| A simple GoLang application that implements the [route service contract](https://docs.cloudfoundry.org/services/route-services.html) | ||
| of Cloud Foundry and checks the client IP address against an allow-list before passing on the | ||
| request. This requires the `x-cf-true-client-ip` header to be set by the foundation which is the | ||
| case on [SAP BTP, Cloud Foundry runtime](https://www.sap.com/products/technology-platform/btp-cloud-foundry-runtime.html). | ||
|
|
||
| ## Usage | ||
|
|
||
| > [!CAUTION] | ||
| > | ||
| > This is a sample application that is only intended to demonstrate the general idea. It is not | ||
| > production grade. | ||
|
|
||
| To deploy this route-service you will first need a separate application you want to put this in | ||
| front of, as well as an allow-list. The allow-list should contain one IP-prefix per line, empty | ||
| lines and lines starting with a `#` are ignored. Example: | ||
|
|
||
| ``` | ||
| # This is a comment | ||
| 10.0.0.0/8 | ||
| 127.0.0.0/8 | ||
| # The following lines will be ignored | ||
|
|
||
|
|
||
| ``` | ||
|
|
||
| Put this file into the app directory next to the manifest file and name it `allowlist.txt`. Now you | ||
| can push the application like this (you may need to adjust the domain depending on the region): | ||
|
|
||
| ``` | ||
| cf push -f ./manifest.yml --var domain=cfapps.example.com --var suffix=foo | ||
| ``` | ||
|
|
||
| This pushes the app and makes it available at `ip-allow-list-rs-foo.cfapps.example.com`. | ||
| Now we can turn it into a route service and bind it to an already existing route, provide the | ||
| domain and host of the route you want restrict access to: | ||
|
|
||
| ``` | ||
| cf create-user-provided-service allow-listing -r https://ip-allow-listing-rs-foo.cfapps.example.com | ||
| cf bind-route-service cfapps.example.com --hostname my-app allow-listing | ||
| ``` | ||
|
|
||
| See the help pages of the individual commands for extended options. | ||
|
|
||
| Now every request sent to the route you have bound the route-service to will pass through the | ||
| allow-listing route-service and is only forwarded to the target application if the client IP | ||
| address is allow-listed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| module github.com/sap-samples/cf-routing-samples/ip-allow-listing-route-service | ||
|
|
||
| go 1.24.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "errors" | ||
| "fmt" | ||
| "log/slog" | ||
| "net/http" | ||
| "net/http/httputil" | ||
| "net/netip" | ||
| "net/url" | ||
| "os" | ||
| "strings" | ||
| ) | ||
|
|
||
| var ( | ||
| ErrBadRequest = fmt.Errorf("bad request") | ||
| ErrForbidden = fmt.Errorf("forbidden") | ||
| ErrMissingOrInvalidForwardedURL = fmt.Errorf("%w: invalid or missing x-cf-forwarded-url header", ErrBadRequest) | ||
| ErrMissingTrueClientIP = fmt.Errorf("%w: missing x-cf-true-client-ip header", ErrBadRequest) | ||
| ErrInvalidTrueClientIP = fmt.Errorf("%w: invalid x-cf-true-client-ip header", ErrBadRequest) | ||
| ) | ||
|
|
||
| func main() { | ||
| slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))) | ||
|
|
||
| err := Main() | ||
| if err != nil { | ||
| slog.Error("route-service failed", "error", err) | ||
| os.Exit(1) | ||
| } | ||
| } | ||
|
|
||
| func Main() error { | ||
| allowListFile := os.Getenv("ALLOW_LIST_FILE") | ||
| if allowListFile == "" { | ||
| return fmt.Errorf("no file to read prefixes from provided") | ||
| } | ||
|
|
||
| allowedPrefixes, err := loadPrefixes(allowListFile) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| s := http.Server{ | ||
| Addr: ":" + os.Getenv("PORT"), | ||
| Handler: &httputil.ReverseProxy{ | ||
| Director: proxyDirector, | ||
| ErrorHandler: proxyErrorHandler, | ||
| Transport: &transport{ | ||
| allowedPrefixes: allowedPrefixes, | ||
| roundTripper: &http.Transport{}, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| slog.Info("starting server", "allow-list", allowedPrefixes, "addr", s.Addr) | ||
|
|
||
| return s.ListenAndServe() | ||
| } | ||
|
|
||
| func loadPrefixes(path string) (prefixes []netip.Prefix, err error) { | ||
| f, err := os.Open(path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| s := bufio.NewScanner(f) | ||
| for s.Scan() { | ||
| l := strings.TrimSpace(s.Text()) | ||
| if len(l) == 0 || l[0] == '#' { | ||
| continue | ||
| } | ||
|
|
||
| p, err := netip.ParsePrefix(l) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| prefixes = append(prefixes, p) | ||
| } | ||
|
|
||
| if s.Err() != nil { | ||
| return nil, s.Err() | ||
| } | ||
|
|
||
| return prefixes, nil | ||
| } | ||
|
|
||
| func proxyDirector(req *http.Request) { | ||
| log := slog.With("component", "proxy-director", "vcap-id", req.Header.Get("x-vcap-request-id")) | ||
|
|
||
| log.Info("handling request", "client-ip", req.RemoteAddr) | ||
|
|
||
| forwardedURL := req.Header.Get("x-cf-forwarded-url") | ||
| if forwardedURL == "" { | ||
| log.Error("missing x-cf-forwarded-url header") | ||
| req.URL = nil | ||
| req.Host = "" | ||
| return // the transport will deal with that | ||
| } | ||
|
|
||
| u, err := url.Parse(forwardedURL) | ||
| if err != nil { | ||
| log.Error("invalid x-cf-forwarded-url header", "error", err) | ||
| req.URL = nil | ||
| req.Host = "" | ||
| return // the transport will deal with that | ||
| } | ||
|
|
||
| log.Debug("obtained forwarded URL", "forwarded-url", u.String()) | ||
|
|
||
| req.URL = u | ||
| req.Host = u.Host | ||
| } | ||
|
|
||
| func proxyErrorHandler(w http.ResponseWriter, r *http.Request, handleErr error) { | ||
| log := slog.With("component", "proxy-error-handler", "vcap-id", r.Header.Get("x-vcap-request-id")) | ||
|
|
||
| if handleErr == nil { | ||
| panic("received nil error") | ||
| } | ||
|
|
||
| var status int | ||
| switch { | ||
| case errors.Is(handleErr, ErrBadRequest): | ||
| status = http.StatusBadRequest | ||
| case errors.Is(handleErr, ErrForbidden): | ||
| status = http.StatusForbidden | ||
| default: | ||
| status = http.StatusBadGateway | ||
| } | ||
|
|
||
| log.Warn("handling error", "error", handleErr, "status", status) | ||
|
|
||
| w.WriteHeader(status) | ||
| _, err := fmt.Fprintf(w, "error: %s", handleErr.Error()) | ||
| if err != nil { | ||
| log.Warn("failed to send handleError to client", "handleError", handleErr, "error", err, "status", status) | ||
| } | ||
| } | ||
|
|
||
| type transport struct { | ||
| allowedPrefixes []netip.Prefix | ||
| roundTripper http.RoundTripper | ||
| } | ||
|
|
||
| func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| // This indicates that an issue was encountered while trying to read the forwarded URL. | ||
| if req.URL == nil { | ||
| return nil, ErrMissingOrInvalidForwardedURL | ||
| } | ||
|
|
||
| addrString := req.Header.Get("x-cf-true-client-ip") | ||
| if addrString == "" { | ||
| return nil, ErrMissingTrueClientIP | ||
| } | ||
|
|
||
| addr, err := netip.ParseAddr(addrString) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("%w: %w", ErrInvalidTrueClientIP, err) | ||
| } | ||
|
|
||
| // This is a bit inefficient but for a good enough for a sample. A proper implementation should | ||
| // use some form of trie for efficient lookups. | ||
| allowed := false | ||
| for _, p := range t.allowedPrefixes { | ||
| if p.Contains(addr) { | ||
| allowed = true | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if !allowed { | ||
| return nil, fmt.Errorf("%w: address '%s' is not in allow-list", ErrForbidden, addr.String()) | ||
| } | ||
|
|
||
| return t.roundTripper.RoundTrip(req) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| applications: | ||
| - name: ip-allow-listing-route-service | ||
| buildpacks: | ||
| - go_buildpack | ||
| memory: 128M | ||
| instances: 1 | ||
| env: | ||
| ALLOW_LIST_FILE: allowlist.txt | ||
| routes: | ||
| - route: ip-allow-list-rs-((suffix)).((domain)) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.