diff --git a/Makefile b/Makefile
index aa104f29b5dcf6996ba3a2d481d86a9898434713..3d30a5380085a9f648bd50901b137339bc092388 100644
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,10 @@ run-client:
 run-client-kcp:
 	KCP=1 ./obfsvpn-client -c ${OBFS4_CERT}
 
+run-client-quic:
+	sudo sysctl -w net.core.rmem_max=2500000
+	QUIC=1 ./obfsvpn-client -c ${OBFS4_CERT}
+
 run-openvpn:
 	./scripts/run-openvpn-client.sh
 
diff --git a/client/client.go b/client/client.go
index c0a42e04dbe9576d8a7aad7ed9f879087c2e612f..371cf885ad94755ffd31ddb494fabb502703e8fe 100644
--- a/client/client.go
+++ b/client/client.go
@@ -1,25 +1,30 @@
 package client
 
 import (
+	"crypto/tls"
 	"fmt"
 	"log"
 	"net"
 
 	"0xacab.org/leap/obfsvpn"
+	"0xacab.org/leap/obfsvpn/quicwrapper"
 
 	"github.com/kalikaneko/socks5"
+	"github.com/lucas-clemente/quic-go"
 	"github.com/xtaci/kcp-go"
 )
 
 type Client struct {
 	kcp       bool
+	quic      bool
 	socksAddr string
 	obfs4Cert string
 }
 
-func NewClient(kcp bool, socksAddr, obfs4Cert string) *Client {
+func NewClient(kcp, quic bool, socksAddr, obfs4Cert string) *Client {
 	return &Client{
 		kcp:       kcp,
+		quic:      quic,
 		socksAddr: socksAddr,
 		obfs4Cert: obfs4Cert,
 	}
@@ -42,6 +47,15 @@ func (c *Client) Start() bool {
 			log.Printf("Dialing kcp://%s\n", address)
 			return kcp.Dial(address)
 		}
+	} else if c.quic {
+		dialer.DialFunc = func(network, address string) (net.Conn, error) {
+			tlsConfig := &tls.Config{
+				InsecureSkipVerify: true,                          // TODO proper pinning
+				NextProtos:         []string{"quic-echo-example"}, // XXX what is this???
+			}
+			c := quicwrapper.NewClient(address, tlsConfig, &quic.Config{}, nil)
+			return c.Dial()
+		}
 	}
 
 	server.Dial = dialer.Dial
diff --git a/cmd/client/main.go b/cmd/client/main.go
index 15f33d5eccea28cb3c0119ac1bfaea58cb839c30..4125a12877c0d027a4448cd1fd3a92df3f7c3c3b 100644
--- a/cmd/client/main.go
+++ b/cmd/client/main.go
@@ -42,14 +42,19 @@ func main() {
 		debug.SetOutput(os.Stderr)
 	}
 
-	kcpTransport := false
+	var (
+		kcpTransport  = false
+		quicTransport = false
+	)
 	// TODO make this configurable via a Config struct
 	// TODO make sure we're disabling all the crypto options for KCP
 	if os.Getenv("KCP") == "1" {
 		kcpTransport = true
+	} else if os.Getenv("QUIC") == "1" {
+		quicTransport = true
 	}
 
 	socksAddr := net.JoinHostPort(socksHost, socksPort)
-	c := client.NewClient(kcpTransport, socksAddr, obfs4Cert)
+	c := client.NewClient(kcpTransport, quicTransport, socksAddr, obfs4Cert)
 	c.Start()
 }
diff --git a/go.mod b/go.mod
index 51ce1a85fe5bc5b1b5f8538995c36cb633458a6f..4482bd50e99b8fa21e0afb340a5c3deb11413e53 100644
--- a/go.mod
+++ b/go.mod
@@ -14,13 +14,18 @@ require (
 	gitlab.com/yawning/obfs4.git v0.0.0-20210511220700-e330d1b7024b
 )
 
+require github.com/getlantern/netx v0.0.0-20211206143627-7ccfeb739cbd
+
 require (
 	github.com/cheekybits/genny v1.0.0 // indirect
 	github.com/dchest/siphash v1.2.1 // indirect
 	github.com/fsnotify/fsnotify v1.4.9 // indirect
 	github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
 	github.com/getlantern/errors v1.0.1 // indirect
+	github.com/getlantern/golog v0.0.0-20210606115803-bce9f9fe5a5f // indirect
+	github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
 	github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
+	github.com/getlantern/iptool v0.0.0-20210721034953-519bf8ce0147 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-stack/stack v1.8.1 // indirect
@@ -32,6 +37,7 @@ require (
 	github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
 	github.com/nxadm/tail v1.4.8 // indirect
 	github.com/onsi/ginkgo v1.16.4 // indirect
+	github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
 	github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
diff --git a/go.sum b/go.sum
index 632bab702afe50386fae33d9aae21d183a982ff7..270659910b315c4d33fd7af1f914fdd2beec9f30 100644
--- a/go.sum
+++ b/go.sum
@@ -44,14 +44,31 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
 github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA=
 github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
 github.com/getlantern/errors v1.0.1 h1:XukU2whlh7OdpxnkXhNH9VTLVz0EVPGKDV5K0oWhvzw=
 github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
+github.com/getlantern/fdcount v0.0.0-20190912142506-f89afd7367c4 h1:JdD4XSaT6/j6InM7MT1E4WRvzR8gurxfq53A3ML3B/Q=
+github.com/getlantern/fdcount v0.0.0-20190912142506-f89afd7367c4/go.mod h1:XZwE+iIlAgr64OFbXKFNCllBwV4wEipPx8Hlo2gZdbM=
+github.com/getlantern/golog v0.0.0-20210606115803-bce9f9fe5a5f h1:wsVt3P/boVKkPFEZkWxgNgRq/+mD7sWHc17+Vw2PgH8=
+github.com/getlantern/golog v0.0.0-20210606115803-bce9f9fe5a5f/go.mod h1:ZyIjgH/1wTCl+B+7yH1DqrWp6MPJqESmwmEQ89ZfhvA=
+github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
 github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU=
 github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM=
+github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
 github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2yaur9Qk3rHYD414j3Q1rl7+L0AylxrE=
 github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A=
+github.com/getlantern/iptool v0.0.0-20210721034953-519bf8ce0147 h1:/4ibPEIbC7c786Ec5Z8QqTti8MAjjTp/LmfuF6frVDM=
+github.com/getlantern/iptool v0.0.0-20210721034953-519bf8ce0147/go.mod h1:hfspzdRcvJ130tpTPL53/L92gG0pFtvQ6ln35ppwhHE=
+github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848 h1:2MhMMVBTnaHrst6HyWFDhwQCaJ05PZuOv1bE2gN8WFY=
+github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848/go.mod h1:+F5GJ7qGpQ03DBtcOEyQpM30ix4BLswdaojecFtsdy8=
+github.com/getlantern/mtime v0.0.0-20200417132445-23682092d1f7 h1:03J6Cb42EG06lHgpOFGm5BOax4qFqlSbSeKO2RGrj2g=
+github.com/getlantern/mtime v0.0.0-20200417132445-23682092d1f7/go.mod h1:GfzwugvtH7YcmNIrHHizeyImsgEdyL88YkdnK28B14c=
+github.com/getlantern/netx v0.0.0-20211206143627-7ccfeb739cbd h1:z5IehLDMqMwJ0oeFIaMHhySRU8r1lRMh7WQ0Wn0LioA=
+github.com/getlantern/netx v0.0.0-20211206143627-7ccfeb739cbd/go.mod h1:WEXF4pfIfnHBUAKwLa4DW7kcEINtG6wjUkbL2btwXZQ=
+github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
+github.com/getlantern/ops v0.0.0-20200403153110-8476b16edcd6/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
 github.com/getlantern/ops v0.0.0-20220418195917-45286e0140f6 h1:8DN68g9BZ8TS0TUQCvQB8R1lhAc60weDFPU++37RcvM=
 github.com/getlantern/ops v0.0.0-20220418195917-45286e0140f6/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -63,6 +80,7 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
 github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
@@ -165,6 +183,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
 github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
 github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
+github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
diff --git a/quicwrapper/dialer.go b/quicwrapper/dialer.go
new file mode 100644
index 0000000000000000000000000000000000000000..554bb14cf7415da48fdf2dce1aa8930eb53f5574
--- /dev/null
+++ b/quicwrapper/dialer.go
@@ -0,0 +1,228 @@
+package quicwrapper
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"net"
+	"sync"
+
+	"github.com/apex/log"
+	"github.com/getlantern/netx"
+	quic "github.com/lucas-clemente/quic-go"
+)
+
+// a QuicDialFn is a function that may be used to establish a new QUIC Session
+type QuicDialFn func(ctx context.Context, addr string, tlsConf *tls.Config, config *quic.Config) (quic.Connection, error)
+type UDPDialFn func(addr string) (net.PacketConn, *net.UDPAddr, error)
+
+var (
+	DialWithNetx    QuicDialFn = newDialerWithUDPDialer(DialUDPNetx)
+	DialWithoutNetx QuicDialFn = quic.DialAddrContext
+	defaultQuicDial QuicDialFn = DialWithNetx
+)
+
+type wrappedSession struct {
+	quic.Connection
+	conn net.PacketConn
+}
+
+func (w wrappedSession) CloseWithError(code quic.ApplicationErrorCode, mesg string) error {
+	err := w.Connection.CloseWithError(code, mesg)
+	err2 := w.conn.Close()
+	if err == nil {
+		err = err2
+	}
+	return err
+}
+
+// Creates a new QuicDialFn that uses the UDPDialFn given to
+// create the underlying net.PacketConn
+func newDialerWithUDPDialer(dial UDPDialFn) QuicDialFn {
+	return func(ctx context.Context, addr string, tlsConf *tls.Config, config *quic.Config) (quic.Connection, error) {
+		udpConn, udpAddr, err := dial(addr)
+		if err != nil {
+			return nil, err
+		}
+		ses, err := quic.DialContext(ctx, udpConn, udpAddr, addr, tlsConf, config)
+		if err != nil {
+			udpConn.Close()
+			return nil, err
+		}
+		return wrappedSession{ses, udpConn}, nil
+	}
+}
+
+// DialUDPNetx is a UDPDialFn that resolves addresses and obtains
+// the net.PacketConn using the netx package.
+func DialUDPNetx(addr string) (net.PacketConn, *net.UDPAddr, error) {
+	udpAddr, err := netx.ResolveUDPAddr("udp", addr)
+	if err != nil {
+		return nil, nil, err
+	}
+	udpConn, err := netx.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
+	if err != nil {
+		return nil, nil, err
+	}
+	return udpConn, udpAddr, nil
+}
+
+// NewClient returns a client that creates multiplexed
+// QUIC connections in a single Session with the given address using
+// the provided configuration.
+//
+// The Session is created using the
+// QuicDialFn given, but is not established until
+// the first call to Dial(), DialContext() or Connect()
+//
+// if dial is nil, the default quic dialer is used
+func NewClient(addr string, tlsConf *tls.Config, config *Config, dial QuicDialFn) *Client {
+	return NewClientWithPinnedCert(addr, tlsConf, config, dial, nil)
+}
+
+// NewClientWithPinnedCert returns a new client configured
+// as with NewClient, but accepting only a specific given
+// certificate.  If the certificate presented by the connected
+// server does match the given certificate, the connection is
+// rejected. This check is performed regardless of tls.Config
+// settings (ie even if InsecureSkipVerify is true)
+//
+// If a nil certificate is given, the check is not performed and
+// any valid certificate according the tls.Config given is accepted
+// (equivalent to NewClient behavior)
+func NewClientWithPinnedCert(addr string, tlsConf *tls.Config, config *Config, dial QuicDialFn, cert *x509.Certificate) *Client {
+	if dial == nil {
+		dial = defaultQuicDial
+	}
+
+	tlsConf = defaultNextProtos(tlsConf, DefaultClientProtos)
+
+	return &Client{
+		session:    nil,
+		address:    addr,
+		tlsConf:    tlsConf,
+		config:     config,
+		dial:       dial,
+		pinnedCert: cert,
+	}
+
+}
+
+type Client struct {
+	session    quic.Connection
+	muSession  sync.Mutex
+	address    string
+	tlsConf    *tls.Config
+	pinnedCert *x509.Certificate
+	config     *Config
+	dial       QuicDialFn
+}
+
+// DialContext creates a new multiplexed QUIC connection to the
+// server configured in the client. The given Context governs
+// cancellation / timeout.  If initial handshaking is performed,
+// the operation is additionally governed by HandshakeTimeout
+// value given in the client Config.
+func (c *Client) DialContext(ctx context.Context) (*Conn, error) {
+	session, err := c.getOrCreateSession(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("connecting session: %w", err)
+	}
+	stream, err := session.OpenStreamSync(ctx)
+	if err != nil {
+		if ne, ok := err.(net.Error); ok && !ne.Temporary() {
+			// start over again when seeing unrecoverable error.
+			c.clearSession(err.Error())
+		}
+		return nil, fmt.Errorf("establishing stream: %w", err)
+	}
+	return newConn(stream, session, nil), nil
+}
+
+// Dial creates a new multiplexed QUIC connection to the
+// server configured for the client.
+func (c *Client) Dial() (*Conn, error) {
+	return c.DialContext(context.Background())
+}
+
+// Connect requests immediate handshaking regardless of
+// whether any specific Dial has been initiated. It is
+// called lazily on the first Dial if not otherwise
+// called.
+//
+// This can serve to pre-establish a multiplexed
+// session, but will also initiate idle timeout
+// tracking, keepalives etc. Returns any error
+// encountered during handshake.
+//
+// This may safely be called concurrently with Dial.
+// The handshake is guaranteed to be completed when the
+// call returns to any caller.
+func (c *Client) Connect(ctx context.Context) error {
+	_, err := c.getOrCreateSession(ctx)
+	return err
+}
+
+func (c *Client) getOrCreateSession(ctx context.Context) (quic.Connection, error) {
+	c.muSession.Lock()
+	defer c.muSession.Unlock()
+	if c.session == nil {
+		session, err := c.dial(ctx, c.address, c.tlsConf, c.config)
+		if err != nil {
+			return nil, err
+		}
+		if c.pinnedCert != nil {
+			if err = c.verifyPinnedCert(session); err != nil {
+				session.CloseWithError(0, "")
+				return nil, err
+			}
+		}
+		c.session = session
+	}
+	return c.session, nil
+}
+
+func (c *Client) verifyPinnedCert(session quic.Connection) error {
+	certs := session.ConnectionState().TLS.PeerCertificates
+	if len(certs) == 0 {
+		return fmt.Errorf("Server did not present any certificates!")
+	}
+
+	serverCert := certs[0]
+	if !serverCert.Equal(c.pinnedCert) {
+		received := pem.EncodeToMemory(&pem.Block{
+			Type:    "CERTIFICATE",
+			Headers: nil,
+			Bytes:   serverCert.Raw,
+		})
+
+		expected := pem.EncodeToMemory(&pem.Block{
+			Type:    "CERTIFICATE",
+			Headers: nil,
+			Bytes:   c.pinnedCert.Raw,
+		})
+
+		return fmt.Errorf("Server's certificate didn't match expected! Server had\n%v\nbut expected:\n%v", received, expected)
+	}
+	return nil
+}
+
+// closes the session established by this client
+// (and all multiplexed connections)
+func (c *Client) Close() error {
+	c.clearSession("client closed")
+	return nil
+}
+
+func (c *Client) clearSession(reason string) {
+	c.muSession.Lock()
+	s := c.session
+	c.session = nil
+	c.muSession.Unlock()
+	if s != nil {
+		log.Debugf("Closing quic session (%v)", reason)
+		s.CloseWithError(0, "")
+	}
+}
diff --git a/server/Makefile b/server/Makefile
index ac4f9016e29e660e7382cc26d41d205d0e548200..c7569d7024145a03304d0699661a772c376b7843 100644
--- a/server/Makefile
+++ b/server/Makefile
@@ -2,7 +2,7 @@ RHOST=163.172.126.44:443
 LHOST="10.0.0.209:443"
 
 build:
-	go build
+	CGO_ENABLED=0 go build
 
 run:
 	sudo ./server -addr ${LHOST} -vpn ${RHOST} -state test_data -c test_data/obfs4.json 
@@ -11,6 +11,7 @@ run-kcp:
 	sudo KCP=1 ./server -addr ${LHOST} -vpn ${RHOST} -state test_data -c test_data/obfs4.json 
 
 run-quic:
+	sudo sysctl -w net.core.rmem_max=2500000
 	sudo QUIC=1 ./server -addr ${LHOST} -vpn ${RHOST} -state test_data -c test_data/obfs4.json 
 
 stop: