-
Couldn't load subscription status.
- Fork 7
Create DevMode proxy through Gravity #469
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
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds an optional in-process HTTP CONNECT proxy to the dev server via a new CLI flag (--proxy-port). Wires ConnectProxyPort through gravity.Config, starts/stops the proxy, extends netstack to handle IPv4+IPv6, and updates inbound packet processing to detect and route by IP version. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor DevUser as Dev CLI User
participant DevCmd as dev command
participant Gravity as gravity.Client
participant Net as Netstack (IPv4/IPv6)
participant Proxy as CONNECT Proxy
participant Tunnel as Gravity Tunnel
participant Remote as Remote Host
DevUser->>DevCmd: run `dev --proxy-port=19081`
DevCmd->>Gravity: New(Config{ConnectProxyPort})
DevCmd->>Gravity: Start()
Gravity->>Net: Initialize IPv4 + IPv6, routing, MTU 1280
Gravity->>Proxy: startConnectProxy(port)
Note right of Proxy: CONNECT proxy listening
rect rgb(235,245,255)
participant ClientApp as Local App
ClientApp->>Proxy: HTTP CONNECT target:port
alt target routed via tunnel
Proxy->>Tunnel: Dial via gravity tunnel
Tunnel-->>Proxy: Connection established
else direct target
Proxy->>Remote: TCP dial target:port (direct)
Remote-->>Proxy: Connection established
end
Proxy-->>ClientApp: 200 Connection Established
Note over Proxy,ClientApp: Bidirectional data relay (proxy)
end
Note over Gravity: On shutdown, stop proxy and cleanup
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧬 Code graph analysis (1)internal/gravity/gravity.go (2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
🔇 Additional comments (9)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (4)
internal/gravity/gravity.go (4)
198-206: Normalize domain patterns to avoid incorrect equality checksMixing dot-prefixed and bare domains makes
host == domain[1:]unsafe when domain doesn’t start with a dot.Apply this diff:
- agentuityDomains := []string{".agentuity.io", ".agentuity.cloud", ".agentuity.run", ".agentuity.com", "agentuity.ai"} + agentuityDomains := []string{".agentuity.io", ".agentuity.cloud", ".agentuity.run", ".agentuity.com", ".agentuity.ai"} isAgentuityDomain := false for _, domain := range agentuityDomains { - if strings.HasSuffix(host, domain) || host == domain[1:] { + d := strings.TrimPrefix(domain, ".") + if strings.HasSuffix(host, domain) || host == d { isAgentuityDomain = true break } }
258-263: Add timeouts to direct outbound dialsUsing net.Dial without deadlines can hang indefinitely. Use a Dialer with reasonable timeouts.
Apply this diff:
- remoteConn, err = net.Dial("tcp", net.JoinHostPort(host, portStr)) + dialer := net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second} + remoteConn, err = dialer.Dial("tcp", net.JoinHostPort(host, portStr))
338-355: Bind proxy server lifecycle to context for cleaner shutdownsOptional: set BaseContext and ConnContext so connections inherit c.context and cancel promptly on shutdown.
Example:
server := &http.Server{ Addr: fmt.Sprintf("127.0.0.1:%d", port), Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.handleConnect(w, r) }), ReadTimeout: 0, WriteTimeout: 0, + BaseContext: func(net.Listener) context.Context { return c.context }, }
746-753: Optional: avoid package shadowing with local variable namenetworkThe local
var network networkProvidershadows the importednetworkpackage elsewhere in the file. While scoped and not a compile error, renaming improves clarity.Example:
- var network networkProvider + var netProv networkProvider - NetworkInterface: &network, + NetworkInterface: &netProv, - network.client = client + netProv.client = client
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
cmd/dev.go(2 hunks)internal/gravity/gravity.go(7 hunks)internal/gravity/provider.go(2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
cmd/*.go
📄 CodeRabbit inference engine (.cursor/rules/code-generation.mdc)
Ensure CLI commands build and test the pipeline (generation → SDK loading → agent usage) without writing into source SDK
Files:
cmd/dev.go
🧬 Code graph analysis (2)
cmd/dev.go (2)
internal/gravity/gravity.go (2)
New(88-107)Config(70-86)internal/dev/server.go (2)
New(95-102)ServerArgs(17-22)
internal/gravity/gravity.go (2)
internal/project/project.go (1)
ProjectContext(379-388)internal/dev/server.go (2)
Server(11-15)New(95-102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build and Test (macos-latest)
- GitHub Check: Analyze (go)
🔇 Additional comments (2)
internal/gravity/provider.go (1)
113-119: PacketBuffer construction and DecRef look correctUsing Payload with a defensive copy and deferring DecRef is the right pattern for channel.Endpoint.InjectInbound.
cmd/dev.go (1)
141-155: Wiring ConnectProxyPort through gravity.Config looks goodPointer semantics (nil disables, >0 enables) are handled correctly.
| connectProxyPort, _ := cmd.Flags().GetInt("proxy-port") | ||
| var connectProxyPortPtr *uint | ||
| if connectProxyPort > 0 { | ||
| port := uint(connectProxyPort) | ||
| connectProxyPortPtr = &port | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default enables proxy despite “disabled if zero” description (and PR states default disabled)
Current default 19081 enables the CONNECT proxy by default. Either set default to 0 (and keep description), or update description/PR to reflect enabled-by-default. Also validate port range.
Apply this diff to make it disabled by default and validate:
- connectProxyPort, _ := cmd.Flags().GetInt("proxy-port")
+ connectProxyPort, _ := cmd.Flags().GetInt("proxy-port")
+ if connectProxyPort < 0 || connectProxyPort > 65535 {
+ log.Fatal("invalid --proxy-port: %d (must be 0-65535)", connectProxyPort)
+ }
var connectProxyPortPtr *uint
if connectProxyPort > 0 {
port := uint(connectProxyPort)
connectProxyPortPtr = &port
}- devCmd.Flags().Int("proxy-port", 19081, "The port to run the HTTP CONNECT proxy server on (disabled if zero)")
+ devCmd.Flags().Int("proxy-port", 0, "The port to run the HTTP CONNECT proxy server on (disabled if zero)")Also applies to: 319-319
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/gravity/provider.go (1)
95-121: Validate minimum header length for IPv4/IPv6 before injectGuard only checks len>=1. Use protocol-specific minimums to prevent malformed packet injection.
Apply this diff:
- if len(payload) < 1 { - return - } + if len(payload) < 1 { + return + } // Detect IP version from the packet header version := header.IPVersion(payload) var protocol tcpip.NetworkProtocolNumber switch version { case 4: protocol = ipv4.ProtocolNumber + if len(payload) < header.IPv4MinimumSize { + p.logger.Trace("dropping IPv4 packet: too short (%d bytes)", len(payload)) + return + } case 6: protocol = ipv6.ProtocolNumber + if len(payload) < header.IPv6MinimumSize { + p.logger.Trace("dropping IPv6 packet: too short (%d bytes)", len(payload)) + return + } default:
🧹 Nitpick comments (2)
cmd/dev.go (1)
129-136: Enabling logic looks fine; minor duplication of “starting …” logPointer gating on port > 0 is correct. Note there will be duplicate “starting CONNECT proxy …” logs here and in gravity.Start; not harmful.
internal/gravity/gravity.go (1)
171-325: Normalize domain matching and use cnet.Addresses consistently
- Replace the mixed leading-dot and bare entries with a uniform list of leading-dot domains (e.g. add “.agentuity.ai”) and match via
for _, d := range domains { if host == d[1:] || strings.HasSuffix(host, d) { … } }- Swap all
network.Addresses[...]usages tocnet.Addresses[...]- Verify that including “agentuity.ai” is intentional
- agentuityDomains := []string{".agentuity.io", ".agentuity.cloud", ".agentuity.run", ".agentuity.com"} + agentuityDomains := []string{".agentuity.io", ".agentuity.cloud", ".agentuity.run", ".agentuity.com", ".agentuity.ai"} isAgentuityDomain := false for _, d := range agentuityDomains { - if strings.HasSuffix(host, d) || host == d[1:] { + if host == d[1:] || strings.HasSuffix(host, d) { isAgentuityDomain = true break } } @@ - ip := network.Addresses["catalyst"] + ip := cnet.Addresses["catalyst"] @@ - if customip, ok := network.Addresses[part]; ok { + if customip, ok := cnet.Addresses[part]; ok { @@ - if customip, ok := network.Addresses[part]; ok { + if customip, ok := cnet.Addresses[part]; ok {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
cmd/dev.go(2 hunks)internal/gravity/gravity.go(7 hunks)internal/gravity/provider.go(2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
cmd/*.go
📄 CodeRabbit inference engine (.cursor/rules/code-generation.mdc)
Ensure CLI commands build and test the pipeline (generation → SDK loading → agent usage) without writing into source SDK
Files:
cmd/dev.go
🧬 Code graph analysis (2)
internal/gravity/gravity.go (1)
internal/project/project.go (1)
ProjectContext(379-388)
cmd/dev.go (2)
internal/gravity/gravity.go (2)
New(88-107)Config(70-86)internal/dev/server.go (2)
New(95-102)ServerArgs(17-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build and Test (macos-latest)
- GitHub Check: Analyze (go)
🔇 Additional comments (12)
cmd/dev.go (1)
142-156: Good propagation of ConnectProxyPort into gravity.ConfigConfig plumb-through is correct and matches internal/gravity changes.
internal/gravity/provider.go (1)
10-14: Imports align with new IP-version-aware handlingNew tcpip/header/ipv4 imports are appropriate.
internal/gravity/gravity.go (10)
41-41: MTU reduction to 1280 is appropriate for IPv6 and avoids PMTUD blackholesGood call.
45-68: Client struct additions look correctFields for connectProxyPort and connectProxy are well-scoped; no behavioral issues.
71-86: Config additions look correctConnectProxyPort is optional via pointer; fits CLI behavior.
90-106: New() wiring is consistentAll new fields are initialized properly.
382-389: Graceful shutdown of connect proxy is correctShutdown with timeout is appropriate; nil the field after.
656-656: Dual-stack network protocols enabledipv4 + ipv6 protocol registration is correct.
668-683: IPv6 address creation is correct; minor nit on error messageAddress addition looks good. The error message says “create IPv6 protocol address” (ok). No action needed.
685-701: IPv4 address configuration looks correctProper AddrFrom4 usage with /24 prefix.
703-720: Default routes for v4 and v6 are reasonableCreating 0/0 subnets for both families and setting route table is correct for this virtual NIC.
825-829: CONNECT proxy startup hook is placed correctlyStarting the proxy after provider connection is established makes sense.
| rootCmd.AddCommand(devCmd) | ||
| devCmd.Flags().StringP("dir", "d", ".", "The directory to run the development server in") | ||
| devCmd.Flags().Int("port", 0, "The port to run the development server on (uses project default if not provided)") | ||
| devCmd.Flags().Int("proxy-port", 19081, "The port to run the HTTP CONNECT proxy server on (disabled if zero)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default should be disabled to match PR description and avoid unexpected proxy start
The flag help says “disabled if zero,” but default 19081 enables it by default. Consider defaulting to 0.
Apply this diff:
- devCmd.Flags().Int("proxy-port", 19081, "The port to run the HTTP CONNECT proxy server on (disabled if zero)")
+ devCmd.Flags().Int("proxy-port", 0, "The port to run the HTTP CONNECT proxy server on (disabled if zero)")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| devCmd.Flags().Int("proxy-port", 19081, "The port to run the HTTP CONNECT proxy server on (disabled if zero)") | |
| devCmd.Flags().Int("proxy-port", 0, "The port to run the HTTP CONNECT proxy server on (disabled if zero)") |
🤖 Prompt for AI Agents
In cmd/dev.go around line 320, the proxy-port flag currently defaults to 19081
which contradicts the help text saying "disabled if zero"; change the flag's
default value from 19081 to 0 so the proxy is disabled by default, leaving the
flag name, type, and help string unchanged.
| // startConnectProxy starts the HTTP CONNECT proxy server if configured. | ||
| func (c *Client) startConnectProxy() error { | ||
| if c.connectProxyPort == nil { | ||
| return nil | ||
| } | ||
|
|
||
| logger := c.logger | ||
| port := *c.connectProxyPort | ||
|
|
||
| logger.Debug("starting CONNECT proxy on port %d", port) | ||
|
|
||
| server := &http.Server{ | ||
| Addr: fmt.Sprintf("127.0.0.1:%d", port), | ||
| Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| c.handleConnect(w, r) | ||
| }), | ||
| ReadTimeout: 0, | ||
| WriteTimeout: 0, | ||
| } | ||
| c.connectProxy = server | ||
|
|
||
| go func() { | ||
| if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | ||
| logger.Fatal("CONNECT proxy server failed: %v", err) | ||
| } | ||
| }() | ||
|
|
||
| logger.Info("CONNECT proxy listening on http://127.0.0.1:%d", port) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pre-bind the listener to surface port conflicts synchronously (avoid Fatal in goroutine)
Currently errors in ListenAndServe are logged as Fatal inside a goroutine. Prefer binding first and returning an error to caller.
Apply this diff:
- server := &http.Server{
+ server := &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", port),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.handleConnect(w, r)
}),
ReadTimeout: 0,
WriteTimeout: 0,
}
c.connectProxy = server
- go func() {
- if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- logger.Fatal("CONNECT proxy server failed: %v", err)
- }
- }()
-
- logger.Info("CONNECT proxy listening on http://127.0.0.1:%d", port)
- return nil
+ ln, err := net.Listen("tcp", server.Addr)
+ if err != nil {
+ return fmt.Errorf("CONNECT proxy listen failed on %s: %w", server.Addr, err)
+ }
+ logger.Info("CONNECT proxy listening on http://%s", server.Addr)
+ go func() {
+ _ = server.Serve(ln) // error handled via Shutdown or context
+ }()
+ return nil📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // startConnectProxy starts the HTTP CONNECT proxy server if configured. | |
| func (c *Client) startConnectProxy() error { | |
| if c.connectProxyPort == nil { | |
| return nil | |
| } | |
| logger := c.logger | |
| port := *c.connectProxyPort | |
| logger.Debug("starting CONNECT proxy on port %d", port) | |
| server := &http.Server{ | |
| Addr: fmt.Sprintf("127.0.0.1:%d", port), | |
| Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| c.handleConnect(w, r) | |
| }), | |
| ReadTimeout: 0, | |
| WriteTimeout: 0, | |
| } | |
| c.connectProxy = server | |
| go func() { | |
| if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | |
| logger.Fatal("CONNECT proxy server failed: %v", err) | |
| } | |
| }() | |
| logger.Info("CONNECT proxy listening on http://127.0.0.1:%d", port) | |
| return nil | |
| } | |
| func (c *Client) startConnectProxy() error { | |
| if c.connectProxyPort == nil { | |
| return nil | |
| } | |
| logger := c.logger | |
| port := *c.connectProxyPort | |
| logger.Debug("starting CONNECT proxy on port %d", port) | |
| server := &http.Server{ | |
| Addr: fmt.Sprintf("127.0.0.1:%d", port), | |
| Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| c.handleConnect(w, r) | |
| }), | |
| ReadTimeout: 0, | |
| WriteTimeout: 0, | |
| } | |
| c.connectProxy = server | |
| ln, err := net.Listen("tcp", server.Addr) | |
| if err != nil { | |
| return fmt.Errorf("CONNECT proxy listen failed on %s: %w", server.Addr, err) | |
| } | |
| logger.Info("CONNECT proxy listening on http://%s", server.Addr) | |
| go func() { | |
| _ = server.Serve(ln) // error handled via Shutdown or context | |
| }() | |
| return nil | |
| } |
🤖 Prompt for AI Agents
internal/gravity/gravity.go around lines 327 to 356: pre-bind the TCP listener
synchronously to detect port conflicts and return an error instead of calling
logger.Fatal inside the goroutine; do this by creating the address string, call
net.Listen("tcp", addr) and if that returns an error return it to the caller,
then assign the listener to the server (or pass the listener to server.Serve),
start a goroutine that calls server.Serve(listener) and only log/handle Serve
errors there (no logger.Fatal from the goroutine), and keep the info log after
successful bind.
HTTP CONNECT Proxy Support for Gravity Tunnel
Summary
Adds optional HTTP CONNECT proxy support to route outgoing HTTPS connections through the gravity tunnel. This enables users to proxy their application's external API calls through the same secure tunnel used for incoming dev mode traffic.
Changes
Core Implementation
internal/gravity/gravity.goconnectProxyPortfield toClientandConfigstructshandleConnect()- handles HTTP CONNECT requestsstartConnectProxy()- starts optional proxy servercleanup()internal/gravity/provider.goProcessInPacket()to detect and handle both IPv4 and IPv6 packetsPayloadAPI withDecRef()for memory safetycmd/dev.go--proxy-portCLI flag (optional, default enabled on port 19081)Technical Details
Netstack Configuration
CONNECT Proxy Flow
For Agentuity Domains:
CONNECT host:port HTTP/1.1.agentuity.io,.agentuity.cloud,.agentuity.run,.agentuity.com)gonet.DialTCP()HTTP/1.1 200 Connection EstablishedFor Other Domains:
CONNECT host:port HTTP/1.1net.Dial()HTTP/1.1 200 Connection EstablishedPacket Handling
ProcessInPacket()→ detects IPv4/IPv6 →InjectInbound()→ netstackUsage
Domain Routing
The CONNECT proxy intelligently routes traffic based on the destination domain:
Agentuity Domains (routed through gravity tunnel):
*.agentuity.io*.agentuity.cloud*.agentuity.run*.agentuity.comAll Other Domains (direct connection):
This allows applications to use a single proxy configuration while automatically routing only agentuity traffic through the dev tunnel.
Testing
Tested with:
Logging
Configuration
--proxy-portBackward Compatibility
Fully backward compatible - proxy is disabled by default. No changes to existing behavior when flag is not provided.
Future Enhancements
Potential future improvements:
Summary by CodeRabbit
New Features
Bug Fixes