Skip to content

Commit dc271ad

Browse files
authored
refactor: introduce tls state manager (#52)
This is the fifth commit in the series of incremental refactoring of the current minivpn tree. In this commit, we introduce the tlsstate package, which is the layer above the control channel tlsstate is a service worker that will attempt to do a TLS negotiation (wrapped in a control packet), and then use this secure channel to exchange keys with the OpenVPN server and use these keys for encrypting and decrypting payloads in the data channel. tlsstate waits for a signal in the NotifyTLS channel and then will start the handshake and the key negotiation. Reference issue: #47
1 parent 1c76bec commit dc271ad

File tree

5 files changed

+758
-0
lines changed

5 files changed

+758
-0
lines changed

internal/tlssession/controlmsg.go

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package tlssession
2+
3+
//
4+
// The functions in this file deal with control messages. These control
5+
// messages are sent and received over the TLS session once we've gone one
6+
// established.
7+
//
8+
// The control **channel** below us will deal with serializing and deserializing them,
9+
// what we receive at this stage are the cleartext payloads obtained after decrypting
10+
// an application data TLS record.
11+
//
12+
13+
import (
14+
"bytes"
15+
"errors"
16+
"fmt"
17+
18+
"github.com/ooni/minivpn/internal/bytesx"
19+
"github.com/ooni/minivpn/internal/model"
20+
"github.com/ooni/minivpn/internal/session"
21+
)
22+
23+
// encodeClientControlMessage returns a byte array with the payload for a control channel packet.
24+
// This is the packet that the client sends to the server with the key
25+
// material, local options and credentials (if username+password authentication is used).
26+
func encodeClientControlMessageAsBytes(k *session.KeySource, o *model.Options) ([]byte, error) {
27+
opt, err := bytesx.EncodeOptionStringToBytes(o.ServerOptionsString())
28+
if err != nil {
29+
return nil, err
30+
}
31+
user, err := bytesx.EncodeOptionStringToBytes(string(o.Username))
32+
if err != nil {
33+
return nil, err
34+
}
35+
pass, err := bytesx.EncodeOptionStringToBytes(string(o.Password))
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
var out bytes.Buffer
41+
out.Write(controlMessageHeader)
42+
out.WriteByte(0x02) // key method (2)
43+
out.Write(k.Bytes())
44+
out.Write(opt)
45+
out.Write(user)
46+
out.Write(pass)
47+
48+
// we could send IV_PLAT too, but afaik declaring the platform does not
49+
// make any difference for our purposes.
50+
rawInfo := fmt.Sprintf("IV_VER=%s\nIV_PROTO=%s\n", ivVer, ivProto)
51+
peerInfo, _ := bytesx.EncodeOptionStringToBytes(rawInfo)
52+
out.Write(peerInfo)
53+
return out.Bytes(), nil
54+
}
55+
56+
// controlMessageHeader is the header prefixed to control messages
57+
var controlMessageHeader = []byte{0x00, 0x00, 0x00, 0x00}
58+
59+
const ivVer = "2.5.5" // OpenVPN version compat that we declare to the server
60+
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.
61+
62+
// errMissingHeader indicates that we're missing the four-byte all-zero header.
63+
var errMissingHeader = errors.New("missing four-byte all-zero header")
64+
65+
// errInvalidHeader indicates that the header is not a sequence of four zeroed bytes.
66+
var errInvalidHeader = errors.New("expected four-byte all-zero header")
67+
68+
// errBadControlMessage indicates that a control message cannot be parsed.
69+
var errBadControlMessage = errors.New("cannot parse control message")
70+
71+
// errBadKeyMethod indicates we don't support a key method
72+
var errBadKeyMethod = errors.New("unsupported key method")
73+
74+
// parseControlMessage gets a server control message and returns the value for
75+
// the remote key, the server remote options, and an error indicating if the
76+
// operation could not be completed.
77+
func parseServerControlMessage(message []byte) (*session.KeySource, string, error) {
78+
if len(message) < 4 {
79+
return nil, "", errMissingHeader
80+
}
81+
if !bytes.Equal(message[:4], controlMessageHeader) {
82+
return nil, "", errInvalidHeader
83+
}
84+
// TODO(ainghazal): figure out why 71 here
85+
if len(message) < 71 {
86+
return nil, "", fmt.Errorf("%w: bad len from server:%d", errBadControlMessage, len(message))
87+
}
88+
keyMethod := message[4]
89+
if keyMethod != 2 {
90+
return nil, "", fmt.Errorf("%w: %d", errBadKeyMethod, keyMethod)
91+
92+
}
93+
var random1, random2 [32]byte
94+
// first chunk of random bytes
95+
copy(random1[:], message[5:37])
96+
// second chunk of random bytes
97+
copy(random2[:], message[37:69])
98+
99+
options, err := bytesx.DecodeOptionStringFromBytes(message[69:])
100+
if err != nil {
101+
return nil, "", fmt.Errorf("%w:%s", errBadControlMessage, "bad options string")
102+
}
103+
104+
remoteKey := &session.KeySource{
105+
R1: random1,
106+
R2: random2,
107+
PreMaster: [48]byte{},
108+
}
109+
return remoteKey, options, nil
110+
}
111+
112+
// serverBadAuth indicates that the authentication failed
113+
var serverBadAuth = []byte("AUTH_FAILED")
114+
115+
// serverPushReply is the response for a successful push request
116+
var serverPushReply = []byte("PUSH_REPLY")
117+
118+
// errBadAuth means we could not authenticate
119+
var errBadAuth = errors.New("server says: bad auth")
120+
121+
// errBadServerReply indicates we didn't get one of the few responses we expected
122+
var errBadServerReply = errors.New("bad server reply")
123+
124+
// parseServerPushReply parses the push reply
125+
func parseServerPushReply(logger model.Logger, resp []byte) (*model.TunnelInfo, error) {
126+
// make sure the server's response contains the expected result
127+
if bytes.HasPrefix(resp, serverBadAuth) {
128+
return nil, errBadAuth
129+
}
130+
if !bytes.HasPrefix(resp, serverPushReply) {
131+
return nil, fmt.Errorf("%w:%s", errBadServerReply, "expected push reply")
132+
}
133+
134+
// TODO(bassosimone): consider moving the two functions below in this package
135+
optsMap := model.PushedOptionsAsMap(resp)
136+
logger.Infof("Server pushed options: %v", optsMap)
137+
ti := model.NewTunnelInfoFromPushedOptions(optsMap)
138+
return ti, nil
139+
}

internal/tlssession/doc.go

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package tlssession performs a TLS handshake over the control channel, and then it
2+
// exchanges keys with the server over this secure channel.
3+
package tlssession

internal/tlssession/tlsbio.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package tlssession
2+
3+
import (
4+
"bytes"
5+
"log"
6+
"net"
7+
"sync"
8+
"time"
9+
)
10+
11+
// tlsBio allows to use channels to read and write
12+
type tlsBio struct {
13+
closeOnce sync.Once
14+
directionDown chan<- []byte
15+
directionUp <-chan []byte
16+
hangup chan any
17+
readBuffer *bytes.Buffer
18+
}
19+
20+
// newTLSBio creates a new tlsBio
21+
func newTLSBio(directionUp <-chan []byte, directionDown chan<- []byte) *tlsBio {
22+
return &tlsBio{
23+
closeOnce: sync.Once{},
24+
directionDown: directionDown,
25+
directionUp: directionUp,
26+
hangup: make(chan any),
27+
readBuffer: &bytes.Buffer{},
28+
}
29+
}
30+
31+
func (c *tlsBio) Close() error {
32+
c.closeOnce.Do(func() {
33+
close(c.hangup)
34+
})
35+
return nil
36+
}
37+
38+
func (c *tlsBio) Read(data []byte) (int, error) {
39+
for {
40+
count, _ := c.readBuffer.Read(data)
41+
if count > 0 {
42+
log.Printf("[tlsbio] received %d bytes", len(data))
43+
return count, nil
44+
}
45+
select {
46+
case extra := <-c.directionUp:
47+
c.readBuffer.Write(extra)
48+
case <-c.hangup:
49+
return 0, net.ErrClosed
50+
}
51+
}
52+
}
53+
54+
func (c *tlsBio) Write(data []byte) (int, error) {
55+
log.Printf("[tlsbio] requested to write %d bytes", len(data))
56+
select {
57+
case c.directionDown <- data:
58+
return len(data), nil
59+
case <-c.hangup:
60+
return 0, net.ErrClosed
61+
}
62+
}
63+
64+
func (c *tlsBio) LocalAddr() net.Addr {
65+
return &tlsBioAddr{}
66+
}
67+
68+
func (c *tlsBio) RemoteAddr() net.Addr {
69+
return &tlsBioAddr{}
70+
}
71+
72+
func (c *tlsBio) SetDeadline(t time.Time) error {
73+
return nil
74+
}
75+
76+
func (c *tlsBio) SetReadDeadline(t time.Time) error {
77+
return nil
78+
}
79+
80+
func (c *tlsBio) SetWriteDeadline(t time.Time) error {
81+
return nil
82+
}
83+
84+
// tlsBioAddr is the type of address returned by [Conn]
85+
type tlsBioAddr struct{}
86+
87+
var _ net.Addr = &tlsBioAddr{}
88+
89+
// Network implements net.Addr
90+
func (*tlsBioAddr) Network() string {
91+
return "tlsBioAddr"
92+
}
93+
94+
// String implements net.Addr
95+
func (*tlsBioAddr) String() string {
96+
return "tlsBioAddr"
97+
}

0 commit comments

Comments
 (0)