Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: introduce tls state manager #52

Merged
merged 6 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions internal/tlssession/controlmsg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package tlssession

//
// 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.
//

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.

// 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
}
3 changes: 3 additions & 0 deletions internal/tlssession/doc.go
Original file line number Diff line number Diff line change
@@ -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
97 changes: 97 additions & 0 deletions internal/tlssession/tlsbio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package tlssession

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"
}
Loading