-
Notifications
You must be signed in to change notification settings - Fork 7
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
Conversation
In order to do that, we refactor the configuration handling. We move options to openvpn options, and we create a config object that is broader in scope. This commit also updates the binary entrypoint to test this functionality. Since I was here, I added the pinger too to exercise the pinger over the tunnel.
f96cb12
to
d4ef98b
Compare
I'm reverting a recent "fix" that was inverting the local-remote session id check - the error in that commit was to change the check to match the description in the comment. In reality, it was the comment that was wrong. The reason why we cannot check remote session id in all packets is because remote-session-id only present if the packet has an ack array. The handshake was broken by the previous change, but I did not catch it because the integration tests were not running for the refactored codebase. Also, this commit enables integration tests for the new internal path to avoid this kind of errors in the future. Some test code was removed, because the script to parse certs etc was written before minivpn could parse inline credentials. not needed anymore.
I've changed the trace marshaling a bit, still playing with readability and redundancy. I think I'll start testing this on the wild. Here's a dump of a valid handshake with the latest in this branch (generated with the binary in #66 ) Right at the beginning, it jumps to the eye that we're not properly initializing the negotiation state before sending the packet. @bassosimone let me know if there's some oddity that you see here: [
{
"operation": "packet_out",
"stage": "UNDEF",
"t": 0.00018644,
"packet": {
"operation": "write",
"opcode": "P_CONTROL_HARD_RESET_CLIENT_V2",
"id": 0,
"acks": null,
"payload_size": 0,
"send_attempts": 1
}
},
{
"operation": "state",
"stage": "PRE_START",
"t": 0.00023592,
"packet": null
},
{
"operation": "state",
"stage": "START",
"t": 0.035379056,
"packet": null
},
{
"operation": "packet_in",
"stage": "START",
"t": 0.035393836,
"packet": {
"operation": "read",
"opcode": "P_CONTROL_HARD_RESET_SERVER_V2",
"id": 0,
"acks": [
0
],
"payload_size": 0,
"send_attempts": null
}
},
{
"operation": "packet_out",
"stage": "START",
"t": 0.035894886,
"packet": {
"operation": "write",
"opcode": "P_CONTROL_V1",
"id": 1,
"acks": [
0
],
"payload_size": 281,
"send_attempts": 1
}
},
{
"operation": "packet_in",
"stage": "START",
"t": 0.069690563,
"packet": {
"operation": "read",
"opcode": "P_ACK_V1",
"id": 0,
"acks": [
1
],
"payload_size": 0,
"send_attempts": null
}
},
{
"operation": "packet_in",
"stage": "START",
"t": 0.069853923,
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 1,
"acks": null,
"payload_size": 1174,
"send_attempts": null
}
},
{
"operation": "packet_in",
"stage": "START",
"t": 0.069863593,
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 2,
"acks": null,
"payload_size": 284,
"send_attempts": null
}
},
{
"operation": "state",
"stage": "SENT_KEY",
"t": 0.071535524,
"packet": null
},
{
"operation": "packet_out",
"stage": "SENT_KEY",
"t": 0.071564284,
"packet": {
"operation": "write",
"opcode": "P_CONTROL_V1",
"id": 2,
"acks": null,
"payload_size": 1036,
"send_attempts": 1
}
},
{
"operation": "packet_out",
"stage": "SENT_KEY",
"t": 0.071570554,
"packet": {
"operation": "write",
"opcode": "P_CONTROL_V1",
"id": 3,
"acks": null,
"payload_size": 296,
"send_attempts": 1
}
},
{
"operation": "packet_in",
"stage": "SENT_KEY",
"t": 0.10563023,
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 3,
"acks": [
2
],
"payload_size": 158,
"send_attempts": null
}
},
{
"operation": "packet_in",
"stage": "SENT_KEY",
"t": 0.10601031,
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 4,
"acks": [
3
],
"payload_size": 200,
"send_attempts": null
}
},
{
"operation": "state",
"stage": "GOT_KEY",
"t": 0.10608311,
"packet": null
},
{
"operation": "packet_out",
"stage": "GOT_KEY",
"t": 0.10612168,
"packet": {
"operation": "write",
"opcode": "P_CONTROL_V1",
"id": 4,
"acks": null,
"payload_size": 35,
"send_attempts": 1
}
},
{
"operation": "packet_in",
"stage": "GOT_KEY",
"t": 0.139239737,
"packet": {
"operation": "read",
"opcode": "P_ACK_V1",
"id": 0,
"acks": [
4
],
"payload_size": 0,
"send_attempts": null
}
},
{
"operation": "packet_in",
"stage": "GOT_KEY",
"t": 0.139274967,
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 5,
"acks": null,
"payload_size": 291,
"send_attempts": null
}
},
{
"operation": "state",
"stage": "ACTIVE",
"t": 0.139348757,
"packet": null
},
{
"operation": "state",
"stage": "GENERATED_KEYS",
"t": 0.139443627,
"packet": null
}
] |
should not return in the presence of malformed packets. we still need to properly log & trace these events, but this is a first defensive reaction to malformed input. - Related: ooni#67
@bassosimone removed all the overlapping changes from #65 and #66 - binary and integration tests can be tested together in the integration branch at https://github.com/ainghazal/minivpn/tree/handshake-trace-integration |
for reference, here's an updated trace (after I changed the initial event timing and added tags as annotations; for client and server hello for now): [
{
"operation": "state",
"stage": "PRE_START",
"t": 0.000493591,
"tags": [],
"packet": null
},
{
"operation": "packet_out",
"stage": "PRE_START",
"t": 0.000507926,
"tags": [],
"packet": {
"operation": "write",
"opcode": "P_CONTROL_HARD_RESET_CLIENT_V2",
"id": 0,
"acks": null,
"payload_size": 0,
"send_attempts": 1
}
},
{
"operation": "state",
"stage": "START",
"t": 0.048686083,
"tags": [],
"packet": null
},
{
"operation": "packet_in",
"stage": "START",
"t": 0.04870451,
"tags": [],
"packet": {
"operation": "read",
"opcode": "P_CONTROL_HARD_RESET_SERVER_V2",
"id": 0,
"acks": [
0
],
"payload_size": 0,
"send_attempts": null
}
},
{
"operation": "packet_out",
"stage": "START",
"t": 0.049519546,
"tags": [
"client_hello"
],
"packet": {
"operation": "write",
"opcode": "P_CONTROL_V1",
"id": 1,
"acks": [
0
],
"payload_size": 281,
"send_attempts": 1
}
},
{
"operation": "packet_in",
"stage": "START",
"t": 0.09883628,
"tags": [],
"packet": {
"operation": "read",
"opcode": "P_ACK_V1",
"id": 0,
"acks": [
1
],
"payload_size": 0,
"send_attempts": null
}
},
{
"operation": "packet_in",
"stage": "START",
"t": 0.099096479,
"tags": [
"server_hello"
],
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 1,
"acks": null,
"payload_size": 1174,
"send_attempts": null
}
},
{
"operation": "packet_in",
"stage": "START",
"t": 0.09935176,
"tags": [],
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 2,
"acks": null,
"payload_size": 1174,
"send_attempts": null
}
},
{
"operation": "packet_in",
"stage": "START",
"t": 0.099410356,
"tags": [],
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 3,
"acks": null,
"payload_size": 36,
"send_attempts": null
}
},
{
"operation": "state",
"stage": "SENT_KEY",
"t": 0.108286602,
"tags": [],
"packet": null
},
{
"operation": "packet_out",
"stage": "SENT_KEY",
"t": 0.108444487,
"tags": [],
"packet": {
"operation": "write",
"opcode": "P_CONTROL_V1",
"id": 2,
"acks": null,
"payload_size": 1284,
"send_attempts": 1
}
},
{
"operation": "packet_out",
"stage": "SENT_KEY",
"t": 0.108468284,
"tags": [],
"packet": {
"operation": "write",
"opcode": "P_CONTROL_V1",
"id": 3,
"acks": null,
"payload_size": 296,
"send_attempts": 1
}
},
{
"operation": "packet_in",
"stage": "SENT_KEY",
"t": 0.155445966,
"tags": [],
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 4,
"acks": [
2
],
"payload_size": 158,
"send_attempts": null
}
},
{
"operation": "packet_in",
"stage": "SENT_KEY",
"t": 0.157441445,
"tags": [],
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 5,
"acks": [
3
],
"payload_size": 228,
"send_attempts": null
}
},
{
"operation": "state",
"stage": "GOT_KEY",
"t": 0.157713868,
"tags": [],
"packet": null
},
{
"operation": "packet_out",
"stage": "GOT_KEY",
"t": 0.157897133,
"tags": [],
"packet": {
"operation": "write",
"opcode": "P_CONTROL_V1",
"id": 4,
"acks": null,
"payload_size": 35,
"send_attempts": 1
}
},
{
"operation": "packet_in",
"stage": "GOT_KEY",
"t": 0.205236791,
"tags": [],
"packet": {
"operation": "read",
"opcode": "P_ACK_V1",
"id": 0,
"acks": [
4
],
"payload_size": 0,
"send_attempts": null
}
},
{
"operation": "packet_in",
"stage": "GOT_KEY",
"t": 0.205351335,
"tags": [],
"packet": {
"operation": "read",
"opcode": "P_CONTROL_V1",
"id": 6,
"acks": null,
"payload_size": 221,
"send_attempts": null
}
},
{
"operation": "state",
"stage": "ACTIVE",
"t": 0.205685896,
"tags": [],
"packet": null
},
{
"operation": "state",
"stage": "GENERATED_KEYS",
"t": 0.205989473,
"tags": [],
"packet": null
}
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's discuss my comments! Thank you!
internal/model/config.go
Outdated
IPAddr string | ||
AddrPort string |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added documentation
internal/model/trace.go
Outdated
func (d Direction) String() string { | ||
switch d { | ||
case DirectionIncoming: | ||
return "recv" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
pkg/tracex/trace.go
Outdated
|
||
stg := session.SessionNegotiationState(stage) | ||
e := newEvent(handshakeEventPacketIn, stg, t.TimeNow(), t.zeroTime) | ||
e.LoggedPacket = logPacket(packet, -1, model.DirectionIncoming) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not super fond of using -1
here. Would it be possible to import and use the proper definitions?
Not using the proper definitions would make it more difficult for consumers, especially if we change the defines. I know it's not likely we'll change them, but I am playing a bit of a what-if+robustness game here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed, moved the creation of optional.Value to the caller
pkg/tracex/trace.go
Outdated
Retries: lp.Retries, | ||
} | ||
return json.Marshal(j) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above: we don't need a custom marshaler, just proper json:"xx"
annotations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the real problem here, to me, is to switch between internal types (like direction) and their string representation. is there any way to force string serialization from the json tag in a more elegant way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nvm, forced the types to string and moved the json tags to the struct
Co-authored-by: Simone Basso <[email protected]>
Co-authored-by: Simone Basso <[email protected]>
Co-authored-by: Simone Basso <[email protected]>
Co-authored-by: Simone Basso <[email protected]>
Co-authored-by: Simone Basso <[email protected]>
Co-authored-by: Simone Basso <[email protected]>
Co-authored-by: Simone Basso <[email protected]>
Co-authored-by: Simone Basso <[email protected]>
ok, I have changed the tracer interface to emit NegotiationsState instead of ints. I had to move the definitions to model to avoid a circular import. one thing I dislike is that there's too much stuff in model - at some point I'd consider putting some useful things (like state) in an importable path like |
Agreed, it's important to note pain points because they're informative on how we can evolve the code. |
Checklist
ARCHITECTURE.md
.Description
In order to do that, we refactor the configuration handling. We move
options to openvpn options, and we create a config object that is
broader in scope.