You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
434 lines
14 KiB
434 lines
14 KiB
#!/usr/bin/python
|
|
#
|
|
# Copyright 2015 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.
|
|
|
|
"""Partial Python implementation of sock_diag functionality."""
|
|
|
|
# pylint: disable=g-bad-todo
|
|
|
|
import errno
|
|
import os
|
|
from socket import * # pylint: disable=wildcard-import
|
|
import struct
|
|
|
|
import csocket
|
|
import cstruct
|
|
import net_test
|
|
import netlink
|
|
|
|
### sock_diag constants. See include/uapi/linux/sock_diag.h.
|
|
# Message types.
|
|
SOCK_DIAG_BY_FAMILY = 20
|
|
SOCK_DESTROY = 21
|
|
|
|
### inet_diag_constants. See include/uapi/linux/inet_diag.h
|
|
# Message types.
|
|
TCPDIAG_GETSOCK = 18
|
|
|
|
# Request attributes.
|
|
INET_DIAG_REQ_BYTECODE = 1
|
|
|
|
# Extensions.
|
|
INET_DIAG_NONE = 0
|
|
INET_DIAG_MEMINFO = 1
|
|
INET_DIAG_INFO = 2
|
|
INET_DIAG_VEGASINFO = 3
|
|
INET_DIAG_CONG = 4
|
|
INET_DIAG_TOS = 5
|
|
INET_DIAG_TCLASS = 6
|
|
INET_DIAG_SKMEMINFO = 7
|
|
INET_DIAG_SHUTDOWN = 8
|
|
INET_DIAG_DCTCPINFO = 9
|
|
INET_DIAG_DCTCPINFO = 9
|
|
INET_DIAG_PROTOCOL = 10
|
|
INET_DIAG_SKV6ONLY = 11
|
|
INET_DIAG_LOCALS = 12
|
|
INET_DIAG_PEERS = 13
|
|
INET_DIAG_PAD = 14
|
|
INET_DIAG_MARK = 15
|
|
|
|
# Bytecode operations.
|
|
INET_DIAG_BC_NOP = 0
|
|
INET_DIAG_BC_JMP = 1
|
|
INET_DIAG_BC_S_GE = 2
|
|
INET_DIAG_BC_S_LE = 3
|
|
INET_DIAG_BC_D_GE = 4
|
|
INET_DIAG_BC_D_LE = 5
|
|
INET_DIAG_BC_AUTO = 6
|
|
INET_DIAG_BC_S_COND = 7
|
|
INET_DIAG_BC_D_COND = 8
|
|
INET_DIAG_BC_DEV_COND = 9
|
|
INET_DIAG_BC_MARK_COND = 10
|
|
|
|
# Data structure formats.
|
|
# These aren't constants, they're classes. So, pylint: disable=invalid-name
|
|
InetDiagSockId = cstruct.Struct(
|
|
"InetDiagSockId", "!HH16s16sI8s", "sport dport src dst iface cookie")
|
|
InetDiagReqV2 = cstruct.Struct(
|
|
"InetDiagReqV2", "=BBBxIS", "family protocol ext states id",
|
|
[InetDiagSockId])
|
|
InetDiagMsg = cstruct.Struct(
|
|
"InetDiagMsg", "=BBBBSLLLLL",
|
|
"family state timer retrans id expires rqueue wqueue uid inode",
|
|
[InetDiagSockId])
|
|
InetDiagMeminfo = cstruct.Struct(
|
|
"InetDiagMeminfo", "=IIII", "rmem wmem fmem tmem")
|
|
InetDiagBcOp = cstruct.Struct("InetDiagBcOp", "BBH", "code yes no")
|
|
InetDiagHostcond = cstruct.Struct("InetDiagHostcond", "=BBxxi",
|
|
"family prefix_len port")
|
|
InetDiagMarkcond = cstruct.Struct("InetDiagMarkcond", "=II", "mark mask")
|
|
|
|
SkMeminfo = cstruct.Struct(
|
|
"SkMeminfo", "=IIIIIIII",
|
|
"rmem_alloc rcvbuf wmem_alloc sndbuf fwd_alloc wmem_queued optmem backlog")
|
|
TcpInfo = cstruct.Struct(
|
|
"TcpInfo", "=BBBBBBBxIIIIIIIIIIIIIIIIIIIIIIII",
|
|
"state ca_state retransmits probes backoff options wscale "
|
|
"rto ato snd_mss rcv_mss "
|
|
"unacked sacked lost retrans fackets "
|
|
"last_data_sent last_ack_sent last_data_recv last_ack_recv "
|
|
"pmtu rcv_ssthresh rtt rttvar snd_ssthresh snd_cwnd advmss reordering "
|
|
"rcv_rtt rcv_space "
|
|
"total_retrans") # As of linux 3.13, at least.
|
|
|
|
TCP_TIME_WAIT = 6
|
|
ALL_NON_TIME_WAIT = 0xffffffff & ~(1 << TCP_TIME_WAIT)
|
|
|
|
|
|
class SockDiag(netlink.NetlinkSocket):
|
|
|
|
NL_DEBUG = []
|
|
|
|
def __init__(self):
|
|
super(SockDiag, self).__init__(netlink.NETLINK_SOCK_DIAG)
|
|
|
|
def _Decode(self, command, msg, nla_type, nla_data):
|
|
"""Decodes netlink attributes to Python types."""
|
|
if msg.family == AF_INET or msg.family == AF_INET6:
|
|
if isinstance(msg, InetDiagReqV2):
|
|
prefix = "INET_DIAG_REQ"
|
|
else:
|
|
prefix = "INET_DIAG"
|
|
name = self._GetConstantName(__name__, nla_type, prefix)
|
|
else:
|
|
# Don't know what this is. Leave it as an integer.
|
|
name = nla_type
|
|
|
|
if name in ["INET_DIAG_SHUTDOWN", "INET_DIAG_TOS", "INET_DIAG_TCLASS",
|
|
"INET_DIAG_SKV6ONLY"]:
|
|
data = ord(nla_data)
|
|
elif name == "INET_DIAG_CONG":
|
|
data = nla_data.strip("\x00")
|
|
elif name == "INET_DIAG_MEMINFO":
|
|
data = InetDiagMeminfo(nla_data)
|
|
elif name == "INET_DIAG_INFO":
|
|
# TODO: Catch the exception and try something else if it's not TCP.
|
|
data = TcpInfo(nla_data)
|
|
elif name == "INET_DIAG_SKMEMINFO":
|
|
data = SkMeminfo(nla_data)
|
|
elif name == "INET_DIAG_MARK":
|
|
data = struct.unpack("=I", nla_data)[0]
|
|
elif name == "INET_DIAG_REQ_BYTECODE":
|
|
data = self.DecodeBytecode(nla_data)
|
|
elif name in ["INET_DIAG_LOCALS", "INET_DIAG_PEERS"]:
|
|
data = []
|
|
while len(nla_data):
|
|
# The SCTP diag code always appears to copy sizeof(sockaddr_storage)
|
|
# bytes, but does so from a union sctp_addr which is at most as long
|
|
# as a sockaddr_in6.
|
|
addr, nla_data = cstruct.Read(nla_data, csocket.SockaddrStorage)
|
|
if addr.family == AF_INET:
|
|
addr = csocket.SockaddrIn(addr.Pack())
|
|
elif addr.family == AF_INET6:
|
|
addr = csocket.SockaddrIn6(addr.Pack())
|
|
data.append(addr)
|
|
else:
|
|
data = nla_data
|
|
|
|
return name, data
|
|
|
|
def MaybeDebugCommand(self, command, unused_flags, data):
|
|
name = self._GetConstantName(__name__, command, "SOCK_")
|
|
if "ALL" not in self.NL_DEBUG and "SOCK" not in self.NL_DEBUG:
|
|
return
|
|
parsed = self._ParseNLMsg(data, InetDiagReqV2)
|
|
print("%s %s" % (name, str(parsed)))
|
|
|
|
@staticmethod
|
|
def _EmptyInetDiagSockId():
|
|
return InetDiagSockId(("\x00" * len(InetDiagSockId)))
|
|
|
|
@staticmethod
|
|
def PackBytecode(instructions):
|
|
"""Compiles instructions to inet_diag bytecode.
|
|
|
|
The input is a list of (INET_DIAG_BC_xxx, yes, no, arg) tuples, where yes
|
|
and no are relative jump offsets measured in instructions. The yes branch
|
|
is taken if the instruction matches.
|
|
|
|
To accept, jump 1 past the last instruction. To reject, jump 2 past the
|
|
last instruction.
|
|
|
|
The target of a no jump is only valid if it is reachable by following
|
|
only yes jumps from the first instruction - see inet_diag_bc_audit and
|
|
valid_cc. This means that if cond1 and cond2 are two mutually exclusive
|
|
filter terms, it is not possible to implement cond1 OR cond2 using:
|
|
|
|
...
|
|
cond1 2 1 arg
|
|
cond2 1 2 arg
|
|
accept
|
|
reject
|
|
|
|
but only using:
|
|
|
|
...
|
|
cond1 1 2 arg
|
|
jmp 1 2
|
|
cond2 1 2 arg
|
|
accept
|
|
reject
|
|
|
|
The jmp instruction ignores yes and always jumps to no, but yes must be 1
|
|
or the bytecode won't validate. It doesn't have to be jmp - any instruction
|
|
that is guaranteed not to match on real data will do.
|
|
|
|
Args:
|
|
instructions: list of instruction tuples
|
|
|
|
Returns:
|
|
A string, the raw bytecode.
|
|
"""
|
|
args = []
|
|
positions = [0]
|
|
|
|
for op, yes, no, arg in instructions:
|
|
|
|
if yes <= 0 or no <= 0:
|
|
raise ValueError("Jumps must be > 0")
|
|
|
|
if op in [INET_DIAG_BC_NOP, INET_DIAG_BC_JMP, INET_DIAG_BC_AUTO]:
|
|
arg = ""
|
|
elif op in [INET_DIAG_BC_S_GE, INET_DIAG_BC_S_LE,
|
|
INET_DIAG_BC_D_GE, INET_DIAG_BC_D_LE]:
|
|
arg = "\x00\x00" + struct.pack("=H", arg)
|
|
elif op in [INET_DIAG_BC_S_COND, INET_DIAG_BC_D_COND]:
|
|
addr, prefixlen, port = arg
|
|
family = AF_INET6 if ":" in addr else AF_INET
|
|
addr = inet_pton(family, addr)
|
|
arg = InetDiagHostcond((family, prefixlen, port)).Pack() + addr
|
|
elif op == INET_DIAG_BC_MARK_COND:
|
|
if isinstance(arg, tuple):
|
|
mark, mask = arg
|
|
else:
|
|
mark, mask = arg, 0xffffffff
|
|
arg = InetDiagMarkcond((mark, mask)).Pack()
|
|
else:
|
|
raise ValueError("Unsupported opcode %d" % op)
|
|
|
|
args.append(arg)
|
|
length = len(InetDiagBcOp) + len(arg)
|
|
positions.append(positions[-1] + length)
|
|
|
|
# Reject label.
|
|
positions.append(positions[-1] + 4) # Why 4? Because the kernel uses 4.
|
|
assert len(args) == len(instructions) == len(positions) - 2
|
|
|
|
# print(positions)
|
|
|
|
packed = ""
|
|
for i, (op, yes, no, arg) in enumerate(instructions):
|
|
yes = positions[i + yes] - positions[i]
|
|
no = positions[i + no] - positions[i]
|
|
instruction = InetDiagBcOp((op, yes, no)).Pack() + args[i]
|
|
#print("%3d: %d %3d %3d %s %s" % (positions[i], op, yes, no,
|
|
# arg, instruction.encode("hex")))
|
|
packed += instruction
|
|
#print
|
|
|
|
return packed
|
|
|
|
@staticmethod
|
|
def DecodeBytecode(bytecode):
|
|
instructions = []
|
|
try:
|
|
while bytecode:
|
|
op, rest = cstruct.Read(bytecode, InetDiagBcOp)
|
|
|
|
if op.code in [INET_DIAG_BC_NOP, INET_DIAG_BC_JMP, INET_DIAG_BC_AUTO]:
|
|
arg = None
|
|
elif op.code in [INET_DIAG_BC_S_GE, INET_DIAG_BC_S_LE,
|
|
INET_DIAG_BC_D_GE, INET_DIAG_BC_D_LE]:
|
|
op, rest = cstruct.Read(rest, InetDiagBcOp)
|
|
arg = op.no
|
|
elif op.code in [INET_DIAG_BC_S_COND, INET_DIAG_BC_D_COND]:
|
|
cond, rest = cstruct.Read(rest, InetDiagHostcond)
|
|
if cond.family == 0:
|
|
arg = (None, cond.prefix_len, cond.port)
|
|
else:
|
|
addrlen = 4 if cond.family == AF_INET else 16
|
|
addr, rest = rest[:addrlen], rest[addrlen:]
|
|
addr = inet_ntop(cond.family, addr)
|
|
arg = (addr, cond.prefix_len, cond.port)
|
|
elif op.code == INET_DIAG_BC_DEV_COND:
|
|
attrlen = struct.calcsize("=I")
|
|
attr, rest = rest[:attrlen], rest[attrlen:]
|
|
arg = struct.unpack("=I", attr)
|
|
elif op.code == INET_DIAG_BC_MARK_COND:
|
|
arg, rest = cstruct.Read(rest, InetDiagMarkcond)
|
|
else:
|
|
raise ValueError("Unknown opcode %d" % op.code)
|
|
instructions.append((op, arg))
|
|
bytecode = rest
|
|
|
|
return instructions
|
|
except (TypeError, ValueError):
|
|
return "???"
|
|
|
|
def Dump(self, diag_req, bytecode):
|
|
if bytecode:
|
|
bytecode = self._NlAttr(INET_DIAG_REQ_BYTECODE, bytecode)
|
|
|
|
out = self._Dump(SOCK_DIAG_BY_FAMILY, diag_req, InetDiagMsg, bytecode)
|
|
return out
|
|
|
|
def DumpAllInetSockets(self, protocol, bytecode, sock_id=None, ext=0,
|
|
states=ALL_NON_TIME_WAIT):
|
|
"""Dumps IPv4 or IPv6 sockets matching the specified parameters."""
|
|
# DumpSockets(AF_UNSPEC) does not result in dumping all inet sockets, it
|
|
# results in ENOENT.
|
|
if sock_id is None:
|
|
sock_id = self._EmptyInetDiagSockId()
|
|
|
|
sockets = []
|
|
for family in [AF_INET, AF_INET6]:
|
|
diag_req = InetDiagReqV2((family, protocol, ext, states, sock_id))
|
|
sockets += self.Dump(diag_req, bytecode)
|
|
|
|
return sockets
|
|
|
|
@staticmethod
|
|
def GetRawAddress(family, addr):
|
|
"""Fetches the source address from an InetDiagMsg."""
|
|
addrlen = {AF_INET:4, AF_INET6: 16}[family]
|
|
return inet_ntop(family, addr[:addrlen])
|
|
|
|
@staticmethod
|
|
def GetSourceAddress(diag_msg):
|
|
"""Fetches the source address from an InetDiagMsg."""
|
|
return SockDiag.GetRawAddress(diag_msg.family, diag_msg.id.src)
|
|
|
|
@staticmethod
|
|
def GetDestinationAddress(diag_msg):
|
|
"""Fetches the source address from an InetDiagMsg."""
|
|
return SockDiag.GetRawAddress(diag_msg.family, diag_msg.id.dst)
|
|
|
|
@staticmethod
|
|
def RawAddress(addr):
|
|
"""Converts an IP address string to binary format."""
|
|
family = AF_INET6 if ":" in addr else AF_INET
|
|
return inet_pton(family, addr)
|
|
|
|
@staticmethod
|
|
def PaddedAddress(addr):
|
|
"""Converts an IP address string to binary format for InetDiagSockId."""
|
|
padded = SockDiag.RawAddress(addr)
|
|
if len(padded) < 16:
|
|
padded += "\x00" * (16 - len(padded))
|
|
return padded
|
|
|
|
@staticmethod
|
|
def DiagReqFromSocket(s):
|
|
"""Creates an InetDiagReqV2 that matches the specified socket."""
|
|
family = s.getsockopt(net_test.SOL_SOCKET, net_test.SO_DOMAIN)
|
|
protocol = s.getsockopt(net_test.SOL_SOCKET, net_test.SO_PROTOCOL)
|
|
if net_test.LINUX_VERSION >= (3, 8):
|
|
iface = s.getsockopt(SOL_SOCKET, net_test.SO_BINDTODEVICE,
|
|
net_test.IFNAMSIZ)
|
|
iface = GetInterfaceIndex(iface) if iface else 0
|
|
else:
|
|
iface = 0
|
|
src, sport = s.getsockname()[:2]
|
|
try:
|
|
dst, dport = s.getpeername()[:2]
|
|
except error as e:
|
|
if e.errno == errno.ENOTCONN:
|
|
dport = 0
|
|
dst = "::" if family == AF_INET6 else "0.0.0.0"
|
|
else:
|
|
raise e
|
|
src = SockDiag.PaddedAddress(src)
|
|
dst = SockDiag.PaddedAddress(dst)
|
|
sock_id = InetDiagSockId((sport, dport, src, dst, iface, "\x00" * 8))
|
|
return InetDiagReqV2((family, protocol, 0, 0xffffffff, sock_id))
|
|
|
|
@staticmethod
|
|
def GetSocketCookie(s):
|
|
cookie = s.getsockopt(net_test.SOL_SOCKET, net_test.SO_COOKIE, 8)
|
|
return struct.unpack("=Q", cookie)[0]
|
|
|
|
def FindSockInfoFromFd(self, s):
|
|
"""Gets a diag_msg and attrs from the kernel for the specified socket."""
|
|
req = self.DiagReqFromSocket(s)
|
|
# The kernel doesn't use idiag_src and idiag_dst when dumping sockets, it
|
|
# only uses them when targeting a specific socket with a cookie. Check the
|
|
# the inode number to ensure we don't mistakenly match another socket on
|
|
# the same port but with a different IP address.
|
|
inode = os.fstat(s.fileno()).st_ino
|
|
results = self.Dump(req, "")
|
|
if len(results) == 0:
|
|
raise ValueError("Dump of %s returned no sockets" % req)
|
|
for diag_msg, attrs in results:
|
|
if diag_msg.inode == inode:
|
|
return diag_msg, attrs
|
|
raise ValueError("Dump of %s did not contain inode %d" % (req, inode))
|
|
|
|
def FindSockDiagFromFd(self, s):
|
|
"""Gets an InetDiagMsg from the kernel for the specified socket."""
|
|
return self.FindSockInfoFromFd(s)[0]
|
|
|
|
def GetSockInfo(self, req):
|
|
"""Gets a diag_msg and attrs from the kernel for the specified request."""
|
|
self._SendNlRequest(SOCK_DIAG_BY_FAMILY, req.Pack(), netlink.NLM_F_REQUEST)
|
|
return self._GetMsg(InetDiagMsg)
|
|
|
|
@staticmethod
|
|
def DiagReqFromDiagMsg(d, protocol):
|
|
"""Constructs a diag_req from a diag_msg the kernel has given us."""
|
|
return InetDiagReqV2((d.family, protocol, 0, 1 << d.state, d.id))
|
|
|
|
def CloseSocket(self, req):
|
|
self._SendNlRequest(SOCK_DESTROY, req.Pack(),
|
|
netlink.NLM_F_REQUEST | netlink.NLM_F_ACK)
|
|
|
|
def CloseSocketFromFd(self, s):
|
|
diag_msg, attrs = self.FindSockInfoFromFd(s)
|
|
protocol = s.getsockopt(SOL_SOCKET, net_test.SO_PROTOCOL)
|
|
req = self.DiagReqFromDiagMsg(diag_msg, protocol)
|
|
return self.CloseSocket(req)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
n = SockDiag()
|
|
n.DEBUG = True
|
|
bytecode = ""
|
|
sock_id = n._EmptyInetDiagSockId()
|
|
sock_id.dport = 443
|
|
ext = 1 << (INET_DIAG_TOS - 1) | 1 << (INET_DIAG_TCLASS - 1)
|
|
states = 0xffffffff
|
|
diag_msgs = n.DumpAllInetSockets(IPPROTO_TCP, "",
|
|
sock_id=sock_id, ext=ext, states=states)
|
|
print(diag_msgs)
|