From 5b3214a6d974f9fa33f6a93bb6e3e794bdbe9f56 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 15 Jan 2024 18:36:31 +0100 Subject: [PATCH 1/6] add tlsstate --- internal/tlsstate/controlmsg.go | 136 +++++++++++++++ internal/tlsstate/tlsbio.go | 97 +++++++++++ internal/tlsstate/tlshandshake.go | 266 ++++++++++++++++++++++++++++++ internal/tlsstate/tlsstate.go | 238 ++++++++++++++++++++++++++ 4 files changed, 737 insertions(+) create mode 100644 internal/tlsstate/controlmsg.go create mode 100644 internal/tlsstate/tlsbio.go create mode 100644 internal/tlsstate/tlshandshake.go create mode 100644 internal/tlsstate/tlsstate.go diff --git a/internal/tlsstate/controlmsg.go b/internal/tlsstate/controlmsg.go new file mode 100644 index 00000000..acde2b25 --- /dev/null +++ b/internal/tlsstate/controlmsg.go @@ -0,0 +1,136 @@ +package tlsstate + +import ( + "bytes" + "errors" + "fmt" + + "github.com/ooni/minivpn/internal/bytesx" + "github.com/ooni/minivpn/internal/model" + "github.com/ooni/minivpn/internal/session" +) + +// encodeClientControlMessage returns a byte array with the payload for a control channel packet. +// This is the packet that the client sends to the server with the key +// material, local options and credentials (if username+password authentication is used). +func encodeClientControlMessageAsBytes(k *session.KeySource, o *model.Options) ([]byte, error) { + opt, err := bytesx.EncodeOptionStringToBytes(o.ServerOptionsString()) + if err != nil { + return nil, err + } + user, err := bytesx.EncodeOptionStringToBytes(string(o.Username)) + if err != nil { + return nil, err + } + pass, err := bytesx.EncodeOptionStringToBytes(string(o.Password)) + if err != nil { + return nil, err + } + + var out bytes.Buffer + out.Write(controlMessageHeader) + out.WriteByte(0x02) // key method (2) + out.Write(k.Bytes()) + out.Write(opt) + out.Write(user) + out.Write(pass) + + // we could send IV_PLAT too, but afaik declaring the platform does not + // make any difference for our purposes. + rawInfo := fmt.Sprintf("IV_VER=%s\nIV_PROTO=%s\n", ivVer, ivProto) + peerInfo, _ := bytesx.EncodeOptionStringToBytes(rawInfo) + out.Write(peerInfo) + return out.Bytes(), nil +} + +// controlMessageHeader is the header prefixed to control messages +var controlMessageHeader = []byte{0x00, 0x00, 0x00, 0x00} + +const ivVer = "2.5.5" // OpenVPN version compat that we declare to the server +const ivProto = "2" // IV_PROTO declared to the server. We need to be sure to enable the peer-id bit to use P_DATA_V2. + +// tlsRecordToControlMessage converts a TLS record to a control message. +//func tlsRecordToControlMessage(tlsRecord []byte) (out []byte) { +// out = append(out, controlMessageHeader...) +// out = append(out, tlsRecord...) +// return out +//} + +// errMissingHeader indicates that we're missing the four-byte all-zero header. +var errMissingHeader = errors.New("missing four-byte all-zero header") + +// errInvalidHeader indicates that the header is not a sequence of four zeroed bytes. +var errInvalidHeader = errors.New("expected four-byte all-zero header") + +// errBadControlMessage indicates that a control message cannot be parsed. +var errBadControlMessage = errors.New("cannot parse control message") + +// errBadKeyMethod indicates we don't support a key method +var errBadKeyMethod = errors.New("unsupported key method") + +// parseControlMessage gets a server control message and returns the value for +// the remote key, the server remote options, and an error indicating if the +// operation could not be completed. +func parseServerControlMessage(message []byte) (*session.KeySource, string, error) { + if len(message) < 4 { + return nil, "", errMissingHeader + } + if !bytes.Equal(message[:4], controlMessageHeader) { + return nil, "", errInvalidHeader + } + // TODO(ainghazal): figure out why 71 here + if len(message) < 71 { + return nil, "", fmt.Errorf("%w: bad len from server:%d", errBadControlMessage, len(message)) + } + keyMethod := message[4] + if keyMethod != 2 { + return nil, "", fmt.Errorf("%w: %d", errBadKeyMethod, keyMethod) + + } + var random1, random2 [32]byte + // first chunk of random bytes + copy(random1[:], message[5:37]) + // second chunk of random bytes + copy(random2[:], message[37:69]) + + options, err := bytesx.DecodeOptionStringFromBytes(message[69:]) + if err != nil { + return nil, "", fmt.Errorf("%w:%s", errBadControlMessage, "bad options string") + } + + remoteKey := &session.KeySource{ + R1: random1, + R2: random2, + PreMaster: [48]byte{}, + } + return remoteKey, options, nil +} + +// serverBadAuth indicates that the authentication failed +var serverBadAuth = []byte("AUTH_FAILED") + +// serverPushReply is the response for a successful push request +var serverPushReply = []byte("PUSH_REPLY") + +// errBadAuth means we could not authenticate +var errBadAuth = errors.New("server says: bad auth") + +// errBadServerReply indicates we didn't get one of the few responses we expected +var errBadServerReply = errors.New("bad server reply") + +// parseServerPushReply parses the push reply +func parseServerPushReply(logger model.Logger, resp []byte) (*model.TunnelInfo, error) { + // make sure the server's response contains the expected result + if bytes.HasPrefix(resp, serverBadAuth) { + return nil, errBadAuth + } + if !bytes.HasPrefix(resp, serverPushReply) { + return nil, fmt.Errorf("%w:%s", errBadServerReply, "expected push reply") + } + + // TODO(bassosimone): consider moving the two functions below in this package + optsMap := model.PushedOptionsAsMap(resp) + logger.Infof("Server pushed options: %v", optsMap) + ti := model.NewTunnelInfoFromPushedOptions(optsMap) + return ti, nil +} diff --git a/internal/tlsstate/tlsbio.go b/internal/tlsstate/tlsbio.go new file mode 100644 index 00000000..fe7c3892 --- /dev/null +++ b/internal/tlsstate/tlsbio.go @@ -0,0 +1,97 @@ +package tlsstate + +import ( + "bytes" + "log" + "net" + "sync" + "time" +) + +// tlsBio allows to use channels to read and write +type tlsBio struct { + closeOnce sync.Once + directionDown chan<- []byte + directionUp <-chan []byte + hangup chan any + readBuffer *bytes.Buffer +} + +// newTLSBio creates a new tlsBio +func newTLSBio(directionUp <-chan []byte, directionDown chan<- []byte) *tlsBio { + return &tlsBio{ + closeOnce: sync.Once{}, + directionDown: directionDown, + directionUp: directionUp, + hangup: make(chan any), + readBuffer: &bytes.Buffer{}, + } +} + +func (c *tlsBio) Close() error { + c.closeOnce.Do(func() { + close(c.hangup) + }) + return nil +} + +func (c *tlsBio) Read(data []byte) (int, error) { + for { + count, _ := c.readBuffer.Read(data) + if count > 0 { + log.Printf("[tlsbio] received %d bytes", len(data)) + return count, nil + } + select { + case extra := <-c.directionUp: + c.readBuffer.Write(extra) + case <-c.hangup: + return 0, net.ErrClosed + } + } +} + +func (c *tlsBio) Write(data []byte) (int, error) { + log.Printf("[tlsbio] requested to write %d bytes", len(data)) + select { + case c.directionDown <- data: + return len(data), nil + case <-c.hangup: + return 0, net.ErrClosed + } +} + +func (c *tlsBio) LocalAddr() net.Addr { + return &tlsBioAddr{} +} + +func (c *tlsBio) RemoteAddr() net.Addr { + return &tlsBioAddr{} +} + +func (c *tlsBio) SetDeadline(t time.Time) error { + return nil +} + +func (c *tlsBio) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *tlsBio) SetWriteDeadline(t time.Time) error { + return nil +} + +// tlsBioAddr is the type of address returned by [Conn] +type tlsBioAddr struct{} + +var _ net.Addr = &tlsBioAddr{} + +// Network implements net.Addr +func (*tlsBioAddr) Network() string { + return "tlsBioAddr" +} + +// String implements net.Addr +func (*tlsBioAddr) String() string { + return "tlsBioAddr" +} diff --git a/internal/tlsstate/tlshandshake.go b/internal/tlsstate/tlshandshake.go new file mode 100644 index 00000000..6075331b --- /dev/null +++ b/internal/tlsstate/tlshandshake.go @@ -0,0 +1,266 @@ +package tlsstate + +import ( + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "io/ioutil" + "net" + + "github.com/ooni/minivpn/internal/model" + "github.com/ooni/minivpn/internal/runtimex" + tls "github.com/refraction-networking/utls" +) + +var ( + // ErrBadTLSInit is returned when TLS configuration cannot be initialized + ErrBadTLSInit = errors.New("TLS init error") + + // ErrBadTLSHandshake is returned when the OpenVPN handshake failed. + ErrBadTLSHandshake = errors.New("handshake failure") + + // ErrBadCA is returned when the CA file cannot be found or is not valid. + ErrBadCA = errors.New("bad ca conf") + + // ErrBadKeypair is returned when the key or cert file cannot be found or is not valid. + ErrBadKeypair = errors.New("bad keypair conf") + + // ErrBadParrot is returned for errors during TLS parroting + ErrBadParrot = errors.New("cannot parrot") + + // ErrCannotVerifyCertChain is returned for certificate chain validation errors. + ErrCannotVerifyCertChain = errors.New("cannot verify chain") +) + +// certVerifyOptionsNoCommonNameCheck returns a x509.VerifyOptions initialized with +// an empty string for the DNSName. This allows to skip CN verification. +func certVerifyOptionsNoCommonNameCheck() x509.VerifyOptions { + return x509.VerifyOptions{DNSName: ""} +} + +// certVerifyOptions is the options factory that the customVerify function will +// use; by default it configures VerifyOptions to skip the DNSName check. +var certVerifyOptions = certVerifyOptionsNoCommonNameCheck + +// certPaths holds the paths for the cert, key, and ca used for OpenVPN +// certificate authentication. +type certPaths struct { + certPath string + keyPath string + caPath string +} + +// loadCertAndCAFromPath parses the PEM certificates contained in the paths pointed by +// the passed certPaths and return a certConfig with the client and CA certificates. +func loadCertAndCAFromPath(pth certPaths) (*certConfig, error) { + ca := x509.NewCertPool() + caData, err := ioutil.ReadFile(pth.caPath) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrBadCA, err) + } + ok := ca.AppendCertsFromPEM(caData) + if !ok { + return nil, fmt.Errorf("%w: %s", ErrBadCA, "cannot parse ca cert") + } + + cfg := &certConfig{ca: ca} + if pth.certPath != "" && pth.keyPath != "" { + cert, err := tls.LoadX509KeyPair(pth.certPath, pth.keyPath) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrBadKeypair, err) + } + cfg.cert = cert + } + return cfg, nil +} + +// certBytes holds the byte arrays for the cert, key, and ca used for OpenVPN +// certificate authentication. +type certBytes struct { + cert []byte + key []byte + ca []byte +} + +// loadCertAndCAFromBytes parses the PEM certificates from the byte arrays in the +// the passed certBytes, and return a certConfig with the client and CA certificates. +func loadCertAndCAFromBytes(crt certBytes) (*certConfig, error) { + ca := x509.NewCertPool() + ok := ca.AppendCertsFromPEM(crt.ca) + if !ok { + return nil, fmt.Errorf("%w: %s", ErrBadCA, "cannot parse ca cert") + } + cfg := &certConfig{ca: ca} + if crt.cert != nil && crt.key != nil { + cert, err := tls.X509KeyPair(crt.cert, crt.key) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrBadKeypair, err) + } + cfg.cert = cert + } + return cfg, nil +} + +// authorityPinner is any object from which we can obtain a certpool containing +// a pinned Certificate Authority for verification. +type authorityPinner interface { + authority() *x509.CertPool +} + +// certConfig holds the parsed certificate and CA used for OpenVPN mutual +// certificate authentication. +type certConfig struct { + cert tls.Certificate + ca *x509.CertPool +} + +// newCertConfigFromOptions is a constructor that returns a certConfig object initialized +// from the paths specified in the passed Options object, and an error if it +// could not be properly built. +func newCertConfigFromOptions(o *model.Options) (*certConfig, error) { + var cfg *certConfig + var err error + if o.ShouldLoadCertsFromPath() { + cfg, err = loadCertAndCAFromPath(certPaths{ + certPath: o.CertPath, + keyPath: o.KeyPath, + caPath: o.CAPath, + }) + } else { + cfg, err = loadCertAndCAFromBytes(certBytes{ + cert: o.Cert, + key: o.Key, + ca: o.CA, + }) + } + return cfg, err +} + +// authority implements authorityPinner interface. +func (c *certConfig) authority() *x509.CertPool { + return c.ca +} + +// ensure certConfig implements authorityPinner. +var _ authorityPinner = &certConfig{} + +// verifyFun is the type expected by the VerifyPeerCertificate callback in tls.Config. +type verifyFun func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error + +// customVerifyFactory returns a verifyFun callback that will verify any received certificates +// against the ca provided by the pased implementation of authorityPinner +func customVerifyFactory(pinner authorityPinner) verifyFun { + // customVerify is a version of the verification routines that does not try to verify + // the Common Name, since we don't know it a priori for a VPN gateway. Returns + // an error if the verification fails. + // From tls/common documentation: If normal verification is disabled by + // setting InsecureSkipVerify, [...] then this callback will be considered but + // the verifiedChains argument will always be nil. + customVerify := func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + // we assume (from docs) that we're always given the + // leaf certificate as the first cert in the array. + leaf, _ := x509.ParseCertificate(rawCerts[0]) + if leaf == nil { + return fmt.Errorf("%w: %s", ErrCannotVerifyCertChain, "nothing to verify") + } + // By default has DNSName verification disabled. + opts := certVerifyOptions() + // Set the configured CA(s) as the certificate pool to verify against. + opts.Roots = pinner.authority() + + if _, err := leaf.Verify(opts); err != nil { + return fmt.Errorf("%w: %s", ErrCannotVerifyCertChain, err) + } + return nil + } + return customVerify +} + +// initTLS returns a tls.Config matching the VPN options. Internally, it uses +// the verify function returned by the global customVerifyFactory, +// verification function since verifying the ServerName does not make sense in +// the context of establishing a VPN session: we perform mutual TLS +// Authentication with the custom CA. +func initTLS(cfg *certConfig) (*tls.Config, error) { + runtimex.Assert(cfg != nil, "passed nil configuration") + + customVerify := customVerifyFactory(cfg) + + tlsConf := &tls.Config{ + // the certificate we've loaded from the config file + Certificates: []tls.Certificate{cfg.cert}, + // crypto/tls wants either ServerName or InsecureSkipVerify set ... + InsecureSkipVerify: true, + // ...but we pass our own verification function that verifies against the CA and ignores the ServerName + VerifyPeerCertificate: customVerify, + // disable DynamicRecordSizing to lower distinguishability. + DynamicRecordSizingDisabled: true, + // uTLS does not pick min/max version from the passed spec + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + } //#nosec G402 + + return tlsConf, nil +} + +// tlsHandshake performs the TLS handshake over the control channel, and return +// the TLS Client as a net.Conn; returns also any error during the handshake. +func tlsHandshake(tlsConn net.Conn, tlsConf *tls.Config) (net.Conn, error) { + tlsClient, err := tlsFactoryFn(tlsConn, tlsConf) + if err != nil { + return nil, err + } + if err := tlsClient.Handshake(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrBadTLSHandshake, err) + } + return tlsClient, nil +} + +// handshaker is a custom interface that we define here to be able to mock +// the tls.Conn implementation. +type handshaker interface { + net.Conn + Handshake() error +} + +// defaultTLSFactory returns an implementer of the handshaker interface; that +// is, the default tls.Client factory; and an error. +// we're not using the default factory right now, but it comes handy to be able +// to compare the fingerprints with a golang TLS handshake. +func defaultTLSFactory(conn net.Conn, config *tls.Config) (handshaker, error) { + c := tls.Client(conn, config) + return c, nil +} + +// vpnClientHelloHex is the hexadecimal representation of a capture from the reference openvpn implementation. +// openvpn=2.5.5,openssl=3.0.2 +// You can use https://github.com/ainghazal/sniff/tree/main/clienthello to +// analyze a ClientHello from the wire or pcap. +var vpnClientHelloHex = `1603010114010001100303534e0a0f2687b240f7c7dfbb51c4aac33639f28173aa5d7bcebb159695ab0855208b835bf240a83df66885d6747b5bbf1b631e8c34ae469c629d7eb76e247128eb0032130213031301c02cc030009fcca9cca8ccaac02bc02f009ec024c028006bc023c0270067c00ac0140039c009c013003300ff01000095000b000403000102000a00160014001d0017001e00190018010001010102010301040016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b0009080304030303020301002d00020101003300260024001d0020a10bc24becb583293c317220e6725205d3a177a4a974090f6ffcf13a43da7035` + +// parrotTLSFactory returns an implementer of the handshaker interface; in this +// case, a parroting implementation; and an error. +func parrotTLSFactory(conn net.Conn, config *tls.Config) (handshaker, error) { + fingerprinter := &tls.Fingerprinter{AllowBluntMimicry: true} + rawOpenVPNClientHelloBytes, err := hex.DecodeString(vpnClientHelloHex) + if err != nil { + return nil, fmt.Errorf("%w: cannot decode raw fingerprint: %s", ErrBadParrot, err) + } + generatedSpec, err := fingerprinter.FingerprintClientHello(rawOpenVPNClientHelloBytes) + if err != nil { + return nil, fmt.Errorf("%w: fingerprinting failed: %s", ErrBadParrot, err) + } + client := tls.UClient(conn, config, tls.HelloCustom) + if err := client.ApplyPreset(generatedSpec); err != nil { + return nil, fmt.Errorf("%w: cannot apply spec: %s", ErrBadParrot, err) + } + return client, nil +} + +// global variables to allow monkeypatching in tests. +var ( + initTLSFn = initTLS + tlsFactoryFn = parrotTLSFactory + tlsHandshakeFn = tlsHandshake +) diff --git a/internal/tlsstate/tlsstate.go b/internal/tlsstate/tlsstate.go new file mode 100644 index 00000000..66d9ac4c --- /dev/null +++ b/internal/tlsstate/tlsstate.go @@ -0,0 +1,238 @@ +package tlsstate + +import ( + "context" + "net" + "time" + + "github.com/ooni/minivpn/internal/model" + "github.com/ooni/minivpn/internal/session" + "github.com/ooni/minivpn/internal/workers" + tls "github.com/refraction-networking/utls" +) + +// Service is the tlsstate service. Make sure you initialize +// the channels before invoking [Service.StartWorkers]. +type Service struct { + NotifyTLS chan *model.Notification + KeyUp *chan *session.DataChannelKey + TLSRecordUp chan []byte + TLSRecordDown *chan []byte +} + +// StartWorkers starts the tls-state workers. See the [ARCHITECTURE] +// file for more information about the packet-muxer workers. +// +// [ARCHITECTURE]: https://github.com/ooni/minivpn/blob/main/ARCHITECTURE.md +func (svc *Service) StartWorkers( + logger model.Logger, + workersManager *workers.Manager, + sessionManager *session.Manager, + options *model.Options, +) { + ws := &workersState{ + logger: logger, + notifyTLS: svc.NotifyTLS, + options: options, + keyUp: *svc.KeyUp, + tlsRecordDown: *svc.TLSRecordDown, + tlsRecordUp: svc.TLSRecordUp, + sessionManager: sessionManager, + workersManager: workersManager, + } + workersManager.StartWorker(ws.worker) +} + +// workersState contains the control channel state. +type workersState struct { + logger model.Logger + notifyTLS <-chan *model.Notification + options *model.Options + tlsRecordDown chan<- []byte + tlsRecordUp <-chan []byte + keyUp chan<- *session.DataChannelKey + sessionManager *session.Manager + workersManager *workers.Manager +} + +// worker is the main loop of the tlsstate +func (ws *workersState) worker() { + defer func() { + ws.workersManager.OnWorkerDone() + ws.workersManager.StartShutdown() + ws.logger.Debug("tlsstate: worker: done") + }() + + ws.logger.Debug("tlsstate: worker: started") + for { + select { + case notif := <-ws.notifyTLS: + if (notif.Flags & model.NotificationReset) != 0 { + if err := ws.tlsAuth(); err != nil { + ws.logger.Warnf("tlsstate: tlsAuth: %s", err.Error()) + // TODO: is it worth checking the return value and stopping? + } + } + + case <-ws.workersManager.ShouldShutdown(): + return + } + } +} + +// tlsAuth runs the TLS auth algorithm +func (ws *workersState) tlsAuth() error { + // create the BIO to use channels as a socket + conn := newTLSBio(ws.tlsRecordUp, ws.tlsRecordDown) + defer conn.Close() + + // we construct the certCfg from options, that has access to the certificate material + certCfg, err := newCertConfigFromOptions(ws.options) + if err != nil { + return err + } + + // tlsConf is a tls.Config obtained from our own initialization function + tlsConf, err := initTLSFn(certCfg) + if err != nil { + return err + } + + // run the real algorithm in a background goroutine + errorch := make(chan error) + go ws.doTLSAuth(conn, tlsConf, errorch) + + // make sure we timeout after 60 seconds anyway + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + select { + case err := <-errorch: + return err + + case <-ctx.Done(): + return ctx.Err() + + case <-ws.workersManager.ShouldShutdown(): + return workers.ErrShutdown + } +} + +// doTLSAuth is the internal implementation of tlsAuth such that tlsAuth +// can interrupt this function early if needed. +func (ws *workersState) doTLSAuth(conn net.Conn, config *tls.Config, errorch chan<- error) { + ws.logger.Debug("tlsstate: doTLSAuth: started") + defer ws.logger.Debug("tlsstate: doTLSAuth: done") + + // do the TLS handshake + tlsConn, err := tlsHandshakeFn(conn, config) + if err != nil { + errorch <- err + return + } + //defer tlsConn.Close() // <- we don't care since the underlying conn is a tlsBio + + // we need the active key to create the first control message + activeKey, err := ws.sessionManager.ActiveKey() + if err != nil { + errorch <- err + return + } + + // send the first control message with random material + if err := ws.sendAuthRequestMessage(tlsConn, activeKey); err != nil { + errorch <- err + return + } + ws.sessionManager.SetNegotiationState(session.S_SENT_KEY) + + // read the server's keySource and options + remoteKey, serverOptions, err := ws.recvAuthReplyMessage(tlsConn) + if err != nil { + errorch <- err + return + } + ws.logger.Debugf("Remote options: %s", serverOptions) + + // init the tunnel info + if err := ws.sessionManager.InitTunnelInfo(serverOptions); err != nil { + errorch <- err + return + } + + // add the remote key to the active key + activeKey.AddRemoteKey(remoteKey) + ws.sessionManager.SetNegotiationState(session.S_GOT_KEY) + + // send the push request + if err := ws.sendPushRequestMessage(tlsConn); err != nil { + errorch <- err + return + } + + // obtain tunnel info from the push response + tinfo, err := ws.recvPushResponseMessage(tlsConn) + if err != nil { + errorch <- err + return + } + + // update with extra information obtained from push response + ws.sessionManager.UpdateTunnelInfo(tinfo) + + // progress to the ACTIVE state + ws.sessionManager.SetNegotiationState(session.S_ACTIVE) + + // notify the datachannel that we've got a key pair ready to use + ws.keyUp <- activeKey + + errorch <- nil +} + +// sendAuthRequestMessage sends the auth request message +func (ws *workersState) sendAuthRequestMessage(tlsConn net.Conn, activeKey *session.DataChannelKey) error { + // this message is sending our options and asking the server to get AUTH + ctrlMsg, err := encodeClientControlMessageAsBytes(activeKey.Local(), ws.options) + if err != nil { + return err + } + + // let's fire off the message + _, err = tlsConn.Write(ctrlMsg) + return err +} + +// recvAuthReplyMessage reads and parses the first control response. +func (ws *workersState) recvAuthReplyMessage(conn net.Conn) (*session.KeySource, string, error) { + // read raw bytes + buffer := make([]byte, 1<<17) + count, err := conn.Read(buffer) + if err != nil { + return nil, "", err + } + data := buffer[:count] + + // parse what we received + return parseServerControlMessage(data) +} + +// sendPushRequestMessage sends the push request message +func (ws *workersState) sendPushRequestMessage(conn net.Conn) error { + data := append([]byte("PUSH_REQUEST"), 0x00) + _, err := conn.Write(data) + return err +} + +// recvPushResponseMessage receives and parses the push response message +func (ws *workersState) recvPushResponseMessage(conn net.Conn) (*model.TunnelInfo, error) { + // read raw bytes + buffer := make([]byte, 1<<17) + count, err := conn.Read(buffer) + if err != nil { + return nil, err + } + data := buffer[:count] + + // parse what we received + return parseServerPushReply(ws.logger, data) +} From 0441b408ed9da18d1208fd3c741beb3b7eebabec Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 15 Jan 2024 18:48:08 +0100 Subject: [PATCH 2/6] add docs --- internal/tlsstate/doc.go | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 internal/tlsstate/doc.go diff --git a/internal/tlsstate/doc.go b/internal/tlsstate/doc.go new file mode 100644 index 00000000..e947683a --- /dev/null +++ b/internal/tlsstate/doc.go @@ -0,0 +1,3 @@ +// Package tlsstate performs a TLS handshake over the control channel, and they +// exchanges keys with the server over this secure channel. +package tlsstate From 45efecf40ec6e5922d1b8641e4d93aeb10c2d1a2 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 15 Jan 2024 18:54:17 +0100 Subject: [PATCH 3/6] add notes --- internal/tlsstate/tlsstate.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/internal/tlsstate/tlsstate.go b/internal/tlsstate/tlsstate.go index 66d9ac4c..9af5f0c7 100644 --- a/internal/tlsstate/tlsstate.go +++ b/internal/tlsstate/tlsstate.go @@ -14,9 +14,23 @@ import ( // Service is the tlsstate service. Make sure you initialize // the channels before invoking [Service.StartWorkers]. type Service struct { - NotifyTLS chan *model.Notification - KeyUp *chan *session.DataChannelKey - TLSRecordUp chan []byte + // NotifyTLS is a channel where we receive incoming notifications. + NotifyTLS chan *model.Notification + + // KeyUP is used to send newly negotiated data channel keys ready to be + // used. + KeyUp *chan *session.DataChannelKey + + // TLSRecordUp is data coming up from the control channel layer to us. + // TODO(ainghazal): considere renaming when we have merged the whole + // set of components. This name might not give a good idea of what the bytes being + // moved around are - this is a serialized control channel packet, which is + // mainly used to do the initial handshake and then receive control + // packets encrypted with this TLS session. + TLSRecordUp chan []byte + + // TLSRecordDown is data being transferred down from us to the control + // channel. TLSRecordDown *chan []byte } From 6740553e621a3ede80bf9ec219498d6ba76d8eed Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Mon, 15 Jan 2024 19:07:11 +0100 Subject: [PATCH 4/6] delete unused code --- internal/tlsstate/controlmsg.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/internal/tlsstate/controlmsg.go b/internal/tlsstate/controlmsg.go index acde2b25..e91b6e2b 100644 --- a/internal/tlsstate/controlmsg.go +++ b/internal/tlsstate/controlmsg.go @@ -49,13 +49,6 @@ var controlMessageHeader = []byte{0x00, 0x00, 0x00, 0x00} const ivVer = "2.5.5" // OpenVPN version compat that we declare to the server const ivProto = "2" // IV_PROTO declared to the server. We need to be sure to enable the peer-id bit to use P_DATA_V2. -// tlsRecordToControlMessage converts a TLS record to a control message. -//func tlsRecordToControlMessage(tlsRecord []byte) (out []byte) { -// out = append(out, controlMessageHeader...) -// out = append(out, tlsRecord...) -// return out -//} - // errMissingHeader indicates that we're missing the four-byte all-zero header. var errMissingHeader = errors.New("missing four-byte all-zero header") From 83cdb2de181bb232bdd0ce2070d2b5e45d1ba672 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Tue, 16 Jan 2024 17:24:06 +0100 Subject: [PATCH 5/6] rename package --- .../{tlsstate => tlssession}/controlmsg.go | 12 +++++++++++- internal/tlssession/doc.go | 3 +++ internal/{tlsstate => tlssession}/tlsbio.go | 2 +- .../{tlsstate => tlssession}/tlshandshake.go | 7 ++++--- .../tlsstate.go => tlssession/tlssession.go} | 18 +++++++++--------- internal/tlsstate/doc.go | 3 --- 6 files changed, 28 insertions(+), 17 deletions(-) rename internal/{tlsstate => tlssession}/controlmsg.go (91%) create mode 100644 internal/tlssession/doc.go rename internal/{tlsstate => tlssession}/tlsbio.go (98%) rename internal/{tlsstate => tlssession}/tlshandshake.go (98%) rename internal/{tlsstate/tlsstate.go => tlssession/tlssession.go} (93%) delete mode 100644 internal/tlsstate/doc.go diff --git a/internal/tlsstate/controlmsg.go b/internal/tlssession/controlmsg.go similarity index 91% rename from internal/tlsstate/controlmsg.go rename to internal/tlssession/controlmsg.go index e91b6e2b..e348cbfe 100644 --- a/internal/tlsstate/controlmsg.go +++ b/internal/tlssession/controlmsg.go @@ -1,4 +1,4 @@ -package tlsstate +package tlssession import ( "bytes" @@ -10,6 +10,16 @@ import ( "github.com/ooni/minivpn/internal/session" ) +// +// The functions in this file deal with control messages. These control +// messages are sent and received over the TLS session once we've gone one +// established. +// +// The control **channel** below us will deal with serializing and deserializing them, +// what we receive at this stage are the cleartext payloads obtained after decrypting +// an application data TLS record. +// + // encodeClientControlMessage returns a byte array with the payload for a control channel packet. // This is the packet that the client sends to the server with the key // material, local options and credentials (if username+password authentication is used). diff --git a/internal/tlssession/doc.go b/internal/tlssession/doc.go new file mode 100644 index 00000000..88c39de2 --- /dev/null +++ b/internal/tlssession/doc.go @@ -0,0 +1,3 @@ +// Package tlssession performs a TLS handshake over the control channel, and then it +// exchanges keys with the server over this secure channel. +package tlssession diff --git a/internal/tlsstate/tlsbio.go b/internal/tlssession/tlsbio.go similarity index 98% rename from internal/tlsstate/tlsbio.go rename to internal/tlssession/tlsbio.go index fe7c3892..6fec09be 100644 --- a/internal/tlsstate/tlsbio.go +++ b/internal/tlssession/tlsbio.go @@ -1,4 +1,4 @@ -package tlsstate +package tlssession import ( "bytes" diff --git a/internal/tlsstate/tlshandshake.go b/internal/tlssession/tlshandshake.go similarity index 98% rename from internal/tlsstate/tlshandshake.go rename to internal/tlssession/tlshandshake.go index 6075331b..3f3bbfb0 100644 --- a/internal/tlsstate/tlshandshake.go +++ b/internal/tlssession/tlshandshake.go @@ -1,12 +1,12 @@ -package tlsstate +package tlssession import ( "crypto/x509" "encoding/hex" "errors" "fmt" - "io/ioutil" "net" + "os" "github.com/ooni/minivpn/internal/model" "github.com/ooni/minivpn/internal/runtimex" @@ -55,7 +55,7 @@ type certPaths struct { // the passed certPaths and return a certConfig with the client and CA certificates. func loadCertAndCAFromPath(pth certPaths) (*certConfig, error) { ca := x509.NewCertPool() - caData, err := ioutil.ReadFile(pth.caPath) + caData, err := os.ReadFile(pth.caPath) if err != nil { return nil, fmt.Errorf("%w: %s", ErrBadCA, err) } @@ -228,6 +228,7 @@ type handshaker interface { // is, the default tls.Client factory; and an error. // we're not using the default factory right now, but it comes handy to be able // to compare the fingerprints with a golang TLS handshake. +// TODO(ainghazal): implement some sort of test that extracts/compares the TLS client hello. func defaultTLSFactory(conn net.Conn, config *tls.Config) (handshaker, error) { c := tls.Client(conn, config) return c, nil diff --git a/internal/tlsstate/tlsstate.go b/internal/tlssession/tlssession.go similarity index 93% rename from internal/tlsstate/tlsstate.go rename to internal/tlssession/tlssession.go index 9af5f0c7..12c6e4ae 100644 --- a/internal/tlsstate/tlsstate.go +++ b/internal/tlssession/tlssession.go @@ -1,4 +1,4 @@ -package tlsstate +package tlssession import ( "context" @@ -11,7 +11,7 @@ import ( tls "github.com/refraction-networking/utls" ) -// Service is the tlsstate service. Make sure you initialize +// Service is the tlssession service. Make sure you initialize // the channels before invoking [Service.StartWorkers]. type Service struct { // NotifyTLS is a channel where we receive incoming notifications. @@ -34,7 +34,7 @@ type Service struct { TLSRecordDown *chan []byte } -// StartWorkers starts the tls-state workers. See the [ARCHITECTURE] +// StartWorkers starts the tlssession workers. See the [ARCHITECTURE] // file for more information about the packet-muxer workers. // // [ARCHITECTURE]: https://github.com/ooni/minivpn/blob/main/ARCHITECTURE.md @@ -69,21 +69,21 @@ type workersState struct { workersManager *workers.Manager } -// worker is the main loop of the tlsstate +// worker is the main loop of the tlssession func (ws *workersState) worker() { defer func() { ws.workersManager.OnWorkerDone() ws.workersManager.StartShutdown() - ws.logger.Debug("tlsstate: worker: done") + ws.logger.Debug("tlssession: worker: done") }() - ws.logger.Debug("tlsstate: worker: started") + ws.logger.Debug("tlssession: worker: started") for { select { case notif := <-ws.notifyTLS: if (notif.Flags & model.NotificationReset) != 0 { if err := ws.tlsAuth(); err != nil { - ws.logger.Warnf("tlsstate: tlsAuth: %s", err.Error()) + ws.logger.Warnf("tlssession: tlsAuth: %s", err.Error()) // TODO: is it worth checking the return value and stopping? } } @@ -135,8 +135,8 @@ func (ws *workersState) tlsAuth() error { // doTLSAuth is the internal implementation of tlsAuth such that tlsAuth // can interrupt this function early if needed. func (ws *workersState) doTLSAuth(conn net.Conn, config *tls.Config, errorch chan<- error) { - ws.logger.Debug("tlsstate: doTLSAuth: started") - defer ws.logger.Debug("tlsstate: doTLSAuth: done") + ws.logger.Debug("tlsession: doTLSAuth: started") + defer ws.logger.Debug("tlssession: doTLSAuth: done") // do the TLS handshake tlsConn, err := tlsHandshakeFn(conn, config) diff --git a/internal/tlsstate/doc.go b/internal/tlsstate/doc.go deleted file mode 100644 index e947683a..00000000 --- a/internal/tlsstate/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package tlsstate performs a TLS handshake over the control channel, and they -// exchanges keys with the server over this secure channel. -package tlsstate From 0603a96ea1269309fcf38bca18834c35777529a0 Mon Sep 17 00:00:00 2001 From: ain ghazal Date: Fri, 19 Jan 2024 13:04:57 +0100 Subject: [PATCH 6/6] move comments above imports --- internal/tlssession/controlmsg.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/tlssession/controlmsg.go b/internal/tlssession/controlmsg.go index e348cbfe..d59ad945 100644 --- a/internal/tlssession/controlmsg.go +++ b/internal/tlssession/controlmsg.go @@ -1,15 +1,5 @@ package tlssession -import ( - "bytes" - "errors" - "fmt" - - "github.com/ooni/minivpn/internal/bytesx" - "github.com/ooni/minivpn/internal/model" - "github.com/ooni/minivpn/internal/session" -) - // // The functions in this file deal with control messages. These control // messages are sent and received over the TLS session once we've gone one @@ -20,6 +10,16 @@ import ( // an application data TLS record. // +import ( + "bytes" + "errors" + "fmt" + + "github.com/ooni/minivpn/internal/bytesx" + "github.com/ooni/minivpn/internal/model" + "github.com/ooni/minivpn/internal/session" +) + // encodeClientControlMessage returns a byte array with the payload for a control channel packet. // This is the packet that the client sends to the server with the key // material, local options and credentials (if username+password authentication is used).