diff --git a/connection.go b/connection.go index fde44048..9b59c156 100644 --- a/connection.go +++ b/connection.go @@ -413,12 +413,7 @@ var newClientConnection = func( if addr, ok := conn.RemoteAddr().(*net.UDPAddr); ok { destAddr = addr } - s.qlogger.RecordEvent(qlog.StartedConnection{ - SrcAddr: srcAddr, - DestAddr: destAddr, - SrcConnectionID: srcConnID, - DestConnectionID: destConnID, - }) + s.qlogger.RecordEvent(startedConnectionEvent(srcAddr, destAddr)) } s.connIDManager = newConnIDManager( destConnID, @@ -1658,12 +1653,7 @@ func (c *Conn) handleUnpackedLongHeaderPacket( if addr, ok := c.conn.RemoteAddr().(*net.UDPAddr); ok { destAddr = addr } - c.qlogger.RecordEvent(qlog.StartedConnection{ - SrcAddr: srcAddr, - DestAddr: destAddr, - SrcConnectionID: packet.hdr.SrcConnectionID, - DestConnectionID: packet.hdr.DestConnectionID, - }) + c.qlogger.RecordEvent(startedConnectionEvent(srcAddr, destAddr)) } } } diff --git a/connection_logging.go b/connection_logging.go index 65132914..9e515ca2 100644 --- a/connection_logging.go +++ b/connection_logging.go @@ -1,6 +1,8 @@ package quic import ( + "net" + "net/netip" "slices" "github.com/quic-go/quic-go/internal/ackhandler" @@ -268,3 +270,53 @@ func toQlogPacketType(pt protocol.PacketType) qlog.PacketType { } return qpt } + +func toPathEndpointInfo(addr *net.UDPAddr) qlog.PathEndpointInfo { + if addr == nil { + return qlog.PathEndpointInfo{} + } + + var info qlog.PathEndpointInfo + if addr.IP == nil || addr.IP.To4() != nil { + addrPort := netip.AddrPortFrom(netip.AddrFrom4([4]byte(addr.IP.To4())), uint16(addr.Port)) + if addrPort.IsValid() { + info.IPv4 = addrPort + } + } else { + addrPort := netip.AddrPortFrom(netip.AddrFrom16([16]byte(addr.IP.To16())), uint16(addr.Port)) + if addrPort.IsValid() { + info.IPv6 = addrPort + } + } + return info +} + +// startedConnectionEvent builds a StartedConnection event using consistent logic +// for both endpoints. If the local address is unspecified (e.g., dual-stack +// listener), it selects the family based on the remote address and uses the +// unspecified address of that family with the local port. +func startedConnectionEvent(local, remote *net.UDPAddr) qlog.StartedConnection { + var localInfo, remoteInfo qlog.PathEndpointInfo + if remote != nil { + remoteInfo = toPathEndpointInfo(remote) + } + if local != nil { + if local.IP == nil || local.IP.IsUnspecified() { + // Choose local family based on the remote address family. + if remote != nil && remote.IP.To4() != nil { + ap := netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), uint16(local.Port)) + if ap.IsValid() { + localInfo.IPv4 = ap + } + } else if remote != nil && remote.IP.To16() != nil && remote.IP.To4() == nil { + ap := netip.AddrPortFrom(netip.AddrFrom16([16]byte{}), uint16(local.Port)) + if ap.IsValid() { + localInfo.IPv6 = ap + } + } + } else { + localInfo = toPathEndpointInfo(local) + } + } + return qlog.StartedConnection{Local: localInfo, Remote: remoteInfo} +} diff --git a/connection_logging_test.go b/connection_logging_test.go index 7d0cf7f2..d93d309e 100644 --- a/connection_logging_test.go +++ b/connection_logging_test.go @@ -1,6 +1,8 @@ package quic import ( + "net" + "net/netip" "testing" "github.com/quic-go/quic-go/internal/wire" @@ -70,3 +72,72 @@ func TestConnectionLoggingOtherFrames(t *testing.T) { f := toQlogFrame(&wire.MaxDataFrame{MaximumData: 1234}) require.Equal(t, &qlog.MaxDataFrame{MaximumData: 1234}, f.Frame) } + +func TestConnectionLoggingStartedConnectionEvent(t *testing.T) { + tests := []struct { + name string + local *net.UDPAddr + remote *net.UDPAddr + wantLocalIP string + wantLocalPort uint16 + wantRemote netip.AddrPort + }{ + { + name: "unspecified local, remote IPv4 -> 0.0.0.0", + local: &net.UDPAddr{Port: 58451}, + remote: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 6121}, + wantLocalIP: "0.0.0.0", + wantLocalPort: 58451, + wantRemote: netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 6121), + }, + { + name: "unspecified local, remote IPv6 -> ::", + local: &net.UDPAddr{Port: 4242}, + remote: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 6121}, + wantLocalIP: "::", + wantLocalPort: 4242, + wantRemote: func() netip.AddrPort { a, _ := netip.ParseAddr("2001:db8::1"); return netip.AddrPortFrom(a, 6121) }(), + }, + { + name: "specified local IPv4", + local: &net.UDPAddr{IP: net.IPv4(192, 168, 1, 10), Port: 9999}, + remote: &net.UDPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 1234}, + wantLocalIP: "192.168.1.10", + wantLocalPort: 9999, + wantRemote: netip.AddrPortFrom(netip.AddrFrom4([4]byte{10, 0, 0, 1}), 1234), + }, + { + name: "specified local IPv6", + local: &net.UDPAddr{IP: net.ParseIP("fe80::1"), Port: 999}, + remote: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 6121}, + wantLocalIP: "fe80::1", + wantLocalPort: 999, + wantRemote: func() netip.AddrPort { a, _ := netip.ParseAddr("2001:db8::1"); return netip.AddrPortFrom(a, 6121) }(), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ev := startedConnectionEvent(tc.local, tc.remote) + var gotIP string + var gotPort uint16 + if ev.Local.IPv4.IsValid() { + gotIP = ev.Local.IPv4.Addr().String() + gotPort = ev.Local.IPv4.Port() + } else if ev.Local.IPv6.IsValid() { + gotIP = ev.Local.IPv6.Addr().String() + gotPort = ev.Local.IPv6.Port() + } + require.Equal(t, tc.wantLocalIP, gotIP) + require.Equal(t, tc.wantLocalPort, gotPort) + + var gotRemote netip.AddrPort + if ev.Remote.IPv4.IsValid() { + gotRemote = ev.Remote.IPv4 + } else if ev.Remote.IPv6.IsValid() { + gotRemote = ev.Remote.IPv6 + } + require.Equal(t, tc.wantRemote, gotRemote) + }) + } +} diff --git a/qlog/event.go b/qlog/event.go index d399abae..7eb44074 100644 --- a/qlog/event.go +++ b/qlog/event.go @@ -3,7 +3,6 @@ package qlog import ( "errors" "fmt" - "net" "net/netip" "time" @@ -56,11 +55,33 @@ func (i RawInfo) encode(enc *jsontext.Encoder) error { return h.err } +type PathEndpointInfo struct { + IPv4 netip.AddrPort + IPv6 netip.AddrPort +} + +func (p PathEndpointInfo) encode(enc *jsontext.Encoder) error { + h := encoderHelper{enc: enc} + h.WriteToken(jsontext.BeginObject) + if p.IPv4.IsValid() { + h.WriteToken(jsontext.String("ip_v4")) + h.WriteToken(jsontext.String(p.IPv4.Addr().String())) + h.WriteToken(jsontext.String("port_v4")) + h.WriteToken(jsontext.Int(int64(p.IPv4.Port()))) + } + if p.IPv6.IsValid() { + h.WriteToken(jsontext.String("ip_v6")) + h.WriteToken(jsontext.String(p.IPv6.Addr().String())) + h.WriteToken(jsontext.String("port_v6")) + h.WriteToken(jsontext.Int(int64(p.IPv6.Port()))) + } + h.WriteToken(jsontext.EndObject) + return h.err +} + type StartedConnection struct { - SrcAddr *net.UDPAddr - DestAddr *net.UDPAddr - SrcConnectionID ConnectionID - DestConnectionID ConnectionID + Local PathEndpointInfo + Remote PathEndpointInfo } func (e StartedConnection) Name() string { return "transport:connection_started" } @@ -68,25 +89,14 @@ func (e StartedConnection) Name() string { return "transport:connection_started" func (e StartedConnection) Encode(enc *jsontext.Encoder, _ time.Time) error { h := encoderHelper{enc: enc} h.WriteToken(jsontext.BeginObject) - if e.SrcAddr.IP.To4() != nil { - h.WriteToken(jsontext.String("ip_version")) - h.WriteToken(jsontext.String("ipv4")) - } else { - h.WriteToken(jsontext.String("ip_version")) - h.WriteToken(jsontext.String("ipv6")) + h.WriteToken(jsontext.String("local")) + if err := e.Local.encode(enc); err != nil { + return err + } + h.WriteToken(jsontext.String("remote")) + if err := e.Remote.encode(enc); err != nil { + return err } - h.WriteToken(jsontext.String("src_ip")) - h.WriteToken(jsontext.String(e.SrcAddr.IP.String())) - h.WriteToken(jsontext.String("src_port")) - h.WriteToken(jsontext.Int(int64(e.SrcAddr.Port))) - h.WriteToken(jsontext.String("dst_ip")) - h.WriteToken(jsontext.String(e.DestAddr.IP.String())) - h.WriteToken(jsontext.String("dst_port")) - h.WriteToken(jsontext.Int(int64(e.DestAddr.Port))) - h.WriteToken(jsontext.String("src_cid")) - h.WriteToken(jsontext.String(e.SrcConnectionID.String())) - h.WriteToken(jsontext.String("dst_cid")) - h.WriteToken(jsontext.String(e.DestConnectionID.String())) h.WriteToken(jsontext.EndObject) return h.err } diff --git a/qlog/event_test.go b/qlog/event_test.go index 96080238..3209b64b 100644 --- a/qlog/event_test.go +++ b/qlog/event_test.go @@ -3,7 +3,6 @@ package qlog import ( "bytes" "encoding/json" - "net" "net/netip" "testing" "time" @@ -58,21 +57,28 @@ func decode(t *testing.T, data string) (string, map[string]any) { } func TestStartedConnection(t *testing.T) { + var localInfo, remoteInfo PathEndpointInfo + localInfo.IPv4 = netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 13, 37}), 42) + ip, err := netip.ParseAddr("2001:db8::1") + require.NoError(t, err) + remoteInfo.IPv6 = netip.AddrPortFrom(ip, 24) + name, ev := testEventEncoding(t, &StartedConnection{ - SrcAddr: &net.UDPAddr{IP: net.IPv4(192, 168, 13, 37), Port: 42}, - DestAddr: &net.UDPAddr{IP: net.IPv4(192, 168, 12, 34), Port: 24}, - SrcConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}), - DestConnectionID: protocol.ParseConnectionID([]byte{5, 6, 7, 8}), + Local: localInfo, + Remote: remoteInfo, }) require.Equal(t, "transport:connection_started", name) - require.Equal(t, "ipv4", ev["ip_version"]) - require.Equal(t, "192.168.13.37", ev["src_ip"]) - require.Equal(t, float64(42), ev["src_port"]) - require.Equal(t, "192.168.12.34", ev["dst_ip"]) - require.Equal(t, float64(24), ev["dst_port"]) - require.Equal(t, "01020304", ev["src_cid"]) - require.Equal(t, "05060708", ev["dst_cid"]) + + local, ok := ev["local"].(map[string]any) + require.True(t, ok) + require.Equal(t, "192.168.13.37", local["ip_v4"]) + require.Equal(t, float64(42), local["port_v4"]) + + remote, ok := ev["remote"].(map[string]any) + require.True(t, ok) + require.Equal(t, "2001:db8::1", remote["ip_v6"]) + require.Equal(t, float64(24), remote["port_v6"]) } func TestVersionInformation(t *testing.T) {