├── .gitignore ├── cpanfile ├── doc └── pcap-to-seqdia.png ├── LICENSE ├── lib └── Netpacket │ └── LinuxSLL.pm ├── pcap2mermaid.pl └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pcap 3 | local 4 | cpanfile.snapshot 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | requires 'Net::Pcap'; 2 | requires 'NetPacket'; 3 | requires 'Net::SIP'; 4 | -------------------------------------------------------------------------------- /doc/pcap-to-seqdia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agranig/pcap2mermaid/HEAD/doc/pcap-to-seqdia.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andreas Granig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/Netpacket/LinuxSLL.pm: -------------------------------------------------------------------------------- 1 | # 2 | # NetPacket::LinuxSLL - Decode and encode Linux SLL (Linux cooked capture 3 | # pseudo-protocol) packets. 4 | # 5 | # $Id: LinuxSLL.pm,v 0.01 2003/00/00 00:00:00 tpot Exp $ 6 | # 7 | 8 | package NetPacket::LinuxSLL; 9 | 10 | # 11 | # Copyright (c) 2003 Greg Zemskov 12 | # Copyright (c) 2008 Dmitry Kokorev 13 | # Copyright (c) 2011 Sergey Afonin 14 | # 15 | # This package is free software and is provided "as is" without express. 16 | # or implied warranty. It may be used, redistributed and/or modified. 17 | # under the terms of the Artistic License 2.0 (see 18 | # http://opensource.org/licenses/artistic-license-2.0.php) 19 | # 20 | 21 | use strict; 22 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); 23 | 24 | BEGIN { 25 | @ISA = qw(Exporter NetPacket); 26 | 27 | @EXPORT = qw( ); 28 | 29 | @EXPORT_OK = qw( 30 | LINUX_SLL_HOST LINUX_SLL_BROADCAST LINUX_SLL_MULTICAST 31 | LINUX_SLL_OTHERHOST LINUX_SLL_OUTGOING 32 | ); 33 | 34 | %EXPORT_TAGS = ( 35 | ALL => [@EXPORT, @EXPORT_OK], 36 | types => [qw( 37 | LINUX_SLL_HOST LINUX_SLL_BROADCAST LINUX_SLL_MULTICAST 38 | LINUX_SLL_OTHERHOST LINUX_SLL_OUTGOING 39 | )], 40 | ); 41 | } 42 | 43 | use constant LINUX_SLL_HOST => 0x0000; 44 | use constant LINUX_SLL_BROADCAST => 0x0001; 45 | use constant LINUX_SLL_MULTICAST => 0x0002; 46 | use constant LINUX_SLL_OTHERHOST => 0x0003; 47 | use constant LINUX_SLL_OUTGOING => 0x0004; 48 | 49 | # 50 | # Decode the packet 51 | # 52 | 53 | sub decode { 54 | my $class = shift; 55 | my ($pkt, $parent, @rest) = @_; 56 | my $self = {}; 57 | 58 | # Class fields 59 | 60 | $self->{_parent} = $parent; 61 | $self->{_frame} = $pkt; 62 | 63 | # Decode packet 64 | 65 | ($self->{type}, $self->{hatype}, $self->{halen}, $self->{addr}, $self->{proto}, $self->{data}) 66 | = unpack('nnna8na*', $pkt); 67 | 68 | bless ($self, $class); 69 | return $self; 70 | } 71 | 72 | sub encode { 73 | die("Not implemented"); 74 | } 75 | 76 | 1; 77 | -------------------------------------------------------------------------------- /pcap2mermaid.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use warnings; 3 | use strict; 4 | 5 | use English; 6 | use Net::Pcap qw/:datalink :functions/; 7 | use NetPacket::LinuxSLL; 8 | use NetPacket::Ethernet; 9 | use NetPacket::IP; 10 | use NetPacket::UDP; 11 | use NetPacket::TCP; 12 | use Net::SIP::Packet; 13 | use Net::SIP::Request; 14 | use Net::SIP::Response; 15 | 16 | my $filter_string = 'port 5060'; 17 | my $skip_provisional = 1; 18 | 19 | my $infile = $ARGV[0]; 20 | my $outfile = $ARGV[1]; 21 | my $mapping = $ARGV[2]; 22 | 23 | unless (defined $infile && defined $outfile) { 24 | die "Usage: $PROGRAM_NAME [mapping]\n"; 25 | } 26 | 27 | my $ret; 28 | my $err = ''; 29 | my $filter; 30 | my @sip_packets = (); 31 | my $filter_unmapped = 0; 32 | my %host2name = (); 33 | 34 | if (defined $mapping) { 35 | $filter_unmapped = 1; 36 | my @hosts = split /,/, $mapping; 37 | foreach my $h (@hosts) { 38 | my ($host, $name) = split /=/, $h; 39 | unless (defined $host && defined $name) { 40 | die "Invalid mapping, must be comma-separated list of ':=' elements\n"; 41 | } 42 | $host2name{$host} = $name; 43 | } 44 | } 45 | 46 | my $pcap = Net::Pcap::open_offline($infile, \$err) 47 | or die "Failed to open pcap input file '$infile': $err\n"; 48 | 49 | open my $outfh, ">", $outfile 50 | or die "Failed to open mermaid output file '$outfile': $!\n"; 51 | 52 | my $linktype = Net::Pcap::datalink($pcap); 53 | unless ($linktype == DLT_LINUX_SLL || $linktype == DLT_EN10MB) { 54 | die "Invalid data link type in pcap file, must be linux sll or ethernet\n"; 55 | } 56 | 57 | $ret = Net::Pcap::compile($pcap, \$filter, $filter_string, 1, 0); 58 | if ($ret == -1) { 59 | die "Failed to compile filter string '$filter_string'\n"; 60 | } 61 | Net::Pcap::setfilter($pcap, $filter); 62 | 63 | $ret = Net::Pcap::loop($pcap, -1, \&process_packet, ''); 64 | if ($ret < 0) { 65 | print "Some error occoured while parsing packets in pcap, trying to continue with what we have so far\n"; 66 | } 67 | 68 | Net::Pcap::pcap_close($pcap); 69 | 70 | my $seq_count = 0; 71 | 72 | print $outfh "sequenceDiagram\n"; 73 | foreach my $pkt (@sip_packets) { 74 | my $a = $pkt->{src}; 75 | my $b = $pkt->{dst}; 76 | if ($filter_unmapped && (!exists $host2name{$a} || !exists $host2name{$b})) { 77 | next; 78 | } elsif ($filter_unmapped) { 79 | $a = $host2name{$a}; 80 | $b = $host2name{$b}; 81 | } 82 | my $arrow = $pkt->{req} ? '->>' : '-->>'; 83 | print $outfh " $a$arrow$b: $$pkt{text}\n"; 84 | $seq_count++; 85 | 86 | } 87 | 88 | close $outfh; 89 | 90 | print "Done, $seq_count SIP packets written to sequence diagram\n"; 91 | 92 | sub process_packet { 93 | my ($user_data, $header, $packet) = @_; 94 | 95 | my $l2; 96 | if ($linktype == DLT_LINUX_SLL) { 97 | $l2 = NetPacket::LinuxSLL->decode($packet); 98 | } else { 99 | $l2 = NetPacket::Ethernet->decode($packet); 100 | } 101 | 102 | my $l3 = NetPacket::IP->decode($l2->{data}); 103 | 104 | my $l4; 105 | if ($l3->{proto} == 17) { 106 | $l4 = NetPacket::UDP->decode($l3->{data}); 107 | } elsif ($l3->{proto} == 6) { 108 | $l4 = NetPacket::TCP->decode($l3->{data}); 109 | } else { 110 | print "unsupported l4 protocol, must be udp or tcp\n"; 111 | return; 112 | } 113 | 114 | my $sip; 115 | eval { $sip = Net::SIP::Packet->new($l4->{data}); }; 116 | if ($@) { 117 | print "invalid sip packet: $@\n"; 118 | return; 119 | } 120 | 121 | if ($sip->is_request) { 122 | push @sip_packets, { 123 | req => 1, 124 | text => $sip->method, 125 | src => "$$l3{src_ip}:$$l4{src_port}", 126 | dst => "$$l3{dest_ip}:$$l4{dest_port}", 127 | }; 128 | } else { 129 | return if ($skip_provisional && $sip->code < 180); 130 | push @sip_packets, { 131 | req => 0, 132 | text => $sip->code . " (" . $sip->method . ")", 133 | src => "$$l3{src_ip}:$$l4{src_port}", 134 | dst => "$$l3{dest_ip}:$$l4{dest_port}", 135 | }; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pcap2mermaid - Convert SIP call flows from PCAP traces to Mermaid sequence diagrams 2 | 3 | This tool parses a pcap file (e.g. captured by tcpdump), extracts SIP 4 | packets from it and converts the flow to a mermaid sequence diagram 5 | while filtering unwanted packets. 6 | 7 | ![pcap to sequence 8 | diagram](https://github.com/agranig/pcap2mermaid/raw/master/doc/pcap-to-seqdia.png 9 | "PCAP to Mermaid sequence diagram") 10 | 11 | ## Motivation and Use Case 12 | 13 | When documenting SIP call flows, a sequence diagram can greatly improve 14 | the understanding of SIP packets going back and forth between involved 15 | endpoints. However, managing pictures or other binary formats produced 16 | by tools like Visio or Dia are a pain to track via version control, and 17 | they are cumbersome to maintain e.g in Markdown documentation. 18 | 19 | [Mermaid](https://mermaidjs.github.io/) is a nice tool you can run 20 | locally to generate such sequence diagrams from simple text based 21 | syntax. When using Pandoc to generate documentation out of markdown 22 | file, you can even use 23 | [mermaid-filter](https://github.com/raghur/mermaid-filter) to embed 24 | mermaid code directly in your documentation and generate the diagrams on 25 | the fly when the markdown is rendered to html or pdf. 26 | 27 | However, manually writing those sequence diagrams can be a quite 28 | repetitive and boring task when documenting a large number of call 29 | flows. Therefore, this tool aims to help automizing this process by 30 | letting you do an actual SIP call (or registration, or any other SIP 31 | scenario), capture a pcap file using tcpdump while doing so, then 32 | generate the sequence diagram automatically using this pcap call trace. 33 | What's left is pasting it into your documentation, and potentially 34 | annotating it with some notes. 35 | 36 | ## Installation and Usage 37 | 38 | ### Debian 39 | 40 | ``` 41 | $ sudo apt install libnet-pcap-perl libnetpacket-perl libnet-sip-perl 42 | ``` 43 | 44 | ### Other 45 | 46 | ``` 47 | $ sudo yum install libpcap-devel \ 48 | # or any other way to install the libpcap development package 49 | $ sudo cpan -i Carton 50 | $ carton install 51 | ``` 52 | 53 | ### Running the tool 54 | 55 | __Usage__: `./pcap2mermaid.pl [mapping string]` 56 | 57 | ``` 58 | PERL5LIB=./lib:./local/lib/perl5 ./pcap2mermaid.pl \ 59 | /tmp/test.pcap /tmp/out.txt \ 60 | "10.15.17.98:46849=DUT,10.15.17.237:5060=SSW" 61 | ``` 62 | 63 | ### Converting the output to a mermaid sequence diagram 64 | 65 | To quickly verify the result without installing mermaid, you can use the 66 | [mermaid live editor](https://mermaidjs.github.io/mermaid-live-editor) 67 | where you can paste the output of the tool for a preview. 68 | 69 | Eventually, you might want to Install the 70 | [mermaid-cli](https://github.com/mermaidjs/mermaid.cli) for a quick 71 | test, then execute: 72 | 73 | ``` 74 | $ cat /tmp/mermaid-config.json 75 | { 76 | "theme": "forest", 77 | "themeCSS": "", 78 | "cloneCssStyles": false, 79 | "sequence": { 80 | "mirrorActors": false, 81 | "useMaxWidth": false 82 | } 83 | } 84 | 85 | $ mmdc -i /tmp/out.txt -o /tmp/out.svg -c /tmp/mermaid-config.json 86 | ``` 87 | 88 | A final solution would also integrate the mermaid-filter to the 89 | documentation generation tool-chain, which is out of scope of this 90 | document. 91 | 92 | ## Options 93 | 94 | ### Pre-Filtering traffic by port 5060 95 | 96 | The tool only considers packets from or to port 5060 hardcodedly. If you 97 | want to capture traffic between different ports, you have to adapt the 98 | variable **$filter_string**, which is in standard pcap filter syntax. 99 | 100 | Also note that all provisional responses with a response code smaller 101 | than 180 are automatically ignored. To change this behavior, the 102 | **$skip_provisional** variable must be set to 0. 103 | 104 | ### Mapping addresses to names 105 | 106 | If no optional _mapping string_ argument is given, every single leg of 107 | all SIP packets are displayed. The name of the endpoints are the 108 | `:` tuples. This can take a long time. 109 | 110 | To optimize this and make the flow chart more readable, a mapping string 111 | accomplishes two things: 112 | 113 | 1. Display a custom name for an endpoint instead of `:` in the 114 | flow chart 115 | 2. Ignore all endpoints that do not have a mapping string 116 | 117 | The format of the mapping string is a comma-separated list of elements, 118 | each containing a string `:=`. 119 | 120 | __Example__: 192.168.0.10:12345=Alice,192.168.0.20:54321=Bob,192.168.0.30=Proxy 121 | --------------------------------------------------------------------------------