quic.go 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. package connection
  2. import (
  3. "context"
  4. "crypto/tls"
  5. "fmt"
  6. "net"
  7. "net/netip"
  8. "runtime"
  9. "sync"
  10. "github.com/quic-go/quic-go"
  11. "github.com/rs/zerolog"
  12. )
  13. var (
  14. portForConnIndex = make(map[uint8]int, 0)
  15. portMapMutex sync.Mutex
  16. )
  17. func DialQuic(
  18. ctx context.Context,
  19. quicConfig *quic.Config,
  20. tlsConfig *tls.Config,
  21. edgeAddr netip.AddrPort,
  22. localAddr net.IP,
  23. connIndex uint8,
  24. logger *zerolog.Logger,
  25. ) (quic.Connection, error) {
  26. udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, logger)
  27. if err != nil {
  28. return nil, err
  29. }
  30. conn, err := quic.Dial(ctx, udpConn, net.UDPAddrFromAddrPort(edgeAddr), tlsConfig, quicConfig)
  31. if err != nil {
  32. // close the udp server socket in case of error connecting to the edge
  33. udpConn.Close()
  34. return nil, &EdgeQuicDialError{Cause: err}
  35. }
  36. // wrap the session, so that the UDPConn is closed after session is closed.
  37. conn = &wrapCloseableConnQuicConnection{
  38. conn,
  39. udpConn,
  40. }
  41. return conn, nil
  42. }
  43. func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, logger *zerolog.Logger) (*net.UDPConn, error) {
  44. portMapMutex.Lock()
  45. defer portMapMutex.Unlock()
  46. listenNetwork := "udp"
  47. // https://github.com/quic-go/quic-go/issues/3793 DF bit cannot be set for dual stack listener ("udp") on macOS,
  48. // to set the DF bit properly, the network string needs to be specific to the IP family.
  49. if runtime.GOOS == "darwin" {
  50. if edgeIP.Addr().Is4() {
  51. listenNetwork = "udp4"
  52. } else {
  53. listenNetwork = "udp6"
  54. }
  55. }
  56. // if port was not set yet, it will be zero, so bind will randomly allocate one.
  57. if port, ok := portForConnIndex[connIndex]; ok {
  58. udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: port})
  59. // if there wasn't an error, or if port was 0 (independently of error or not, just return)
  60. if err == nil {
  61. return udpConn, nil
  62. } else {
  63. logger.Debug().Err(err).Msgf("Unable to reuse port %d for connIndex %d. Falling back to random allocation.", port, connIndex)
  64. }
  65. }
  66. // if we reached here, then there was an error or port as not been allocated it.
  67. udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: 0})
  68. if err == nil {
  69. udpAddr, ok := (udpConn.LocalAddr()).(*net.UDPAddr)
  70. if !ok {
  71. return nil, fmt.Errorf("unable to cast to udpConn")
  72. }
  73. portForConnIndex[connIndex] = udpAddr.Port
  74. } else {
  75. delete(portForConnIndex, connIndex)
  76. }
  77. return udpConn, err
  78. }
  79. type wrapCloseableConnQuicConnection struct {
  80. quic.Connection
  81. udpConn *net.UDPConn
  82. }
  83. func (w *wrapCloseableConnQuicConnection) CloseWithError(errorCode quic.ApplicationErrorCode, reason string) error {
  84. err := w.Connection.CloseWithError(errorCode, reason)
  85. w.udpConn.Close()
  86. return err
  87. }