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

feat: add ability to trace handshake #64

Merged
merged 39 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8fe4e52
feat: add ability to trace handshake
ainghazal Feb 7, 2024
d4ef98b
remove binary
ainghazal Feb 8, 2024
4a2404a
comments after review
ainghazal Feb 8, 2024
9b96d65
tests: update tests after config refactor
ainghazal Feb 9, 2024
420c9f8
add documentation to package
ainghazal Feb 9, 2024
4a06e7e
typo
ainghazal Feb 9, 2024
81232cc
bug: fix incoming session check
ainghazal Feb 8, 2024
e8eaeaf
make tracer public
ainghazal Feb 9, 2024
6bd3583
iterate on tracer
ainghazal Feb 9, 2024
79c9cf0
x
ainghazal Feb 9, 2024
b2fcbfd
trace outgoing hard reset too
ainghazal Feb 9, 2024
8e6e391
change state before sending hard reset packet
ainghazal Feb 10, 2024
c36b7f1
annotate tls client hello & server hello
ainghazal Feb 11, 2024
5ac4cde
bug: continue if received malformed input
ainghazal Feb 11, 2024
fef4a84
remove todo
ainghazal Feb 11, 2024
93331de
remove on handshake done from interface
ainghazal Feb 11, 2024
3233f5a
revert binary to main
ainghazal Feb 11, 2024
9e245c9
remove binary from this branch
ainghazal Feb 11, 2024
39a9c9a
revert changes to pinger
ainghazal Feb 11, 2024
882725b
revert integration tests, they are on their own pr
ainghazal Feb 11, 2024
866cdb4
delete main.go
ainghazal Feb 11, 2024
90e333a
document public methods
ainghazal Feb 11, 2024
a3dba34
linter
ainghazal Feb 11, 2024
b37696b
Update internal/model/config.go
ainghazal Feb 13, 2024
d7a931b
Update internal/model/config.go
ainghazal Feb 13, 2024
a149648
Update internal/model/trace.go
ainghazal Feb 13, 2024
dd3d3e8
document remote fields
ainghazal Feb 13, 2024
c05ba96
fix direction serialization
ainghazal Feb 13, 2024
514adf2
remove marshaling, cast fields into strings
ainghazal Feb 13, 2024
46fb370
grammar
ainghazal Feb 13, 2024
66762e0
minor fixes
ainghazal Feb 13, 2024
07bcd61
Update internal/optional/optional.go
ainghazal Feb 13, 2024
dc5b90d
Update internal/packetmuxer/service.go
ainghazal Feb 13, 2024
06b7488
Update internal/packetmuxer/service.go
ainghazal Feb 13, 2024
c4fc44a
Update internal/reliabletransport/receiver.go
ainghazal Feb 13, 2024
23539c7
Update pkg/tracex/trace.go
ainghazal Feb 13, 2024
e9c54b3
minor fixes
ainghazal Feb 13, 2024
5c1f86a
move comment out of the struct
ainghazal Feb 13, 2024
b16e276
move session negotiation states to model to avoid circular import
ainghazal Feb 13, 2024
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
12 changes: 0 additions & 12 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,3 @@ jobs:
go-version: '1.20'
- name: Ensure coverage threshold
run: make test-coverage-threshold

integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: setup go
uses: actions/setup-go@v2
with:
go-version: '1.20'
- name: run integration tests
run: go test -v ./tests/integration

6 changes: 4 additions & 2 deletions internal/controlchannel/controlchannel.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package controlchannel implements the control channel logic. The control channel sits
// above the reliable transport and below the TLS layer.
package controlchannel

import (
Expand Down Expand Up @@ -36,12 +38,12 @@ type Service struct {
//
// [ARCHITECTURE]: https://github.com/ooni/minivpn/blob/main/ARCHITECTURE.md
func (svc *Service) StartWorkers(
logger model.Logger,
config *model.Config,
workersManager *workers.Manager,
sessionManager *session.Manager,
) {
ws := &workersState{
logger: logger,
logger: config.Logger(),
notifyTLS: *svc.NotifyTLS,
controlToReliable: *svc.ControlToReliable,
reliableToControl: svc.ReliableToControl,
Expand Down
4 changes: 2 additions & 2 deletions internal/datachannel/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type dataChannelHandler interface {
// DataChannel represents the data "channel", that will encrypt and decrypt the tunnel payloads.
// data implements the dataHandler interface.
type DataChannel struct {
options *model.Options
options *model.OpenVPNOptions
sessionManager *session.Manager
state *dataChannelState
decodeFn func(model.Logger, []byte, *session.Manager, *dataChannelState) (*encryptedData, error)
Expand All @@ -39,7 +39,7 @@ var _ dataChannelHandler = &DataChannel{} // Ensure that we implement dataChanne
// NewDataChannelFromOptions returns a new data object, initialized with the
// options given. it also returns any error raised.
func NewDataChannelFromOptions(log model.Logger,
opt *model.Options,
opt *model.OpenVPNOptions,
sessionManager *session.Manager) (*DataChannel, error) {
runtimex.Assert(opt != nil, "openvpn datachannel: opts cannot be nil")
runtimex.Assert(opt != nil, "openvpn datachannel: opts cannot be nil")
Expand Down
2 changes: 1 addition & 1 deletion internal/datachannel/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func decodeEncryptedPayloadNonAEAD(log model.Logger, buf []byte, session *sessio
// modes are supported at the moment, so no real decompression is done. It
// returns a byte array, and an error if the operation could not be completed
// successfully.
func maybeDecompress(b []byte, st *dataChannelState, opt *model.Options) ([]byte, error) {
func maybeDecompress(b []byte, st *dataChannelState, opt *model.OpenVPNOptions) ([]byte, error) {
if st == nil || st.dataCipher == nil {
return []byte{}, fmt.Errorf("%w:%s", errBadInput, "bad state")
}
Expand Down
9 changes: 4 additions & 5 deletions internal/datachannel/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,21 @@ type Service struct {
// 3. keyWorker BLOCKS on keyUp to read a dataChannelKey and
// initializes the internal state with the resulting key;
func (s *Service) StartWorkers(
logger model.Logger,
config *model.Config,
workersManager *workers.Manager,
sessionManager *session.Manager,
options *model.Options,
) {
dc, err := NewDataChannelFromOptions(logger, options, sessionManager)
dc, err := NewDataChannelFromOptions(config.Logger(), config.OpenVPNOptions(), sessionManager)
if err != nil {
logger.Warnf("cannot initialize channel %v", err)
config.Logger().Warnf("cannot initialize channel %v", err)
return
}
ws := &workersState{
dataChannel: dc,
dataOrControlToMuxer: *s.DataOrControlToMuxer,
dataToTUN: s.DataToTUN,
keyReady: s.KeyReady,
logger: logger,
logger: config.Logger(),
muxerToData: s.MuxerToData,
sessionManager: sessionManager,
tunToData: s.TUNToData,
Expand Down
92 changes: 92 additions & 0 deletions internal/model/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package model

import (
"net"

"github.com/apex/log"
"github.com/ooni/minivpn/internal/runtimex"
)

// Config contains options to initialize the OpenVPN tunnel.
type Config struct {
// openVPNOptions contains options related to openvpn.
openvpnOptions *OpenVPNOptions

// logger will be used to log events.
logger Logger

// if a tracer is provided, it will be used to trace the openvpn handshake.
tracer HandshakeTracer
}

// NewConfig returns a Config ready to intialize a vpn tunnel.
func NewConfig(options ...Option) *Config {
cfg := &Config{
openvpnOptions: &OpenVPNOptions{},
logger: log.Log,
tracer: &dummyTracer{},
}
for _, opt := range options {
opt(cfg)
}
return cfg
}

// Option is an option you can pass to initialize minivpn.
type Option func(config *Config)

// WithConfigFile configures OpenVPNOptions parsed from the given file.
func WithConfigFile(configPath string) Option {
return func(config *Config) {
openvpnOpts, err := ReadConfigFile(configPath)
runtimex.PanicOnError(err, "cannot parse config file")
runtimex.PanicIfFalse(openvpnOpts.HasAuthInfo(), "missing auth info")
config.openvpnOptions = openvpnOpts
}

ainghazal marked this conversation as resolved.
Show resolved Hide resolved
}

// WithLogger configures the passed [Logger].
func WithLogger(logger Logger) Option {
return func(config *Config) {
config.logger = logger
}
}

// WithHandshakeTracer configures the passed [HandshakeTracer].
func WithHandshakeTracer(tracer HandshakeTracer) Option {
return func(config *Config) {
config.tracer = tracer
}
}

// Logger returns the configured logger.
func (c *Config) Logger() Logger {
return c.logger
}

// OpenVPNOptions returns the configured openvpn options.
func (c *Config) OpenVPNOptions() *OpenVPNOptions {
return c.openvpnOptions
}

// Remote returns the openvpn remote.
func (c *Config) Remote() *Remote {
return &Remote{
IPAddr: c.openvpnOptions.Remote,
AddrPort: net.JoinHostPort(c.openvpnOptions.Remote, c.openvpnOptions.Port),
Protocol: c.openvpnOptions.Proto.String(),
}
}

// Tracer returns the handshake tracer.
func (c *Config) Tracer() HandshakeTracer {
return c.tracer
}

// Remote has info about the OpenVPNRemote.
ainghazal marked this conversation as resolved.
Show resolved Hide resolved
type Remote struct {
IPAddr string
AddrPort string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two fields are a bit obscure to me? One of them is the address and the other the endpoint? Maybe call the second one endpoint? Maybe document each field? None of them is super obvious to me. What is protocol? "tcp" or "udp"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one is only the ip address, the other (endpoint) includes the port. proto is either tcp or udp, correct. I'm going to add comments to those.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added documentation

Protocol string
}
7 changes: 1 addition & 6 deletions internal/model/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

// OpenVPN packets opcodes.
const (
P_CONTROL_HARD_RESET_CLIENT_V1 = Opcode(iota + 1) // 1

Check warning on line 24 in internal/model/packet.go

View workflow job for this annotation

GitHub Actions / lint

don't use ALL_CAPS in Go names; use CamelCase
P_CONTROL_HARD_RESET_SERVER_V1 // 2

Check warning on line 25 in internal/model/packet.go

View workflow job for this annotation

GitHub Actions / lint

don't use ALL_CAPS in Go names; use CamelCase
P_CONTROL_SOFT_RESET_V1 // 3
P_CONTROL_V1 // 4
P_ACK_V1 // 5
Expand Down Expand Up @@ -334,13 +334,8 @@
return p.Opcode.IsData()
}

const (
DirectionIncoming = iota
DirectionOutgoing
)

// Log writes an entry in the passed logger with a representation of this packet.
func (p *Packet) Log(logger Logger, direction int) {
func (p *Packet) Log(logger Logger, direction Direction) {
var dir string
switch direction {
case DirectionIncoming:
Expand Down
75 changes: 75 additions & 0 deletions internal/model/trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package model

import (
"fmt"
"time"
)

// HandshakeTracer allows to collect traces for a given OpenVPN handshake. A HandshakeTracer can be optionally
// added to the top-level TUN constructor, and it will be propagated to any layer that needs to register an event.
type HandshakeTracer interface {
// TimeNow allows to inject time for deterministic tests.
TimeNow() time.Time

// OnStateChange is called for each transition in the state machine.
OnStateChange(state int)

// OnIncomingPacket is called when a packet is received.
OnIncomingPacket(packet *Packet, stage int)

// OnOutgoingPacket is called when a packet is about to be sent.
OnOutgoingPacket(packet *Packet, stage int, retries int)

// OnDroppedPacket is called whenever a packet is dropped (in/out)
OnDroppedPacket(direction Direction, stage int, packet *Packet)
}

// Direction is one of two directions on a packet.
type Direction int

const (
// DirectionIncoming marks received packets.
DirectionIncoming = iota
ainghazal marked this conversation as resolved.
Show resolved Hide resolved

// DirectionOutgoing marks packets to be sent.
DirectionOutgoing
)

var _ fmt.Stringer = Direction(0)

// String implements fmt.Stringer
func (d Direction) String() string {
switch d {
case DirectionIncoming:
return "recv"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more clear if DirectionIncoming mapped to "incoming", but maybe there is a reason by which you are using "recv" here that I am missing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incoming and outgoing is more clear here indeed. I was thinking about what would be serialized in the trace. first I thought receive/send, and later read/write.

beyond any choice, I think the important problem is where the serialization happens. Probably we can just document the integer and let any user-facing string be the responsibility of the tracer.

case 1:
ainghazal marked this conversation as resolved.
Show resolved Hide resolved
return "send"
default:
return "undefined"
}
}

// dummyTracer is a no-op implementation of [model.HandshakeTracer] that does nothing
// but can be safely passed as a default implementation.
type dummyTracer struct{}

// TimeNow allows to manipulate time for deterministic tests.
func (dt *dummyTracer) TimeNow() time.Time { return time.Now() }

// OnStateChange is called for each transition in the state machine.
func (dt *dummyTracer) OnStateChange(state int) {}

Check warning on line 60 in internal/model/trace.go

View workflow job for this annotation

GitHub Actions / lint

parameter 'state' seems to be unused, consider removing or renaming it as _
ainghazal marked this conversation as resolved.
Show resolved Hide resolved

// OnIncomingPacket is called when a packet is received.
func (dt *dummyTracer) OnIncomingPacket(*Packet, int) {}

// OnOutgoingPacket is called when a packet is about to be sent.
func (dt *dummyTracer) OnOutgoingPacket(*Packet, int, int) {}

// OnDroppedPacket is called whenever a packet is dropped (in/out)
func (dt *dummyTracer) OnDroppedPacket(Direction, int, *Packet) {
}
ainghazal marked this conversation as resolved.
Show resolved Hide resolved

func (dt *dummyTracer) OnHandshakeDone(remoteAddr string) {}

Check warning on line 72 in internal/model/trace.go

View workflow job for this annotation

GitHub Actions / lint

parameter 'remoteAddr' seems to be unused, consider removing or renaming it as _
ainghazal marked this conversation as resolved.
Show resolved Hide resolved

// Assert that dummyTracer implements [model.HandshakeTracer].
var _ HandshakeTracer = &dummyTracer{}
Loading
Loading