Network Troubleshooting Notes for Running Antigravity CLI in Docker

Network Troubleshooting Notes for Running Antigravity CLI in Docker

1. Background

The original Docker-based development environment was mainly intended for using Codex.

That initial setup already included a Squid container.

app-editor
  Container for Codex and general development work

squid
  Proxy container inside the Docker network

git-bare
  Internal Git server on the restricted network

app-editor was attached to multiple Docker networks.

services:
  app-editor:
    networks:
      - restricted
      - ingress
      - outbound

  git-bare:
    networks:
      restricted:
        aliases:
          - git

The internal Git server was available on the restricted network and was referenced from app-editor like this:

git://git:9418/app.git

At this stage, no Antigravity CLI-specific networking requirements had surfaced yet.


2. Architecture: Before and After

2.1 Before: Initial Architecture

flowchart LR
    subgraph Docker["Docker Compose"]
        subgraph Restricted["restricted network"]
            App["app-editor<br/>Codex / development work / agy added later"]
            Git["git-bare<br/>git://git:9418/app.git"]
        end

        subgraph ProxyNet["proxy / outbound network"]
            SquidC["Squid container<br/>http://squid:3128"]
        end

        App -->|internal git| Git
        App -.->|HTTP_PROXY / HTTPS_PROXY| SquidC
        SquidC --> Internet["Internet"]
    end

    App -.->|Some Antigravity CLI traffic bypassed the proxy| Internet

    classDef problem fill:#ffe6e6,stroke:#cc0000,color:#000;
    class Internet,App problem;

The initial environment was a Docker development setup designed around Codex, and it already included a Squid container.

After adding Antigravity CLI, some traffic appeared to bypass Squid and attempt direct outbound connections. This exposed DNS, proxy, and routing issues.

2.2 After: Final Architecture

flowchart LR
    subgraph Docker["Docker Compose"]
        subgraph Internal["internal / private networks"]
            App2["app-editor<br/>Codex / Antigravity / npm / Go"]
            Git2["git-bare<br/>git://git:9418/app.git"]
        end
    end

    App2 -->|private IP destinations are allowed| Git2

    App2 --> Bridge["Docker bridge<br/>docker0 / br-*"]
    Bridge --> DU["DOCKER-USER"]
    DU --> Filter["DOCKER-EGRESS-FILTER"]
    Filter --> Private["private IP ranges<br/>10/8, 172.16/12, 192.168/16<br/>RETURN"]
    Filter --> IPSet["ipset docker_allowed_v4<br/>allowed IPs generated from DNS"]
    Filter --> Drop["LOG + DROP<br/>unauthorized traffic"]

    IPSet -->|TCP 80/443 only| Internet2["Internet"]
    Drop -.-> Blocked["Unauthorized destinations<br/>such as example.com"]

    DockerNAT["Docker NAT / MASQUERADE<br/>left to Docker"] -.-> Bridge

    classDef ok fill:#e6ffed,stroke:#008000,color:#000;
    classDef block fill:#ffe6e6,stroke:#cc0000,color:#000;
    classDef control fill:#e6f0ff,stroke:#005fcc,color:#000;

    class Private,IPSet,Internet2 ok;
    class Drop,Blocked block;
    class DU,Filter,DockerNAT control;

In the final setup, Squid, transparent proxying, and standalone nftables-based filtering were removed from the core design.

Outbound traffic is controlled using Docker’s DOCKER-USER chain and ipset.

Docker continues to manage NAT and MASQUERADE, while custom rules only allow TCP 80/443 to approved IPs and allow private IP destinations for internal Docker communication.


3. Problem

After adding Antigravity CLI (agy), outbound networking issues appeared.

A typical error looked like this:

Eligibility check failed:
Post "https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist":
dial tcp: lookup daily-cloudcode-pa.googleapis.com on 127.0.0.11:53:
server misbehaving

In other cases, the error looked like this:

dial tcp 142.251.24.95:443: connect: network is unreachable

In short, Antigravity CLI introduced networking requirements that had not been a problem when the environment was only used with Codex.

The affected areas included:

Docker DNS
HTTPS traffic to external APIs
proxy configuration
Docker network default routes
whether Antigravity CLI respects proxy settings

4. Attempt 1: Use the Existing Squid Container

4.1 Goal

The first idea was to use the existing Squid container and route Antigravity CLI’s outbound traffic through it.

4.2 Attempted Setup

From app-editor, the Squid container inside the Docker network was referenced as:

http://squid:3128

The following environment variables were configured:

HTTP_PROXY=http://squid:3128
HTTPS_PROXY=http://squid:3128
http_proxy=http://squid:3128
https_proxy=http://squid:3128

Basic TCP connectivity and HTTP CONNECT through Squid were verified from Python.

CONNECT requests to the following destinations were confirmed to work:

oauth2.googleapis.com:443
daily-cloudcode-pa.googleapis.com:443

4.3 Result

The Squid container itself was working.

However, Antigravity CLI appeared to bypass the proxy for some of its traffic.

As a result, simply relying on HTTP_PROXY and HTTPS_PROXY did not solve the issue.

4.4 Lesson Learned

Being able to reach the Squid container
does not mean that Antigravity CLI will always use it.

5. Attempt 2: Fix Squid Hostname and IP Resolution

5.1 Goal

The goal was to avoid errors such as:

proxyconnect tcp: lookup squid ... no such host

5.2 Attempt

Because name resolution for squid looked unstable in some cases, the Squid container’s IP was fixed in Docker Compose, and proxy settings were also tested using the IP address directly.

Example:

HTTPS_PROXY=http://172.30.0.52:3128

5.3 Result

This helped clarify name resolution and reachability issues for the Squid container.

However, it did not fix the direct outbound connections made by Antigravity CLI during the eligibility check.

5.4 Lesson Learned

Fixing Squid hostname resolution does not help
if the application bypasses the proxy.

6. Attempt 3: Test CoreDNS

6.1 Positioning

CoreDNS was not part of the initial architecture.

It was only tested as part of investigating DNS control for Claude-related URLs and other external service names.

This was an experiment, not a committed part of the architecture.

6.2 Goal

The idea was to check whether pointing a specific domain to a chosen IP could control the traffic.

Example:

daily-cloudcode-pa.googleapis.com -> Squid IP

6.3 Result

This did not solve the problem.

Even if DNS points the domain to the Squid IP, the HTTPS client still connects to:

Squid-IP:443

But Squid listens as an HTTP proxy on port 3128.

Squid:
  listens on 3128 as an HTTP proxy

Antigravity CLI:
  tries to connect to 443 as an HTTPS client

DNS rewriting does not magically convert 443 traffic into proxy traffic on 3128.

6.4 Lesson Learned

DNS can change the destination IP.
It cannot change the destination port or protocol into an HTTP proxy connection.

7. Attempt 4: iptables + redsocks Inside the Container

7.1 Goal

If Antigravity CLI ignored proxy settings, the next idea was to forcibly redirect TCP 80/443 traffic to a proxy from inside the container.

7.2 Attempted Setup

app-editor
  ↓ iptables OUTPUT
redsocks

Squid container

7.3 Result

When app-editor had no default route, connections to external IPs failed at the routing stage.

network is unreachable

In that situation, the packet never reached the NAT OUTPUT rule.

The kernel rejected the connection before redsocks had a chance to intercept it.

7.4 Lesson Learned

To intercept traffic with iptables OUTPUT,
the destination must first be routable.

With only an internal network and no default route,
the connection fails before NAT OUTPUT can redirect it.

8. Attempt 5: Host-Level Squid + Transparent Proxy

8.1 Goal

The next idea was to avoid doing traffic interception inside the container and instead redirect Docker bridge traffic on the host.

8.2 Attempted Setup

At this point, Squid was also installed on the host.

Docker container

Docker bridge
  ↓ DNAT
redsocks / sing-box

host Squid

Internet

Because redsocks looked old and lightly maintained, sing-box was also tested.

8.3 Result

Redirecting TCP 80/443 from Docker bridge traffic to host-side Squid worked.

However, this exposed a new problem.

Squid’s access log showed entries like this:

CONNECT 172.66.147.243:443
CONNECT 104.20.23.154:443

The expected logs would have looked like this:

CONNECT daily-cloudcode-pa.googleapis.com:443
CONNECT api.openai.com:443

8.4 Cause

With transparent proxying, the application first resolves the domain to an IP address and then attempts to connect to that IP.

The transparent proxy intercepts the TCP connection after that has already happened.

Application
  ↓ DNS resolution
domain -> IP

connects to IP:443

transparent proxy intercepts the TCP connection

Squid sees IP:443

8.5 Lesson Learned

Explicit proxy:
  Squid sees the domain name.
  dstdomain ACL works.

Transparent proxy:
  Squid only sees IP:port.
  dstdomain ACL does not work as expected.

9. Attempt 6: IP Allowlist Control with Squid

9.1 Goal

Since domains were no longer visible through the transparent proxy, the next idea was to allow or deny traffic based on IP addresses generated from DNS resolution.

9.2 Attempted Setup

allowed domains
  ↓ DNS resolution
allowed IPs

Squid dst ACL

Instead of Squid’s dstdomain ACL, the dst ACL was considered.

9.3 Result

IP-based allow/deny control looked feasible.

However, this raised a design question:

If traffic is controlled only by IP,
why keep Squid in the path at all?

9.4 Lesson Learned

Squid is most useful when it can see the CONNECT hostname.

If transparent proxying only gives Squid an IP address,
Squid becomes little more than an IP filter.

10. Attempt 7: IP Allowlist Control with nftables

10.1 Goal

The next idea was to remove Squid and sing-box entirely, and directly filter Docker outbound traffic using nftables.

10.2 Attempted Setup

Docker container

Docker bridge

nftables forward hook

allow only TCP 80/443 to approved IPs

10.3 Result

Unauthorized destinations such as example.com were successfully dropped.

However, allowed destinations such as api.openai.com still timed out, even though they were not being dropped.

The situation was:

example.com:
  drop log exists
  connection fails

api.openai.com:
  no drop log
  IP exists in allowed_v4
  but connection times out

The cause was that Docker NAT / MASQUERADE was not working correctly in this setup.

Adding the following rule manually fixed the connection:

iptables -t nat -A POSTROUTING -s 172.18.0.0/16 -j MASQUERADE

10.4 Lesson Learned

Accepting traffic in nftables filter rules is not enough.
Without Docker source NAT, return traffic cannot reach the container.

Mixing Docker-managed iptables/NAT rules
with custom nftables rules can easily lead to subtle breakage.

11. Final Approach: DOCKER-USER + ipset

11.1 Policy

Docker should continue to manage NAT and MASQUERADE.

Custom outbound filtering should be placed in Docker’s DOCKER-USER chain.

11.2 Final Architecture

Docker container

Docker bridge

DOCKER-USER

DOCKER-EGRESS-FILTER

ipset docker_allowed_v4

11.3 Filtering Logic

private IP destinations
  → RETURN

allowed IP + TCP 80/443
  → RETURN

UDP 443
  → DROP

everything else
  → LOG + DROP

11.4 Result

This avoided fighting Docker’s NAT behavior while still allowing outbound filtering.

Unauthorized destinations such as example.com are dropped.

Allowed destinations such as api.openai.com are allowed if their resolved IPs exist in the allowlist.


12. strict / relaxed Modes

Two allowlist modes were prepared for operational convenience.

12.1 strict

This mode allows a relatively minimal set of domains for Antigravity, OpenAI, Anthropic, and Google APIs.

api.openai.com
auth.openai.com
chatgpt.com
ab.chatgpt.com
platform.openai.com
oauth2.googleapis.com
antigravity-unleash.goog
www.googleapis.com
playwright.azureedge.net
antigravity-cli-auto-updater-974169037036.us-central1.run.app
storage.googleapis.com
api.anthropic.com
platform.claude.com
daily-cloudcode-pa.googleapis.com

12.2 relaxed

This mode adds development-related domains such as npm, GitHub, Go modules, and Playwright.

api.openai.com
auth.openai.com
chatgpt.com
ab.chatgpt.com
platform.openai.com
oauth2.googleapis.com
antigravity-unleash.goog
www.googleapis.com
playwright.azureedge.net
api.github.com
github.com
codeload.github.com
objects.githubusercontent.com
raw.githubusercontent.com
proxy.golang.org
sum.golang.org
storage.googleapis.com
registry.npmjs.org
npmjs.com
www.npmjs.com
cdn.playwright.dev
api.anthropic.com
platform.claude.com

The mode can be switched using:

docker-egress-strict
docker-egress-relaxed

13. Handling Internal Docker Traffic

The Compose setup also included an internal Git server.

app-editor accessed it as:

git://git:9418/app.git

If outbound filtering is applied broadly to Docker bridge traffic, internal Docker traffic can accidentally be affected.

To avoid that, private IP destinations were excluded from filtering.

10.0.0.0/8
172.16.0.0/12
192.168.0.0/16

This allows internal services such as the Git server to keep working while still filtering external traffic.


14. Reapplying Rules After Docker Restart

iptables and ipset rules are not persistent by themselves.

Also, Docker may reconstruct its iptables rules when the Docker daemon restarts.

To reapply the custom rules after Docker starts, a systemd drop-in was added:

[Service]
ExecStartPost=/usr/local/sbin/update-docker-egress-ipset.sh

This reapplies the DOCKER-USER + ipset rules after Docker daemon startup or restart.


15. Final Compromise

In the end, strict FQDN-based control was abandoned.

The reasons were:

Antigravity CLI does not appear to fully respect proxy settings.
Transparent proxying does not preserve domain names for Squid.
SNI-based filtering adds implementation complexity and can be spoofed.
Direct nftables filtering can interfere with Docker NAT.

The final compromise was:

Use DNS-resolution-based IP allowlists.
Let Docker manage NAT.
Use DOCKER-USER + ipset for outbound filtering.
Switch between strict and relaxed modes depending on the task.

This is not strict domain-level filtering.

If a CDN or shared IP hosts multiple domains, allowing that IP may also allow access to other domains on the same IP.

However, for this use case, it was a practical compromise.


16. Final Architecture Summary

app-editor container

Docker bridge

DOCKER-USER

DOCKER-EGRESS-FILTER

ipset docker_allowed_v4
  ├─ private IP → RETURN
  ├─ allowed IP + TCP 80/443 → RETURN
  ├─ UDP 443 → DROP
  └─ others → LOG + DROP

Docker NAT / MASQUERADE is left under Docker’s control.

Docker NAT:
  managed by Docker

Egress filtering:
  managed by DOCKER-USER + ipset

17. Conclusion

The original environment was a Docker development setup designed around Codex.

It already included a Squid container, but Antigravity CLI-specific networking issues had not yet appeared.

After adding Antigravity CLI, problems surfaced around outbound HTTPS traffic, DNS, proxy behavior, and Docker network routing.

The following approaches were tested in sequence:

existing Squid container
Squid hostname/IP fixes
CoreDNS experiment
iptables + redsocks inside the container
host-side Squid + transparent proxy
Squid IP allowlist
nftables IP allowlist

In the end, both Squid and transparent proxying were removed from the final design.

The final solution was DOCKER-USER + ipset, which works more naturally with Docker.

This is not strict FQDN-based filtering. It is DNS-resolution-based IP filtering.

However, it is a practical solution for restricting outbound traffic from Docker containers, even for tools such as Antigravity CLI that may bypass proxy settings.