carcal 0.2.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
carcal/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """carcal — a terminal packet analyzer. See `carcal --help`."""
carcal/__main__.py ADDED
@@ -0,0 +1,41 @@
1
+ """Entry point for the `carcal` console script."""
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _BUNDLE = Path(__file__).resolve().parent / "_bundle"
7
+
8
+
9
+ def _layout():
10
+ """(executable, protos, grammars) — the Windows bundle is flat, the others
11
+ are bin/ + lib/ + share/."""
12
+ win = _BUNDLE / "carcal.exe"
13
+ if win.exists():
14
+ return win, _BUNDLE / "protos", _BUNDLE / "grammars"
15
+ share = _BUNDLE / "share" / "carcal"
16
+ return _BUNDLE / "bin" / "carcal", share / "protos", share / "grammars"
17
+
18
+
19
+ def main() -> int:
20
+ exe, protos, grammars = _layout()
21
+ if not exe.exists():
22
+ sys.exit(f"carcal: bundled binary is missing from {_BUNDLE} "
23
+ f"(broken install — try reinstalling)")
24
+
25
+ env = dict(os.environ)
26
+ # setdefault, not assignment: an explicit CARCAL_PROTOS_DIR is how a user
27
+ # points carcal at their own .posa decoders, and it must still win.
28
+ env.setdefault("CARCAL_PROTOS_DIR", str(protos))
29
+ env.setdefault("CARCAL_GRAMMARS_DIR", str(grammars))
30
+
31
+ argv = [str(exe), *sys.argv[1:]]
32
+ if os.name == "nt":
33
+ # execve on Windows returns control to the shell immediately, which
34
+ # would hand a live TUI a prompt it is fighting for; spawn and wait.
35
+ import subprocess
36
+ return subprocess.run(argv, env=env).returncode
37
+ os.execve(exe, argv, env) # no return
38
+
39
+
40
+ if __name__ == "__main__":
41
+ sys.exit(main())
Binary file
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "posa",
3
+ "scopeName": "source.posa",
4
+ "fileTypes": ["posa"],
5
+ "patterns": [
6
+ { "name": "comment.line.number-sign.posa", "match": "#.*$" },
7
+ { "name": "keyword.other.binding.posa", "match": "^\\s*(rule|color)\\b" },
8
+ { "name": "keyword.control.posa",
9
+ "match": "\\b(Object|protocol|end|default|required|optional|when|else|scope|repeat|until|as|label|bits|seek|layer|include|info|col|abbrev|mask|hex)\\b" },
10
+ { "name": "storage.type.posa",
11
+ "match": "\\b(uint8|uint16|uint24|uint32|uint64|le_uint16|le_uint32|le_uint64|mac|ip4|ip6|cstring|string|payload|dnsname)\\b" },
12
+ { "name": "storage.type.posa", "match": "\\b(bytes|str|utf16)(?=[<\\[])" },
13
+ { "name": "entity.name.type.parent.posa", "match": "<[A-Za-z0-9_]+>" },
14
+ { "name": "string.quoted.double.posa", "match": "\"(\\\\.|[^\"\\\\])*\"" },
15
+ { "name": "keyword.operator.posa", "match": "(==|!=|<=|>=|=>|<|>|=|&)" },
16
+ { "name": "constant.numeric.hex.posa", "match": "\\b0[xX][0-9a-fA-F]+\\b" },
17
+ { "name": "constant.numeric.posa", "match": "\\b[0-9]+\\b" },
18
+ { "name": "variable.other.field.posa", "match": "\\b[a-z_][a-z0-9_]*\\b" }
19
+ ]
20
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,114 @@
1
+ # dhcp.posa — DHCP / BOOTP (RFC 2131, 2132).
2
+ #
3
+ # A fixed BOOTP header, then a magic cookie, then options: a stream of
4
+ # type-length-value records with no count in front of them. That is
5
+ #
6
+ # repeat until end as option records until the packet runs out
7
+ # scope opt_len each option's value is opt_len bytes, and the
8
+ # scope ends there whether we decoded it or not —
9
+ # so an option this file has never heard of still
10
+ # leaves the walk on the right byte
11
+ # when opt_code == 53: the ones worth naming get named…
12
+ # else: …and the rest still show their bytes
13
+ #
14
+ # Pad (0) and End (255) carry no length byte, hence the `when opt_code > 0` /
15
+ # `when opt_code < 255` nesting around the length.
16
+
17
+ Object<main> DHCP
18
+ abbrev "dhcp"
19
+ col "DHCP"
20
+
21
+ required uint8 op "Message Type"
22
+ Boot Request = 1
23
+ Boot Reply = 2
24
+ required uint8 htype "Hardware Type"
25
+ Ethernet = 1
26
+ required uint8 hlen "Hardware Address Length"
27
+ required uint8 hops "Hops"
28
+ required uint32 xid hex "Transaction ID"
29
+ required uint16 secs "Seconds Elapsed"
30
+ required uint16 flags hex "Flags"
31
+ bits flags broadcast 15 1 "Broadcast"
32
+ required ip4 ciaddr "Client IP Address"
33
+ required ip4 yiaddr "Your (client) IP Address"
34
+ required ip4 siaddr "Next Server IP Address"
35
+ required ip4 giaddr "Relay Agent IP Address"
36
+ required mac chaddr "Client MAC Address"
37
+ required bytes<10> chaddr_pad "Client Hardware Address Padding"
38
+ required bytes<64> sname "Server Host Name"
39
+ required bytes<128> file "Boot File Name"
40
+ required uint32 cookie hex "Magic Cookie"
41
+
42
+ repeat until end as option "Options"
43
+ label "%s: %s%s%s" opt_code, msg_type, opt_ip, opt_text
44
+ required uint8 opt_code "Option"
45
+ Pad = 0
46
+ Subnet Mask = 1
47
+ Router = 3
48
+ Domain Name Server = 6
49
+ Host Name = 12
50
+ Domain Name = 15
51
+ Requested IP Address = 50
52
+ IP Address Lease Time = 51
53
+ DHCP Message Type = 53
54
+ Server Identifier = 54
55
+ Parameter Request List = 55
56
+ Maximum DHCP Message Size = 57
57
+ Renewal Time Value = 58
58
+ Rebinding Time Value = 59
59
+ Vendor Class Identifier = 60
60
+ Client Identifier = 61
61
+ End = 255
62
+ when opt_code > 0:
63
+ when opt_code < 255:
64
+ required uint8 opt_len "Length"
65
+ scope opt_len
66
+ when opt_code == 53:
67
+ required uint8 msg_type "DHCP Message Type"
68
+ Discover = 1
69
+ Offer = 2
70
+ Request = 3
71
+ Decline = 4
72
+ ACK = 5
73
+ NAK = 6
74
+ Release = 7
75
+ Inform = 8
76
+ when opt_code == 1:
77
+ required ip4 opt_ip "Subnet Mask"
78
+ when opt_code == 3:
79
+ required ip4 opt_ip "Router"
80
+ when opt_code == 6:
81
+ repeat until end as dns_server
82
+ label "DNS Server: %s" opt_ip
83
+ required ip4 opt_ip "Domain Name Server"
84
+ when opt_code == 50:
85
+ required ip4 opt_ip "Requested IP Address"
86
+ when opt_code == 54:
87
+ required ip4 opt_ip "Server Identifier"
88
+ when opt_code == 51:
89
+ required uint32 lease_time "IP Address Lease Time"
90
+ when opt_code == 12:
91
+ required str[opt_len] opt_text "Host Name"
92
+ when opt_code == 15:
93
+ required str[opt_len] opt_text "Domain Name"
94
+ when opt_code == 60:
95
+ required str[opt_len] opt_text "Vendor Class Identifier"
96
+ when opt_code == 55:
97
+ repeat until end as requested
98
+ label "Requested: %s" param
99
+ required uint8 param "Parameter"
100
+ Subnet Mask = 1
101
+ Router = 3
102
+ Domain Name Server = 6
103
+ Host Name = 12
104
+ Domain Name = 15
105
+ Broadcast Address = 28
106
+ else:
107
+ required payload opt_value "Value"
108
+
109
+ info "DHCP %s - Transaction ID 0x%x" msg_type, xid
110
+
111
+ rule udp.port == 67 => DHCP
112
+ rule udp.port == 68 => DHCP
113
+
114
+ color dhcp => black lightblue
@@ -0,0 +1,176 @@
1
+ # dns.posa — DNS / mDNS / LLMNR (RFC 1035, RFC 6762), the whole message.
2
+ #
3
+ # This is the decoder carcal uses for ports 53, 5353 and 5355 — there is no C
4
+ # dissector behind it. Read it as the spec: a header, then four sections of
5
+ # records, each section repeated as many times as its counter says.
6
+ #
7
+ # The pieces of posa that make a record protocol expressible:
8
+ #
9
+ # repeat <count> as <item> one subtree per record, count taken from a field
10
+ # repeat until end as <item> records until the enclosing scope runs out
11
+ # label "..." a, b how to title a record's subtree, from its fields
12
+ # dnsname a name, following 0xc0 compression pointers
13
+ # bits <src> <n> <sh> <w> a bitfield carved out of an already-parsed field
14
+ # scope <lenfield> bound the next fields to <lenfield> bytes (rdata)
15
+ # include <Object> inline another object's fields (the RR body,
16
+ # shared by the answer/authority/additional sections)
17
+ #
18
+ # To change what you see: edit this file (Analyze ▸ Decoders ▸ DNS ▸ Enter) and
19
+ # save — no rebuild. Add an RR type by adding a `when type == N:` block.
20
+
21
+ # ── A resource record ────────────────────────────────────────────────────────
22
+ # Name, type, class, TTL, then rdlength bytes of type-dependent data. `scope
23
+ # rdlength` bounds the rdata so an unknown type still leaves the walk on the
24
+ # right byte — the scope ends where rdlength says, decoded or not.
25
+ #
26
+ # Defined first because the three record sections below `include` it.
27
+ Object<main> RR
28
+ abbrev "dns"
29
+
30
+ required dnsname name "Name"
31
+ required uint16 type "Type"
32
+ A = 1
33
+ NS = 2
34
+ CNAME = 5
35
+ SOA = 6
36
+ PTR = 12
37
+ HINFO = 13
38
+ MX = 15
39
+ TXT = 16
40
+ AAAA = 28
41
+ SRV = 33
42
+ OPT = 41
43
+ NSEC = 47
44
+ HTTPS = 65
45
+ required uint16 class mask 0x7fff "Class"
46
+ IN = 1
47
+ CH = 3
48
+ ANY = 255
49
+ # mDNS reuses the top class bit to mean "flush your cache for this name"
50
+ bits class flush 15 1 "Cache flush"
51
+ required uint32 ttl "Time to live"
52
+ required uint16 rdlength "Data length"
53
+
54
+ scope rdlength
55
+ when type == 1:
56
+ required ip4 rdata "Address"
57
+ when type == 28:
58
+ required ip6 rdata "Address"
59
+ when type == 12:
60
+ required dnsname rdata "Domain name"
61
+ when type == 5:
62
+ required dnsname rdata "CNAME"
63
+ when type == 2:
64
+ required dnsname rdata "Name server"
65
+ when type == 15:
66
+ required uint16 preference "Preference"
67
+ required dnsname rdata "Mail exchange"
68
+ when type == 33:
69
+ required uint16 priority "Priority"
70
+ required uint16 weight "Weight"
71
+ required uint16 port "Port"
72
+ required dnsname rdata "Target"
73
+ when type == 6:
74
+ required dnsname rdata "Primary name server"
75
+ required dnsname rname "Responsible mailbox"
76
+ required uint32 serial "Serial"
77
+ required uint32 refresh "Refresh"
78
+ required uint32 retry "Retry"
79
+ required uint32 expire "Expire"
80
+ required uint32 minimum "Minimum TTL"
81
+ # TXT is a run of length-prefixed strings — "model=J96AP", "osxvers=21", …
82
+ # There is no count for them, so repeat until the rdata scope runs out.
83
+ when type == 16:
84
+ repeat until end as txt
85
+ label "%s" rdata
86
+ required uint8 txtlen "Length"
87
+ required str[txtlen] rdata "Text"
88
+ when type == 41:
89
+ required payload rdata "Options"
90
+ when type == 47:
91
+ required dnsname rdata "Next domain name"
92
+
93
+ Object<main> DNS
94
+ abbrev "dns"
95
+ col "DNS"
96
+
97
+ required uint16 id "Transaction ID"
98
+ required uint16 flags "Flags"
99
+ bits flags qr 15 1 "Response"
100
+ Query = 0
101
+ Response = 1
102
+ bits flags opcode 11 4 "Opcode"
103
+ QUERY = 0
104
+ IQUERY = 1
105
+ STATUS = 2
106
+ NOTIFY = 4
107
+ UPDATE = 5
108
+ bits flags aa 10 1 "Authoritative"
109
+ bits flags tc 9 1 "Truncated"
110
+ bits flags rd 8 1 "Recursion desired"
111
+ bits flags ra 7 1 "Recursion available"
112
+ bits flags rcode 0 4 "Reply code"
113
+ NoError = 0
114
+ FormErr = 1
115
+ ServFail = 2
116
+ NXDomain = 3
117
+ NotImp = 4
118
+ Refused = 5
119
+ required uint16 questions "Questions"
120
+ required uint16 answers "Answer RRs"
121
+ required uint16 authority "Authority RRs"
122
+ required uint16 additional "Additional RRs"
123
+
124
+ # ── Queries ───────────────────────────────────────────────────────────────
125
+ # A question is a name, a type, and a class. In mDNS the top class bit is the
126
+ # "QU" bit (send the reply unicast), so the class is masked and the bit shown
127
+ # on its own — that is what `mask` and `bits` are for.
128
+ repeat questions as query "Queries"
129
+ label "%s: type %s, class %s" qname, qtype, qclass
130
+ required dnsname qname "Name"
131
+ required uint16 qtype "Type"
132
+ A = 1
133
+ NS = 2
134
+ CNAME = 5
135
+ SOA = 6
136
+ PTR = 12
137
+ HINFO = 13
138
+ MX = 15
139
+ TXT = 16
140
+ AAAA = 28
141
+ SRV = 33
142
+ OPT = 41
143
+ NSEC = 47
144
+ HTTPS = 65
145
+ ANY = 255
146
+ required uint16 qclass mask 0x7fff "Class"
147
+ IN = 1
148
+ CH = 3
149
+ NONE = 254
150
+ ANY = 255
151
+ bits qclass qu 15 1 "QU (reply wanted unicast)"
152
+ Multicast = 0
153
+ Unicast = 1
154
+
155
+ repeat answers as answer "Answers"
156
+ label "%s: type %s, class %s, %s" name, type, class, rdata
157
+ include RR
158
+
159
+ repeat authority as authority_rr "Authoritative nameservers"
160
+ label "%s: type %s, class %s, %s" name, type, class, rdata
161
+ include RR
162
+
163
+ repeat additional as additional_rr "Additional records"
164
+ label "%s: type %s, class %s, %s" name, type, class, rdata
165
+ include RR
166
+
167
+ # A query has questions and no records; a response is usually the reverse —
168
+ # name both, and the fields the packet does not carry expand to nothing.
169
+ info "%s 0x%x %s %s %s %s" qr, id, qtype, qname, type, rdata
170
+
171
+ rule udp.port == 53 => DNS
172
+ rule udp.port == 5353 => DNS
173
+ rule udp.port == 5355 => DNS
174
+
175
+ color dns.rcode > 0 => yellow red
176
+ color dns => white blue
@@ -0,0 +1,59 @@
1
+ # http.posa — HTTP/1.x (RFC 9112).
2
+ #
3
+ # HTTP has no lengths in front of anything: it is lines of text ending in CRLF,
4
+ # then a blank line, then the body. Two posa constructs cover that shape:
5
+ #
6
+ # string <name> until "<delim>" one delimited token
7
+ # repeat until "<delim>" as <item> records until those bytes come next —
8
+ # here, header lines until the blank line
9
+ #
10
+ # A request and a response differ only in their first line, so they are two
11
+ # objects in an Object<HTTP> group, told apart by their first field: a response
12
+ # starts with "HTTP", a request with a method. The group is dispatched on the
13
+ # first field's value, so "HTTP" as a uint32 (0x48545450) selects the response.
14
+ #
15
+ # Not covered (add it here, no rebuild): chunked transfer-encoding is shown as
16
+ # the raw body, and the body is not decompressed.
17
+
18
+ Object<HTTP> HTTP_RESPONSE
19
+ abbrev "http"
20
+ col "HTTP"
21
+
22
+ # the group is dispatched on the first field's value, so a response is
23
+ # recognised by its "HTTP" magic — then `seek 0` rewinds over it, and the
24
+ # status line is read as the text it actually is
25
+ required uint32 magic = 0x48545450 hex "Magic"
26
+ seek 0
27
+ required string version until " " "Version"
28
+ required string status_code until " " "Status Code"
29
+ required string reason until "\r\n" "Reason Phrase"
30
+ repeat until "\r\n" as header "Headers"
31
+ label "%s" line
32
+ required string line until "\r\n" "Header"
33
+ required string blank until "\r\n" "End of headers"
34
+ required payload body "Body"
35
+
36
+ info "%s %s" status_code, reason
37
+
38
+ Object<HTTP> HTTP_REQUEST default
39
+ abbrev "http"
40
+ col "HTTP"
41
+
42
+ required string method until " " "Method"
43
+ required string uri until " " "Request URI"
44
+ required string version until "\r\n" "Version"
45
+ repeat until "\r\n" as header "Headers"
46
+ label "%s" line
47
+ required string line until "\r\n" "Header"
48
+ required string blank until "\r\n" "End of headers"
49
+ required payload body "Body"
50
+
51
+ info "%s %s %s" method, uri, version
52
+
53
+ rule tcp.port == 80 => HTTP
54
+ rule tcp.port == 8080 => HTTP
55
+ rule tcp.port == 8000 => HTTP
56
+ rule tcp.port == 8888 => HTTP
57
+ rule tcp.port == 3128 => HTTP
58
+
59
+ color http => black lightgray
@@ -0,0 +1,56 @@
1
+ # igmp.posa — IGMP v1/v2/v3 (RFC 1112, 2236, 3376).
2
+ #
3
+ # Bound by IP protocol number, not a port: rule ip.proto == 2 => IGMP
4
+ #
5
+ # The v3 membership report (type 0x22) is a different shape from the queries and
6
+ # the v1/v2 reports: no group address in the header, just a count of the group
7
+ # records that follow, each with its own list of source addresses. That is two
8
+ # nested `repeat`s — the outer one counted by a header field, the inner by a
9
+ # field of the record it is inside.
10
+ #
11
+ # Every field a posa decoder produces carries its byte range, so selecting a row
12
+ # in the details pane highlights the right bytes in the hex pane. That is free
13
+ # here; in hand-written C it is a call you can forget to make.
14
+
15
+ Object<main> IGMP
16
+ abbrev "igmp"
17
+ col "IGMP"
18
+
19
+ required uint8 type "Type"
20
+ Membership Query = 0x11
21
+ IGMPv1 Membership Report = 0x12
22
+ IGMPv2 Membership Report = 0x16
23
+ Leave Group = 0x17
24
+ IGMPv3 Membership Report = 0x22
25
+ required uint8 max_resp "Max Resp Time"
26
+ required uint16 checksum "Checksum"
27
+
28
+ # v3 membership report: reserved, then N group records
29
+ when type == 0x22:
30
+ required uint16 reserved "Reserved"
31
+ required uint16 num_records "Num Group Records"
32
+ repeat num_records as record "Group Records"
33
+ label "%s: %s (%u source(s))" record_type, maddr, num_sources
34
+ required uint8 record_type "Record Type"
35
+ MODE_IS_INCLUDE = 1
36
+ MODE_IS_EXCLUDE = 2
37
+ CHANGE_TO_INCLUDE = 3
38
+ CHANGE_TO_EXCLUDE = 4
39
+ ALLOW_NEW_SOURCES = 5
40
+ BLOCK_OLD_SOURCES = 6
41
+ required uint8 aux_len "Aux Data Length"
42
+ required uint16 num_sources "Num Sources"
43
+ required ip4 maddr "Multicast Address"
44
+ repeat num_sources as source "Source Addresses"
45
+ label "Source: %s" saddr
46
+ required ip4 saddr "Source Address"
47
+
48
+ # everything else (queries, v1/v2 reports, leave) carries a group address
49
+ when type != 0x22:
50
+ required ip4 maddr "Multicast Address"
51
+
52
+ info "%s %s" type, maddr
53
+
54
+ rule ip.proto == 2 => IGMP
55
+
56
+ color igmp => black lightgreen
@@ -0,0 +1,57 @@
1
+ # rdp.posa — Remote Desktop Protocol over TPKT / ISO-8073 (COTP), fully in posa.
2
+ # Demonstrates the extended grammar: layered sub-protocols, length scopes,
3
+ # conditional layout, a delimiter field, a derived Info string, and a port rule.
4
+
5
+ protocol TPKT
6
+ col "RDP"
7
+ abbrev "rdp"
8
+ required uint8 version
9
+ required uint8 reserved
10
+ required uint16 length
11
+ layer cotp COTP
12
+
13
+ protocol COTP
14
+ abbrev "cotp"
15
+ required uint8 li
16
+ required uint8 pdu_type
17
+ CR = 0xe0
18
+ CC = 0xd0
19
+ DT = 0xf0
20
+ scope li
21
+ when pdu_type & 0xf0 == 0xe0:
22
+ required uint16 dst_ref
23
+ required uint16 src_ref
24
+ required uint8 class
25
+ layer neg RDP_NEGOTIATION
26
+ when pdu_type & 0xf0 == 0xd0:
27
+ required uint16 dst_ref
28
+ required uint16 src_ref
29
+ required uint8 class
30
+ layer neg RDP_NEGOTIATION
31
+ when pdu_type & 0xf0 == 0xf0:
32
+ required uint8 eot
33
+ info "COTP %s" pdu_type
34
+
35
+ protocol RDP_NEGOTIATION
36
+ abbrev "rdp"
37
+ optional string cookie until "\r\n"
38
+ when remaining >= 8:
39
+ required uint8 type
40
+ Request = 1
41
+ Response = 2
42
+ Failure = 3
43
+ required uint8 flags
44
+ required le_uint16 length
45
+ required le_uint32 protocols
46
+ TLS = 0x1
47
+ CredSSP = 0x3
48
+ info "%s Negotiate %s" cookie, type
49
+
50
+ rule tcp.port == 3389 => TPKT
51
+
52
+ # Coloring — carcal paints a matching packet with these colors, first match wins.
53
+ # Declared here so the decoder ships its own look: drop this file into protos/
54
+ # and RDP is both decoded and colored, with no rebuild. Consulted before
55
+ # carcal's generic tcp rule, so they win over it.
56
+ color rdp.type == 3 => yellow red # negotiation Failure
57
+ color rdp => black lightcyan