diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e66bb90 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "travis"] + path = travis + url = ../travis diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..453076f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,117 @@ +# Travis configuration for gardener. +language: go +services: + - docker + +go: + - 1.9.x + + +########################################################################### +before_install: +# Coverage tools +- go get github.com/mattn/goveralls +- go get github.com/wadey/gocovmerge + +- echo Branch is ${TRAVIS_BRANCH} and Tag is $TRAVIS_TAG + +# Install test credentials. +# The service account variables are uploaded to travis by running, +# from root of repo directory: +# travis/setup_service_accounts_for_travis.sh +# +# All of the gcloud library calls will detect the GOOGLE_APPLICATION_CREDENTIALS +# environment variable, and use that file for authentication. +# If the application requires authentication outside the libraries, consider +# also using travis/activate_service_account.sh +- if [[ -n "$SERVICE_ACCOUNT_mlab_testing" ]] ; then + echo "$SERVICE_ACCOUNT_mlab_testing" > $TRAVIS_BUILD_DIR/creds.json ; + export GOOGLE_APPLICATION_CREDENTIALS=$TRAVIS_BUILD_DIR/creds.json ; + fi + +# These directories will be cached on successful "script" builds, and restored, +# if available, to save time on future builds. +cache: + directories: + - "$HOME/google-cloud-sdk/" + +install: +# Install dependencies +- GO_IMPORTS=$(go list -f '{{join .Imports "\n"}}{{"\n"}}{{join .TestImports "\n"}}' ./... | sort | uniq | grep -v etl-gardener) +- go get -u -v -d $GO_IMPORTS + +script: +# To start, run all the non-integration tests. +- MODULES="inetdiag" +- for module in $MODULES; do + COVER_PKGS=${COVER_PKGS}./$module/..., ; + done +- COVER_PKGS=${COVER_PKGS::-1} # Trim the trailing comma +- EC=0 +# Note that for modules in subdirectories, this replaces separating slashes with _. +- for module in $MODULES; do + go test -v -coverpkg=$COVER_PKGS -coverprofile=${module//\//_}.cov github.com/m-lab/tcp-info/$module ; + EC=$[ $EC || $? ] ; + done +- echo "summary status $EC" ; +- if [[ $EC != 0 ]]; then false; fi + + +# Rerun modules with integration tests. This means that some tests are repeated, but otherwise +# we lose some coverage. The corresponding cov files are overwritten, but that is OK since +# the non-integration tests are repeated. If we change the unit tests to NOT run when integration +# test tag is set, then we would need to have separate cov files. +# Note: we do not run integration tests from forked repos b/c the SA is unavailable. +# Note that for modules in subdirectories, this replaces separating slashes with _. +- if [[ -n "$SERVICE_ACCOUNT_mlab_testing" ]] ; then + for module in ; do + go test -v -coverpkg=$COVER_PKGS -coverprofile=${module//\//_}.cov github.com/m-lab/tcp-info/$module -tags=integration ; + EC=$[ $EC || $? ] ; + done ; + echo "summary status $EC" ; + if [[ $EC != 0 ]]; then false; fi ; + fi + +# Coveralls +# Run "unit tests" with coverage. +- $HOME/gopath/bin/gocovmerge *.cov > merge.cov +- $HOME/gopath/bin/goveralls -coverprofile=merge.cov -service=travis-ci + + +################################################################################# +# Deployment Section +# +# Overview: +# 1. Test in sandbox during development +# 2. Deploy to staging on commit to integration +# 3. Deploy to prod when a branch is tagged with prod-* or xxx-prod-* +# +# We want to test individual components in sandbox, and avoid stepping on each +# other, so we do NOT automate deployment to sandbox. Each person should +# use a branch name to trigger the single deployment that they are working on. +# +# We want to soak all code in staging before deploying to prod. To avoid +# incompatible components, we deploy ALL elements to staging when we merge +# to integration branch. +# +# Deployments to prod are done by deliberately tagging a specific commit, +# typically in the integration branch, with a tag starting with prod-*. +# DO NOT just tag the latest version in integration, as someone may have +# pushed new code that hasn't had a chance to soak in staging. +# +# +# Deploy steps never trigger on a new Pull Request. Deploy steps will trigger +# on specific branch name patterns, after a merge to integration, or on +# an explicit tag that matches "on:" conditions. +################################################################################# + + +deploy: +######################################### +## Sandbox + +######################################### +## Staging + +######################################### +## Production diff --git a/README.md b/README.md index 88796a7..6595d37 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ # tcp-info +| branch | travis-ci | report-card | coveralls | +|--------|-----------|-----------|-------------| +| master | [![Travis Build Status](https://travis-ci.org/m-lab/tcp-info.svg?branch=master)](https://travis-ci.org/m-lab/tcp-info) | [![Go Report Card](https://goreportcard.com/badge/github.com/m-lab/tcp-info)](https://goreportcard.com/report/github.com/m-lab/tcp-info) | [![Coverage Status](https://coveralls.io/repos/m-lab/tcp-info/badge.svg?branch=master)](https://coveralls.io/github/m-lab/tcp-info?branch=master) | + + + Fast tcp-info collector in Go diff --git a/inetdiag/inetdiag.go b/inetdiag/inetdiag.go new file mode 100644 index 0000000..8c4b5ee --- /dev/null +++ b/inetdiag/inetdiag.go @@ -0,0 +1,205 @@ +// Package inetdiag provides basic structs and utilities for INET_DIAG messaages. +// Based on uapi/linux/inet_diag.h. +package inetdiag + +// Pretty basic code slightly adapted from code copied from +// https://gist.github.com/gwind/05f5f649d93e6015cf47ffa2b2fd9713 +// Original source no longer available at https://github.com/eleme/netlink/blob/master/inetdiag.go + +// Adaptations are Copyright 2018 M-Lab Authors +// +// 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. + +/* IMPORTANT NOTES +This 2002 article describes Netlink Sockets +https://pdfs.semanticscholar.org/6efd/e161a2582ba5846e4b8fea5a53bc305a64f3.pdf + +"Netlink messages are aligned to 32 bits and, generally speaking, they contain data that is +expressed in host-byte order" +*/ + +import ( + "fmt" + "log" + "net" + "syscall" + "unsafe" +) + +// Constants from linux. +const ( + TCPDIAG_GETSOCK = 18 // uapi/linux/inet_diag.h + SOCK_DIAG_BY_FAMILY = 20 // uapi/linux/sock_diag.h +) + +// netinet/tcp.h +const ( + _ = iota + TCP_ESTABLISHED = iota + TCP_SYN_SENT + TCP_SYN_RECV + TCP_FIN_WAIT1 + TCP_FIN_WAIT2 + TCP_TIME_WAIT + TCP_CLOSE + TCP_CLOSE_WAIT + TCP_LAST_ACK + TCP_LISTEN + TCP_CLOSING +) + +const ( + TCP_ALL_STATES = 0xFFF +) + +var tcpStatesMap = map[uint8]string{ + TCP_ESTABLISHED: "established", + TCP_SYN_SENT: "syn_sent", + TCP_SYN_RECV: "syn_recv", + TCP_FIN_WAIT1: "fin_wait1", + TCP_FIN_WAIT2: "fin_wait2", + TCP_TIME_WAIT: "time_wait", + TCP_CLOSE: "close", + TCP_CLOSE_WAIT: "close_wait", + TCP_LAST_ACK: "last_ack", + TCP_LISTEN: "listen", + TCP_CLOSING: "closing", +} + +var diagFamilyMap = map[uint8]string{ + syscall.AF_INET: "tcp", + syscall.AF_INET6: "tcp6", +} + +// InetDiagSockID is the binary linux representation of a socket, as in linux/inet_diag.h +// Note that netlink messages use host byte ordering, unless NLA_F_NET_BYTEORDER flag is present. +type InetDiagSockID struct { + IDiagSPort uint16 + IDiagDPort uint16 + IDiagSrc [16]byte + IDiagDst [16]byte + IDiagIf uint32 + IDiagCookie [2]uint32 // This cannot be uint64, because of alignment rules. +} + +// SrcIP returns a golang net encoding of source address. +func (id *InetDiagSockID) SrcIP() net.IP { + return ip(id.IDiagSrc) +} + +// DstIP returns a golang net encoding of destination address. +func (id *InetDiagSockID) DstIP() net.IP { + return ip(id.IDiagDst) +} + +// TODO should use more net.IP code instead of custom code. +func ip(bytes [16]byte) net.IP { + if isIpv6(bytes) { + return ipv6(bytes) + } else { + return ipv4(bytes) + } +} + +func isIpv6(original [16]byte) bool { + for i := 4; i < 16; i++ { + if original[i] != 0 { + return true + } + } + return false +} + +func ipv4(original [16]byte) net.IP { + return net.IPv4(original[0], original[1], original[2], original[3]) +} + +func ipv6(original [16]byte) net.IP { + return original[:] +} + +func (id *InetDiagSockID) String() string { + return fmt.Sprintf("%s:%d -> %s:%d", id.SrcIP().String(), id.IDiagSPort, id.DstIP().String(), id.IDiagDPort) +} + +// InetDiagReqV2 is the Netlink request struct, as in linux/inet_diag.h +// Note that netlink messages use host byte ordering, unless NLA_F_NET_BYTEORDER flag is present. +type InetDiagReqV2 struct { + SDiagFamily uint8 + SDiagProtocol uint8 + IDiagExt uint8 + Pad uint8 + IDiagStates uint32 + ID InetDiagSockID +} + +// SizeofInetDiagReqV2 is the size of the struct. +// TODO should we just make this explicit in the code? +const SizeofInetDiagReqV2 = int(unsafe.Sizeof(InetDiagReqV2{})) // Should be 0x38 + +// Serialize is provided for json serialization? +// TODO - should use binary functions instead? +func (req *InetDiagReqV2) Serialize() []byte { + return (*(*[SizeofInetDiagReqV2]byte)(unsafe.Pointer(req)))[:] +} + +// Len is provided for json serialization? +func (req *InetDiagReqV2) Len() int { + return SizeofInetDiagReqV2 +} + +// NewInetDiagReqV2 creates a new request. +func NewInetDiagReqV2(family, protocol uint8, states uint32) *InetDiagReqV2 { + return &InetDiagReqV2{ + SDiagFamily: family, + SDiagProtocol: protocol, + IDiagStates: states, + } +} + +// InetDiagMsg is the linux binary representation of a InetDiag message header, as in linus/inet_diag.h +// Note that netlink messages use host byte ordering, unless NLA_F_NET_BYTEORDER flag is present. +type InetDiagMsg struct { + IDiagFamily uint8 + IDiagState uint8 + IDiagTimer uint8 + IDiagRetrans uint8 + ID InetDiagSockID + IDiagExpires uint32 + IDiagRqueue uint32 + IDiagWqueue uint32 + IDiagUID uint32 + IDiagInode uint32 +} + +func (msg *InetDiagMsg) String() string { + return fmt.Sprintf("%s, %s, %s", diagFamilyMap[msg.IDiagFamily], tcpStatesMap[msg.IDiagState], msg.ID.String()) +} + +// rtaAlignOf round the length of a netlink route attribute up to align it +// properly. +func rtaAlignOf(attrlen int) int { + return (attrlen + syscall.RTA_ALIGNTO - 1) & ^(syscall.RTA_ALIGNTO - 1) +} + +// ParseInetDiagMsg returns the InetDiagMsg itself, and the aligned byte array containing the message content. +// Modified from original to also return attribute data array. +func ParseInetDiagMsg(data []byte) (*InetDiagMsg, []byte) { + align := rtaAlignOf(int(unsafe.Sizeof(InetDiagMsg{}))) + if len(data) < align { + log.Println("Wrong length", len(data), "<", align) + log.Println(data) + return nil, nil + } + return (*InetDiagMsg)(unsafe.Pointer(&data[0])), data[rtaAlignOf(int(unsafe.Sizeof(InetDiagMsg{}))):] +} diff --git a/inetdiag/inetdiag_test.go b/inetdiag/inetdiag_test.go new file mode 100644 index 0000000..61a1998 --- /dev/null +++ b/inetdiag/inetdiag_test.go @@ -0,0 +1,107 @@ +package inetdiag_test + +import ( + "log" + "syscall" + "testing" + "unsafe" + + "github.com/m-lab/tcp-info/inetdiag" +) + +// This is not exhaustive, but covers the basics. Integration tests will expose any more subtle +// problems. + +func TestSizes(t *testing.T) { + if unsafe.Sizeof(inetdiag.InetDiagSockID{}) != 48 { + t.Error("SockID wrong size", unsafe.Sizeof(inetdiag.InetDiagSockID{})) + } + + hdr := inetdiag.InetDiagMsg{} + if unsafe.Sizeof(hdr) != 4*6+unsafe.Sizeof(inetdiag.InetDiagSockID{}) { + t.Error("Header is wrong size", unsafe.Sizeof(hdr)) + } +} + +func TestParseInetDiagMsg(t *testing.T) { + var data [100]byte + for i := range data { + data[i] = byte(i + 2) + } + hdr, value := inetdiag.ParseInetDiagMsg(data[:]) + if hdr.IDiagFamily != syscall.AF_INET { + t.Errorf("Failed %+v\n", hdr) + } + if hdr.IDiagState != inetdiag.TCP_SYN_RECV { + t.Errorf("Failed %+v\n", hdr) + } + + if len(value) != 28 { + t.Error("Len", len(value)) + } +} + +func TestSerialize(t *testing.T) { + v2 := inetdiag.NewInetDiagReqV2(syscall.AF_INET, 23, 0x0E) + data := v2.Serialize() + if v2.Len() != len(data) { + t.Error("That's odd") + } +} + +func TestID4(t *testing.T) { + var data [unsafe.Sizeof(inetdiag.InetDiagMsg{})]byte + srcIPOffset := unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID) + unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID.IDiagSrc) + data[srcIPOffset] = 127 + data[srcIPOffset+1] = 0 + data[srcIPOffset+2] = 0 + data[srcIPOffset+3] = 1 + + srcPortOffset := unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID) + unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID.IDiagSPort) + // netlink uses host byte ordering, which may or may not be network byte ordering. So no swapping should be + // done. + *(*uint16)(unsafe.Pointer(&data[srcPortOffset])) = 0x1234 + + dstIPOffset := unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID) + unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID.IDiagDst) + data[dstIPOffset] = 1 + data[dstIPOffset+1] = 0 + data[dstIPOffset+2] = 0 + data[dstIPOffset+3] = 127 // Looks like localhost, but its reversed. + + hdr, _ := inetdiag.ParseInetDiagMsg(data[:]) + if !hdr.ID.SrcIP().IsLoopback() { + log.Println(hdr.ID.SrcIP().IsLoopback()) + } + if hdr.ID.IDiagSPort != 0x1234 { + t.Errorf("SPort should be 0x1234 %+v\n", hdr.ID) + } + + if !hdr.ID.SrcIP().IsLoopback() { + t.Errorf("Should be identified as loopback") + } + if hdr.ID.DstIP().IsLoopback() { + t.Errorf("Should not be identified as loopback") // Yeah I know this is not self-consistent. :P + } +} + +func TestID6(t *testing.T) { + var data [unsafe.Sizeof(inetdiag.InetDiagMsg{})]byte + srcIPOffset := unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID) + unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID.IDiagSrc) + for i := 0; i < 8; i++ { + data[srcIPOffset] = byte(0x0A + i) + } + + dstIPOffset := unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID) + unsafe.Offsetof(inetdiag.InetDiagMsg{}.ID.IDiagDst) + for i := 0; i < 8; i++ { + data[dstIPOffset] = byte(i + 1) + } + + hdr, _ := inetdiag.ParseInetDiagMsg(data[:]) + + if hdr.ID.SrcIP().IsLoopback() { + t.Errorf("Should not be identified as loopback") + } + if hdr.ID.DstIP().IsLoopback() { + t.Errorf("Should not be identified as loopback") + } +} diff --git a/travis b/travis new file mode 160000 index 0000000..5f139a8 --- /dev/null +++ b/travis @@ -0,0 +1 @@ +Subproject commit 5f139a87a39e67c3e18a13965169de38f3134c51