├── topology.png ├── testresults.png ├── main.go ├── cmd └── root.go ├── go.mod ├── internal ├── proxy │ ├── quic.go │ ├── tcp.go │ ├── proxy.go │ └── ws.go └── tlsconfig │ └── tlsconfig.go ├── README.md └── go.sum /topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atoonk/tcp-to-quic-proxy/HEAD/topology.png -------------------------------------------------------------------------------- /testresults.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atoonk/tcp-to-quic-proxy/HEAD/testresults.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/atoonk/tcp-to-any-proxy/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/atoonk/tcp-to-any-proxy/internal/proxy" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var rootCmd = &cobra.Command{ 12 | Run: proxy.RunProxy, 13 | } 14 | 15 | func init() { 16 | rootCmd.PersistentFlags().StringP("localAddr", "l", "localhost:9999", "local address") 17 | rootCmd.PersistentFlags().StringP("remoteAddr", "r", "127.0.0.1:5201", "remote address") 18 | rootCmd.PersistentFlags().StringP("listenProto", "p", "tcp", "listen protocol, either tcp, quic or ws") 19 | rootCmd.PersistentFlags().StringP("remoteProto", "u", "tcp", "remote protocol either tcp, quic or ws") 20 | 21 | } 22 | 23 | func Execute() { 24 | if err := rootCmd.Execute(); err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/atoonk/tcp-to-any-proxy 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/quic-go/quic-go v0.33.0 7 | github.com/spf13/cobra v1.6.1 8 | nhooyr.io/websocket v1.8.7 9 | ) 10 | 11 | require ( 12 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 13 | github.com/golang/mock v1.6.0 // indirect 14 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect 15 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 16 | github.com/klauspost/compress v1.10.3 // indirect 17 | github.com/onsi/ginkgo/v2 v2.2.0 // indirect 18 | github.com/quic-go/qtls-go1-19 v0.2.1 // indirect 19 | github.com/quic-go/qtls-go1-20 v0.1.1 // indirect 20 | github.com/spf13/pflag v1.0.5 // indirect 21 | golang.org/x/crypto v0.4.0 // indirect 22 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect 23 | golang.org/x/mod v0.6.0 // indirect 24 | golang.org/x/net v0.4.0 // indirect 25 | golang.org/x/sys v0.3.0 // indirect 26 | golang.org/x/tools v0.2.0 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /internal/proxy/quic.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | 8 | "github.com/quic-go/quic-go" 9 | "nhooyr.io/websocket" 10 | ) 11 | 12 | func proxyQUICtoTCP(quicStream quic.Stream, remoteAddr string) { 13 | defer quicStream.Close() 14 | 15 | tcpConn, err := net.Dial("tcp", remoteAddr) 16 | if err != nil { 17 | log.Println("Error dialing remote TCP address:", err) 18 | return 19 | } 20 | defer tcpConn.Close() 21 | 22 | transferData(quicStream, tcpConn) 23 | } 24 | 25 | func proxyQUICtoWS(quicStream quic.Stream, remoteAddr string) { 26 | defer quicStream.Close() 27 | 28 | fmt.Println("Dialing remote WebSocket address:", remoteAddr) 29 | wsConn, _, err := websocket.Dial(quicStream.Context(), remoteAddr, nil) 30 | if err != nil { 31 | log.Println("Error dialing remote WebSocket address:", err) 32 | return 33 | } 34 | defer wsConn.Close(websocket.StatusNormalClosure, "") 35 | wsNetConn := NetConn(quicStream.Context(), wsConn, websocket.MessageBinary) 36 | 37 | transferData(quicStream, wsNetConn) 38 | } 39 | -------------------------------------------------------------------------------- /internal/tlsconfig/tlsconfig.go: -------------------------------------------------------------------------------- 1 | package tlsconfig 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "math/big" 10 | ) 11 | 12 | // GenerateTLSConfig generates a TLS config with a self-signed certificate. 13 | 14 | func GenerateTLSConfig() (*tls.Config, error) { 15 | key, err := rsa.GenerateKey(rand.Reader, 2048) 16 | if err != nil { 17 | return nil, err 18 | } 19 | template := x509.Certificate{SerialNumber: big.NewInt(1)} 20 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) 21 | if err != nil { 22 | return nil, err 23 | } 24 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) 25 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 26 | 27 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &tls.Config{ 32 | InsecureSkipVerify: true, 33 | Certificates: []tls.Certificate{tlsCert}, 34 | NextProtos: []string{"proto"}, 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/proxy/tcp.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | 9 | "github.com/atoonk/tcp-to-any-proxy/internal/tlsconfig" 10 | "github.com/quic-go/quic-go" 11 | "nhooyr.io/websocket" 12 | // (...) 13 | ) 14 | 15 | func proxyTCPtoQUIC(localConn net.Conn, remoteAddr string) { 16 | defer localConn.Close() 17 | 18 | tlsConfig, err := tlsconfig.GenerateTLSConfig() 19 | if err != nil { 20 | log.Println("Error generating TLS config:", err) 21 | return 22 | } 23 | 24 | quicConfig := &quic.Config{} 25 | 26 | quicSession, err := quic.DialAddr(remoteAddr, tlsConfig, quicConfig) 27 | if err != nil { 28 | log.Println("Error dialing remote QUIC address:", err) 29 | return 30 | } 31 | defer quicSession.CloseWithError(0, "") 32 | 33 | quicStream, err := quicSession.OpenStreamSync(context.Background()) 34 | if err != nil { 35 | log.Println("Error opening QUIC stream:", err) 36 | return 37 | } 38 | defer quicStream.Close() 39 | 40 | transferData(localConn, quicStream) 41 | } 42 | 43 | func proxyTCPtoWS(localConn net.Conn, remoteAddr string) { 44 | defer localConn.Close() 45 | ctx := context.Background() 46 | 47 | fmt.Println("Dialing remote WebSocket address:", remoteAddr) 48 | wsConn, _, err := websocket.Dial(ctx, remoteAddr, nil) 49 | if err != nil { 50 | log.Println("Error dialing remote WebSocket address:", err) 51 | return 52 | } 53 | defer wsConn.Close(websocket.StatusNormalClosure, "") 54 | wsNetConn := NetConn(ctx, wsConn, websocket.MessageBinary) 55 | 56 | transferData(localConn, wsNetConn) 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tcp-to-any-proxy 2 | Quic to TCP proxy and vice versa. 3 | or 4 | Websocket to TCP proxy and vice versa. 5 | 6 | Just a POC built on a rainy vancouver sunday afternoon to explore the Quic Go library 7 | 8 | In this example we have 4 nodes, as the diagram below. These are the nodes: 9 | 1) a TCP listener, that proxies to an upstream Quic server 10 | 2) a Quic listener, that proxies to an upstream TCP server 11 | 3) an Iperf3 client (tcp) 12 | 4) an Iperf3 server (tcp) 13 | 14 | (note you can replace quic with ws to use websockets as the transport instead) 15 | 16 | ![Topology](topology.png) 17 | 18 | to build, run ```go build```, then test: 19 | 20 | Node 1 is started like this 21 | ``` 22 | ./tcp-to-any-proxy -l 127.0.0.1:4201 -r 127.0.0.1:4202 -u quic -p tcp 23 | ``` 24 | Means it's listening on protocol TCP 127.0.0.1:4201, and connects to 127.0.0.1:4202 as an upstream server. The Upstream protocol is Quic 25 | 26 | Node 2 is started like this 27 | ``` 28 | ./tcp-to-any-proxy -l 127.0.0.1:4202 -r 127.0.0.1:4203 -u tcp -p quic 29 | ``` 30 | Means it's listening on protocol Quic 127.0.0.1:4202, and connects to 127.0.0.1:4203 as an upstream server. The Upstream protocol is TCP 31 | 32 | Node 3 is the Iperf client, which connects to the node1 (traffic will be send upstream) 33 | ``` 34 | iperf3 -c 127.0.0.1 -p 4201 -P 1 35 | ``` 36 | 37 | Node 4 is the Iperf Server, which will receive the traffic after it went through node 1 and 2, possibly with a tcp to quic to tcp translation 38 | ``` 39 | iperf3 -s -p 4203 40 | ``` 41 | 42 | 43 | # websocket example: 44 | node 1 45 | ``` 46 | ./tcp-to-any-proxy -l 127.0.0.1:4201 -r http://127.0.0.1:4202 -p tcp -u ws 47 | ``` 48 | 49 | node 2 50 | ``` 51 | ./tcp-to-any-proxy -l 127.0.0.1:4202 -r 127.0.0.1:4203 -p ws -u tcp 52 | ``` 53 | 54 | # Results 55 | Using the -u and -p flag you can control the protocol between node 1 and node 2 (either tcp or quick). 56 | testing on my laptop I see quite a big difference between TCP and Quic 57 | Quic performance: ~ 1.25 Gbits/sec 58 | TCP performance: ~ 18.3 Gbits/sec 59 | 60 | ⚠️ Maybe related to TCP TSO? Likely packet size. The QUIC draft forbids using packet sizes larger than 1280 bytes (UDP packet size) if no PMTUD is done. 61 | 62 | Update: yah, if we set the lo0 mtu to 1200 i get very similar results between "tcp-tcp" vs "quic-quic". Can't compete with bigger packets.. no matter how fancy your congestion algortihm or transport protocol us ;) 63 | 64 | ![Iperf restuls Quic](testresults.png) 65 | 66 | -------------------------------------------------------------------------------- /internal/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/atoonk/tcp-to-any-proxy/internal/tlsconfig" 13 | "github.com/quic-go/quic-go" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func RunProxy(cmd *cobra.Command, args []string) { 18 | localAddr, _ := cmd.Flags().GetString("localAddr") 19 | remoteAddr, _ := cmd.Flags().GetString("remoteAddr") 20 | listenProto, _ := cmd.Flags().GetString("listenProto") 21 | remoteProto, _ := cmd.Flags().GetString("remoteProto") 22 | fmt.Printf("Listening on: %s %s\nProxying to: %s %s\n\n", listenProto, localAddr, remoteProto, remoteAddr) 23 | 24 | tlsConfig, err := tlsconfig.GenerateTLSConfig() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | quicConfig := &quic.Config{ 30 | KeepAlivePeriod: time.Duration(10) * time.Second, 31 | } 32 | 33 | if listenProto == "tcp" { 34 | listener, err := net.Listen("tcp", localAddr) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | defer listener.Close() 39 | 40 | for { 41 | localConn, err := listener.Accept() 42 | if err != nil { 43 | log.Println("Error accepting connection:", err) 44 | continue 45 | } 46 | if remoteProto == "ws" { 47 | go proxyTCPtoWS(localConn, remoteAddr) 48 | } else if remoteProto == "quic" { 49 | go proxyTCPtoQUIC(localConn, remoteAddr) 50 | } else { 51 | log.Fatal("Invalid remote protocol") 52 | } 53 | 54 | } 55 | } 56 | 57 | if listenProto == "quic" { 58 | listener, err := quic.ListenAddr(localAddr, tlsConfig, quicConfig) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | defer listener.Close() 63 | 64 | for { 65 | quicSession, err := listener.Accept(context.Background()) 66 | if err != nil { 67 | log.Println("Error accepting QUIC session:", err) 68 | continue 69 | } 70 | 71 | go func() { 72 | quicStream, err := quicSession.AcceptStream(context.Background()) 73 | if err != nil { 74 | log.Println("Error accepting QUIC stream:", err) 75 | return 76 | } 77 | if remoteProto == "ws" { 78 | proxyQUICtoWS(quicStream, remoteAddr) 79 | } else if remoteProto == "tcp" { 80 | proxyQUICtoTCP(quicStream, remoteAddr) 81 | } else { 82 | log.Fatal("Invalid remote protocol") 83 | } 84 | }() 85 | } 86 | } 87 | if listenProto == "ws" { 88 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 89 | proxyWStoTCP(w, r, remoteAddr) 90 | }) 91 | // Start the server 92 | log.Fatal(http.ListenAndServe(localAddr, nil)) 93 | } 94 | } 95 | 96 | func transferData(a, b io.ReadWriteCloser) { 97 | done := make(chan bool) 98 | 99 | go func() { 100 | io.Copy(a, b) 101 | done <- true 102 | }() 103 | 104 | go func() { 105 | io.Copy(b, a) 106 | done <- true 107 | }() 108 | 109 | <-done 110 | } 111 | -------------------------------------------------------------------------------- /internal/proxy/ws.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math" 9 | "net" 10 | "net/http" 11 | "os" 12 | "sync" 13 | "sync/atomic" 14 | "time" 15 | 16 | "nhooyr.io/websocket" 17 | ) 18 | 19 | func proxyWStoTCP(w http.ResponseWriter, r *http.Request, remoteAddr string) { 20 | wsConn, err := websocket.Accept(w, r, nil) 21 | if err != nil { 22 | log.Printf("failed to set websocket upgrade: %s", err) 23 | return 24 | } 25 | defer wsConn.Close(websocket.StatusNormalClosure, "") 26 | 27 | wsNetConn := NetConn(r.Context(), wsConn, websocket.MessageBinary) 28 | 29 | tcpConn, err := net.Dial("tcp", remoteAddr) 30 | if err != nil { 31 | log.Println("Error dialing remote TCP address:", err) 32 | return 33 | } 34 | defer tcpConn.Close() 35 | 36 | transferData(wsNetConn, tcpConn) 37 | 38 | if websocket.CloseStatus(err) == websocket.StatusNormalClosure { 39 | return 40 | } 41 | if err != nil { 42 | log.Printf("failed to proxy traffic from %s: %s", r.RemoteAddr, err) 43 | return 44 | } 45 | } 46 | 47 | 48 | func NetConn(ctx context.Context, c *websocket.Conn, msgType websocket.MessageType) net.Conn { 49 | nc := &netConn{ 50 | c: c, 51 | msgType: msgType, 52 | } 53 | 54 | var writeCancel context.CancelFunc 55 | nc.writeContext, writeCancel = context.WithCancel(ctx) 56 | nc.writeTimer = time.AfterFunc(math.MaxInt64, func() { 57 | nc.afterWriteDeadline.Store(true) 58 | if nc.writing.Load() { 59 | writeCancel() 60 | } 61 | }) 62 | if !nc.writeTimer.Stop() { 63 | <-nc.writeTimer.C 64 | } 65 | 66 | var readCancel context.CancelFunc 67 | nc.readContext, readCancel = context.WithCancel(ctx) 68 | nc.readTimer = time.AfterFunc(math.MaxInt64, func() { 69 | nc.afterReadDeadline.Store(true) 70 | if nc.reading.Load() { 71 | readCancel() 72 | } 73 | }) 74 | if !nc.readTimer.Stop() { 75 | <-nc.readTimer.C 76 | } 77 | 78 | return nc 79 | } 80 | 81 | type netConn struct { 82 | c *websocket.Conn 83 | msgType websocket.MessageType 84 | 85 | writeTimer *time.Timer 86 | writeContext context.Context 87 | writing atomic.Bool 88 | afterWriteDeadline atomic.Bool 89 | 90 | readTimer *time.Timer 91 | readContext context.Context 92 | reading atomic.Bool 93 | afterReadDeadline atomic.Bool 94 | 95 | readMu sync.Mutex 96 | eofed bool 97 | reader io.Reader 98 | } 99 | 100 | var _ net.Conn = &netConn{} 101 | 102 | func (c *netConn) Close() error { 103 | return c.c.Close(websocket.StatusNormalClosure, "") 104 | } 105 | 106 | func (c *netConn) Write(p []byte) (int, error) { 107 | if c.afterWriteDeadline.Load() { 108 | return 0, os.ErrDeadlineExceeded 109 | } 110 | 111 | if swapped := c.writing.CompareAndSwap(false, true); !swapped { 112 | panic("Concurrent writes not allowed") 113 | } 114 | defer c.writing.Store(false) 115 | 116 | err := c.c.Write(c.writeContext, c.msgType, p) 117 | if err != nil { 118 | return 0, err 119 | } 120 | 121 | return len(p), nil 122 | } 123 | 124 | func (c *netConn) Read(p []byte) (int, error) { 125 | if c.afterReadDeadline.Load() { 126 | return 0, os.ErrDeadlineExceeded 127 | } 128 | 129 | c.readMu.Lock() 130 | defer c.readMu.Unlock() 131 | if swapped := c.reading.CompareAndSwap(false, true); !swapped { 132 | panic("Concurrent reads not allowed") 133 | } 134 | defer c.reading.Store(false) 135 | 136 | if c.eofed { 137 | return 0, io.EOF 138 | } 139 | 140 | if c.reader == nil { 141 | typ, r, err := c.c.Reader(c.readContext) 142 | if err != nil { 143 | switch websocket.CloseStatus(err) { 144 | case websocket.StatusNormalClosure, websocket.StatusGoingAway: 145 | c.eofed = true 146 | return 0, io.EOF 147 | } 148 | return 0, err 149 | } 150 | if typ != c.msgType { 151 | err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ) 152 | c.c.Close(websocket.StatusUnsupportedData, err.Error()) 153 | return 0, err 154 | } 155 | c.reader = r 156 | } 157 | 158 | n, err := c.reader.Read(p) 159 | if err == io.EOF { 160 | c.reader = nil 161 | err = nil 162 | } 163 | return n, err 164 | } 165 | 166 | type websocketAddr struct { 167 | } 168 | 169 | func (a websocketAddr) Network() string { 170 | return "websocket" 171 | } 172 | 173 | func (a websocketAddr) String() string { 174 | return "websocket/unknown-addr" 175 | } 176 | 177 | func (c *netConn) RemoteAddr() net.Addr { 178 | return websocketAddr{} 179 | } 180 | 181 | func (c *netConn) LocalAddr() net.Addr { 182 | return websocketAddr{} 183 | } 184 | 185 | func (c *netConn) SetDeadline(t time.Time) error { 186 | c.SetWriteDeadline(t) 187 | c.SetReadDeadline(t) 188 | return nil 189 | } 190 | 191 | func (c *netConn) SetWriteDeadline(t time.Time) error { 192 | if t.IsZero() { 193 | c.writeTimer.Stop() 194 | } else { 195 | c.writeTimer.Reset(time.Until(t)) 196 | } 197 | c.afterWriteDeadline.Store(false) 198 | return nil 199 | } 200 | 201 | func (c *netConn) SetReadDeadline(t time.Time) error { 202 | if t.IsZero() { 203 | c.readTimer.Stop() 204 | } else { 205 | c.readTimer.Reset(time.Until(t)) 206 | } 207 | c.afterReadDeadline.Store(false) 208 | return nil 209 | } 210 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 3 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 9 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 10 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 11 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 12 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 13 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 14 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 15 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 16 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 17 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 18 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 19 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 20 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 21 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 22 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 23 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= 24 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 25 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= 26 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 27 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 28 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 29 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 30 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 31 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 32 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 33 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 34 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 35 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 36 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 37 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 38 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 39 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 40 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 41 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 42 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 43 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 44 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 45 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 46 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 47 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 48 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 49 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 51 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 52 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 53 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 54 | github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= 55 | github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= 56 | github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= 57 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 59 | github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A= 60 | github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= 61 | github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk= 62 | github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= 63 | github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= 64 | github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= 65 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 66 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 67 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 68 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 69 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 70 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 72 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 73 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 74 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 75 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 76 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 77 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 78 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 79 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 80 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 81 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 82 | golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= 83 | golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= 84 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= 85 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 86 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 87 | golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= 88 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 89 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 90 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 91 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 92 | golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= 93 | golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 94 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 97 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 104 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 106 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 107 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 108 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 109 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 110 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 111 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 112 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 113 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 114 | golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= 115 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 116 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 117 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 118 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 119 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 120 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 121 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 122 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 123 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 124 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 125 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 126 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 127 | nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= 128 | nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 129 | --------------------------------------------------------------------------------