From 2fb1c82e6fc5ad7cb5e683cf548a041267d2b28b Mon Sep 17 00:00:00 2001 From: Clint J Edwards Date: Tue, 31 Mar 2026 15:32:47 -0400 Subject: [PATCH] feat: Adding support for ResVPNProxy data Adding a lookup function for ResVPNProxy. This enables VPN/Proxy intelligence data for specific IP addresses. --- _examples/resvpnproxy/fastly.toml | 8 + _examples/resvpnproxy/main.go | 61 ++++++ fsthttp/request.go | 1 - fsthttp/resvpnproxy.go | 97 +++++++++ internal/abi/fastly/hostcalls_noguest.go | 45 ++++ internal/abi/fastly/http_guest.go | 249 ++++++++++++++++++++++- internal/abi/fastly/resvpnproxy_guest.go | 22 ++ 7 files changed, 481 insertions(+), 2 deletions(-) create mode 100644 _examples/resvpnproxy/fastly.toml create mode 100644 _examples/resvpnproxy/main.go create mode 100644 fsthttp/resvpnproxy.go create mode 100644 internal/abi/fastly/resvpnproxy_guest.go diff --git a/_examples/resvpnproxy/fastly.toml b/_examples/resvpnproxy/fastly.toml new file mode 100644 index 0000000..3be9b62 --- /dev/null +++ b/_examples/resvpnproxy/fastly.toml @@ -0,0 +1,8 @@ +# This file describes a Fastly Compute package. To learn more visit: +# https://developer.fastly.com/reference/fastly-toml/ + +authors = ["oss@fastly.com"] +description = "Test ResVPNProxy hostcalls" +language = "go" +manifest_version = 2 +name = "resvpnproxy-test" diff --git a/_examples/resvpnproxy/main.go b/_examples/resvpnproxy/main.go new file mode 100644 index 0000000..a9d4eea --- /dev/null +++ b/_examples/resvpnproxy/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/fastly/compute-sdk-go/fsthttp" +) + +// Response represents the complete response structure +type Response struct { + ClientIP string `json:"client_ip"` + *fsthttp.ResVPNProxyResult `json:",inline"` +} + +// ErrorResponse represents an error response structure +type ErrorResponse struct { + Error string `json:"error"` + ClientIP string `json:"client_ip"` +} + +func main() { + fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { + w.Header().Set("Content-Type", "application/json") + + // Test the ResVPNProxy data using the request method + vpnData, err := r.ResVPNProxyData() + if err != nil { + w.WriteHeader(fsthttp.StatusInternalServerError) + errorResp := ErrorResponse{ + Error: fmt.Sprintf("Error getting ResVPNProxy data: %s", err), + ClientIP: r.RemoteAddr, + } + if jsonData, jsonErr := json.Marshal(errorResp); jsonErr != nil { + fmt.Fprintf(w, `{"error": "JSON marshaling failed"}`) + } else { + w.Write(jsonData) + } + return + } + + // Success case - return the ResVPNProxy data + w.WriteHeader(fsthttp.StatusOK) + response := Response{ + ClientIP: r.RemoteAddr, + ResVPNProxyResult: vpnData, + } + + if jsonData, err := json.Marshal(response); err != nil { + w.WriteHeader(fsthttp.StatusInternalServerError) + fmt.Fprintf(w, `{"error": "JSON marshaling failed"}`) + } else { + w.Write(jsonData) + } + + // Log to console for debugging + log.Printf("ResVPNProxy analysis complete for client %s", r.RemoteAddr) + }) +} diff --git a/fsthttp/request.go b/fsthttp/request.go index 2c4908a..c2da751 100644 --- a/fsthttp/request.go +++ b/fsthttp/request.go @@ -1228,7 +1228,6 @@ func (inspect *InspectResponse) IsRedirect() bool { // {"waf_response":200,"redirect_url":"","tags":[],"verdict":"allow","decision_ms":0} func (r *Request) Inspect(opts *InspectOptions) (*InspectResponse, error) { - type inspectJSON struct { WafResponse int `json:"waf_response"` RedirectURL string `json:"redirect_url"` diff --git a/fsthttp/resvpnproxy.go b/fsthttp/resvpnproxy.go new file mode 100644 index 0000000..71d352a --- /dev/null +++ b/fsthttp/resvpnproxy.go @@ -0,0 +1,97 @@ +package fsthttp + +import ( + "fmt" +) + +// ResVPNProxyResult represents additional IP Proxy and VPN Intelligence data for a request. +type ResVPNProxyResult struct { + IsAnonymous bool `json:"is_anonymous"` // True if the IP address is present in one or more categories of anonymous flags. + IsAnonymousVPN bool `json:"is_anonymous_vpn"` // True if the IP address was identified as being from a Virtual Private Network (VPN) exit node. + IsHostingProvider bool `json:"is_hosting_provider"` // True if the IP address was identified as being from a hosting provider or data center. + IsProxyOverVPN bool `json:"is_proxy_over_vpn"` // True if the IP address was detected with the Proxy over VPN technique from premium VPN providers like ExpressVPN. + IsPublicProxy bool `json:"is_public_proxy"` // True if the IP address was identified as being from a proxy exit node. + IsRelayProxy bool `json:"is_relay_proxy"` // True if the IP address was identified as being from a relay proxy. + IsResidentialProxy bool `json:"is_residential_proxy"` // True if the IP address was identified as being from a proxy associated with a residential ISP. + IsSmartDNSProxy bool `json:"is_smart_dns_proxy"` // True if the IP address was identified as being from a SmartDNS exit node. + IsTorExitNode bool `json:"is_tor_exit_node"` // True if the IP address was identified as being from a Tor exit node. + IsVPNDatacenter bool `json:"is_vpn_datacenter"` // True if the IP address was identified as being part of a known VPN data center or IP address range. + VPNServiceName string `json:"vpn_service_name"` // Displays the name of the VPN associated with the network of the IP address. +} + +// ResVPNProxyData analyzes the current downstream request's IP address and returns VPN and proxy intelligence data. +// +// Returns an error if the ResVPNProxy feature is not enabled for your service or intelligence data is not available. +// +// Example usage: +// +// vpnData, err := r.ResVPNProxyData() +// if err != nil { +// // Feature not enabled or other error +// return err +// } +func (r *Request) ResVPNProxyData() (*ResVPNProxyResult, error) { + if r.downstream.req == nil { + return nil, fmt.Errorf("downstream request not available") + } + + result := &ResVPNProxyResult{} + var err error + + result.IsAnonymous, err = r.downstream.req.DownstreamResVPNProxyIsAnonymous() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_anonymous: %w", err) + } + + result.IsAnonymousVPN, err = r.downstream.req.DownstreamResVPNProxyIsAnonymousVPN() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_anonymous_vpn: %w", err) + } + + result.IsHostingProvider, err = r.downstream.req.DownstreamResVPNProxyIsHostingProvider() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_hosting_provider: %w", err) + } + + result.IsProxyOverVPN, err = r.downstream.req.DownstreamResVPNProxyIsProxyOverVPN() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_proxy_over_vpn: %w", err) + } + + result.IsPublicProxy, err = r.downstream.req.DownstreamResVPNProxyIsPublicProxy() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_public_proxy: %w", err) + } + + result.IsRelayProxy, err = r.downstream.req.DownstreamResVPNProxyIsRelayProxy() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_relay_proxy: %w", err) + } + + result.IsResidentialProxy, err = r.downstream.req.DownstreamResVPNProxyIsResidentialProxy() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_residential_proxy: %w", err) + } + + result.IsSmartDNSProxy, err = r.downstream.req.DownstreamResVPNProxyIsSmartDNSProxy() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_smart_dns_proxy: %w", err) + } + + result.IsTorExitNode, err = r.downstream.req.DownstreamResVPNProxyIsTorExitNode() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_tor_exit_node: %w", err) + } + + result.IsVPNDatacenter, err = r.downstream.req.DownstreamResVPNProxyIsVPNDatacenter() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("is_vpn_datacenter: %w", err) + } + + result.VPNServiceName, err = r.downstream.req.DownstreamResVPNProxyVPNServiceName() + if err = ignoreNoneError(err); err != nil { + return nil, fmt.Errorf("vpn_service_name: %w", err) + } + + return result, nil +} diff --git a/internal/abi/fastly/hostcalls_noguest.go b/internal/abi/fastly/hostcalls_noguest.go index 988984e..8a3b6af 100644 --- a/internal/abi/fastly/hostcalls_noguest.go +++ b/internal/abi/fastly/hostcalls_noguest.go @@ -171,6 +171,50 @@ func (r *HTTPRequest) DownstreamBotVerified() (bool, error) { return false, fmt.Errorf("not implemented") } +func (r *HTTPRequest) DownstreamResVPNProxyIsAnonymous() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyIsAnonymousVPN() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyIsHostingProvider() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyIsProxyOverVPN() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyIsPublicProxy() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyIsRelayProxy() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyIsResidentialProxy() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyIsSmartDNSProxy() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyIsTorExitNode() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyIsVPNDatacenter() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamResVPNProxyVPNServiceName() (string, error) { + return "", fmt.Errorf("not implemented") +} + func NewHTTPRequest() (*HTTPRequest, error) { return nil, fmt.Errorf("not implemented") } @@ -473,6 +517,7 @@ func GeoLookup(ip net.IP) ([]byte, error) { return nil, fmt.Errorf("not implemented") } + type KVStore struct{} func OpenKVStore(name string) (*KVStore, error) { diff --git a/internal/abi/fastly/http_guest.go b/internal/abi/fastly/http_guest.go index 81fbccf..e004b73 100644 --- a/internal/abi/fastly/http_guest.go +++ b/internal/abi/fastly/http_guest.go @@ -1832,6 +1832,254 @@ func (r *HTTPRequest) DownstreamFastlyKeyIsValid() (bool, error) { return valid.b, nil } +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_anonymous +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsAnonymous( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsAnonymous() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsAnonymous( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_anonymous_vpn +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsAnonymousVPN( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsAnonymousVPN() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsAnonymousVPN( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_hosting_provider +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsHostingProvider( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsHostingProvider() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsHostingProvider( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_proxy_over_vpn +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsProxyOverVPN( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsProxyOverVPN() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsProxyOverVPN( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_public_proxy +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsPublicProxy( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsPublicProxy() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsPublicProxy( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_relay_proxy +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsRelayProxy( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsRelayProxy() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsRelayProxy( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_residential_proxy +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsResidentialProxy( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsResidentialProxy() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsResidentialProxy( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_smart_dns_proxy +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsSmartDNSProxy( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsSmartDNSProxy() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsSmartDNSProxy( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_tor_exit_node +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsTorExitNode( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsTorExitNode() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsTorExitNode( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_is_vpn_datacenter +//go:noescape +func fastlyHTTPDownstreamResVPNProxyIsVPNDatacenter( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyIsVPNDatacenter() (bool, error) { + var result struct { + b bool + _ prim.Usize // align padding + } + if err := fastlyHTTPDownstreamResVPNProxyIsVPNDatacenter( + r.h, + prim.ToPointer(&result.b), + ).toError(); err != nil { + return false, err + } + + return result.b, nil +} + +//go:wasmimport fastly_http_downstream downstream_resvpnproxy_vpn_service_name +//go:noescape +func fastlyHTTPDownstreamResVPNProxyVPNServiceName( + req requestHandle, + serviceNameOut prim.Pointer[prim.Char8], + serviceNameMaxLen prim.Usize, + nwrittenOut prim.Pointer[prim.Usize], +) FastlyStatus + +func (r *HTTPRequest) DownstreamResVPNProxyVPNServiceName() (string, error) { + value, err := withAdaptiveBuffer(DefaultMediumBufLen, func(buf *prim.WriteBuffer) FastlyStatus { + return fastlyHTTPDownstreamResVPNProxyVPNServiceName( + r.h, + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + }) + if err != nil { + status, ok := IsFastlyError(err) + if ok && status == FastlyStatusNone { + return "", status.toError() + } + return "", err + } + return value.ToString(), nil +} + // witx: // // (@interface func (export "downstream_bot_analyzed") @@ -2044,7 +2292,6 @@ func fastlyHTTPReqInspect( // Inspect HTTP traffic with NGWAF func (r *HTTPRequest) Inspect(info *InspectInfo, b *HTTPBody) ([]byte, error) { - value, err := withAdaptiveBuffer(DefaultMediumBufLen, func(buf *prim.WriteBuffer) FastlyStatus { return fastlyHTTPReqInspect( r.h, diff --git a/internal/abi/fastly/resvpnproxy_guest.go b/internal/abi/fastly/resvpnproxy_guest.go new file mode 100644 index 0000000..a3b5b83 --- /dev/null +++ b/internal/abi/fastly/resvpnproxy_guest.go @@ -0,0 +1,22 @@ +//go:build wasip1 && !nofastlyhostcalls + +// Copyright 2026 Fastly, Inc. + +package fastly + + +// witx: +// +// (module $fastly_resvpnproxy +// (@interface func (export "lookup") +// (param $addr_octets (@witx pointer (@witx char8))) +// (param $addr_len (@witx usize)) +// (param $buf (@witx pointer (@witx char8))) +// (param $buf_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) +// +// ) +// +