From f919473598e73a57dc7dde987f33b9d2822e8893 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Sun, 30 Jul 2023 13:42:15 -0400 Subject: [PATCH] add support for writing the ECN control message (Linux, macOS) --- send_conn.go | 13 ++++++++++--- sys_conn_df_windows.go | 4 ++++ sys_conn_helper_darwin.go | 2 ++ sys_conn_helper_freebsd.go | 2 ++ sys_conn_helper_linux.go | 2 ++ sys_conn_no_oob.go | 5 +++++ sys_conn_oob.go | 30 ++++++++++++++++++++++++++++++ sys_conn_oob_test.go | 36 ++++++++++++++++++++++++++++++++++++ 8 files changed, 91 insertions(+), 3 deletions(-) diff --git a/send_conn.go b/send_conn.go index 272c61b1..030e0fba 100644 --- a/send_conn.go +++ b/send_conn.go @@ -3,6 +3,7 @@ package quic import ( "net" + "github.com/quic-go/quic-go/internal/protocol" "github.com/quic-go/quic-go/internal/utils" ) @@ -42,10 +43,16 @@ func newSendConn(c rawConn, remote net.Addr, info packetInfo, logger utils.Logge } oob := info.OOB() - // add 32 bytes, so we can add the UDP_SEGMENT msg + if remoteUDPAddr, ok := remote.(*net.UDPAddr); ok { + if remoteUDPAddr.IP.To4() != nil { + oob = appendIPv4ECNMsg(oob, protocol.ECT1) + } else { + oob = appendIPv6ECNMsg(oob, protocol.ECT1) + } + } + // increase oob slice capacity, so we can add the UDP_SEGMENT and ECN control messages without allocating l := len(oob) - oob = append(oob, make([]byte, 32)...) - oob = oob[:l] + oob = append(oob, make([]byte, 64)...)[:l] return &sconn{ rawConn: c, localAddr: localAddr, diff --git a/sys_conn_df_windows.go b/sys_conn_df_windows.go index e27635ec..e56b7460 100644 --- a/sys_conn_df_windows.go +++ b/sys_conn_df_windows.go @@ -8,6 +8,7 @@ import ( "golang.org/x/sys/windows" + "github.com/quic-go/quic-go/internal/protocol" "github.com/quic-go/quic-go/internal/utils" ) @@ -52,3 +53,6 @@ func isRecvMsgSizeErr(err error) bool { // https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2 return errors.Is(err, windows.WSAEMSGSIZE) } + +func appendIPv4ECNMsg([]byte, protocol.ECN) []byte { return nil } +func appendIPv6ECNMsg([]byte, protocol.ECN) []byte { return nil } diff --git a/sys_conn_helper_darwin.go b/sys_conn_helper_darwin.go index 758cf778..d761072f 100644 --- a/sys_conn_helper_darwin.go +++ b/sys_conn_helper_darwin.go @@ -15,6 +15,8 @@ const ( ipv4PKTINFO = unix.IP_RECVPKTINFO ) +const ecnIPv4DataLen = 4 + // ReadBatch only returns a single packet on OSX, // see https://godoc.org/golang.org/x/net/ipv4#PacketConn.ReadBatch. const batchSize = 1 diff --git a/sys_conn_helper_freebsd.go b/sys_conn_helper_freebsd.go index a2baae3b..5e635b18 100644 --- a/sys_conn_helper_freebsd.go +++ b/sys_conn_helper_freebsd.go @@ -14,6 +14,8 @@ const ( ipv4PKTINFO = 0x7 ) +const ecnIPv4DataLen = 4 + const batchSize = 8 func parseIPv4PktInfo(body []byte) (ip netip.Addr, _ uint32, ok bool) { diff --git a/sys_conn_helper_linux.go b/sys_conn_helper_linux.go index 6a049241..622f4e6f 100644 --- a/sys_conn_helper_linux.go +++ b/sys_conn_helper_linux.go @@ -19,6 +19,8 @@ const ( ipv4PKTINFO = unix.IP_PKTINFO ) +const ecnIPv4DataLen = 4 + const batchSize = 8 // needs to smaller than MaxUint8 (otherwise the type of oobConn.readPos has to be changed) func forceSetReceiveBuffer(c syscall.RawConn, bytes int) error { diff --git a/sys_conn_no_oob.go b/sys_conn_no_oob.go index 2a1f807e..c1fb6fb1 100644 --- a/sys_conn_no_oob.go +++ b/sys_conn_no_oob.go @@ -5,6 +5,8 @@ package quic import ( "net" "net/netip" + + "github.com/quic-go/quic-go/internal/protocol" ) func newConn(c net.PacketConn, supportsDF bool) (*basicConn, error) { @@ -14,6 +16,9 @@ func newConn(c net.PacketConn, supportsDF bool) (*basicConn, error) { func inspectReadBuffer(any) (int, error) { return 0, nil } func inspectWriteBuffer(any) (int, error) { return 0, nil } +func appendIPv4ECNMsg([]byte, protocol.ECN) []byte { return nil } +func appendIPv6ECNMsg([]byte, protocol.ECN) []byte { return nil } + type packetInfo struct { addr netip.Addr } diff --git a/sys_conn_oob.go b/sys_conn_oob.go index 24b73e9c..6446d893 100644 --- a/sys_conn_oob.go +++ b/sys_conn_oob.go @@ -11,6 +11,7 @@ import ( "sync" "syscall" "time" + "unsafe" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" @@ -279,3 +280,32 @@ func (info *packetInfo) OOB() []byte { } return nil } + +func appendIPv4ECNMsg(b []byte, val protocol.ECN) []byte { + startLen := len(b) + b = append(b, make([]byte, unix.CmsgSpace(ecnIPv4DataLen))...) + h := (*unix.Cmsghdr)(unsafe.Pointer(&b[startLen])) + h.Level = syscall.IPPROTO_IP + h.Type = unix.IP_TOS + h.SetLen(unix.CmsgLen(ecnIPv4DataLen)) + + // UnixRights uses the private `data` method, but I *think* this achieves the same goal. + offset := startLen + unix.CmsgSpace(0) + b[offset] = uint8(val) + return b +} + +func appendIPv6ECNMsg(b []byte, val protocol.ECN) []byte { + startLen := len(b) + const dataLen = 4 + b = append(b, make([]byte, unix.CmsgSpace(dataLen))...) + h := (*unix.Cmsghdr)(unsafe.Pointer(&b[startLen])) + h.Level = syscall.IPPROTO_IPV6 + h.Type = unix.IPV6_TCLASS + h.SetLen(unix.CmsgLen(dataLen)) + + // UnixRights uses the private `data` method, but I *think* this achieves the same goal. + offset := startLen + unix.CmsgSpace(0) + b[offset] = uint8(val) + return b +} diff --git a/sys_conn_oob_test.go b/sys_conn_oob_test.go index 54dac82d..9d0f5e9d 100644 --- a/sys_conn_oob_test.go +++ b/sys_conn_oob_test.go @@ -139,6 +139,42 @@ var _ = Describe("OOB Conn Test", func() { Expect(utils.IsIPv4(p.remoteAddr.(*net.UDPAddr).IP)).To(BeFalse()) Expect(p.ecn).To(Equal(protocol.ECT1)) }) + + It("sends packets with ECN on IPv4", func() { + conn, packetChan := runServer("udp4", "localhost:0") + defer conn.Close() + + c, err := net.ListenUDP("udp4", nil) + Expect(err).ToNot(HaveOccurred()) + defer c.Close() + + for _, val := range []protocol.ECN{protocol.ECNNon, protocol.ECT1, protocol.ECT0, protocol.ECNCE} { + _, _, err = c.WriteMsgUDP([]byte("foobar"), appendIPv4ECNMsg([]byte{}, val), conn.LocalAddr().(*net.UDPAddr)) + Expect(err).ToNot(HaveOccurred()) + var p receivedPacket + Eventually(packetChan).Should(Receive(&p)) + Expect(p.data).To(Equal([]byte("foobar"))) + Expect(p.ecn).To(Equal(val)) + } + }) + + It("sends packets with ECN on IPv6", func() { + conn, packetChan := runServer("udp6", "[::1]:0") + defer conn.Close() + + c, err := net.ListenUDP("udp6", nil) + Expect(err).ToNot(HaveOccurred()) + defer c.Close() + + for _, val := range []protocol.ECN{protocol.ECNNon, protocol.ECT1, protocol.ECT0, protocol.ECNCE} { + _, _, err = c.WriteMsgUDP([]byte("foobar"), appendIPv6ECNMsg([]byte{}, val), conn.LocalAddr().(*net.UDPAddr)) + Expect(err).ToNot(HaveOccurred()) + var p receivedPacket + Eventually(packetChan).Should(Receive(&p)) + Expect(p.data).To(Equal([]byte("foobar"))) + Expect(p.ecn).To(Equal(val)) + } + }) }) Context("Packet Info conn", func() {