blob: e6036b5c22d8a43fb93d9901d46c26a9d876d62e [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed 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 com.android.networkstack.netlink;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
/**
* Class for tcp_info.
*
* Corresponds to {@code struct tcp_info} from bionic/libc/kernel/uapi/linux/tcp.h
*/
public class TcpInfo {
public enum Field {
STATE(Byte.BYTES),
CASTATE(Byte.BYTES),
RETRANSMITS(Byte.BYTES),
PROBES(Byte.BYTES),
BACKOFF(Byte.BYTES),
OPTIONS(Byte.BYTES),
WSCALE(Byte.BYTES),
DELIVERY_RATE_APP_LIMITED(Byte.BYTES),
RTO(Integer.BYTES),
ATO(Integer.BYTES),
SND_MSS(Integer.BYTES),
RCV_MSS(Integer.BYTES),
UNACKED(Integer.BYTES),
SACKED(Integer.BYTES),
LOST(Integer.BYTES),
RETRANS(Integer.BYTES),
FACKETS(Integer.BYTES),
LAST_DATA_SENT(Integer.BYTES),
LAST_ACK_SENT(Integer.BYTES),
LAST_DATA_RECV(Integer.BYTES),
LAST_ACK_RECV(Integer.BYTES),
PMTU(Integer.BYTES),
RCV_SSTHRESH(Integer.BYTES),
RTT(Integer.BYTES),
RTTVAR(Integer.BYTES),
SND_SSTHRESH(Integer.BYTES),
SND_CWND(Integer.BYTES),
ADVMSS(Integer.BYTES),
REORDERING(Integer.BYTES),
RCV_RTT(Integer.BYTES),
RCV_SPACE(Integer.BYTES),
TOTAL_RETRANS(Integer.BYTES),
PACING_RATE(Long.BYTES),
MAX_PACING_RATE(Long.BYTES),
BYTES_ACKED(Long.BYTES),
BYTES_RECEIVED(Long.BYTES),
SEGS_OUT(Integer.BYTES),
SEGS_IN(Integer.BYTES),
NOTSENT_BYTES(Integer.BYTES),
MIN_RTT(Integer.BYTES),
DATA_SEGS_IN(Integer.BYTES),
DATA_SEGS_OUT(Integer.BYTES),
DELIVERY_RATE(Long.BYTES),
BUSY_TIME(Long.BYTES),
RWND_LIMITED(Long.BYTES),
SNDBUF_LIMITED(Long.BYTES);
public final int size;
Field(int s) {
size = s;
}
}
private static final String TAG = "TcpInfo";
private final Map<Field, Number> mFieldsValues;
private TcpInfo(@NonNull ByteBuffer bytes, int infolen) {
final int start = bytes.position();
final LinkedHashMap<Field, Number> fields = new LinkedHashMap<>();
for (final Field field : Field.values()) {
switch (field.size) {
case Byte.BYTES:
fields.put(field, getByte(bytes, start, infolen));
break;
case Integer.BYTES:
fields.put(field, getInt(bytes, start, infolen));
break;
case Long.BYTES:
fields.put(field, getLong(bytes, start, infolen));
break;
default:
Log.e(TAG, "Unexpected size:" + field.size);
}
}
mFieldsValues = Collections.unmodifiableMap(fields);
// tcp_info structure grows over time as new fields are added. Jump to the end of the
// structure, as unknown fields might remain at the end of the structure if the tcp_info
// struct was expanded.
bytes.position(Math.min(infolen + start, bytes.limit()));
}
@VisibleForTesting
TcpInfo(@NonNull Map<Field, Number> info) {
final LinkedHashMap<Field, Number> fields = new LinkedHashMap<>();
for (final Field field : Field.values()) {
fields.put(field, info.get(field));
}
mFieldsValues = Collections.unmodifiableMap(fields);
}
/** Parse a TcpInfo from a giving ByteBuffer with a specific length. */
@Nullable
public static TcpInfo parse(@NonNull ByteBuffer bytes, int infolen) {
try {
return new TcpInfo(bytes, infolen);
} catch (BufferUnderflowException | IllegalArgumentException e) {
Log.e(TAG, "parsing error.", e);
return null;
}
}
/**
* Helper function for handling different struct tcp_info versions in the kernel.
*/
private static boolean isValidTargetPosition(int start, int len, int pos, int targetBytes)
throws IllegalArgumentException {
// Equivalent to new Range(start, start + len).contains(new Range(pos, pos + targetBytes))
if (len < 0 || targetBytes < 0) throw new IllegalArgumentException();
// Check that start < pos < start + len
if (pos < start || pos > start + len) return false;
// Pos is inside the range and targetBytes is positive. Offset is valid if end of 2nd range
// is below end of 1st range.
return pos + targetBytes <= start + len;
}
/** Get value for specific key. */
@Nullable
public Number getValue(@NonNull Field key) {
return mFieldsValues.get(key);
}
@Nullable
private static Byte getByte(@NonNull ByteBuffer buffer, int start, int len) {
if (!isValidTargetPosition(start, len, buffer.position(), Byte.BYTES)) return null;
return buffer.get();
}
@Nullable
private static Integer getInt(@NonNull ByteBuffer buffer, int start, int len) {
if (!isValidTargetPosition(start, len, buffer.position(), Integer.BYTES)) return null;
return buffer.getInt();
}
@Nullable
private static Long getLong(@NonNull ByteBuffer buffer, int start, int len) {
if (!isValidTargetPosition(start, len, buffer.position(), Long.BYTES)) return null;
return buffer.getLong();
}
private static String decodeWscale(byte num) {
return String.valueOf((num >> 4) & 0x0f) + ":" + String.valueOf(num & 0x0f);
}
/**
* Returns a string representing a given tcp state.
* Map to enum in bionic/libc/include/netinet/tcp.h
*/
@VisibleForTesting
static String getTcpStateName(int state) {
switch (state) {
case 1: return "TCP_ESTABLISHED";
case 2: return "TCP_SYN_SENT";
case 3: return "TCP_SYN_RECV";
case 4: return "TCP_FIN_WAIT1";
case 5: return "TCP_FIN_WAIT2";
case 6: return "TCP_TIME_WAIT";
case 7: return "TCP_CLOSE";
case 8: return "TCP_CLOSE_WAIT";
case 9: return "TCP_LAST_ACK";
case 10: return "TCP_LISTEN";
case 11: return "TCP_CLOSING";
default: return "UNKNOWN:" + Integer.toString(state);
}
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TcpInfo)) return false;
TcpInfo other = (TcpInfo) obj;
for (final Field key : mFieldsValues.keySet()) {
if (!Objects.equals(mFieldsValues.get(key), other.mFieldsValues.get(key))) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
return Objects.hash(mFieldsValues.values().toArray());
}
@Override
public String toString() {
String str = "TcpInfo{ ";
for (final Field key : mFieldsValues.keySet()) {
str += key.name().toLowerCase() + "=";
if (key == Field.STATE) {
str += getTcpStateName(mFieldsValues.get(key).intValue()) + " ";
} else if (key == Field.WSCALE) {
str += decodeWscale(mFieldsValues.get(key).byteValue()) + " ";
} else {
str += mFieldsValues.get(key) + " ";
}
}
str += "}";
return str;
}
}