Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion pkg/a2a/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ import (
"github.com/docker/cagent/pkg/version"
)

// routableAddr replaces wildcard listen addresses (like "0.0.0.0" or "::") with
// "localhost" so the agent card URL is actually usable by clients.
func routableAddr(addr string) string {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
if host == "" || host == "0.0.0.0" || host == "::" {
return net.JoinHostPort("localhost", port)
}
return addr
}

func Run(ctx context.Context, agentFilename, agentName string, runConfig *config.RuntimeConfig, ln net.Listener) error {
slog.Debug("Starting A2A server", "agent", agentName, "addr", ln.Addr().String())

Expand All @@ -46,7 +59,7 @@ func Run(ctx context.Context, agentFilename, agentName string, runConfig *config
return fmt.Errorf("failed to create ADK agent adapter: %w", err)
}

baseURL := &url.URL{Scheme: "http", Host: ln.Addr().String()}
baseURL := &url.URL{Scheme: "http", Host: routableAddr(ln.Addr().String())}

slog.Debug("A2A server listening", "url", baseURL.String())

Expand Down
33 changes: 33 additions & 0 deletions pkg/a2a/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package a2a

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRoutableAddr(t *testing.T) {
t.Parallel()

tests := []struct {
name string
addr string
want string
}{
{"ipv6 wildcard", "[::]:8080", "localhost:8080"},
{"ipv4 wildcard", "0.0.0.0:8080", "localhost:8080"},
{"empty host", ":8080", "localhost:8080"},
{"localhost stays", "localhost:8080", "localhost:8080"},
{"ipv4 loopback stays", "127.0.0.1:8080", "127.0.0.1:8080"},
{"specific ip stays", "192.168.1.1:9090", "192.168.1.1:9090"},
{"hostname stays", "my-host:8080", "my-host:8080"},
{"invalid addr returned as-is", "not-a-host-port", "not-a-host-port"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.want, routableAddr(tt.addr))
})
}
}
2 changes: 1 addition & 1 deletion pkg/server/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ func listenUnix(ctx context.Context, path string) (net.Listener, error) {

func listenTCP(ctx context.Context, addr string) (net.Listener, error) {
var lc net.ListenConfig
return lc.Listen(ctx, "tcp", addr)
return lc.Listen(ctx, "tcp4", addr)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential Breaking Change: IPv6 Support Removed

Changing from "tcp" to "tcp4" restricts socket binding to IPv4 only. While this aligns with the PR description ("avoid binding to IPv6 on dual-stack systems"), it introduces a breaking change:

  • Impact: If users pass IPv6 addresses like [::1]:8080 or [2001:db8::1]:8080, binding will fail with a cryptic error: "listen tcp4: address ::1: no suitable address found"
  • No validation: There's no check to prevent IPv6 addresses from being passed or to provide a clear error message
  • Tests consider IPv6: The routableAddr() tests include IPv6 cases ("[::]:8080"), suggesting IPv6 was considered in the design

Alternative approach: Consider keeping "tcp" for backward compatibility and let routableAddr() handle the agent card URL transformation. This would:

  • Fix the wildcard address advertising issue (the original bug)
  • Maintain IPv6 support for explicit addresses
  • Avoid a breaking change

If tcp4 is the desired approach: Consider adding validation to detect IPv6 addresses and return a clear error message like "IPv6 addresses are not supported, please use IPv4".

Thoughts?

}