diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 20de7b28198a..fd2dbd45d003 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -833,6 +833,7 @@ field. You can revert this change by configuring tags for the module and omittin port. {pull}19209[19209] - Add ECS fields for x509 certs, event categorization, and related IP info. {pull}19167[19167] - Add 100-continue support {issue}15830[15830] {pull}19349[19349] +- Add initial SIP protocol support {pull}21221[21221] *Functionbeat* diff --git a/packetbeat/_meta/config/beat.docker.yml.tmpl b/packetbeat/_meta/config/beat.docker.yml.tmpl index f4f0db1f7e6e..f9b573edfebc 100644 --- a/packetbeat/_meta/config/beat.docker.yml.tmpl +++ b/packetbeat/_meta/config/beat.docker.yml.tmpl @@ -36,3 +36,6 @@ packetbeat.protocols.cassandra: packetbeat.protocols.tls: ports: [443, 993, 995, 5223, 8443, 8883, 9243] + +packetbeat.protocols.sip: + ports: [5060] diff --git a/packetbeat/_meta/config/beat.reference.yml.tmpl b/packetbeat/_meta/config/beat.reference.yml.tmpl index 1a3aab315d76..722c47102dc8 100644 --- a/packetbeat/_meta/config/beat.reference.yml.tmpl +++ b/packetbeat/_meta/config/beat.reference.yml.tmpl @@ -531,6 +531,19 @@ packetbeat.protocols: # Set to true to publish fields with null values in events. #keep_null: false +- type: sip + # Configure the ports where to listen for SIP traffic. You can disable the SIP protocol by commenting out the list of ports. + ports: [5060] + + # Parse the authorization headers + parse_authorization: true + + # Parse body contents (only when body is SDP) + parse_body: true + + # Preserve original contents in event.original + keep_original: true + {{header "Monitored processes"}} # Packetbeat can enrich events with information about the process associated diff --git a/packetbeat/_meta/config/beat.yml.tmpl b/packetbeat/_meta/config/beat.yml.tmpl index fb221cba3c9a..c5f9cfc0c235 100644 --- a/packetbeat/_meta/config/beat.yml.tmpl +++ b/packetbeat/_meta/config/beat.yml.tmpl @@ -101,6 +101,10 @@ packetbeat.protocols: - 8883 # Secure MQTT - 9243 # Elasticsearch +- type: sip + # Configure the ports where to listen for SIP traffic. You can disable the SIP protocol by commenting out the list of ports. + ports: [5060] + {{header "Elasticsearch template setting"}} setup.template.settings: diff --git a/packetbeat/_meta/sample_outputs/sip.json b/packetbeat/_meta/sample_outputs/sip.json new file mode 100644 index 000000000000..4a57d85908ff --- /dev/null +++ b/packetbeat/_meta/sample_outputs/sip.json @@ -0,0 +1,77 @@ +{ + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "192.168.1.2", + "client.port": 5060, + "destination.ip": "212.242.33.35", + "destination.port": 5060, + "event.action": "sip_register", + "event.category": [ + "network", + "protocol", + "authentication" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "REGISTER sip:sip.cybercity.dk SIP/2.0\r\nVia: SIP/2.0/UDP 192.168.1.2;branch=z9hG4bKnp112903503-43a64480192.168.1.2;rport\r\nFrom: ;tag=6bac55c\r\nTo: \r\nCall-ID: 578222729-4665d775@578222732-4665d772\r\nContact: ;expires=1200;q=0.500\r\nExpires: 1200\r\nCSeq: 75 REGISTER\r\nContent-Length: 0\r\nAuthorization: Digest username=\"voi18062\",realm=\"sip.cybercity.dk\",uri=\"sip:192.168.1.2\",nonce=\"1701b22972b90f440c3e4eb250842bb\",opaque=\"1701a1351f70795\",nc=\"00000001\",response=\"79a0543188495d288c9ebbe0c881abdc\"\r\nMax-Forwards: 70\r\nUser-Agent: Nero SIPPS IP Phone Version 2.0.51.16\r\n\r\n", + "event.sequence": 75, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:dOa61R2NaaJsJlcFAiMIiyXX+Kk=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "sip.cybercity.dk" + ], + "related.ip": [ + "192.168.1.2", + "212.242.33.35" + ], + "related.user": [ + "voi18062" + ], + "server.ip": "212.242.33.35", + "server.port": 5060, + "sip.auth.realm": "sip.cybercity.dk", + "sip.auth.scheme": "Digest", + "sip.auth.uri.host": "192.168.1.2", + "sip.auth.uri.original": "sip:192.168.1.2", + "sip.auth.uri.scheme": "sip", + "sip.call_id": "578222729-4665d775@578222732-4665d772", + "sip.contact.uri.host": "sip.cybercity.dk", + "sip.contact.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.contact.uri.scheme": "sip", + "sip.contact.uri.username": "voi18062", + "sip.cseq.code": 75, + "sip.cseq.method": "REGISTER", + "sip.from.tag": "6bac55c", + "sip.from.uri.host": "sip.cybercity.dk", + "sip.from.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "voi18062", + "sip.max_forwards": 70, + "sip.method": "REGISTER", + "sip.to.uri.host": "sip.cybercity.dk", + "sip.to.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "voi18062", + "sip.type": "request", + "sip.uri.host": "sip.cybercity.dk", + "sip.uri.original": "sip:sip.cybercity.dk", + "sip.uri.scheme": "sip", + "sip.user_agent.original": "Nero SIPPS IP Phone Version 2.0.51.16", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 192.168.1.2;branch=z9hG4bKnp112903503-43a64480192.168.1.2;rport" + ], + "source.ip": "192.168.1.2", + "source.port": 5060, + "status": "OK", + "type": "sip", + "user.name": "voi18062" +} diff --git a/packetbeat/docs/fields.asciidoc b/packetbeat/docs/fields.asciidoc index f68faa240730..5be8d29de72a 100644 --- a/packetbeat/docs/fields.asciidoc +++ b/packetbeat/docs/fields.asciidoc @@ -35,6 +35,7 @@ grouped in the following categories: * <> * <> * <> +* <> * <> * <> * <> @@ -11678,6 +11679,667 @@ The return value of the Redis command in a human readable format. If the Redis command has resulted in an error, this field contains the error message returned by the Redis server. +-- + +[[exported-fields-sip]] +== SIP fields + +SIP-specific event fields. + + +[float] +=== sip + +Information about SIP traffic. + + +*`sip.timestamp`*:: ++ +-- +Timestamp with nano second precision. + +type: date_nanos + +-- + +*`sip.code`*:: ++ +-- +Response status code. + +type: keyword + +-- + +*`sip.method`*:: ++ +-- +Request method. + +type: keyword + +-- + +*`sip.status`*:: ++ +-- +Response status phrase. + +type: keyword + +-- + +*`sip.type`*:: ++ +-- +Either request or response. + +type: keyword + +-- + +*`sip.version`*:: ++ +-- +SIP protocol version. + +type: keyword + +-- + +*`sip.uri.original`*:: ++ +-- +The original URI. + +type: keyword + +-- + +*`sip.uri.original.text`*:: ++ +-- +type: text + +-- + +*`sip.uri.scheme`*:: ++ +-- +The URI scheme. + +type: keyword + +-- + +*`sip.uri.username`*:: ++ +-- +The URI user name. + +type: keyword + +-- + +*`sip.uri.host`*:: ++ +-- +The URI host. + +type: keyword + +-- + +*`sip.uri.port`*:: ++ +-- +The URI port. + +type: keyword + +-- + +*`sip.accept`*:: ++ +-- +Accept header value. + +type: keyword + +-- + +*`sip.allow`*:: ++ +-- +Allowed methods. + +type: keyword + +-- + +*`sip.call_id`*:: ++ +-- +Call ID. + +type: keyword + +-- + +*`sip.content_length`*:: ++ +-- +type: long + +-- + +*`sip.content_type`*:: ++ +-- +type: keyword + +-- + +*`sip.max_forwards`*:: ++ +-- +type: long + +-- + +*`sip.supported`*:: ++ +-- +Supported methods. + +type: keyword + +-- + +*`sip.user_agent.original`*:: ++ +-- +type: keyword + +-- + +*`sip.user_agent.original.text`*:: ++ +-- +type: text + +-- + +*`sip.private.uri.original`*:: ++ +-- +Private original URI. + +type: keyword + +-- + +*`sip.private.uri.original.text`*:: ++ +-- +type: text + +-- + +*`sip.private.uri.scheme`*:: ++ +-- +Private URI scheme. + +type: keyword + +-- + +*`sip.private.uri.username`*:: ++ +-- +Private URI user name. + +type: keyword + +-- + +*`sip.private.uri.host`*:: ++ +-- +Private URI host. + +type: keyword + +-- + +*`sip.private.uri.port`*:: ++ +-- +Private URI port. + +type: keyword + +-- + +*`sip.cseq.code`*:: ++ +-- +Sequence code. + +type: keyword + +-- + +*`sip.cseq.method`*:: ++ +-- +Sequence method. + +type: keyword + +-- + +*`sip.via.original`*:: ++ +-- +The original Via value. + +type: keyword + +-- + +*`sip.via.original.text`*:: ++ +-- +type: text + +-- + +*`sip.to.display_info`*:: ++ +-- +To display info + +type: keyword + +-- + +*`sip.to.uri.original`*:: ++ +-- +To original URI + +type: keyword + +-- + +*`sip.to.uri.original.text`*:: ++ +-- +type: text + +-- + +*`sip.to.uri.scheme`*:: ++ +-- +To URI scheme + +type: keyword + +-- + +*`sip.to.uri.username`*:: ++ +-- +To URI user name + +type: keyword + +-- + +*`sip.to.uri.host`*:: ++ +-- +To URI host + +type: keyword + +-- + +*`sip.to.uri.port`*:: ++ +-- +To URI port + +type: keyword + +-- + +*`sip.to.tag`*:: ++ +-- +To tag + +type: keyword + +-- + +*`sip.from.display_info`*:: ++ +-- +From display info + +type: keyword + +-- + +*`sip.from.uri.original`*:: ++ +-- +From original URI + +type: keyword + +-- + +*`sip.from.uri.original.text`*:: ++ +-- +type: text + +-- + +*`sip.from.uri.scheme`*:: ++ +-- +From URI scheme + +type: keyword + +-- + +*`sip.from.uri.username`*:: ++ +-- +From URI user name + +type: keyword + +-- + +*`sip.from.uri.host`*:: ++ +-- +From URI host + +type: keyword + +-- + +*`sip.from.uri.port`*:: ++ +-- +From URI port + +type: keyword + +-- + +*`sip.from.tag`*:: ++ +-- +From tag + +type: keyword + +-- + +*`sip.contact.display_info`*:: ++ +-- +Contact display info + +type: keyword + +-- + +*`sip.contact.uri.original`*:: ++ +-- +Contact original URI + +type: keyword + +-- + +*`sip.contact.uri.original.text`*:: ++ +-- +type: text + +-- + +*`sip.contact.uri.scheme`*:: ++ +-- +Contat URI scheme + +type: keyword + +-- + +*`sip.contact.uri.username`*:: ++ +-- +Contact URI user name + +type: keyword + +-- + +*`sip.contact.uri.host`*:: ++ +-- +Contact URI host + +type: keyword + +-- + +*`sip.contact.uri.port`*:: ++ +-- +Contact URI port + +type: keyword + +-- + +*`sip.contact.transport`*:: ++ +-- +Contact transport + +type: keyword + +-- + +*`sip.contact.line`*:: ++ +-- +Contact line + +type: keyword + +-- + +*`sip.contact.expires`*:: ++ +-- +Contact expires + +type: keyword + +-- + +*`sip.contact.q`*:: ++ +-- +Contact Q + +type: keyword + +-- + +*`sip.auth.scheme`*:: ++ +-- +Auth scheme + +type: keyword + +-- + +*`sip.auth.realm`*:: ++ +-- +Auth realm + +type: keyword + +-- + +*`sip.auth.uri.original`*:: ++ +-- +Auth original URI + +type: keyword + +-- + +*`sip.auth.uri.original.text`*:: ++ +-- +type: text + +-- + +*`sip.auth.uri.scheme`*:: ++ +-- +Auth URI scheme + +type: keyword + +-- + +*`sip.auth.uri.host`*:: ++ +-- +Auth URI host + +type: keyword + +-- + +*`sip.auth.uri.port`*:: ++ +-- +Auth URI port + +type: keyword + +-- + +*`sip.sdp.version`*:: ++ +-- +SDP version + +type: keyword + +-- + +*`sip.sdp.owner.username`*:: ++ +-- +SDP owner user name + +type: keyword + +-- + +*`sip.sdp.owner.session_id`*:: ++ +-- +SDP owner session ID + +type: keyword + +-- + +*`sip.sdp.owner.version`*:: ++ +-- +SDP owner version + +type: keyword + +-- + +*`sip.sdp.owner.ip`*:: ++ +-- +SDP owner IP + +type: ip + +-- + +*`sip.sdp.session.name`*:: ++ +-- +SDP session name + +type: keyword + +-- + +*`sip.sdp.connection.info`*:: ++ +-- +SDP connection info + +type: keyword + +-- + +*`sip.sdp.connection.address`*:: ++ +-- +SDP connection address + +type: keyword + +-- + +*`sip.sdp.body.original`*:: ++ +-- +SDP original body + +type: keyword + +-- + +*`sip.sdp.body.original.text`*:: ++ +-- +type: text + -- [[exported-fields-thrift]] diff --git a/packetbeat/docs/shared-protocol-list.asciidoc b/packetbeat/docs/shared-protocol-list.asciidoc index 3e18fc35eb8f..fdac127050ac 100644 --- a/packetbeat/docs/shared-protocol-list.asciidoc +++ b/packetbeat/docs/shared-protocol-list.asciidoc @@ -18,3 +18,4 @@ - Memcache - NFS - TLS + - SIP/SDP (beta) diff --git a/packetbeat/include/list.go b/packetbeat/include/list.go index 0dc1f0bd053c..748d525eb2f8 100644 --- a/packetbeat/include/list.go +++ b/packetbeat/include/list.go @@ -33,6 +33,7 @@ import ( _ "github.com/elastic/beats/v7/packetbeat/protos/nfs" _ "github.com/elastic/beats/v7/packetbeat/protos/pgsql" _ "github.com/elastic/beats/v7/packetbeat/protos/redis" + _ "github.com/elastic/beats/v7/packetbeat/protos/sip" _ "github.com/elastic/beats/v7/packetbeat/protos/thrift" _ "github.com/elastic/beats/v7/packetbeat/protos/tls" ) diff --git a/packetbeat/packetbeat.docker.yml b/packetbeat/packetbeat.docker.yml index edffce726947..4cf9016a9261 100644 --- a/packetbeat/packetbeat.docker.yml +++ b/packetbeat/packetbeat.docker.yml @@ -37,6 +37,9 @@ packetbeat.protocols.cassandra: packetbeat.protocols.tls: ports: [443, 993, 995, 5223, 8443, 8883, 9243] +packetbeat.protocols.sip: + ports: [5060] + processors: - add_cloud_metadata: ~ - add_docker_metadata: ~ diff --git a/packetbeat/packetbeat.reference.yml b/packetbeat/packetbeat.reference.yml index f6447294265d..3d6ba0efcc67 100644 --- a/packetbeat/packetbeat.reference.yml +++ b/packetbeat/packetbeat.reference.yml @@ -531,6 +531,19 @@ packetbeat.protocols: # Set to true to publish fields with null values in events. #keep_null: false +- type: sip + # Configure the ports where to listen for SIP traffic. You can disable the SIP protocol by commenting out the list of ports. + ports: [5060] + + # Parse the authorization headers + parse_authorization: true + + # Parse body contents (only when body is SDP) + parse_body: true + + # Preserve original contents in event.original + keep_original: true + # ============================ Monitored processes ============================= # Packetbeat can enrich events with information about the process associated diff --git a/packetbeat/packetbeat.yml b/packetbeat/packetbeat.yml index 53f87d730035..31c229b1ef76 100644 --- a/packetbeat/packetbeat.yml +++ b/packetbeat/packetbeat.yml @@ -101,6 +101,10 @@ packetbeat.protocols: - 8883 # Secure MQTT - 9243 # Elasticsearch +- type: sip + # Configure the ports where to listen for SIP traffic. You can disable the SIP protocol by commenting out the list of ports. + ports: [5060] + # ======================= Elasticsearch template setting ======================= setup.template.settings: diff --git a/packetbeat/pb/ecs.go b/packetbeat/pb/ecs.go index b7722c2c22ae..4594f7cd8c45 100644 --- a/packetbeat/pb/ecs.go +++ b/packetbeat/pb/ecs.go @@ -54,7 +54,11 @@ type ecsRelated struct { User []string `ecs:"user"` // overridden because this needs to be an array Hash []string `ecs:"hash"` + // overridden because this needs to be an array + Hosts []string `ecs:"hosts"` // for de-dup - ipSet map[string]struct{} + ipSet map[string]struct{} + userSet map[string]struct{} + hostSet map[string]struct{} } diff --git a/packetbeat/pb/event.go b/packetbeat/pb/event.go index 73387c7f796d..5ad94c2d4409 100644 --- a/packetbeat/pb/event.go +++ b/packetbeat/pb/event.go @@ -147,10 +147,15 @@ func (f *Fields) SetDestination(endpoint *common.Endpoint) { func (f *Fields) AddIP(ip ...string) { if f.Related == nil { f.Related = &ecsRelated{ - ipSet: make(map[string]struct{}), + ipSet: make(map[string]struct{}), + userSet: make(map[string]struct{}), + hostSet: make(map[string]struct{}), } } for _, ipAddress := range ip { + if ipAddress == "" { + continue + } if _, ok := f.Related.ipSet[ipAddress]; !ok { f.Related.ipSet[ipAddress] = struct{}{} f.Related.IP = append(f.Related.IP, ipAddress) @@ -158,6 +163,46 @@ func (f *Fields) AddIP(ip ...string) { } } +// AddUser adds the given user names to the related ECS User field +func (f *Fields) AddUser(u ...string) { + if f.Related == nil { + f.Related = &ecsRelated{ + ipSet: make(map[string]struct{}), + userSet: make(map[string]struct{}), + hostSet: make(map[string]struct{}), + } + } + for _, user := range u { + if user == "" { + continue + } + if _, ok := f.Related.userSet[user]; !ok { + f.Related.userSet[user] = struct{}{} + f.Related.User = append(f.Related.User, user) + } + } +} + +// AddHost adds the given hosts to the related ECS Hosts field +func (f *Fields) AddHost(h ...string) { + if f.Related == nil { + f.Related = &ecsRelated{ + ipSet: make(map[string]struct{}), + userSet: make(map[string]struct{}), + hostSet: make(map[string]struct{}), + } + } + for _, host := range h { + if host == "" { + continue + } + if _, ok := f.Related.hostSet[host]; !ok { + f.Related.hostSet[host] = struct{}{} + f.Related.Hosts = append(f.Related.Hosts, host) + } + } +} + func makeProcess(p *common.Process) *ecs.Process { return &ecs.Process{ Name: p.Name, diff --git a/packetbeat/protos/sip/README.md b/packetbeat/protos/sip/README.md new file mode 100644 index 000000000000..5e5962675a1d --- /dev/null +++ b/packetbeat/protos/sip/README.md @@ -0,0 +1,123 @@ +# SIP (Session Initiation Protocol) for packetbeat + +The SIP (Session Initiation Protocol) is a communications protocol for signaling and controlling multimedia communication sessions. SIP is used by many VoIP applications, not only for enterprise uses but also telecom carriers. + +SIP is a text-based protocol like HTTP. But SIP has various unique features like : +- SIP is server-client model, but its role may change in a per call basis. +- SIP is request-response model, but server may (usually) reply with many responses to a single request. +- There are many requests and responses in one call. +- It is not known when the call will end. + +## Implementation + +### Published for each SIP message (request or response) + +- SIP is not a one to one message with request and response. Also order to each message is not determined (a response may be sent after previous response). +- Therefore the SIP responses and requests are published when packetbeat receives them immediately. +- If you need all SIP messages in throughout of SIP dialog, you need to retrieve from Elasticsearch using the SIP Call ID field etc. + +### Notes +* ``transport=tcp`` is not supported yet. +* ``content-encoding`` is not supported yet. +* Default timestamp field(@timestamp) precision is not sufficient(the sip response is often send immediately when request received eg. 100 Trying). You can sort to keep the message correct order using the ``sip.timestamp``(`date_nanos`) field. +* Body parsing is partially supported for ``application/sdp`` content type only. + +## Configuration + +```yaml +- type: sip + # Configure the ports where to listen for SIP traffic. You can disable the SIP protocol by commenting out the list of ports. + ports: [5060] + + # Parse the authorization headers + parse_authorization: true + + # Parse body contents (only when body is SDP) + parse_body: true + + # Preserve original contents in event.original + keep_original: true +``` + +### Sample Full JSON Output + +```json +{ + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "192.168.1.2", + "client.port": 5060, + "destination.ip": "212.242.33.35", + "destination.port": 5060, + "event.action": "sip_register", + "event.category": [ + "network", + "protocol", + "authentication" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "REGISTER sip:sip.cybercity.dk SIP/2.0\r\nVia: SIP/2.0/UDP 192.168.1.2;branch=z9hG4bKnp112903503-43a64480192.168.1.2;rport\r\nFrom: ;tag=6bac55c\r\nTo: \r\nCall-ID: 578222729-4665d775@578222732-4665d772\r\nContact: ;expires=1200;q=0.500\r\nExpires: 1200\r\nCSeq: 75 REGISTER\r\nContent-Length: 0\r\nAuthorization: Digest username=\"voi18062\",realm=\"sip.cybercity.dk\",uri=\"sip:192.168.1.2\",nonce=\"1701b22972b90f440c3e4eb250842bb\",opaque=\"1701a1351f70795\",nc=\"00000001\",response=\"79a0543188495d288c9ebbe0c881abdc\"\r\nMax-Forwards: 70\r\nUser-Agent: Nero SIPPS IP Phone Version 2.0.51.16\r\n\r\n", + "event.sequence": 75, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:dOa61R2NaaJsJlcFAiMIiyXX+Kk=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "sip.cybercity.dk" + ], + "related.ip": [ + "192.168.1.2", + "212.242.33.35" + ], + "related.user": [ + "voi18062" + ], + "server.ip": "212.242.33.35", + "server.port": 5060, + "sip.auth.realm": "sip.cybercity.dk", + "sip.auth.scheme": "Digest", + "sip.auth.uri.host": "192.168.1.2", + "sip.auth.uri.original": "sip:192.168.1.2", + "sip.auth.uri.scheme": "sip", + "sip.call_id": "578222729-4665d775@578222732-4665d772", + "sip.contact.uri.host": "sip.cybercity.dk", + "sip.contact.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.contact.uri.scheme": "sip", + "sip.contact.uri.username": "voi18062", + "sip.cseq.code": 75, + "sip.cseq.method": "REGISTER", + "sip.from.tag": "6bac55c", + "sip.from.uri.host": "sip.cybercity.dk", + "sip.from.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "voi18062", + "sip.max_forwards": 70, + "sip.method": "REGISTER", + "sip.to.uri.host": "sip.cybercity.dk", + "sip.to.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "voi18062", + "sip.type": "request", + "sip.uri.host": "sip.cybercity.dk", + "sip.uri.original": "sip:sip.cybercity.dk", + "sip.uri.scheme": "sip", + "sip.user_agent.original": "Nero SIPPS IP Phone Version 2.0.51.16", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 192.168.1.2;branch=z9hG4bKnp112903503-43a64480192.168.1.2;rport" + ], + "source.ip": "192.168.1.2", + "source.port": 5060, + "status": "OK", + "type": "sip", + "user.name": "voi18062" +} +``` + diff --git a/packetbeat/protos/sip/_meta/fields.yml b/packetbeat/protos/sip/_meta/fields.yml new file mode 100644 index 000000000000..fcbb1349dd15 --- /dev/null +++ b/packetbeat/protos/sip/_meta/fields.yml @@ -0,0 +1,238 @@ +- key: sip + title: "SIP" + description: SIP-specific event fields. + fields: + - name: sip + type: group + description: Information about SIP traffic. + fields: + - name: timestamp + type: date_nanos + description: Timestamp with nano second precision. + - name: code + type: keyword + description: Response status code. + - name: method + type: keyword + description: Request method. + - name: status + type: keyword + description: Response status phrase. + - name: type + type: keyword + description: Either request or response. + - name: version + type: keyword + description: SIP protocol version. + - name: uri.original + type: keyword + description: The original URI. + multi_fields: + - name: text + type: text + analyzer: simple + - name: uri.scheme + type: keyword + description: The URI scheme. + - name: uri.username + type: keyword + description: The URI user name. + - name: uri.host + type: keyword + description: The URI host. + - name: uri.port + type: keyword + description: The URI port. + - name: accept + type: keyword + description: Accept header value. + - name: allow + type: keyword + description: Allowed methods. + - name: call_id + type: keyword + description: Call ID. + - name: content_length + type: long + - name: content_type + type: keyword + - name: max_forwards + type: long + - name: supported + type: keyword + description: Supported methods. + - name: user_agent.original + type: keyword + multi_fields: + - name: text + type: text + analyzer: simple + - name: private.uri.original + type: keyword + description: Private original URI. + multi_fields: + - name: text + type: text + analyzer: simple + - name: private.uri.scheme + type: keyword + description: Private URI scheme. + - name: private.uri.username + type: keyword + description: Private URI user name. + - name: private.uri.host + type: keyword + description: Private URI host. + - name: private.uri.port + type: keyword + description: Private URI port. + - name: cseq.code + type: keyword + description: Sequence code. + - name: cseq.method + type: keyword + description: Sequence method. + - name: via.original + type: keyword + description: The original Via value. + multi_fields: + - name: text + type: text + analyzer: simple + - name: to.display_info + type: keyword + description: "To display info" + - name: to.uri.original + type: keyword + description: "To original URI" + multi_fields: + - name: text + type: text + analyzer: simple + - name: to.uri.scheme + type: keyword + description: "To URI scheme" + - name: to.uri.username + type: keyword + description: "To URI user name" + - name: to.uri.host + type: keyword + description: "To URI host" + - name: to.uri.port + type: keyword + description: "To URI port" + - name: to.tag + type: keyword + description: "To tag" + - name: from.display_info + type: keyword + description: "From display info" + - name: from.uri.original + type: keyword + description: "From original URI" + multi_fields: + - name: text + type: text + analyzer: simple + - name: from.uri.scheme + type: keyword + description: "From URI scheme" + - name: from.uri.username + type: keyword + description: "From URI user name" + - name: from.uri.host + type: keyword + description: "From URI host" + - name: from.uri.port + type: keyword + description: "From URI port" + - name: from.tag + type: keyword + description: "From tag" + - name: contact.display_info + type: keyword + description: "Contact display info" + - name: contact.uri.original + type: keyword + description: "Contact original URI" + multi_fields: + - name: text + type: text + analyzer: simple + - name: contact.uri.scheme + type: keyword + description: "Contat URI scheme" + - name: contact.uri.username + type: keyword + description: "Contact URI user name" + - name: contact.uri.host + type: keyword + description: "Contact URI host" + - name: contact.uri.port + type: keyword + description: "Contact URI port" + - name: contact.transport + type: keyword + description: "Contact transport" + - name: contact.line + type: keyword + description: "Contact line" + - name: contact.expires + type: keyword + description: "Contact expires" + - name: contact.q + type: keyword + description: "Contact Q" + - name: auth.scheme + type: keyword + description: "Auth scheme" + - name: auth.realm + type: keyword + description: "Auth realm" + - name: auth.uri.original + type: keyword + description: "Auth original URI" + multi_fields: + - name: text + type: text + analyzer: simple + - name: auth.uri.scheme + type: keyword + description: "Auth URI scheme" + - name: auth.uri.host + type: keyword + description: "Auth URI host" + - name: auth.uri.port + type: keyword + description: "Auth URI port" + - name: sdp.version + type: keyword + description: "SDP version" + - name: sdp.owner.username + type: keyword + description: "SDP owner user name" + - name: sdp.owner.session_id + type: keyword + description: "SDP owner session ID" + - name: sdp.owner.version + type: keyword + description: "SDP owner version" + - name: sdp.owner.ip + type: ip + description: "SDP owner IP" + - name: sdp.session.name + type: keyword + description: "SDP session name" + - name: sdp.connection.info + type: keyword + description: "SDP connection info" + - name: sdp.connection.address + type: keyword + description: "SDP connection address" + - name: sdp.body.original + type: keyword + description: "SDP original body" + multi_fields: + - name: text + type: text + analyzer: simple diff --git a/packetbeat/protos/sip/config.go b/packetbeat/protos/sip/config.go new file mode 100644 index 000000000000..58a92606e80d --- /dev/null +++ b/packetbeat/protos/sip/config.go @@ -0,0 +1,41 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package sip + +import ( + cfg "github.com/elastic/beats/v7/packetbeat/config" + "github.com/elastic/beats/v7/packetbeat/protos" +) + +type config struct { + cfg.ProtocolCommon `config:",inline"` + ParseAuthorization bool `config:"parse_authorization"` + ParseBody bool `config:"parse_body"` + KeepOriginal bool `config:"keep_original"` +} + +var ( + defaultConfig = config{ + ProtocolCommon: cfg.ProtocolCommon{ + TransactionTimeout: protos.DefaultTransactionExpiration, + }, + ParseAuthorization: true, + ParseBody: true, + KeepOriginal: true, + } +) diff --git a/packetbeat/protos/sip/event.go b/packetbeat/protos/sip/event.go new file mode 100644 index 000000000000..fcb9112fe93c --- /dev/null +++ b/packetbeat/protos/sip/event.go @@ -0,0 +1,102 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package sip + +import ( + "github.com/elastic/beats/v7/libbeat/common" +) + +// ProtocolFields contains SIP fields. +type ProtocolFields struct { + Timestamp int64 `ecs:"timestamp"` + Code int `ecs:"code"` + Method common.NetString `ecs:"method"` + Status common.NetString `ecs:"status"` + Type string `ecs:"type"` + Version string `ecs:"version"` + + URIOriginal common.NetString `ecs:"uri.original"` + URIScheme common.NetString `ecs:"uri.scheme"` + URIUsername common.NetString `ecs:"uri.username"` + URIHost common.NetString `ecs:"uri.host"` + URIPort int `ecs:"uri.port"` + + Accept common.NetString `ecs:"accept"` + Allow []string `ecs:"allow"` + CallID common.NetString `ecs:"call_id"` + ContentLength int `ecs:"content_length"` + ContentType common.NetString `ecs:"content_type"` + MaxForwards int `ecs:"max_forwards"` + Supported []string `ecs:"supported"` + UserAgentOriginal common.NetString `ecs:"user_agent.original"` + + PrivateURIOriginal common.NetString `ecs:"private.uri.original"` + PrivateURIScheme common.NetString `ecs:"private.uri.scheme"` + PrivateURIUsername common.NetString `ecs:"private.uri.username"` + PrivateURIHost common.NetString `ecs:"private.uri.host"` + PrivateURIPort int `ecs:"private.uri.port"` + + CseqCode int `ecs:"cseq.code"` + CseqMethod common.NetString `ecs:"cseq.method"` + + ViaOriginal []common.NetString `ecs:"via.original"` + + ToDisplayInfo common.NetString `ecs:"to.display_info"` + ToURIOriginal common.NetString `ecs:"to.uri.original"` + ToURIScheme common.NetString `ecs:"to.uri.scheme"` + ToURIUsername common.NetString `ecs:"to.uri.username"` + ToURIHost common.NetString `ecs:"to.uri.host"` + ToURIPort int `ecs:"to.uri.port"` + ToTag common.NetString `ecs:"to.tag"` + + FromDisplayInfo common.NetString `ecs:"from.display_info"` + FromURIOriginal common.NetString `ecs:"from.uri.original"` + FromURIScheme common.NetString `ecs:"from.uri.scheme"` + FromURIUsername common.NetString `ecs:"from.uri.username"` + FromURIHost common.NetString `ecs:"from.uri.host"` + FromURIPort int `ecs:"from.uri.port"` + FromTag common.NetString `ecs:"from.tag"` + + ContactDisplayInfo common.NetString `ecs:"contact.display_info"` + ContactURIOriginal common.NetString `ecs:"contact.uri.original"` + ContactURIScheme common.NetString `ecs:"contact.uri.scheme"` + ContactURIUsername common.NetString `ecs:"contact.uri.username"` + ContactURIHost common.NetString `ecs:"contact.uri.host"` + ContactURIPort int `ecs:"contact.uri.port"` + ContactTransport common.NetString `ecs:"contact.transport"` + ContactLine common.NetString `ecs:"contact.line"` + ContactExpires int `ecs:"contact.expires"` + ContactQ float64 `ecs:"contact.q"` + + AuthScheme common.NetString `ecs:"auth.scheme"` + AuthRealm common.NetString `ecs:"auth.realm"` + AuthURIOriginal common.NetString `ecs:"auth.uri.original"` + AuthURIScheme common.NetString `ecs:"auth.uri.scheme"` + AuthURIHost common.NetString `ecs:"auth.uri.host"` + AuthURIPort int `ecs:"auth.uri.port"` + + SDPVersion string `ecs:"sdp.version"` + SDPOwnerUsername common.NetString `ecs:"sdp.owner.username"` + SDPOwnerSessID common.NetString `ecs:"sdp.owner.session_id"` + SDPOwnerVersion common.NetString `ecs:"sdp.owner.version"` + SDPOwnerIP common.NetString `ecs:"sdp.owner.ip"` + SDPSessName common.NetString `ecs:"sdp.session.name"` + SDPConnInfo common.NetString `ecs:"sdp.connection.info"` + SDPConnAddr common.NetString `ecs:"sdp.connection.address"` + SDPBodyOriginal common.NetString `ecs:"sdp.body.original"` +} diff --git a/packetbeat/protos/sip/fields.go b/packetbeat/protos/sip/fields.go new file mode 100644 index 000000000000..87c93ab0cad2 --- /dev/null +++ b/packetbeat/protos/sip/fields.go @@ -0,0 +1,36 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Code generated by beats/dev-tools/cmd/asset/asset.go - DO NOT EDIT. + +package sip + +import ( + "github.com/elastic/beats/v7/libbeat/asset" +) + +func init() { + if err := asset.SetFields("packetbeat", "sip", asset.ModuleFieldsPri, AssetSip); err != nil { + panic(err) + } +} + +// AssetSip returns asset data. +// This is the base64 encoded gzipped contents of protos/sip. +func AssetSip() string { + return "eJzEmcFy4jgQhu95ii7u8QNwm5rsVnHLhsxeKY3dxqqRJUdqQ9in35KxFGEkZqRUhZzAcX+/1Pz9ozKP8AtPazB8eAAgTgLXsNpunlcPAA2aWvOBuJJr2G6eH82ANW95DXhASdByFI2pHmB+tX4AAHgEyXp0SPtHpwHXsNdqdFcuyBvZKt0z+wbYTzWS1QLSrG15Xc0VocKHBvEeDbHecZ1Wwwh3kkll/D8uJF9dHRw5dWDvBIO1kg0MGmtuuJLVQqtWDS5kfuHpqHQT13hBMyhpEAwxGs1Uv2T2SJ1q8qhvIxqaK5e8s9RnVjl0mpmrdVpQDvUvTh1q0PNilX151lmSD6hts3Pg1h2DVqRqJVz9EjtqXinN91wykcN+7RBcHfx42VT+tn4UxHeXNrxoEb5TcNmpXV1mkonTf6jtgPSDwMjCTd1hn9Vvu+wfLxs4V8aaMRrU9l0J1dZOqBi4U4ZKoLYuxhuULuLZuiWP1TUOWbRvUwV0yBrUcGBivNo0E0Ids5i2AJt5ZM1VrjAhdjwrBL4zIWDzdJ1QklDSTqDcU7cACiX3ift/O90+rtj7rlX6yHSzDJkI3oyD/VAwa2tbV5RqlzXjju1R0p8O+BdN7qD5gRFWpdHzfK6/b/yEm8iPIbeFdBSF/JJIChWSsRSK5MZTKBCLqJCdG1UhOxZXtcG3KveYsbXfsbLG6PliIuYfMjwzfso4cPb5L9d/ObvM1y/zOKmq4WYQ7LTjslU5O1i9KphrwdaurtGl42/R4eiv7tCWsqm3K/+Y+ERLSobdgf2gJ9i5M+64ti6BzB1th7R1ESSxfS6N2H4JarXqy637t1b9TfNO+GL7Tvi7Gtivv8DC0+rTJvboIht7eNLInp9tZc+Omdljs+3ssTFDT9hcS0/EiKntIZTVVO7r72fATWs7kWJ3O5G7GjzcRYHHpz3QDZeH/CKjuy7d9Hqokm33UCHm+BCebfoQHvO9g5Nm0hTTfXUKL7gsa7stTEHxfeAasx4Nee5cm0K/FUH/WeLYSF2Jrb+N1CUMPSE1MtHnE6eyKLA4RCbuXRPEr7+0z+nw8Ojsmfbg2EB7bPa8eWxs1kwzVAWPHlfbp2f3yDGGVEeJuiw7LXkqTyfnh4JBY5eQ+dgo0JgBsHlKi5T256zw2y7x5UP74EIKev5dYsmbN1MV9dx1ItXwWkmJtS2oso8mlv9RHz2ZLCRY02g0eTG9UJkRMaGfqjmVRdf0EbjkspgviK7/AwAA///sesAq" +} diff --git a/packetbeat/protos/sip/parser.go b/packetbeat/protos/sip/parser.go new file mode 100644 index 000000000000..55e66045e956 --- /dev/null +++ b/packetbeat/protos/sip/parser.go @@ -0,0 +1,469 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package sip + +import ( + "bytes" + "errors" + "fmt" + "time" + "unicode" + + "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/libbeat/common/streambuf" + "github.com/elastic/beats/v7/packetbeat/procs" + "github.com/elastic/beats/v7/packetbeat/protos" +) + +// Http Message +type message struct { + ts time.Time + hasContentLength bool + headerOffset int + + isRequest bool + ipPortTuple common.IPPortTuple + cmdlineTuple *common.ProcessTuple + + // Info + requestURI common.NetString + method common.NetString + statusCode uint16 + statusPhrase common.NetString + version version + + // Headers + contentLength int + contentType common.NetString + userAgent common.NetString + to common.NetString + from common.NetString + cseq common.NetString + callID common.NetString + maxForwards int + via []common.NetString + allow []string + supported []string + + headers map[string][]common.NetString + size uint64 + + firstLine []byte + rawHeaders []byte + body []byte + rawData []byte +} + +type version struct { + major uint8 + minor uint8 +} + +func (v version) String() string { + return fmt.Sprintf("%d.%d", v.major, v.minor) +} + +type parserState uint8 + +const ( + stateStart parserState = iota + stateHeaders + stateBody +) + +type parser struct { +} + +type parsingInfo struct { + tuple *common.IPPortTuple + + data []byte + + parseOffset int + state parserState + bodyReceived int + + pkt *protos.Packet +} + +var ( + constCRLF = []byte("\r\n") + constSIPVersion = []byte("SIP/") + nameContentLength = []byte("content-length") + nameContentType = []byte("content-type") + nameUserAgent = []byte("user-agent") + nameTo = []byte("to") + nameFrom = []byte("from") + nameCseq = []byte("cseq") + nameCallID = []byte("call-id") + nameMaxForwards = []byte("max-forwards") + nameAllow = []byte("allow") + nameSupported = []byte("supported") + nameVia = []byte("via") +) + +func newParser() *parser { + return &parser{} +} + +func (parser *parser) parse(pi *parsingInfo) (*message, error) { + m := &message{ + ts: pi.pkt.Ts, + ipPortTuple: pi.pkt.Tuple, + cmdlineTuple: procs.ProcWatcher.FindProcessesTupleTCP(&pi.pkt.Tuple), + rawData: pi.data, + } + for pi.parseOffset < len(pi.data) { + switch pi.state { + case stateStart: + if err := parser.parseSIPLine(pi, m); err != nil { + return m, err + } + case stateHeaders: + if err := parser.parseHeaders(pi, m); err != nil { + return m, err + } + case stateBody: + parser.parseBody(pi, m) + } + } + return m, nil +} + +func (*parser) parseSIPLine(pi *parsingInfo, m *message) error { + // ignore any CRLF appearing before the start-line (RFC3261 7.5) + pi.data = bytes.TrimLeft(pi.data[pi.parseOffset:], string(constCRLF)) + + i := bytes.Index(pi.data[pi.parseOffset:], constCRLF) + if i == -1 { + return errors.New("not found expected CRLF") + } + + // Very basic tests on the first line. Just to check that + // we have what looks as a SIP message + var ( + version []byte + err error + ) + + fline := pi.data[pi.parseOffset:i] + if len(fline) < 16 { // minimum line will be "SIP/2.0 XXX OK\r\n" + if isDebug { + debugf("First line too small") + } + return errors.New("first line too small") + } + + m.firstLine = fline + + if bytes.Equal(fline[0:4], constSIPVersion) { + // RESPONSE + m.isRequest = false + version = fline[4:7] + m.statusCode, m.statusPhrase, err = parseResponseStatus(fline[8:]) + if err != nil { + if isDebug { + debugf("Failed to understand SIP response status: %s", fline[8:]) + } + return errors.New("failed to parse response status") + } + + if isDebug { + debugf("SIP status_code=%d, status_phrase=%s", m.statusCode, m.statusPhrase) + } + } else { + // REQUEST + afterMethodIdx := bytes.IndexFunc(fline, unicode.IsSpace) + afterRequestURIIdx := bytes.LastIndexFunc(fline, unicode.IsSpace) + + // Make sure we have the VERB + URI + SIP_VERSION + if afterMethodIdx == -1 || afterRequestURIIdx == -1 || afterMethodIdx == afterRequestURIIdx { + if isDebug { + debugf("Couldn't understand SIP request: %s", fline) + } + return errors.New("failed to parse SIP request") + } + + m.method = common.NetString(fline[:afterMethodIdx]) + m.requestURI = common.NetString(fline[afterMethodIdx+1 : afterRequestURIIdx]) + + versionIdx := afterRequestURIIdx + len(constSIPVersion) + 1 + if len(fline) > versionIdx && bytes.Equal(fline[afterRequestURIIdx+1:versionIdx], constSIPVersion) { + m.isRequest = true + version = fline[versionIdx:] + } else { + if isDebug { + debugf("Couldn't understand SIP version: %s", fline) + } + return errors.New("failed to parse SIP version") + } + } + + m.version.major, m.version.minor, err = parseVersion(version) + if err != nil { + if isDebug { + debugf(err.Error(), version) + } + return err + } + if isDebug { + debugf("SIP version %d.%d", m.version.major, m.version.minor) + } + + // ok so far + pi.parseOffset = i + 2 + m.headerOffset = pi.parseOffset + pi.state = stateHeaders + + return nil +} + +func parseResponseStatus(s []byte) (uint16, []byte, error) { + if isDebug { + debugf("parseResponseStatus: %s", s) + } + + var phrase []byte + p := bytes.IndexByte(s, ' ') + if p == -1 { + p = len(s) + } else { + phrase = s[p+1:] + } + statusCode, err := parseInt(s[0:p]) + if err != nil { + return 0, nil, fmt.Errorf("Unable to parse status code from [%s]", s) + } + return uint16(statusCode), phrase, nil +} + +func parseVersion(s []byte) (uint8, uint8, error) { + if len(s) < 3 { + return 0, 0, errors.New("Invalid version") + } + + major := s[0] - '0' + minor := s[2] - '0' + + return uint8(major), uint8(minor), nil +} + +func (parser *parser) parseHeaders(pi *parsingInfo, m *message) error { + // check if it isn't headers end yet with /r/n/r/n + if !(len(pi.data)-pi.parseOffset >= 2 && + bytes.Equal(pi.data[pi.parseOffset:pi.parseOffset+2], constCRLF)) { + offset, err := parser.parseHeader(m, pi.data[pi.parseOffset:]) + if err != nil { + return err + } + + pi.parseOffset += offset + + return nil + } + + m.size = uint64(pi.parseOffset + 2) + m.rawHeaders = pi.data[:m.size] + pi.data = pi.data[m.size:] + pi.parseOffset = 0 + + if m.contentLength == 0 && (m.isRequest || m.hasContentLength) { + if isDebug { + debugf("Empty content length, ignore body") + } + return nil + } + + if isDebug { + debugf("Read body") + } + + pi.state = stateBody + + return nil +} + +func (parser *parser) parseHeader(m *message, data []byte) (int, error) { + if m.headers == nil { + m.headers = make(map[string][]common.NetString) + } + + i := bytes.Index(data, []byte(":")) + if i == -1 { + // Expected \":\" in headers. Assuming incomplete + if isDebug { + debugf("ignoring incomplete header %s", data) + } + return len(data), nil + } + + // enabled if required. Allocs for parameters slow down parser big times + if isDetailed { + detailedf("Data: %s", data) + detailedf("Header: %s", data[:i]) + } + + // skip folding line + for p := i + 1; p < len(data); { + q := bytes.Index(data[p:], constCRLF) + if q == -1 { + if isDebug { + debugf("ignoring incomplete header %s", data) + } + return len(data), nil + } + + p += q + if len(data) > p && (data[p+1] == ' ' || data[p+1] == '\t') { + p = p + 2 + continue + } + + headerName := getExpandedHeaderName(bytes.ToLower(data[:i])) + headerVal := bytes.TrimSpace(data[i+1 : p]) + if isDebug { + debugf("Header: '%s' Value: '%s'\n", data[:i], headerVal) + } + + // Headers we need for parsing. Make sure we always + // capture their value + switch { + case bytes.Equal(headerName, nameMaxForwards): + m.maxForwards, _ = parseInt(headerVal) + case bytes.Equal(headerName, nameContentLength): + m.contentLength, _ = parseInt(headerVal) + m.hasContentLength = true + case bytes.Equal(headerName, nameContentType): + m.contentType = headerVal + case bytes.Equal(headerName, nameUserAgent): + m.userAgent = headerVal + case bytes.Equal(headerName, nameTo): + m.to = headerVal + case bytes.Equal(headerName, nameFrom): + m.from = headerVal + case bytes.Equal(headerName, nameCseq): + m.cseq = headerVal + case bytes.Equal(headerName, nameCallID): + m.callID = headerVal + case bytes.Equal(headerName, nameAllow): + m.allow = parseCommaSeparatedList(headerVal) + case bytes.Equal(headerName, nameSupported): + m.supported = parseCommaSeparatedList(headerVal) + case bytes.Equal(headerName, nameVia): + m.via = append(m.via, headerVal) + } + + m.headers[string(headerName)] = append( + m.headers[string(headerName)], + headerVal, + ) + + return p + 2, nil + } + + return len(data), nil +} + +func parseCommaSeparatedList(s common.NetString) (list []string) { + values := bytes.Split(s, []byte(",")) + list = make([]string, len(values)) + for idx := range values { + list[idx] = string(bytes.ToLower(bytes.Trim(values[idx], " "))) + } + return list +} + +func (*parser) parseBody(pi *parsingInfo, m *message) { + nbytes := len(pi.data) + if nbytes >= m.contentLength-pi.bodyReceived { + wanted := m.contentLength - pi.bodyReceived + m.body = append(m.body, pi.data[:wanted]...) + pi.bodyReceived = m.contentLength + m.size += uint64(wanted) + pi.data = pi.data[wanted:] + } else { + m.body = append(m.body, pi.data...) + pi.data = nil + pi.bodyReceived += nbytes + m.size += uint64(nbytes) + if isDebug { + debugf("bodyReceived: %d", pi.bodyReceived) + } + } +} + +func (m *message) getEndpoints() (src *common.Endpoint, dst *common.Endpoint) { + source, destination := common.MakeEndpointPair(m.ipPortTuple.BaseTuple, m.cmdlineTuple) + src, dst = &source, &destination + return src, dst +} + +func parseInt(line []byte) (int, error) { + buf := streambuf.NewFixed(line) + i, err := buf.IntASCII(false) + return int(i), err + // TODO: is it an error if 'buf.Len() != 0 {}' ? +} + +func getExpandedHeaderName(n []byte) []byte { + if len(n) > 1 { + return n + } + switch string(n) { + // referfenced by https://www.iana.org/assignments/sip-parameters/sip-parameters.xhtml + case "a": + return []byte("accept-contact") //[RFC3841] + case "b": + return []byte("referred-by") //[RFC3892] + case "c": + return []byte("content-type") //[RFC3261] + case "d": + return []byte("request-disposition") //[RFC3841] + case "e": + return []byte("content-encoding") //[RFC3261] + case "f": + return []byte("from") //[RFC3261] + case "i": + return []byte("call-id") //[RFC3261] + case "j": + return []byte("reject-contact") //[RFC3841] + case "k": + return []byte("supported") //[RFC3261] + case "l": + return []byte("content-length") //[RFC3261] + case "m": + return []byte("contact") //[RFC3261] + case "o": + return []byte("event") //[RFC666)5] [RFC6446] + case "r": + return []byte("refer-to") //[RFC3515] + case "s": + return []byte("subject") //[RFC3261] + case "t": + return []byte("to") //[RFC3261] + case "u": + return []byte("allow-events") //[RFC6665] + case "v": + return []byte("via") //[RFC326)1] [RFC7118] + case "x": + return []byte("session-expires") //[RFC4028] + case "y": + return []byte("identity") //[RFC8224] + } + return n +} diff --git a/packetbeat/protos/sip/plugin.go b/packetbeat/protos/sip/plugin.go new file mode 100644 index 000000000000..e4cc0364d9a1 --- /dev/null +++ b/packetbeat/protos/sip/plugin.go @@ -0,0 +1,617 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package sip + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "time" + + "github.com/elastic/beats/v7/libbeat/beat" + "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/libbeat/common/cfgwarn" + "github.com/elastic/beats/v7/libbeat/logp" + "github.com/elastic/beats/v7/packetbeat/pb" + "github.com/elastic/beats/v7/packetbeat/protos" +) + +var ( + debugf = logp.MakeDebug("sip") + detailedf = logp.MakeDebug("sipdetailed") +) + +// SIP application level protocol analyser plugin. +type plugin struct { + // config + ports []int + parseAuthorization bool + parseBody bool + keepOriginal bool + + results protos.Reporter +} + +var ( + isDebug = false + isDetailed = false +) + +func init() { + protos.Register("sip", New) +} + +func New( + testMode bool, + results protos.Reporter, + cfg *common.Config, +) (protos.Plugin, error) { + cfgwarn.Beta("packetbeat SIP protocol is used") + + p := &plugin{} + config := defaultConfig + if !testMode { + if err := cfg.Unpack(&config); err != nil { + return nil, err + } + } + + if err := p.init(results, &config); err != nil { + return nil, err + } + return p, nil +} + +// Init initializes the HTTP protocol analyser. +func (p *plugin) init(results protos.Reporter, config *config) error { + p.setFromConfig(config) + + isDebug = logp.IsDebug("sip") + isDetailed = logp.IsDebug("sipdetailed") + p.results = results + return nil +} + +func (p *plugin) setFromConfig(config *config) { + p.ports = config.Ports + p.keepOriginal = config.KeepOriginal + p.parseAuthorization = config.ParseAuthorization + p.parseBody = config.ParseBody +} + +func (p *plugin) GetPorts() []int { + return p.ports +} + +func (p *plugin) ParseUDP(pkt *protos.Packet) { + defer logp.Recover("SIP ParseUDP exception") + + if err := p.doParse(pkt); err != nil { + debugf("error: %s", err) + } +} + +func (p *plugin) doParse(pkt *protos.Packet) error { + if isDetailed { + detailedf("Payload received: [%s]", pkt.Payload) + } + + parser := newParser() + + pi := newParsingInfo(pkt) + m, err := parser.parse(pi) + if err != nil { + return err + } + + evt, err := p.buildEvent(m, pkt) + if err != nil { + return err + } + + p.publish(*evt) + + return nil +} + +func (p *plugin) publish(evt beat.Event) { + if p.results != nil { + p.results(evt) + } +} + +func newParsingInfo(pkt *protos.Packet) *parsingInfo { + return &parsingInfo{ + tuple: &pkt.Tuple, + data: pkt.Payload, + pkt: pkt, + } +} + +func (p *plugin) buildEvent(m *message, pkt *protos.Packet) (*beat.Event, error) { + status := common.OK_STATUS + if m.statusCode >= 400 { + status = common.ERROR_STATUS + } + + evt, pbf := pb.NewBeatEvent(m.ts) + fields := evt.Fields + fields["type"] = "sip" + fields["status"] = status + + var sipFields ProtocolFields + sipFields.Timestamp = time.Now().UnixNano() + if m.isRequest { + populateRequestFields(m, pbf, &sipFields) + } else { + populateResponseFields(m, &sipFields) + } + + p.populateHeadersFields(m, evt, pbf, &sipFields) + + if p.parseBody { + populateBodyFields(m, pbf, &sipFields) + } + + pbf.Network.IANANumber = "17" + pbf.Network.Application = "sip" + pbf.Network.Protocol = "sip" + pbf.Network.Transport = "udp" + + src, dst := m.getEndpoints() + pbf.SetSource(src) + pbf.SetDestination(dst) + + p.populateEventFields(m, pbf, sipFields) + + if err := pb.MarshalStruct(evt.Fields, "sip", sipFields); err != nil { + return nil, err + } + + return &evt, nil +} + +func populateRequestFields(m *message, pbf *pb.Fields, fields *ProtocolFields) { + fields.Type = "request" + fields.Method = bytes.ToUpper(m.method) + fields.URIOriginal = m.requestURI + scheme, username, host, port, _ := parseURI(fields.URIOriginal) + fields.URIScheme = scheme + fields.URIHost = host + if !bytes.Equal(username, []byte(" ")) && !bytes.Equal(username, []byte("-")) { + fields.URIUsername = username + pbf.AddUser(string(username)) + } + fields.URIPort = port + fields.Version = m.version.String() + pbf.AddHost(string(host)) +} + +func populateResponseFields(m *message, fields *ProtocolFields) { + fields.Type = "response" + fields.Code = int(m.statusCode) + fields.Status = m.statusPhrase + fields.Version = m.version.String() +} + +func (p *plugin) populateHeadersFields(m *message, evt beat.Event, pbf *pb.Fields, fields *ProtocolFields) { + fields.Allow = m.allow + fields.CallID = m.callID + fields.ContentLength = m.contentLength + fields.ContentType = bytes.ToLower(m.contentType) + fields.MaxForwards = m.maxForwards + fields.Supported = m.supported + fields.UserAgentOriginal = m.userAgent + fields.ViaOriginal = m.via + + privateURI, found := m.headers["p-associated-uri"] + if found && len(privateURI) > 0 { + scheme, username, host, port, _ := parseURI(privateURI[0]) + fields.PrivateURIOriginal = privateURI[0] + fields.PrivateURIScheme = scheme + fields.PrivateURIHost = host + if !bytes.Equal(username, []byte(" ")) && !bytes.Equal(username, []byte("-")) { + fields.PrivateURIUsername = username + pbf.AddUser(string(username)) + } + fields.PrivateURIPort = port + pbf.AddHost(string(host)) + } + + if accept, found := m.headers["accept"]; found && len(accept) > 0 { + fields.Accept = bytes.ToLower(accept[0]) + } + + cseqParts := bytes.Split(m.cseq, []byte(" ")) + if len(cseqParts) == 2 { + fields.CseqCode, _ = strconv.Atoi(string(cseqParts[0])) + fields.CseqMethod = bytes.ToUpper(cseqParts[1]) + } + + populateFromFields(m, pbf, fields) + + populateToFields(m, pbf, fields) + + populateContactFields(m, pbf, fields) + + if p.parseAuthorization { + populateAuthFields(m, evt, pbf, fields) + } +} + +func populateFromFields(m *message, pbf *pb.Fields, fields *ProtocolFields) { + if len(m.from) > 0 { + displayInfo, uri, params := parseFromToContact(m.from) + fields.FromDisplayInfo = displayInfo + fields.FromTag = params["tag"] + scheme, username, host, port, _ := parseURI(uri) + fields.FromURIOriginal = uri + fields.FromURIScheme = scheme + fields.FromURIHost = host + if !bytes.Equal(username, []byte(" ")) && !bytes.Equal(username, []byte("-")) { + fields.FromURIUsername = username + pbf.AddUser(string(username)) + } + fields.FromURIPort = port + pbf.AddHost(string(host)) + } +} + +func populateToFields(m *message, pbf *pb.Fields, fields *ProtocolFields) { + if len(m.to) > 0 { + displayInfo, uri, params := parseFromToContact(m.to) + fields.ToDisplayInfo = displayInfo + fields.ToTag = params["tag"] + scheme, username, host, port, _ := parseURI(uri) + fields.ToURIOriginal = uri + fields.ToURIScheme = scheme + fields.ToURIHost = host + if !bytes.Equal(username, []byte(" ")) && !bytes.Equal(username, []byte("-")) { + fields.ToURIUsername = username + pbf.AddUser(string(username)) + } + fields.ToURIPort = port + pbf.AddHost(string(host)) + } +} + +func populateContactFields(m *message, pbf *pb.Fields, fields *ProtocolFields) { + if contact, found := m.headers["contact"]; found && len(contact) > 0 { + displayInfo, uri, params := parseFromToContact(m.to) + fields.ContactDisplayInfo = displayInfo + fields.ContactExpires, _ = strconv.Atoi(string(params["expires"])) + fields.ContactQ, _ = strconv.ParseFloat(string(params["q"]), 64) + scheme, username, host, port, urlparams := parseURI(uri) + fields.ContactURIOriginal = uri + fields.ContactURIScheme = scheme + fields.ContactURIHost = host + if !bytes.Equal(username, []byte(" ")) && !bytes.Equal(username, []byte("-")) { + fields.ContactURIUsername = username + pbf.AddUser(string(username)) + } + fields.ContactURIPort = port + fields.ContactLine = urlparams["line"] + fields.ContactTransport = bytes.ToLower(urlparams["transport"]) + pbf.AddHost(string(host)) + } +} + +func (p *plugin) populateEventFields(m *message, pbf *pb.Fields, fields ProtocolFields) { + pbf.Event.Kind = "event" + pbf.Event.Type = []string{"info"} + pbf.Event.Dataset = "sip" + pbf.Event.Sequence = int64(fields.CseqCode) + + // TODO: Get these values from body + pbf.Event.Start = m.ts + pbf.Event.End = m.ts + // + + if p.keepOriginal { + pbf.Event.Original = string(m.rawData) + } + + pbf.Event.Category = []string{"network", "protocol"} + if _, found := m.headers["authorization"]; found { + pbf.Event.Category = append(pbf.Event.Category, "authentication") + } + + pbf.Event.Action = func() string { + if m.isRequest { + return fmt.Sprintf("sip-%s", strings.ToLower(string(m.method))) + } + return fmt.Sprintf("sip-%s", strings.ToLower(string(fields.CseqMethod))) + }() + + pbf.Event.Outcome = func() string { + switch { + case m.statusCode < 200: + return "" + case m.statusCode > 299: + return "failure" + } + return "success" + }() + + pbf.Event.Reason = string(fields.Status) +} + +func populateAuthFields(m *message, evt beat.Event, pbf *pb.Fields, fields *ProtocolFields) { + auths, found := m.headers["authorization"] + if !found || len(auths) == 0 { + if isDetailed { + detailedf("sip packet without authorization header") + } + return + } + + if isDetailed { + detailedf("sip packet with authorization header") + } + + auth := bytes.TrimSpace(auths[0]) + pos := bytes.IndexByte(auth, ' ') + if pos == -1 { + if isDebug { + debugf("malformed authorization header: missing scheme") + } + return + } + + fields.AuthScheme = auth[:pos] + + pos += 1 + for _, param := range bytes.Split(auth[pos:], []byte(",")) { + kv := bytes.SplitN(param, []byte("="), 2) + if len(kv) != 2 { + continue + } + kv[1] = bytes.Trim(kv[1], "'\" \t") + switch string(bytes.ToLower(bytes.TrimSpace(kv[0]))) { + case "realm": + fields.AuthRealm = kv[1] + case "username": + username := string(kv[1]) + if username != "" && username != "-" { + _, _ = evt.Fields.Put("user.name", username) + pbf.AddUser(username) + } + case "uri": + scheme, _, host, port, _ := parseURI(kv[1]) + fields.AuthURIOriginal = kv[1] + fields.AuthURIScheme = scheme + fields.AuthURIHost = host + fields.AuthURIPort = port + } + } +} + +var constSDPContentType = []byte("application/sdp") + +func populateBodyFields(m *message, pbf *pb.Fields, fields *ProtocolFields) { + if !m.hasContentLength { + return + } + + if !bytes.Equal(m.contentType, constSDPContentType) { + if isDebug { + debugf("body content-type: %s is not supported", m.contentType) + } + return + } + + if _, found := m.headers["content-encoding"]; found { + if isDebug { + debugf("body decoding is not supported yet if content-endcoding is present") + } + return + } + + fields.SDPBodyOriginal = m.body + + var isInMedia bool + for _, line := range bytes.Split(m.body, []byte("\r\n")) { + kv := bytes.SplitN(line, []byte("="), 2) + if len(kv) != 2 { + continue + } + + kv[1] = bytes.TrimSpace(kv[1]) + ch := string(bytes.ToLower(bytes.TrimSpace(kv[0]))) + switch ch { + case "v": + fields.SDPVersion = string(kv[1]) + case "o": + var pos int + if kv[1][pos] == '"' { + endUserPos := bytes.IndexByte(kv[1][pos+1:], '"') + if !bytes.Equal(kv[1][pos+1:endUserPos], []byte("-")) { + fields.SDPOwnerUsername = kv[1][pos+1 : endUserPos] + } + pos = endUserPos + 1 + } + nParts := func() int { + if pos == 0 { + return 4 + } + return 3 // already have user + }() + parts := bytes.SplitN(kv[1][pos:], []byte(" "), nParts) + if len(parts) != nParts { + if isDebug { + debugf("malformed owner SDP line") + } + continue + } + if nParts == 4 { + if !bytes.Equal(parts[0], []byte("-")) { + fields.SDPOwnerUsername = parts[0] + } + parts = parts[1:] + } + fields.SDPOwnerSessID = parts[0] + fields.SDPOwnerVersion = parts[1] + fields.SDPOwnerIP = func() common.NetString { + p := bytes.Split(parts[2], []byte(" ")) + return p[len(p)-1] + }() + pbf.AddUser(string(fields.SDPOwnerUsername)) + pbf.AddIP(string(fields.SDPOwnerIP)) + case "s": + if !bytes.Equal(kv[1], []byte("-")) { + fields.SDPSessName = kv[1] + } + case "c": + if isInMedia { + continue + } + fields.SDPConnInfo = kv[1] + fields.SDPConnAddr = func() common.NetString { + p := bytes.Split(kv[1], []byte(" ")) + return p[len(p)-1] + }() + pbf.AddHost(string(fields.SDPConnAddr)) + case "m": + isInMedia = true + // TODO + case "i", "u", "e", "p", "b", "t", "r", "z", "k", "a": + // TODO + } + } +} + +func parseFromToContact(fromTo common.NetString) (displayInfo, uri common.NetString, params map[string]common.NetString) { + params = make(map[string]common.NetString) + + pos := bytes.IndexByte(fromTo, '<') + if pos == -1 { + pos = bytes.IndexByte(fromTo, ' ') + } + + displayInfo = bytes.Trim(fromTo[:pos], "'\"\t ") + + endURIPos := func() int { + if fromTo[pos] == '<' { + return bytes.IndexByte(fromTo, '>') + } + return bytes.IndexByte(fromTo, ';') + }() + + if endURIPos == -1 { + uri = bytes.TrimRight(fromTo[pos:], ">") + return + } + pos += 1 + uri = fromTo[pos:endURIPos] + + pos = endURIPos + 1 + for _, param := range bytes.Split(fromTo[pos:], []byte(";")) { + kv := bytes.SplitN(param, []byte("="), 2) + if len(kv) != 2 { + continue + } + params[string(bytes.ToLower(bytes.TrimSpace(kv[0])))] = kv[1] + } + + return displayInfo, uri, params +} + +func parseURI(uri common.NetString) (scheme, username, host common.NetString, port int, params map[string]common.NetString) { + var ( + prevChar rune + inIPv6 bool + idx int + hasParams bool + ) + uri = bytes.TrimSpace(uri) + prevChar = ' ' + pos := -1 + ppos := -1 + epos := len(uri) + + params = make(map[string]common.NetString) +loop: + for idx = 0; idx < len(uri); idx++ { + curChar := rune(uri[idx]) + + switch { + case idx == 0: + colonIdx := bytes.Index(uri, []byte(":")) + if colonIdx == -1 { + break loop + } + scheme = uri[:colonIdx] + idx += colonIdx + pos = idx + 1 + + case curChar == '[' && prevChar != '\\': + inIPv6 = true + + case curChar == ']' && prevChar != '\\': + inIPv6 = false + + case curChar == ';' && prevChar != '\\': + // we found end of URI + hasParams = true + epos = idx + break loop + + default: + // select wich part + switch curChar { + case '@': + if len(host) > 0 { + pos = ppos + host = nil + } + username = uri[pos:idx] + ppos = pos + pos = idx + 1 + case ':': + if !inIPv6 { + host = uri[pos:idx] + ppos = pos + pos = idx + 1 + } + } + } + + prevChar = curChar + } + + if pos > 0 && epos <= len(uri) && pos <= epos { + if len(host) == 0 { + host = bytes.TrimSpace(uri[pos:epos]) + } else { + port, _ = strconv.Atoi(string(bytes.TrimSpace(uri[pos:epos]))) + } + } + + if hasParams { + for _, param := range bytes.Split(uri[epos+1:], []byte(";")) { + kv := bytes.Split(param, []byte("=")) + if len(kv) != 2 { + continue + } + params[string(bytes.ToLower(bytes.TrimSpace(kv[0])))] = kv[1] + } + } + + return scheme, username, host, port, params +} diff --git a/packetbeat/protos/sip/plugin_test.go b/packetbeat/protos/sip/plugin_test.go new file mode 100644 index 000000000000..fc9ee53aff2a --- /dev/null +++ b/packetbeat/protos/sip/plugin_test.go @@ -0,0 +1,168 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build !integration + +package sip + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/elastic/beats/v7/libbeat/beat" + "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/packetbeat/protos" +) + +func TestParseURI(t *testing.T) { + scheme, username, host, port, params := parseURI(common.NetString("sip:test@10.0.2.15:5060")) + assert.Equal(t, common.NetString("sip"), scheme) + assert.Equal(t, common.NetString("test"), username) + assert.Equal(t, common.NetString("10.0.2.15"), host) + assert.Equal(t, map[string]common.NetString{}, params) + assert.Equal(t, 5060, port) + + scheme, username, host, port, params = parseURI(common.NetString("sips:test@10.0.2.15:5061 ; transport=udp")) + assert.Equal(t, common.NetString("sips"), scheme) + assert.Equal(t, common.NetString("test"), username) + assert.Equal(t, common.NetString("10.0.2.15"), host) + assert.Equal(t, common.NetString("udp"), params["transport"]) + assert.Equal(t, 5061, port) + + scheme, username, host, port, params = parseURI(common.NetString("mailto:192.168.0.2")) + assert.Equal(t, common.NetString("mailto"), scheme) + assert.Equal(t, common.NetString(nil), username) + assert.Equal(t, common.NetString("192.168.0.2"), host) + assert.Equal(t, map[string]common.NetString{}, params) + assert.Equal(t, 0, port) +} + +func TestParseFromTo(t *testing.T) { + // To + displayInfo, uri, params := parseFromToContact(common.NetString("test ;tag=QvN921")) + assert.Equal(t, common.NetString("test"), displayInfo) + assert.Equal(t, common.NetString("sip:test@10.0.2.15:5060"), uri) + assert.Equal(t, common.NetString("QvN921"), params["tag"]) + displayInfo, uri, params = parseFromToContact(common.NetString("test ")) + assert.Equal(t, common.NetString("test"), displayInfo) + assert.Equal(t, common.NetString("sip:test@10.0.2.15:5060"), uri) + assert.Equal(t, common.NetString(nil), params["tag"]) + + // From + displayInfo, uri, params = parseFromToContact(common.NetString("\"PCMU/8000\" ;tag=1")) + assert.Equal(t, common.NetString("PCMU/8000"), displayInfo) + assert.Equal(t, common.NetString("sip:sipp@10.0.2.15:5060"), uri) + assert.Equal(t, common.NetString("1"), params["tag"]) + displayInfo, uri, params = parseFromToContact(common.NetString("\"Matthew Hodgson\" ;tag=5c7cdb68")) + assert.Equal(t, common.NetString("Matthew Hodgson"), displayInfo) + assert.Equal(t, common.NetString("sip:matthew@mxtelecom.com"), uri) + assert.Equal(t, common.NetString("5c7cdb68"), params["tag"]) + displayInfo, uri, params = parseFromToContact(common.NetString(";tag=5c7cdb68")) + assert.Equal(t, common.NetString(nil), displayInfo) + assert.Equal(t, common.NetString("sip:matthew@mxtelecom.com"), uri) + assert.Equal(t, common.NetString("5c7cdb68"), params["tag"]) + displayInfo, uri, params = parseFromToContact(common.NetString("")) + assert.Equal(t, common.NetString(nil), displayInfo) + assert.Equal(t, common.NetString("sip:matthew@mxtelecom.com"), uri) + assert.Equal(t, common.NetString(nil), params["tag"]) + + // Contact + displayInfo, uri, _ = parseFromToContact(common.NetString("")) + assert.Equal(t, common.NetString(nil), displayInfo) + assert.Equal(t, common.NetString("sip:test@10.0.2.15:5060;transport=udp"), uri) + displayInfo, uri, params = parseFromToContact(common.NetString(";expires=1200;q=0.500")) + assert.Equal(t, common.NetString(nil), displayInfo) + assert.Equal(t, common.NetString("sip:voi18062@192.168.1.2:5060;line=aca6b97ca3f5e51a"), uri) + assert.Equal(t, common.NetString("1200"), params["expires"]) + assert.Equal(t, common.NetString("0.500"), params["q"]) + displayInfo, uri, params = parseFromToContact(common.NetString(" \"Mr. Watson\" ;q=0.7; expires=3600")) + assert.Equal(t, common.NetString("Mr. Watson"), displayInfo) + assert.Equal(t, common.NetString("sip:watson@worcester.bell-telephone.com"), uri) + assert.Equal(t, common.NetString("3600"), params["expires"]) + assert.Equal(t, common.NetString("0.7"), params["q"]) + displayInfo, uri, params = parseFromToContact(common.NetString(" \"Mr. Watson\" ;q=0.1")) + assert.Equal(t, common.NetString("Mr. Watson"), displayInfo) + assert.Equal(t, common.NetString("mailto:watson@bell-telephone.com"), uri) + assert.Equal(t, common.NetString("0.1"), params["q"]) +} + +func TestParseUDP(t *testing.T) { + gotEvent := new(beat.Event) + reporter := func(evt beat.Event) { + gotEvent = &evt + } + const data = "INVITE sip:test@10.0.2.15:5060 SIP/2.0\r\nVia: SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2187-1-0\r\nFrom: \"DVI4/8000\" ;tag=1\r\nTo: test \r\nCall-ID: 1-2187@10.0.2.20\r\nCSeq: 1 INVITE\r\nContact: sip:sipp@10.0.2.20:5060\r\nMax-Forwards: 70\r\nContent-Type: application/sdp\r\nContent-Length: 123\r\n\r\nv=0\r\no=- 42 42 IN IP4 10.0.2.20\r\ns=-\r\nc=IN IP4 10.0.2.20\r\nt=0 0\r\nm=audio 6000 RTP/AVP 5\r\na=rtpmap:5 DVI4/8000\r\na=recvonly\r\n" + p, _ := New(true, reporter, nil) + plugin := p.(*plugin) + plugin.ParseUDP(&protos.Packet{ + Ts: time.Now(), + Tuple: common.IPPortTuple{}, + Payload: []byte(data), + }) + fields := *gotEvent + + assert.Equal(t, common.NetString("1-2187@10.0.2.20"), getVal(fields, "sip.call_id")) + assert.Equal(t, common.NetString("test"), getVal(fields, "sip.contact.display_info")) + assert.Equal(t, common.NetString("10.0.2.15"), getVal(fields, "sip.contact.uri.host")) + assert.Equal(t, common.NetString("sip:test@10.0.2.15:5060"), getVal(fields, "sip.contact.uri.original")) + assert.Equal(t, 5060, getVal(fields, "sip.contact.uri.port")) + assert.Equal(t, common.NetString("sip"), getVal(fields, "sip.contact.uri.scheme")) + assert.Equal(t, common.NetString("test"), getVal(fields, "sip.contact.uri.username")) + assert.Equal(t, 123, getVal(fields, "sip.content_length")) + assert.Equal(t, common.NetString("application/sdp"), getVal(fields, "sip.content_type")) + assert.Equal(t, 1, getVal(fields, "sip.cseq.code")) + assert.Equal(t, common.NetString("INVITE"), getVal(fields, "sip.cseq.method")) + assert.Equal(t, common.NetString("DVI4/8000"), getVal(fields, "sip.from.display_info")) + assert.Equal(t, common.NetString("1"), getVal(fields, "sip.from.tag")) + assert.Equal(t, common.NetString("10.0.2.20"), getVal(fields, "sip.from.uri.host")) + assert.Equal(t, common.NetString("sip:sipp@10.0.2.20:5060"), getVal(fields, "sip.from.uri.original")) + assert.Equal(t, 5060, getVal(fields, "sip.from.uri.port")) + assert.Equal(t, common.NetString("sip"), getVal(fields, "sip.from.uri.scheme")) + assert.Equal(t, common.NetString("sipp"), getVal(fields, "sip.from.uri.username")) + assert.Equal(t, 70, getVal(fields, "sip.max_forwards")) + assert.Equal(t, common.NetString("INVITE"), getVal(fields, "sip.method")) + assert.Equal(t, common.NetString("10.0.2.20"), getVal(fields, "sip.sdp.connection.address")) + assert.Equal(t, common.NetString("IN IP4 10.0.2.20"), getVal(fields, "sip.sdp.connection.info")) + assert.Equal(t, common.NetString("10.0.2.20"), getVal(fields, "sip.sdp.owner.ip")) + assert.Equal(t, common.NetString("42"), getVal(fields, "sip.sdp.owner.session_id")) + assert.Equal(t, common.NetString("42"), getVal(fields, "sip.sdp.owner.version")) + assert.Equal(t, nil, getVal(fields, "sip.sdp.owner.username")) + assert.Equal(t, nil, getVal(fields, "sip.sdp.session.name")) + assert.Equal(t, "0", getVal(fields, "sip.sdp.version")) + assert.Equal(t, common.NetString("test"), getVal(fields, "sip.to.display_info")) + assert.Equal(t, nil, getVal(fields, "sip.to.tag")) + assert.Equal(t, common.NetString("10.0.2.15"), getVal(fields, "sip.to.uri.host")) + assert.Equal(t, common.NetString("sip:test@10.0.2.15:5060"), getVal(fields, "sip.to.uri.original")) + assert.Equal(t, 5060, getVal(fields, "sip.to.uri.port")) + assert.Equal(t, common.NetString("sip"), getVal(fields, "sip.to.uri.scheme")) + assert.Equal(t, common.NetString("test"), getVal(fields, "sip.to.uri.username")) + assert.Equal(t, "request", getVal(fields, "sip.type")) + assert.Equal(t, common.NetString("10.0.2.15"), getVal(fields, "sip.uri.host")) + assert.Equal(t, common.NetString("sip:test@10.0.2.15:5060"), getVal(fields, "sip.uri.original")) + assert.Equal(t, 5060, getVal(fields, "sip.uri.port")) + assert.Equal(t, common.NetString("sip"), getVal(fields, "sip.uri.scheme")) + assert.Equal(t, common.NetString("test"), getVal(fields, "sip.uri.username")) + assert.Equal(t, "2.0", getVal(fields, "sip.version")) + assert.EqualValues(t, []common.NetString{common.NetString("SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2187-1-0")}, getVal(fields, "sip.via.original")) +} + +func getVal(f beat.Event, k string) interface{} { + v, _ := f.GetValue(k) + return v +} diff --git a/packetbeat/tests/system/config/golden-tests.yml b/packetbeat/tests/system/config/golden-tests.yml index 6d49ce9c2211..42ad0c746d28 100644 --- a/packetbeat/tests/system/config/golden-tests.yml +++ b/packetbeat/tests/system/config/golden-tests.yml @@ -27,3 +27,11 @@ test_cases: - name: TLS 1.3 pcap: pcaps/tls-version-13.pcap config: {} + + - name: SIP + pcap: pcaps/sip.pcap + config: {} + + - name: SIP Authenticated Register + pcap: pcaps/sip_authenticated_register.pcap + config: {} diff --git a/packetbeat/tests/system/config/packetbeat.yml.j2 b/packetbeat/tests/system/config/packetbeat.yml.j2 index b687f6f0402c..7b253d8ec2c7 100644 --- a/packetbeat/tests/system/config/packetbeat.yml.j2 +++ b/packetbeat/tests/system/config/packetbeat.yml.j2 @@ -148,6 +148,9 @@ packetbeat.protocols: {% if mongodb_max_docs is not none %} max_docs: {{mongodb_max_docs}}{% endif %} {% if mongodb_max_doc_length is not none %} max_doc_length: {{mongodb_max_doc_length}}{% endif %} +- type: sip + ports: [{{ sip_ports|default([5060])|join(", ") }}] + {% if procs_enabled %} #=========================== Monitored processes ============================== diff --git a/packetbeat/tests/system/golden/sip-expected.json b/packetbeat/tests/system/golden/sip-expected.json new file mode 100644 index 000000000000..37d95d715a79 --- /dev/null +++ b/packetbeat/tests/system/golden/sip-expected.json @@ -0,0 +1,668 @@ +[ + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "10.0.2.20", + "client.port": 5060, + "destination.ip": "10.0.2.15", + "destination.port": 5060, + "event.action": "sip-invite", + "event.category": [ + "network", + "protocol" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "INVITE sip:test@10.0.2.15:5060 SIP/2.0\r\nVia: SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2187-1-0\r\nFrom: \"DVI4/8000\" ;tag=1\r\nTo: test \r\nCall-ID: 1-2187@10.0.2.20\r\nCSeq: 1 INVITE\r\nContact: sip:sipp@10.0.2.20:5060\r\nMax-Forwards: 70\r\nContent-Type: application/sdp\r\nContent-Length: 123\r\n\r\nv=0\r\no=- 42 42 IN IP4 10.0.2.20\r\ns=-\r\nc=IN IP4 10.0.2.20\r\nt=0 0\r\nm=audio 6000 RTP/AVP 5\r\na=rtpmap:5 DVI4/8000\r\na=recvonly\r\n", + "event.sequence": 1, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:xDRQZvk3ErEhBDslXv1c6EKI804=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "10.0.2.15", + "10.0.2.20" + ], + "related.ip": [ + "10.0.2.20", + "10.0.2.15" + ], + "related.user": [ + "test", + "sipp" + ], + "server.ip": "10.0.2.15", + "server.port": 5060, + "sip.call_id": "1-2187@10.0.2.20", + "sip.contact.display_info": "test", + "sip.contact.uri.host": "10.0.2.15", + "sip.contact.uri.original": "sip:test@10.0.2.15:5060", + "sip.contact.uri.port": 5060, + "sip.contact.uri.scheme": "sip", + "sip.contact.uri.username": "test", + "sip.content_length": 123, + "sip.content_type": "application/sdp", + "sip.cseq.code": 1, + "sip.cseq.method": "INVITE", + "sip.from.display_info": "DVI4/8000", + "sip.from.tag": "1", + "sip.from.uri.host": "10.0.2.20", + "sip.from.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.from.uri.port": 5060, + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "sipp", + "sip.max_forwards": 70, + "sip.method": "INVITE", + "sip.sdp.body.original": "v=0\r\no=- 42 42 IN IP4 10.0.2.20\r\ns=-\r\nc=IN IP4 10.0.2.20\r\nt=0 0\r\nm=audio 6000 RTP/AVP 5\r\na=rtpmap:5 DVI4/8000\r\na=recvonly\r\n", + "sip.sdp.connection.address": "10.0.2.20", + "sip.sdp.connection.info": "IN IP4 10.0.2.20", + "sip.sdp.owner.ip": "10.0.2.20", + "sip.sdp.owner.session_id": "42", + "sip.sdp.owner.version": "42", + "sip.sdp.version": "0", + "sip.to.display_info": "test", + "sip.to.uri.host": "10.0.2.15", + "sip.to.uri.original": "sip:test@10.0.2.15:5060", + "sip.to.uri.port": 5060, + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "test", + "sip.type": "request", + "sip.uri.host": "10.0.2.15", + "sip.uri.original": "sip:test@10.0.2.15:5060", + "sip.uri.port": 5060, + "sip.uri.scheme": "sip", + "sip.uri.username": "test", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2187-1-0" + ], + "source.ip": "10.0.2.20", + "source.port": 5060, + "status": "OK", + "type": "sip" + }, + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "10.0.2.15", + "client.port": 5060, + "destination.ip": "10.0.2.20", + "destination.port": 5060, + "event.action": "sip-invite", + "event.category": [ + "network", + "protocol" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "SIP/2.0 100 Trying\r\nVia: SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2187-1-0\r\nFrom: \"DVI4/8000\" ;tag=1\r\nTo: test \r\nCall-ID: 1-2187@10.0.2.20\r\nCSeq: 1 INVITE\r\nUser-Agent: FreeSWITCH-mod_sofia/1.6.12-20-b91a0a6~64bit\r\nContent-Length: 0\r\n\r\n", + "event.reason": "Trying", + "event.sequence": 1, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:xDRQZvk3ErEhBDslXv1c6EKI804=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "10.0.2.20", + "10.0.2.15" + ], + "related.ip": [ + "10.0.2.15", + "10.0.2.20" + ], + "related.user": [ + "sipp", + "test" + ], + "server.ip": "10.0.2.20", + "server.port": 5060, + "sip.call_id": "1-2187@10.0.2.20", + "sip.code": 100, + "sip.cseq.code": 1, + "sip.cseq.method": "INVITE", + "sip.from.display_info": "DVI4/8000", + "sip.from.tag": "1", + "sip.from.uri.host": "10.0.2.20", + "sip.from.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.from.uri.port": 5060, + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "sipp", + "sip.status": "Trying", + "sip.to.display_info": "test", + "sip.to.uri.host": "10.0.2.15", + "sip.to.uri.original": "sip:test@10.0.2.15:5060", + "sip.to.uri.port": 5060, + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "test", + "sip.type": "response", + "sip.user_agent.original": "FreeSWITCH-mod_sofia/1.6.12-20-b91a0a6~64bit", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2187-1-0" + ], + "source.ip": "10.0.2.15", + "source.port": 5060, + "status": "OK", + "type": "sip" + }, + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "10.0.2.20", + "client.port": 5060, + "destination.ip": "10.0.2.15", + "destination.port": 5060, + "event.action": "sip-ack", + "event.category": [ + "network", + "protocol" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "ACK sip:test@10.0.2.15:5060 SIP/2.0\r\nVia: SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2187-1-5\r\nFrom: \"DVI4/8000\" ;tag=1\r\nTo: test ;tag=e2jv529vDZ3eQ\r\nCall-ID: 1-2187@10.0.2.20\r\nCSeq: 1 ACK\r\nContact: sip:sipp@10.0.2.20:5060\r\nMax-Forwards: 70\r\nContent-Length: 0\r\n\r\n", + "event.sequence": 1, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:xDRQZvk3ErEhBDslXv1c6EKI804=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "10.0.2.15", + "10.0.2.20" + ], + "related.ip": [ + "10.0.2.20", + "10.0.2.15" + ], + "related.user": [ + "test", + "sipp" + ], + "server.ip": "10.0.2.15", + "server.port": 5060, + "sip.call_id": "1-2187@10.0.2.20", + "sip.contact.display_info": "test", + "sip.contact.uri.host": "10.0.2.15", + "sip.contact.uri.original": "sip:test@10.0.2.15:5060", + "sip.contact.uri.port": 5060, + "sip.contact.uri.scheme": "sip", + "sip.contact.uri.username": "test", + "sip.cseq.code": 1, + "sip.cseq.method": "ACK", + "sip.from.display_info": "DVI4/8000", + "sip.from.tag": "1", + "sip.from.uri.host": "10.0.2.20", + "sip.from.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.from.uri.port": 5060, + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "sipp", + "sip.max_forwards": 70, + "sip.method": "ACK", + "sip.to.display_info": "test", + "sip.to.tag": "e2jv529vDZ3eQ", + "sip.to.uri.host": "10.0.2.15", + "sip.to.uri.original": "sip:test@10.0.2.15:5060", + "sip.to.uri.port": 5060, + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "test", + "sip.type": "request", + "sip.uri.host": "10.0.2.15", + "sip.uri.original": "sip:test@10.0.2.15:5060", + "sip.uri.port": 5060, + "sip.uri.scheme": "sip", + "sip.uri.username": "test", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2187-1-5" + ], + "source.ip": "10.0.2.20", + "source.port": 5060, + "status": "OK", + "type": "sip" + }, + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "10.0.2.15", + "client.port": 5060, + "destination.ip": "10.0.2.20", + "destination.port": 5060, + "event.action": "sip-bye", + "event.category": [ + "network", + "protocol" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "BYE sip:sipp@10.0.2.20:5060 SIP/2.0\r\nVia: SIP/2.0/UDP 10.0.2.15;rport;branch=z9hG4bKDQ7XK6BBH57ya\r\nMax-Forwards: 70\r\nFrom: test ;tag=e2jv529vDZ3eQ\r\nTo: \"DVI4/8000\" ;tag=1\r\nCall-ID: 1-2187@10.0.2.20\r\nCSeq: 99750433 BYE\r\nUser-Agent: FreeSWITCH-mod_sofia/1.6.12-20-b91a0a6~64bit\r\nAllow: INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, UPDATE, REGISTER, REFER, NOTIFY, PUBLISH, SUBSCRIBE\r\nSupported: timer, path, replaces\r\nReason: Q.850;cause=16;text=\"NORMAL_CLEARING\"\r\nContent-Length: 0\r\n\r\n", + "event.sequence": 99750433, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:xDRQZvk3ErEhBDslXv1c6EKI804=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "10.0.2.20", + "10.0.2.15" + ], + "related.ip": [ + "10.0.2.15", + "10.0.2.20" + ], + "related.user": [ + "sipp", + "test" + ], + "server.ip": "10.0.2.20", + "server.port": 5060, + "sip.allow": [ + "invite", + "ack", + "bye", + "cancel", + "options", + "message", + "info", + "update", + "register", + "refer", + "notify", + "publish", + "subscribe" + ], + "sip.call_id": "1-2187@10.0.2.20", + "sip.cseq.code": 99750433, + "sip.cseq.method": "BYE", + "sip.from.display_info": "test", + "sip.from.tag": "e2jv529vDZ3eQ", + "sip.from.uri.host": "10.0.2.15", + "sip.from.uri.original": "sip:test@10.0.2.15:5060", + "sip.from.uri.port": 5060, + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "test", + "sip.max_forwards": 70, + "sip.method": "BYE", + "sip.supported": [ + "timer", + "path", + "replaces" + ], + "sip.to.display_info": "DVI4/8000", + "sip.to.tag": "1", + "sip.to.uri.host": "10.0.2.20", + "sip.to.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.to.uri.port": 5060, + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "sipp", + "sip.type": "request", + "sip.uri.host": "10.0.2.20", + "sip.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.uri.port": 5060, + "sip.uri.scheme": "sip", + "sip.uri.username": "sipp", + "sip.user_agent.original": "FreeSWITCH-mod_sofia/1.6.12-20-b91a0a6~64bit", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 10.0.2.15;rport;branch=z9hG4bKDQ7XK6BBH57ya" + ], + "source.ip": "10.0.2.15", + "source.port": 5060, + "status": "OK", + "type": "sip" + }, + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "10.0.2.20", + "client.port": 5060, + "destination.ip": "10.0.2.15", + "destination.port": 5060, + "event.action": "sip-invite", + "event.category": [ + "network", + "protocol" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "INVITE sip:test@10.0.2.15:5060 SIP/2.0\r\nVia: SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2189-1-0\r\nFrom: \"DVI4/16000\" ;tag=1\r\nTo: test \r\nCall-ID: 1-2189@10.0.2.20\r\nCSeq: 1 INVITE\r\nContact: sip:sipp@10.0.2.20:5060\r\nMax-Forwards: 70\r\nContent-Type: application/sdp\r\nContent-Length: 124\r\n\r\nv=0\r\no=- 42 42 IN IP4 10.0.2.20\r\ns=-\r\nc=IN IP4 10.0.2.20\r\nt=0 0\r\nm=audio 6000 RTP/AVP 6\r\na=rtpmap:6 DVI4/16000\r\na=recvonly\r\n", + "event.sequence": 1, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:xDRQZvk3ErEhBDslXv1c6EKI804=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "10.0.2.15", + "10.0.2.20" + ], + "related.ip": [ + "10.0.2.20", + "10.0.2.15" + ], + "related.user": [ + "test", + "sipp" + ], + "server.ip": "10.0.2.15", + "server.port": 5060, + "sip.call_id": "1-2189@10.0.2.20", + "sip.contact.display_info": "test", + "sip.contact.uri.host": "10.0.2.15", + "sip.contact.uri.original": "sip:test@10.0.2.15:5060", + "sip.contact.uri.port": 5060, + "sip.contact.uri.scheme": "sip", + "sip.contact.uri.username": "test", + "sip.content_length": 124, + "sip.content_type": "application/sdp", + "sip.cseq.code": 1, + "sip.cseq.method": "INVITE", + "sip.from.display_info": "DVI4/16000", + "sip.from.tag": "1", + "sip.from.uri.host": "10.0.2.20", + "sip.from.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.from.uri.port": 5060, + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "sipp", + "sip.max_forwards": 70, + "sip.method": "INVITE", + "sip.sdp.body.original": "v=0\r\no=- 42 42 IN IP4 10.0.2.20\r\ns=-\r\nc=IN IP4 10.0.2.20\r\nt=0 0\r\nm=audio 6000 RTP/AVP 6\r\na=rtpmap:6 DVI4/16000\r\na=recvonly\r\n", + "sip.sdp.connection.address": "10.0.2.20", + "sip.sdp.connection.info": "IN IP4 10.0.2.20", + "sip.sdp.owner.ip": "10.0.2.20", + "sip.sdp.owner.session_id": "42", + "sip.sdp.owner.version": "42", + "sip.sdp.version": "0", + "sip.to.display_info": "test", + "sip.to.uri.host": "10.0.2.15", + "sip.to.uri.original": "sip:test@10.0.2.15:5060", + "sip.to.uri.port": 5060, + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "test", + "sip.type": "request", + "sip.uri.host": "10.0.2.15", + "sip.uri.original": "sip:test@10.0.2.15:5060", + "sip.uri.port": 5060, + "sip.uri.scheme": "sip", + "sip.uri.username": "test", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2189-1-0" + ], + "source.ip": "10.0.2.20", + "source.port": 5060, + "status": "OK", + "type": "sip" + }, + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "10.0.2.15", + "client.port": 5060, + "destination.ip": "10.0.2.20", + "destination.port": 5060, + "event.action": "sip-invite", + "event.category": [ + "network", + "protocol" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "SIP/2.0 100 Trying\r\nVia: SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2189-1-0\r\nFrom: \"DVI4/16000\" ;tag=1\r\nTo: test \r\nCall-ID: 1-2189@10.0.2.20\r\nCSeq: 1 INVITE\r\nUser-Agent: FreeSWITCH-mod_sofia/1.6.12-20-b91a0a6~64bit\r\nContent-Length: 0\r\n\r\n", + "event.reason": "Trying", + "event.sequence": 1, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:xDRQZvk3ErEhBDslXv1c6EKI804=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "10.0.2.20", + "10.0.2.15" + ], + "related.ip": [ + "10.0.2.15", + "10.0.2.20" + ], + "related.user": [ + "sipp", + "test" + ], + "server.ip": "10.0.2.20", + "server.port": 5060, + "sip.call_id": "1-2189@10.0.2.20", + "sip.code": 100, + "sip.cseq.code": 1, + "sip.cseq.method": "INVITE", + "sip.from.display_info": "DVI4/16000", + "sip.from.tag": "1", + "sip.from.uri.host": "10.0.2.20", + "sip.from.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.from.uri.port": 5060, + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "sipp", + "sip.status": "Trying", + "sip.to.display_info": "test", + "sip.to.uri.host": "10.0.2.15", + "sip.to.uri.original": "sip:test@10.0.2.15:5060", + "sip.to.uri.port": 5060, + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "test", + "sip.type": "response", + "sip.user_agent.original": "FreeSWITCH-mod_sofia/1.6.12-20-b91a0a6~64bit", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2189-1-0" + ], + "source.ip": "10.0.2.15", + "source.port": 5060, + "status": "OK", + "type": "sip" + }, + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "10.0.2.20", + "client.port": 5060, + "destination.ip": "10.0.2.15", + "destination.port": 5060, + "event.action": "sip-ack", + "event.category": [ + "network", + "protocol" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "ACK sip:test@10.0.2.15:5060 SIP/2.0\r\nVia: SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2189-1-5\r\nFrom: \"DVI4/16000\" ;tag=1\r\nTo: test ;tag=FBcN7Xt0a8S1j\r\nCall-ID: 1-2189@10.0.2.20\r\nCSeq: 1 ACK\r\nContact: sip:sipp@10.0.2.20:5060\r\nMax-Forwards: 70\r\nContent-Length: 0\r\n\r\n", + "event.sequence": 1, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:xDRQZvk3ErEhBDslXv1c6EKI804=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "10.0.2.15", + "10.0.2.20" + ], + "related.ip": [ + "10.0.2.20", + "10.0.2.15" + ], + "related.user": [ + "test", + "sipp" + ], + "server.ip": "10.0.2.15", + "server.port": 5060, + "sip.call_id": "1-2189@10.0.2.20", + "sip.contact.display_info": "test", + "sip.contact.uri.host": "10.0.2.15", + "sip.contact.uri.original": "sip:test@10.0.2.15:5060", + "sip.contact.uri.port": 5060, + "sip.contact.uri.scheme": "sip", + "sip.contact.uri.username": "test", + "sip.cseq.code": 1, + "sip.cseq.method": "ACK", + "sip.from.display_info": "DVI4/16000", + "sip.from.tag": "1", + "sip.from.uri.host": "10.0.2.20", + "sip.from.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.from.uri.port": 5060, + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "sipp", + "sip.max_forwards": 70, + "sip.method": "ACK", + "sip.to.display_info": "test", + "sip.to.tag": "FBcN7Xt0a8S1j", + "sip.to.uri.host": "10.0.2.15", + "sip.to.uri.original": "sip:test@10.0.2.15:5060", + "sip.to.uri.port": 5060, + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "test", + "sip.type": "request", + "sip.uri.host": "10.0.2.15", + "sip.uri.original": "sip:test@10.0.2.15:5060", + "sip.uri.port": 5060, + "sip.uri.scheme": "sip", + "sip.uri.username": "test", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 10.0.2.20:5060;branch=z9hG4bK-2189-1-5" + ], + "source.ip": "10.0.2.20", + "source.port": 5060, + "status": "OK", + "type": "sip" + }, + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "10.0.2.15", + "client.port": 5060, + "destination.ip": "10.0.2.20", + "destination.port": 5060, + "event.action": "sip-bye", + "event.category": [ + "network", + "protocol" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "BYE sip:sipp@10.0.2.20:5060 SIP/2.0\r\nVia: SIP/2.0/UDP 10.0.2.15;rport;branch=z9hG4bKe00pN1veeeyHp\r\nMax-Forwards: 70\r\nFrom: test ;tag=FBcN7Xt0a8S1j\r\nTo: \"DVI4/16000\" ;tag=1\r\nCall-ID: 1-2189@10.0.2.20\r\nCSeq: 99750437 BYE\r\nUser-Agent: FreeSWITCH-mod_sofia/1.6.12-20-b91a0a6~64bit\r\nAllow: INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, UPDATE, REGISTER, REFER, NOTIFY, PUBLISH, SUBSCRIBE\r\nSupported: timer, path, replaces\r\nReason: Q.850;cause=16;text=\"NORMAL_CLEARING\"\r\nContent-Length: 0\r\n\r\n", + "event.sequence": 99750437, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:xDRQZvk3ErEhBDslXv1c6EKI804=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "10.0.2.20", + "10.0.2.15" + ], + "related.ip": [ + "10.0.2.15", + "10.0.2.20" + ], + "related.user": [ + "sipp", + "test" + ], + "server.ip": "10.0.2.20", + "server.port": 5060, + "sip.allow": [ + "invite", + "ack", + "bye", + "cancel", + "options", + "message", + "info", + "update", + "register", + "refer", + "notify", + "publish", + "subscribe" + ], + "sip.call_id": "1-2189@10.0.2.20", + "sip.cseq.code": 99750437, + "sip.cseq.method": "BYE", + "sip.from.display_info": "test", + "sip.from.tag": "FBcN7Xt0a8S1j", + "sip.from.uri.host": "10.0.2.15", + "sip.from.uri.original": "sip:test@10.0.2.15:5060", + "sip.from.uri.port": 5060, + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "test", + "sip.max_forwards": 70, + "sip.method": "BYE", + "sip.supported": [ + "timer", + "path", + "replaces" + ], + "sip.to.display_info": "DVI4/16000", + "sip.to.tag": "1", + "sip.to.uri.host": "10.0.2.20", + "sip.to.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.to.uri.port": 5060, + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "sipp", + "sip.type": "request", + "sip.uri.host": "10.0.2.20", + "sip.uri.original": "sip:sipp@10.0.2.20:5060", + "sip.uri.port": 5060, + "sip.uri.scheme": "sip", + "sip.uri.username": "sipp", + "sip.user_agent.original": "FreeSWITCH-mod_sofia/1.6.12-20-b91a0a6~64bit", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 10.0.2.15;rport;branch=z9hG4bKe00pN1veeeyHp" + ], + "source.ip": "10.0.2.15", + "source.port": 5060, + "status": "OK", + "type": "sip" + } +] \ No newline at end of file diff --git a/packetbeat/tests/system/golden/sip_authenticated_register-expected.json b/packetbeat/tests/system/golden/sip_authenticated_register-expected.json new file mode 100644 index 000000000000..133792cc1573 --- /dev/null +++ b/packetbeat/tests/system/golden/sip_authenticated_register-expected.json @@ -0,0 +1,142 @@ +[ + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "192.168.1.2", + "client.port": 5060, + "destination.ip": "212.242.33.35", + "destination.port": 5060, + "event.action": "sip-register", + "event.category": [ + "network", + "protocol", + "authentication" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "REGISTER sip:sip.cybercity.dk SIP/2.0\r\nVia: SIP/2.0/UDP 192.168.1.2;branch=z9hG4bKnp112903503-43a64480192.168.1.2;rport\r\nFrom: ;tag=6bac55c\r\nTo: \r\nCall-ID: 578222729-4665d775@578222732-4665d772\r\nContact: ;expires=1200;q=0.500\r\nExpires: 1200\r\nCSeq: 75 REGISTER\r\nContent-Length: 0\r\nAuthorization: Digest username=\"voi18062\",realm=\"sip.cybercity.dk\",uri=\"sip:192.168.1.2\",nonce=\"1701b22972b90f440c3e4eb250842bb\",opaque=\"1701a1351f70795\",nc=\"00000001\",response=\"79a0543188495d288c9ebbe0c881abdc\"\r\nMax-Forwards: 70\r\nUser-Agent: Nero SIPPS IP Phone Version 2.0.51.16\r\n\r\n", + "event.sequence": 75, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:dOa61R2NaaJsJlcFAiMIiyXX+Kk=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "sip.cybercity.dk" + ], + "related.ip": [ + "192.168.1.2", + "212.242.33.35" + ], + "related.user": [ + "voi18062" + ], + "server.ip": "212.242.33.35", + "server.port": 5060, + "sip.auth.realm": "sip.cybercity.dk", + "sip.auth.scheme": "Digest", + "sip.auth.uri.host": "192.168.1.2", + "sip.auth.uri.original": "sip:192.168.1.2", + "sip.auth.uri.scheme": "sip", + "sip.call_id": "578222729-4665d775@578222732-4665d772", + "sip.contact.uri.host": "sip.cybercity.dk", + "sip.contact.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.contact.uri.scheme": "sip", + "sip.contact.uri.username": "voi18062", + "sip.cseq.code": 75, + "sip.cseq.method": "REGISTER", + "sip.from.tag": "6bac55c", + "sip.from.uri.host": "sip.cybercity.dk", + "sip.from.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "voi18062", + "sip.max_forwards": 70, + "sip.method": "REGISTER", + "sip.to.uri.host": "sip.cybercity.dk", + "sip.to.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "voi18062", + "sip.type": "request", + "sip.uri.host": "sip.cybercity.dk", + "sip.uri.original": "sip:sip.cybercity.dk", + "sip.uri.scheme": "sip", + "sip.user_agent.original": "Nero SIPPS IP Phone Version 2.0.51.16", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 192.168.1.2;branch=z9hG4bKnp112903503-43a64480192.168.1.2;rport" + ], + "source.ip": "192.168.1.2", + "source.port": 5060, + "status": "OK", + "type": "sip", + "user.name": "voi18062" + }, + { + "@metadata.beat": "packetbeat", + "@metadata.type": "_doc", + "client.ip": "212.242.33.35", + "client.port": 5060, + "destination.ip": "192.168.1.2", + "destination.port": 5060, + "event.action": "sip-register", + "event.category": [ + "network", + "protocol" + ], + "event.dataset": "sip", + "event.duration": 0, + "event.kind": "event", + "event.original": "SIP/2.0 100 Trying\r\nCall-ID: 578222729-4665d775@578222732-4665d772\r\nCSeq: 75 REGISTER\r\nFrom: ;tag=6bac55c\r\nTo: \r\nVia: SIP/2.0/UDP 192.168.1.2;received=80.230.219.70;rport=5060;branch=z9hG4bKnp112903503-43a64480192.168.1.2\r\nContent-Length: 0\r\n\r\n", + "event.reason": "Trying", + "event.sequence": 75, + "event.type": [ + "info" + ], + "network.application": "sip", + "network.community_id": "1:dOa61R2NaaJsJlcFAiMIiyXX+Kk=", + "network.iana_number": "17", + "network.protocol": "sip", + "network.transport": "udp", + "network.type": "ipv4", + "related.hosts": [ + "sip.cybercity.dk" + ], + "related.ip": [ + "212.242.33.35", + "192.168.1.2" + ], + "related.user": [ + "voi18062" + ], + "server.ip": "192.168.1.2", + "server.port": 5060, + "sip.call_id": "578222729-4665d775@578222732-4665d772", + "sip.code": 100, + "sip.cseq.code": 75, + "sip.cseq.method": "REGISTER", + "sip.from.tag": "6bac55c", + "sip.from.uri.host": "sip.cybercity.dk", + "sip.from.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.from.uri.scheme": "sip", + "sip.from.uri.username": "voi18062", + "sip.status": "Trying", + "sip.to.uri.host": "sip.cybercity.dk", + "sip.to.uri.original": "sip:voi18062@sip.cybercity.dk", + "sip.to.uri.scheme": "sip", + "sip.to.uri.username": "voi18062", + "sip.type": "response", + "sip.version": "2.0", + "sip.via.original": [ + "SIP/2.0/UDP 192.168.1.2;received=80.230.219.70;rport=5060;branch=z9hG4bKnp112903503-43a64480192.168.1.2" + ], + "source.ip": "212.242.33.35", + "source.port": 5060, + "status": "OK", + "type": "sip" + } +] \ No newline at end of file diff --git a/packetbeat/tests/system/pcaps/sip.pcap b/packetbeat/tests/system/pcaps/sip.pcap new file mode 100644 index 000000000000..7ec19fb525be Binary files /dev/null and b/packetbeat/tests/system/pcaps/sip.pcap differ diff --git a/packetbeat/tests/system/pcaps/sip_authenticated_register.pcap b/packetbeat/tests/system/pcaps/sip_authenticated_register.pcap new file mode 100644 index 000000000000..25f10e5cf880 Binary files /dev/null and b/packetbeat/tests/system/pcaps/sip_authenticated_register.pcap differ diff --git a/packetbeat/tests/system/test_0099_golden_files.py b/packetbeat/tests/system/test_0099_golden_files.py index 5f747a3c83c8..c02dd54df893 100644 --- a/packetbeat/tests/system/test_0099_golden_files.py +++ b/packetbeat/tests/system/test_0099_golden_files.py @@ -80,6 +80,9 @@ def clean_keys(obj): # Network direction is populated based on local-IPs which is misleading # when reading from a pcap and leads to inconsistent results. "network.direction", + + # module specific + "sip.timestamp", ] for key in keys: if key in obj: