Skip to content

Commit

Permalink
Merge branch 'feature/gsm-pcap-logging' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
christianrowlands committed Aug 4, 2021
2 parents d599305 + 2e1ab0d commit bd7e9d5
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 7 deletions.
14 changes: 7 additions & 7 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ android {
minSdkVersion 26
targetSdkVersion 29
versionCode 6
versionName "0.4.0-SNAPSHOT"
versionName "0.4.0-alpha01"
setProperty("archivesBaseName", "$applicationName-$versionName")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -67,8 +67,8 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.guava:guava:29.0-android'
implementation 'androidx.preference:preference:1.1.1'
Expand All @@ -79,10 +79,10 @@ dependencies {

implementation "com.craxiom:network-survey-messaging:${networkSurveyMessagingVersion}"
implementation 'com.google.protobuf:protobuf-java-util:3.15.3'
implementation 'com.craxiom:mqtt-library:0.4.2'
implementation 'com.craxiom:mqtt-library:0.4.3'

testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import com.craxiom.networksurveyplus.messages.DiagCommand;
import com.craxiom.networksurveyplus.messages.QcdmConstants;
import com.craxiom.networksurveyplus.messages.QcdmGsmParser;
import com.craxiom.networksurveyplus.messages.QcdmLteParser;
import com.craxiom.networksurveyplus.messages.QcdmMessage;
import com.craxiom.networksurveyplus.messages.QcdmUmtsParser;
Expand Down Expand Up @@ -148,6 +149,15 @@ public void onQcdmMessage(QcdmMessage qcdmMessage)
case QcdmConstants.UMTS_NAS_OTA_DSDS:
pcapRecord = QcdmUmtsParser.convertUmtsNasOtaDsds(qcdmMessage, gpsListener.getLatestLocation());
break;

case QcdmConstants.GSM_RR_SIGNALING_MESSAGES:
pcapRecord = QcdmGsmParser.convertGsmSignalingMessage(qcdmMessage, gpsListener.getLatestLocation());
break;

/*case QcdmConstants.GSM_RR_CELL_INFORMATION_C:
// TODO delete me once I find a record for this
Timber.i("GSM RR Cell Information: %s", qcdmMessage);
break;*/
}

if (pcapRecord != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.craxiom.networksurveyplus.messages;

/**
* The GSMTAP mapping for the GSM subtypes. The ordinal value of these enums map to the value that is used in the
* GSMTAP header, so don't change the order of the enum values, and don't insert any new values in the middle.
* <p>
* These values are pulled from Osmocom and scat.
* https://github.com/osmocom/libosmocore/blob/master/include/osmocom/core/gsmtap.h
*
* @since 0.4.0
*/
public enum GsmSubtypes
{
GSMTAP_CHANNEL_UNKNOWN, // 0x00
GSMTAP_CHANNEL_BCCH, // 0x01
GSMTAP_CHANNEL_CCCH, // 0x02
GSMTAP_CHANNEL_RACH, // 0x03
GSMTAP_CHANNEL_AGCH, // 0x04
GSMTAP_CHANNEL_PCH, // 0x05
GSMTAP_CHANNEL_SDCCH, // 0x06
GSMTAP_CHANNEL_SDCCH4, // 0x07
GSMTAP_CHANNEL_SDCCH8, // 0x08
GSMTAP_CHANNEL_TCH_F, // 0x09
GSMTAP_CHANNEL_TCH_H, // 0x0a = 10
GSMTAP_CHANNEL_PACCH, // 0x0b = 11
GSMTAP_CHANNEL_CBCH52, // 0x0c = 12
GSMTAP_CHANNEL_PDCH, // 0x0d = 13
GSMTAP_CHANNEL_PTCCH, // 0x0e = 14
GSMTAP_CHANNEL_CBCH51, // 0x0f = 15
GSMTAP_CHANNEL_VOICE_F, // 0x10 = 16 /* voice codec payload (FR/EFR/AMR) */
GSMTAP_CHANNEL_VOICE_H, // 0x11 = 17 /* voice codec payload (HR/AMR) */
}
Original file line number Diff line number Diff line change
Expand Up @@ -359,4 +359,25 @@ public static String convertBytesToHexString(byte[] bytes, int offset, int lengt
}
return stringBuilder.toString();
}

/**
* Converts a byte array to a hex string with the Java byte cast as a prefix and a comma at the end of each byte.
* This allows for the output to be placed in a byte array initializer for use in unit tests.
*
* @param bytes The byte array to convert.
* @param offset Offset within bytes to start processing.
* @param length Number of bytes to process.
* @return The byte array represented as a hex string.
* @since 0.4.0
*/
public static String convertBytesToHexStringByteCast(byte[] bytes, int offset, int length)
{
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.setLength(0);
for (int i = offset, end = offset + length; i < end; i++)
{
stringBuilder.append(String.format("(byte) 0x%02x, ", bytes[i]));
}
return stringBuilder.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ private QcdmConstants()
{
}

// GSM Signaling
public static final int GSM_RR_SIGNALING_MESSAGES = 0x512F;
public static final int GSM_POWER_SCAN_C = 0x64; //Used to view the BA List power levels
public static final int GSM_RR_CELL_INFORMATION_C = 0x134;

// UMTS/WCDMA
public static final int WCDMA_SEARCH_CELL_RESELECTION_RANK = 0x4005;
public static final int WCDMA_RRC_STATES = 0x4125;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.craxiom.networksurveyplus.messages;

import android.location.Location;

import java.util.Arrays;

import timber.log.Timber;

/**
* Contains parser methods for converting the QCDM GSM messages to various formats, like pcap records or protobuf
* objects.
*
* @since 0.4.0
*/
public class QcdmGsmParser
{
private static final int GSM_SIGNAL_HEADER_LENGTH = 3;

private QcdmGsmParser()
{
}

/**
* Given a {@link QcdmMessage} that contains a WCDMA Signaling message {@link QcdmConstants#WCDMA_SIGNALING_MESSAGES},
* convert it to a pcap record byte array that can be consumed by tools like Wireshark.
* <p>
* The base header structure for the GSM RR Signaling Message:
* *************************************************************
* | Channel Type | Message Type | Message Length | L3 Message |
* | 1 byte | 1 byte | 1 byte | n |
* *************************************************************
* <p>
* The code for this method was taken from SCAT:
* https://github.com/fgsect/scat/blob/0c1fe579376460ba5cd42d82a556fb88cf89da61/parsers/qualcomm/diaggsmlogparser.py#L191
*
* @param qcdmMessage The QCDM message to convert into a pcap record.
* @param location The location to tie to the QCDM message when writing it to a pcap file. If null then no
* location will be added to the PPI header.
* @return The pcap record byte array to write to a pcap file, or null if the message could not be parsed.
*/
public static byte[] convertGsmSignalingMessage(QcdmMessage qcdmMessage, Location location)
{
Timber.v("Handling a GSM RR Signaling message");

final byte[] logPayload = qcdmMessage.getLogPayload();

final int channelTypeDir = logPayload[0] & 0xFF;
//final int messageType = logPayload[1] & 0xFF;
final int messageLength = logPayload[2] & 0xFF;

if (logPayload.length < messageLength + GSM_SIGNAL_HEADER_LENGTH)
{
Timber.e("The qcdm log payload is shorter than the defined length for a GSM signal message");
return null;
}

byte[] l3Message = Arrays.copyOfRange(logPayload, GSM_SIGNAL_HEADER_LENGTH, messageLength + GSM_SIGNAL_HEADER_LENGTH);

// Not sure why we take the channelTypeDir and do this to get the chan, but SCAT and all the other apps do it
int chan = channelTypeDir & 0x7F;

final int subtype = getGsmtapGsmChannelType(chan);

if (chan == 0 || chan == 4)
{
// SDCCH/8 expects LAPDm header
if (messageLength > 63)
{
Timber.w("The GSM signal message length is longer than 63 bytes, actual length=%d", messageLength);
return null;
}

// SACCH/8 expects SACCH L1/LAPDm header
byte[] sacchL1 = chan == 4 ? new byte[]{0x00, 0x00} : new byte[]{};

l3Message = PcapUtils.concatenateByteArrays(
sacchL1, // SAACH header only if it is SAACH/8 (0x88?)
new byte[]{0x01}, // LAPDM Address Field
new byte[]{0x03}, // LAPDM Control Field
new byte[]{(byte) ((messageLength << 2) | 0x01)}, // LAPDM Length
l3Message);
}

// Any channel type dir that has the 0x80 bit set is downlink, everything else is uplink
final boolean isUplink = (channelTypeDir & 0x80) == 0x00;

return PcapUtils.getGsmtapPcapRecord(GsmtapConstants.GSMTAP_TYPE_UM, l3Message, subtype, 0,
isUplink, 0, 0, qcdmMessage.getSimId(), location);
}

/**
* Converts the QCDM chan to the GSMTAP defined channel type that needs to be included in the GSMTAP pcap header.
*
* @param channelType The channel type found in the QCDM header.
* @return The GSMTAP Channel Type / Subtype that specifies what kind of message the payload of the GSMTAP frame
* contains, or 0 if a mapping was not found.
*/
public static int getGsmtapGsmChannelType(int channelType)
{
switch (channelType)
{
// Channel Type Map
case 0:
return GsmSubtypes.GSMTAP_CHANNEL_SDCCH8.ordinal();
case 1:
return GsmSubtypes.GSMTAP_CHANNEL_BCCH.ordinal();
case 3: // 0x03
return GsmSubtypes.GSMTAP_CHANNEL_CCCH.ordinal();
case 4: // 0x04
return 0x88; // Not sure why 4 maps to 0x88, but that is what SCAT and others do

default:
return 0;
}
}
}
Binary file modified app/src/main/res/raw/ns_plus_diag.cfg
Binary file not shown.
48 changes: 48 additions & 0 deletions app/src/test/java/com/craxiom/networksurveyplus/GsmParserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.craxiom.networksurveyplus;

import android.location.Location;

import com.craxiom.networksurveyplus.messages.QcdmConstants;
import com.craxiom.networksurveyplus.messages.QcdmGsmParser;
import com.craxiom.networksurveyplus.messages.QcdmMessage;

import org.junit.Test;

import java.util.Arrays;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

/**
* Tests for the GSM QCDM Parser that takes QCDM input and converts it to a PCAP record.
*
* @since 0.4.0
*/
public class GsmParserTest
{
@Test
public void testGsmQcdmMessage_signalingMessage()
{
final byte[] qcdmMessageBytes = {(byte) 0x10, (byte) 0x00, (byte) 0x26, (byte) 0x00, (byte) 0x26, (byte) 0x00, (byte) 0x2f, (byte) 0x51, (byte) 0xfd, (byte) 0x19, (byte) 0x53, (byte) 0x7b, (byte) 0x87, (byte) 0x62, (byte) 0xf4, (byte) 0x00, (byte) 0x81, (byte) 0x1b, (byte) 0x17, (byte) 0x49, (byte) 0x06, (byte) 0x1b, (byte) 0x00, (byte) 0x3e, (byte) 0x62, (byte) 0xf2, (byte) 0x20, (byte) 0x1c, (byte) 0x4e, (byte) 0xd0, (byte) 0x01, (byte) 0x0a, (byte) 0x15, (byte) 0x65, (byte) 0x44, (byte) 0xb8, (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x1f, (byte) 0x01, (byte) 0x1b};
final byte[] expectedQcdmMessagePayloadBytes = {(byte) 0x81, (byte) 0x1b, (byte) 0x17, (byte) 0x49, (byte) 0x06, (byte) 0x1b, (byte) 0x00, (byte) 0x3e, (byte) 0x62, (byte) 0xf2, (byte) 0x20, (byte) 0x1c, (byte) 0x4e, (byte) 0xd0, (byte) 0x01, (byte) 0x0a, (byte) 0x15, (byte) 0x65, (byte) 0x44, (byte) 0xb8, (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x1f, (byte) 0x01, (byte) 0x1b};
final byte[] expectedPcapRecordBytes = {(byte) 0x63, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0xe4, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x75, (byte) 0x14, (byte) 0x00, (byte) 0x02, (byte) 0xcf, (byte) 0x14, (byte) 0x00, (byte) 0x0e, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x05, (byte) 0x21, (byte) 0x05, (byte) 0x84, (byte) 0x01, (byte) 0x8f, (byte) 0x90, (byte) 0x35, (byte) 0x3f, (byte) 0x1d, (byte) 0x61, (byte) 0x6b, (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x43, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x40, (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x12, (byte) 0x79, (byte) 0x12, (byte) 0x79, (byte) 0x00, (byte) 0x2f, (byte) 0x00, (byte) 0x00, (byte) 0x02, (byte) 0x04, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x49, (byte) 0x06, (byte) 0x1b, (byte) 0x00, (byte) 0x3e, (byte) 0x62, (byte) 0xf2, (byte) 0x20, (byte) 0x1c, (byte) 0x4e, (byte) 0xd0, (byte) 0x01, (byte) 0x0a, (byte) 0x15, (byte) 0x65, (byte) 0x44, (byte) 0xb8, (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x1f, (byte) 0x01, (byte) 0x1b};

final QcdmMessage qcdmMessage = new QcdmMessage(qcdmMessageBytes, 0);
assertEquals(16, qcdmMessage.getOpCode());
assertEquals(QcdmConstants.GSM_RR_SIGNALING_MESSAGES, qcdmMessage.getLogType());
assertArrayEquals("The qcdm message bytes did not match what was expected", expectedQcdmMessagePayloadBytes, qcdmMessage.getLogPayload());

final Location location = new FakeLocation();
location.setLatitude(41.4928645);
location.setLongitude(-90.1333759);
location.setAltitude(152.6591);

final byte[] pcapRecordBytes = QcdmGsmParser.convertGsmSignalingMessage(qcdmMessage, location);

assertNotNull(pcapRecordBytes);

// Ignore the first 8 bytes since it contains the record timestamp.
assertArrayEquals(expectedPcapRecordBytes, Arrays.copyOfRange(pcapRecordBytes, 8, pcapRecordBytes.length));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ public void testCrc16X25UmtsDiagCommandCalculation()
assertEquals(Integer.toHexString(expectedCrc), Integer.toHexString(crc >= 0 ? crc : 0x10000 + crc));
}

/**
* Tests the modified Diag Command that is used to activate the GSM messages as part of the ns_plus_diag.cfg.
*/
@Test
public void testCrc16X25GsmDiagCommandCalculation()
{
final int expectedCrc = (short) 0x8bf8;
final byte[] diagCommandBytes = {(byte) 0x73, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x05, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x34, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x10};

final int crc = ParserUtils.calculateCrc16X25(diagCommandBytes, diagCommandBytes.length);
assertEquals(Integer.toHexString(0x10000 + expectedCrc), Integer.toHexString(crc >= 0 ? crc : 0x10000 + crc));
}

@Test
public void testCrcQcdmMessage()
{
Expand Down

0 comments on commit bd7e9d5

Please sign in to comment.