summaryrefslogtreecommitdiffhomepage
path: root/feature/capture/dissector/ts-dissector.lua
blob: c2ee2b755f266f36b0d77416b386a5ee12907672 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
function hasbit(x, p)
  return bit.band(x, p) ~= 0
end

tsdebug_ll = Proto("tsdebug", "Tailscale debug")
PATH = ProtoField.string("tsdebug.PATH","PATH", base.ASCII)
SNAT_IP_4 = ProtoField.ipv4("tsdebug.SNAT_IP_4", "Pre-NAT Source IPv4 address")
SNAT_IP_6 = ProtoField.ipv6("tsdebug.SNAT_IP_6", "Pre-NAT Source IPv6 address")
DNAT_IP_4 = ProtoField.ipv4("tsdebug.DNAT_IP_4", "Pre-NAT Dest IPv4 address")
DNAT_IP_6 = ProtoField.ipv6("tsdebug.DNAT_IP_6", "Pre-NAT Dest IPv6 address")
tsdebug_ll.fields = {PATH, SNAT_IP_4, SNAT_IP_6, DNAT_IP_4, DNAT_IP_6}

function tsdebug_ll.dissector(buffer, pinfo, tree)
    pinfo.cols.protocol = tsdebug_ll.name
    packet_length = buffer:len()
    local offset = 0
    local subtree = tree:add(tsdebug_ll, buffer(), "Tailscale packet")

    -- -- Get path UINT16
    local path_id = buffer:range(offset, 2):le_uint()
    if     path_id == 0   then subtree:add(PATH, "FromLocal")
    elseif path_id == 1   then subtree:add(PATH, "FromPeer")
    elseif path_id == 2   then subtree:add(PATH, "Synthesized (Inbound / ToLocal)")
    elseif path_id == 3   then subtree:add(PATH, "Synthesized (Outbound / ToPeer)")
    elseif path_id == 254 then subtree:add(PATH, "Disco frame")
    end
    offset = offset + 2

    -- -- Get SNAT address
    local snat_addr_len = buffer:range(offset, 1):le_uint()
    if     snat_addr_len == 4 then subtree:add(SNAT_IP_4, buffer:range(offset + 1, snat_addr_len))
    elseif snat_addr_len > 0  then subtree:add(SNAT_IP_6, buffer:range(offset + 1, snat_addr_len))
    end
    offset = offset + 1 + snat_addr_len

    -- -- Get DNAT address
    local dnat_addr_len = buffer:range(offset, 1):le_uint()
    if     dnat_addr_len == 4 then subtree:add(DNAT_IP_4, buffer:range(offset + 1, dnat_addr_len))
    elseif dnat_addr_len > 0  then subtree:add(DNAT_IP_6, buffer:range(offset + 1, dnat_addr_len))
    end
    offset = offset + 1 + dnat_addr_len

    -- -- Handover rest of data to lower-level dissector
    local data_buffer = buffer:range(offset, packet_length-offset):tvb()
    if path_id == 254 then
        Dissector.get("tsdisco"):call(data_buffer, pinfo, tree)
    else
        Dissector.get("ip"):call(data_buffer, pinfo, tree)
    end
end

-- Install the dissector on link-layer ID 147 (User-defined protocol 0)
local eth_table = DissectorTable.get("wtap_encap")
eth_table:add(wtap.USER0, tsdebug_ll)


local ts_dissectors = DissectorTable.new("ts.proto", "Tailscale-specific dissectors", ftypes.STRING, base.NONE)


--
-- DISCO metadata dissector
--
tsdisco_meta = Proto("tsdisco", "Tailscale DISCO metadata")
DISCO_IS_DERP = ProtoField.bool("tsdisco.IS_DERP","From DERP")
DISCO_SRC_IP_4 = ProtoField.ipv4("tsdisco.SRC_IP_4", "Source IPv4 address")
DISCO_SRC_IP_6 = ProtoField.ipv6("tsdisco.SRC_IP_6", "Source IPv6 address")
DISCO_SRC_PORT = ProtoField.uint16("tsdisco.SRC_PORT","Source port", base.DEC)
DISCO_DERP_PUB = ProtoField.bytes("tsdisco.DERP_PUB", "DERP public key", base.SPACE)
tsdisco_meta.fields = {DISCO_IS_DERP, DISCO_SRC_PORT, DISCO_DERP_PUB, DISCO_SRC_IP_4, DISCO_SRC_IP_6}

function tsdisco_meta.dissector(buffer, pinfo, tree)
    pinfo.cols.protocol = tsdisco_meta.name
    packet_length = buffer:len()
    local offset = 0
    local subtree = tree:add(tsdisco_meta, buffer(), "DISCO metadata")

    -- Parse flags
    local from_derp = hasbit(buffer(offset, 1):le_uint(), 0)
    subtree:add(DISCO_IS_DERP, from_derp) -- Flag bit 0
    offset = offset + 1
    -- Parse DERP public key
    if from_derp then
        subtree:add(DISCO_DERP_PUB, buffer(offset, 32))
    end
    offset = offset + 32

    -- Parse source port
    subtree:add(DISCO_SRC_PORT, buffer:range(offset, 2):le_uint())
    offset = offset + 2

    -- Parse source address
    local addr_len = buffer:range(offset, 2):le_uint()
    offset = offset + 2
    if addr_len == 4 then subtree:add(DISCO_SRC_IP_4, buffer:range(offset, addr_len))
    else subtree:add(DISCO_SRC_IP_6, buffer:range(offset, addr_len))
    end
    offset = offset + addr_len

    -- Handover to the actual disco frame dissector
    offset = offset + 2 -- skip over payload len
    local data_buffer = buffer:range(offset, packet_length-offset):tvb()
    Dissector.get("disco"):call(data_buffer, pinfo, tree)
end

ts_dissectors:add(1, tsdisco_meta)

--
-- DISCO frame dissector
--
tsdisco_frame = Proto("disco", "Tailscale DISCO frame")
DISCO_TYPE = ProtoField.string("disco.TYPE", "Message type", base.ASCII)
DISCO_VERSION = ProtoField.uint8("disco.VERSION","Protocol version", base.DEC)
DISCO_TXID = ProtoField.bytes("disco.TXID", "Transaction ID", base.SPACE)
DISCO_NODEKEY = ProtoField.bytes("disco.NODE_KEY", "Node key", base.SPACE)
DISCO_PONG_SRC = ProtoField.ipv6("disco.PONG_SRC", "Pong source")
DISCO_PONG_SRC_PORT = ProtoField.uint16("disco.PONG_SRC_PORT","Source port", base.DEC)
DISCO_UNKNOWN = ProtoField.bytes("disco.UNKNOWN_DATA", "Trailing data", base.SPACE)
tsdisco_frame.fields = {DISCO_TYPE, DISCO_VERSION, DISCO_TXID, DISCO_NODEKEY, DISCO_PONG_SRC, DISCO_PONG_SRC_PORT, DISCO_UNKNOWN}

function tsdisco_frame.dissector(buffer, pinfo, tree)
    packet_length = buffer:len()
    local offset = 0
    local subtree = tree:add(tsdisco_frame, buffer(), "DISCO frame")

    -- Message type
    local message_type = buffer(offset, 1):le_uint()
    offset = offset + 1
    if     message_type == 1 then subtree:add(DISCO_TYPE, "Ping")
    elseif message_type == 2 then subtree:add(DISCO_TYPE, "Pong")
    elseif message_type == 3 then subtree:add(DISCO_TYPE, "Call me maybe")
    elseif message_type == 4 then subtree:add(DISCO_TYPE, "Bind UDP Relay Endpoint")
    elseif message_type == 5 then subtree:add(DISCO_TYPE, "Bind UDP Relay Endpoint Challenge")
    elseif message_type == 6 then subtree:add(DISCO_TYPE, "Bind UDP Relay Endpoint Answer")
    elseif message_type == 7 then subtree:add(DISCO_TYPE, "Call me maybe via")
    end

    -- Message version
    local message_version = buffer(offset, 1):le_uint()
    offset = offset + 1
    subtree:add(DISCO_VERSION, message_version)

    -- TXID (Ping / Pong)
    if message_type == 1 or message_type == 2 then
        subtree:add(DISCO_TXID, buffer(offset, 12))
        offset = offset + 12
    end

    -- NodeKey (Ping)
    if message_type == 1 then
        subtree:add(DISCO_NODEKEY, buffer(offset, 32))
        offset = offset + 32
    end

    -- Src (Pong)
    if message_type == 2 then
        subtree:add(DISCO_PONG_SRC, buffer:range(offset, 16))
        offset = offset + 16
    end
    -- Src port (Pong)
    if message_type == 2 then
        subtree:add(DISCO_PONG_SRC_PORT, buffer(offset, 2):le_uint())
        offset = offset + 2
    end

    -- TODO(tom): Parse CallMeMaybe.MyNumber

    local trailing = buffer:range(offset, packet_length-offset)
    if trailing:len() > 0 then
        subtree:add(DISCO_UNKNOWN, trailing)
    end
end

ts_dissectors:add(2, tsdisco_frame)