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

Conversation

ainghazal
Copy link
Collaborator

@ainghazal ainghazal commented Feb 8, 2024

Checklist

  • I have read the contribution guidelines
  • Iff you changed code related to services, or inter-service communication, make sure you update the diagrams in ARCHITECTURE.md.
  • Reference issue for this pull request:

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.

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.
@ainghazal ainghazal mentioned this pull request Feb 8, 2024
3 tasks
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.
@ainghazal
Copy link
Collaborator Author

ainghazal commented Feb 9, 2024

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
  }
]

@ainghazal
Copy link
Collaborator Author

@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

@ainghazal
Copy link
Collaborator Author

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
  }
]

Copy link
Contributor

@bassosimone bassosimone left a 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 Show resolved Hide resolved
Comment on lines 89 to 90
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

internal/model/trace.go Outdated Show resolved Hide resolved
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.

internal/model/trace.go Outdated Show resolved Hide resolved

stg := session.SessionNegotiationState(stage)
e := newEvent(handshakeEventPacketIn, stg, t.TimeNow(), t.zeroTime)
e.LoggedPacket = logPacket(packet, -1, model.DirectionIncoming)
Copy link
Contributor

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.

Copy link
Collaborator Author

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 Show resolved Hide resolved
pkg/tracex/trace.go Outdated Show resolved Hide resolved
Retries: lp.Retries,
}
return json.Marshal(j)
}
Copy link
Contributor

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.

Copy link
Collaborator Author

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?

Copy link
Collaborator Author

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

pkg/tracex/trace.go Show resolved Hide resolved
ainghazal and others added 16 commits February 13, 2024 11:23
Co-authored-by: Simone Basso <bassosimone@gmail.com>
Co-authored-by: Simone Basso <bassosimone@gmail.com>
Co-authored-by: Simone Basso <bassosimone@gmail.com>
Co-authored-by: Simone Basso <bassosimone@gmail.com>
Co-authored-by: Simone Basso <bassosimone@gmail.com>
Co-authored-by: Simone Basso <bassosimone@gmail.com>
Co-authored-by: Simone Basso <bassosimone@gmail.com>
Co-authored-by: Simone Basso <bassosimone@gmail.com>
@ainghazal
Copy link
Collaborator Author

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 pkg/state, but deferring on that for now.

@bassosimone
Copy link
Contributor

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 pkg/state, but deferring on that for now.

Agreed, it's important to note pain points because they're informative on how we can evolve the code.

@ainghazal ainghazal merged commit aa16602 into ooni:main Feb 13, 2024
8 checks passed
@ainghazal ainghazal deleted the feat/handshake-trace branch February 13, 2024 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants