Repository: prometheus/procfs
Pull request #332 (2020-09-20 19:02:59)

support to read from /proc/net/tcp{,6}
This provides support to read from /proc/net/tcp and /proc/net/tcp6. Which had originally been done with https://github.com/prometheus/procfs/pull/20, but that was considered out of date, so @SuperQ suggested I open a new PR.

This change is not as extensive as it looks.

Because /proc/net/tcp{,6} is identical to /proc/net/udp{,6} and udp support already exists in net_udp.go, I went and refactored the net_udp.go to create a new set of base classes in net_ip_socket.go that could be used for both net_udp.go and net_tcp.go.

I moved most of the test code from net_udp_test.go and into net_ip_socket_test.go. I replicated the existing net_udp_test.go tests into net_tcp_test.go, and added a new tcp fixture identical to the udp one. I validated that the old UDP tests still work and new identical TCP tests also work.

I'm happy to rewrite this in a different way if people don't think this is a good idea. I had considered doing a copy of the existing net_udp.go code and just changing names to support TCP, but thought this was a bit more reusable if formats change or bugs are uncovered they would likely apply to both tcp and udp.


SuperQ: Can you fix the DCO with `git commit -s --amend`?

Patch:
diff --git a/Makefile.common b/Makefile.common
index 3f3d02c..3ac29c6 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -78,7 +78,7 @@ ifneq ($(shell which gotestsum),)
 endif
 endif
 
-PROMU_VERSION ?= 0.6.0
+PROMU_VERSION ?= 0.7.0
 PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
 
 GOLANGCI_LINT :=
@@ -245,10 +245,12 @@ common-docker-publish: $(PUBLISH_DOCKER_ARCHS)
 $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:
 	docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)"
 
+DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION)))
 .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS)
 common-docker-tag-latest: $(TAG_DOCKER_ARCHS)
 $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:
 	docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"
+	docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"
 
 .PHONY: common-docker-manifest
 common-docker-manifest:
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..67741f0
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,6 @@
+# Reporting a security issue
+
+The Prometheus security policy, including how to report vulnerabilities, can be
+found here:
+
+https://prometheus.io/docs/operating/security/
diff --git a/fixtures.ttar b/fixtures.ttar
index 4eda11e..acbdcb0 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -2204,10 +2204,25 @@ Lines: 1
 00015c73 00020e76 F0000769 00000000
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/tcp
+Lines: 4
+  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
+   0: 0500000A:0016 00000000:0000 0A 00000000:00000001 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0
+   1: 00000000:0016 00000000:0000 0A 00000001:00000000 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0
+   2: 00000000:0016 00000000:0000 0A 00000001:00000001 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/tcp6
+Lines: 3
+  sl  local_address                         remote_address                        st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode ref pointer drops
+ 1315: 00000000000000000000000000000000:14EB 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000   981        0 21040 2 0000000013726323 0
+ 6073: 000080FE00000000FFADE15609667CFE:C781 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000  1000        0 11337031 2 00000000b9256fdd 0
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/net/udp
 Lines: 4
   sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
-   0: 0A000005:0016 00000000:0000 0A 00000000:00000001 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0
+   0: 0500000A:0016 00000000:0000 0A 00000000:00000001 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0
    1: 00000000:0016 00000000:0000 0A 00000001:00000000 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0
    2: 00000000:0016 00000000:0000 0A 00000001:00000001 00:00000000 00000000     0        0 2740 1 ffff88003d3af3c0 100 0 0 10 0
 Mode: 644
diff --git a/net_ip_socket.go b/net_ip_socket.go
new file mode 100644
index 0000000..bb9e966
--- /dev/null
+++ b/net_ip_socket.go
@@ -0,0 +1,228 @@
+// Copyright 2020 The Prometheus 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.
+
+package procfs
+
+import (
+	"bufio"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"net"
+	"os"
+	"strconv"
+	"strings"
+)
+
+const (
+	// readLimit is used by io.LimitReader while reading the content of the
+	// /proc/net/udp{,6} files. The number of lines inside such a file is dynamic
+	// as each line represents a single used socket.
+	// In theory, the number of available sockets is 65535 (2^16 - 1) per IP.
+	// With e.g. 150 Byte per line and the maximum number of 65535,
+	// the reader needs to handle 150 Byte * 65535 =~ 10 MB for a single IP.
+	readLimit = 4294967296 // Byte -> 4 GiB
+)
+
+// this contains generic data structures for both udp and tcp sockets
+type (
+	// NetIPSocket represents the contents of /proc/net/{t,u}dp{,6} file without the header.
+	NetIPSocket []*netIPSocketLine
+
+	// NetIPSocketSummary provides already computed values like the total queue lengths or
+	// the total number of used sockets. In contrast to NetIPSocket it does not collect
+	// the parsed lines into a slice.
+	NetIPSocketSummary struct {
+		// TxQueueLength shows the total queue length of all parsed tx_queue lengths.
+		TxQueueLength uint64
+		// RxQueueLength shows the total queue length of all parsed rx_queue lengths.
+		RxQueueLength uint64
+		// UsedSockets shows the total number of parsed lines representing the
+		// number of used sockets.
+		UsedSockets uint64
+	}
+
+	// netIPSocketLine represents the fields parsed from a single line
+	// in /proc/net/{t,u}dp{,6}. Fields which are not used by IPSocket are skipped.
+	// For the proc file format details, see https://linux.die.net/man/5/proc.
+	netIPSocketLine struct {
+		Sl        uint64
+		LocalAddr net.IP
+		LocalPort uint64
+		RemAddr   net.IP
+		RemPort   uint64
+		St        uint64
+		TxQueue   uint64
+		RxQueue   uint64
+		UID       uint64
+	}
+)
+
+func newNetIPSocket(file string) (NetIPSocket, error) {
+	f, err := os.Open(file)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	var netIPSocket NetIPSocket
+
+	lr := io.LimitReader(f, readLimit)
+	s := bufio.NewScanner(lr)
+	s.Scan() // skip first line with headers
+	for s.Scan() {
+		fields := strings.Fields(s.Text())
+		line, err := parseNetIPSocketLine(fields)
+		if err != nil {
+			return nil, err
+		}
+		netIPSocket = append(netIPSocket, line)
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+	return netIPSocket, nil
+}
+
+// newNetIPSocketSummary creates a new NetIPSocket{,6} from the contents of the given file.
+func newNetIPSocketSummary(file string) (*NetIPSocketSummary, error) {
+	f, err := os.Open(file)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	var netIPSocketSummary NetIPSocketSummary
+
+	lr := io.LimitReader(f, readLimit)
+	s := bufio.NewScanner(lr)
+	s.Scan() // skip first line with headers
+	for s.Scan() {
+		fields := strings.Fields(s.Text())
+		line, err := parseNetIPSocketLine(fields)
+		if err != nil {
+			return nil, err
+		}
+		netIPSocketSummary.TxQueueLength += line.TxQueue
+		netIPSocketSummary.RxQueueLength += line.RxQueue
+		netIPSocketSummary.UsedSockets++
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+	return &netIPSocketSummary, nil
+}
+
+// the /proc/net/{t,u}dp{,6} files are network byte order for ipv4 and for ipv6 the address is four words consisting of four bytes each. In each of those four words the four bytes are written in reverse order.
+
+func parseIP(hexIP string) (net.IP, error) {
+	var byteIP []byte
+	byteIP, err := hex.DecodeString(hexIP)
+	if err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse address field in socket line: %s", hexIP)
+	}
+	switch len(byteIP) {
+	case 4:
+		return net.IP{byteIP[3], byteIP[2], byteIP[1], byteIP[0]}, nil
+	case 16:
+		i := net.IP{
+			byteIP[3], byteIP[2], byteIP[1], byteIP[0],
+			byteIP[7], byteIP[6], byteIP[5], byteIP[4],
+			byteIP[11], byteIP[10], byteIP[9], byteIP[8],
+			byteIP[15], byteIP[14], byteIP[13], byteIP[12],
+		}
+		return i, nil
+	default:
+		return nil, fmt.Errorf("Unable to parse IP %s", hexIP)
+	}
+}
+
+// parseNetIPSocketLine parses a single line, represented by a list of fields.
+func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
+	line := &netIPSocketLine{}
+	if len(fields) < 8 {
+		return nil, fmt.Errorf(
+			"cannot parse net socket line as it has less then 8 columns: %s",
+			strings.Join(fields, " "),
+		)
+	}
+	var err error // parse error
+
+	// sl
+	s := strings.Split(fields[0], ":")
+	if len(s) != 2 {
+		return nil, fmt.Errorf(
+			"cannot parse sl field in socket line: %s", fields[0])
+	}
+
+	if line.Sl, err = strconv.ParseUint(s[0], 0, 64); err != nil {
+		return nil, fmt.Errorf("cannot parse sl value in socket line: %s", err)
+	}
+	// local_address
+	l := strings.Split(fields[1], ":")
+	if len(l) != 2 {
+		return nil, fmt.Errorf(
+			"cannot parse local_address field in socket line: %s", fields[1])
+	}
+	if line.LocalAddr, err = parseIP(l[0]); err != nil {
+		return nil, err
+	}
+	if line.LocalPort, err = strconv.ParseUint(l[1], 16, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse local_address port value in socket line: %s", err)
+	}
+
+	// remote_address
+	r := strings.Split(fields[2], ":")
+	if len(r) != 2 {
+		return nil, fmt.Errorf(
+			"cannot parse rem_address field in socket line: %s", fields[1])
+	}
+	if line.RemAddr, err = parseIP(r[0]); err != nil {
+		return nil, err
+	}
+	if line.RemPort, err = strconv.ParseUint(r[1], 16, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse rem_address port value in socket line: %s", err)
+	}
+
+	// st
+	if line.St, err = strconv.ParseUint(fields[3], 16, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse st value in socket line: %s", err)
+	}
+
+	// tx_queue and rx_queue
+	q := strings.Split(fields[4], ":")
+	if len(q) != 2 {
+		return nil, fmt.Errorf(
+			"cannot parse tx/rx queues in socket line as it has a missing colon: %s",
+			fields[4],
+		)
+	}
+	if line.TxQueue, err = strconv.ParseUint(q[0], 16, 64); err != nil {
+		return nil, fmt.Errorf("cannot parse tx_queue value in socket line: %s", err)
+	}
+	if line.RxQueue, err = strconv.ParseUint(q[1], 16, 64); err != nil {
+		return nil, fmt.Errorf("cannot parse rx_queue value in socket line: %s", err)
+	}
+
+	// uid
+	if line.UID, err = strconv.ParseUint(fields[7], 0, 64); err != nil {
+		return nil, fmt.Errorf(
+			"cannot parse uid value in socket line: %s", err)
+	}
+
+	return line, nil
+}
diff --git a/net_tcp.go b/net_tcp.go
new file mode 100644
index 0000000..5277629
--- /dev/null
+++ b/net_tcp.go
@@ -0,0 +1,64 @@
+// Copyright 2020 The Prometheus 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.
+
+package procfs
+
+type (
+	// NetTCP represents the contents of /proc/net/tcp{,6} file without the header.
+	NetTCP []*netIPSocketLine
+
+	// NetTCPSummary provides already computed values like the total queue lengths or
+	// the total number of used sockets. In contrast to NetTCP it does not collect
+	// the parsed lines into a slice.
+	NetTCPSummary NetIPSocketSummary
+)
+
+// NetTCP returns the IPv4 kernel/networking statistics for TCP datagrams
+// read from /proc/net/tcp.
+func (fs FS) NetTCP() (NetTCP, error) {
+	return newNetTCP(fs.proc.Path("net/tcp"))
+}
+
+// NetTCP6 returns the IPv6 kernel/networking statistics for TCP datagrams
+// read from /proc/net/tcp6.
+func (fs FS) NetTCP6() (NetTCP, error) {
+	return newNetTCP(fs.proc.Path("net/tcp6"))
+}
+
+// NetTCPSummary returns already computed statistics like the total queue lengths
+// for TCP datagrams read from /proc/net/tcp.
+func (fs FS) NetTCPSummary() (*NetTCPSummary, error) {
+	return newNetTCPSummary(fs.proc.Path("net/tcp"))
+}
+
+// NetTCP6Summary returns already computed statistics like the total queue lengths
+// for TCP datagrams read from /proc/net/tcp6.
+func (fs FS) NetTCP6Summary() (*NetTCPSummary, error) {
+	return newNetTCPSummary(fs.proc.Path("net/tcp6"))
+}
+
+// newNetTCP creates a new NetTCP{,6} from the contents of the given file.
+func newNetTCP(file string) (NetTCP, error) {
+	n, err := newNetIPSocket(file)
+	n1 := NetTCP(n)
+	return n1, err
+}
+
+func newNetTCPSummary(file string) (*NetTCPSummary, error) {
+	n, err := newNetIPSocketSummary(file)
+	if n == nil {
+		return nil, err
+	}
+	n1 := NetTCPSummary(*n)
+	return &n1, err
+}
diff --git a/net_udp.go b/net_udp.go
index d017e3f..1785a2b 100644
--- a/net_udp.go
+++ b/net_udp.go
@@ -13,58 +13,14 @@
 
 package procfs
 
-import (
-	"bufio"
-	"encoding/hex"
-	"fmt"
-	"io"
-	"net"
-	"os"
-	"strconv"
-	"strings"
-)
-
-const (
-	// readLimit is used by io.LimitReader while reading the content of the
-	// /proc/net/udp{,6} files. The number of lines inside such a file is dynamic
-	// as each line represents a single used socket.
-	// In theory, the number of available sockets is 65535 (2^16 - 1) per IP.
-	// With e.g. 150 Byte per line and the maximum number of 65535,
-	// the reader needs to handle 150 Byte * 65535 =~ 10 MB for a single IP.
-	readLimit = 4294967296 // Byte -> 4 GiB
-)
-
 type (
 	// NetUDP represents the contents of /proc/net/udp{,6} file without the header.
-	NetUDP []*netUDPLine
+	NetUDP []*netIPSocketLine
 
 	// NetUDPSummary provides already computed values like the total queue lengths or
 	// the total number of used sockets. In contrast to NetUDP it does not collect
 	// the parsed lines into a slice.
-	NetUDPSummary struct {
-		// TxQueueLength shows the total queue length of all parsed tx_queue lengths.
-		TxQueueLength uint64
-		// RxQueueLength shows the total queue length of all parsed rx_queue lengths.
-		RxQueueLength uint64
-		// UsedSockets shows the total number of parsed lines representing the
-		// number of used sockets.
-		UsedSockets uint64
-	}
-
-	// netUDPLine represents the fields parsed from a single line
-	// in /proc/net/udp{,6}. Fields which are not used by UDP are skipped.
-	// For the proc file format details, see https://linux.die.net/man/5/proc.
-	netUDPLine struct {
-		Sl        uint64
-		LocalAddr net.IP
-		LocalPort uint64
-		RemAddr   net.IP
-		RemPort   uint64
-		St        uint64
-		TxQueue   uint64
-		RxQueue   uint64
-		UID       uint64
-	}
+	NetUDPSummary NetIPSocketSummary
 )
 
 // NetUDP returns the IPv4 kernel/networking statistics for UDP datagrams
@@ -82,148 +38,31 @@ func (fs FS) NetUDP6() (NetUDP, error) {
 // NetUDPSummary returns already computed statistics like the total queue lengths
 // for UDP datagrams read from /proc/net/udp.
 func (fs FS) NetUDPSummary() (*NetUDPSummary, error) {
-	return newNetUDPSummary(fs.proc.Path("net/udp"))
+	n, err := newNetUDPSummary(fs.proc.Path("net/udp"))
+	n1 := NetUDPSummary(*n)
+	return &n1, err
 }
 
 // NetUDP6Summary returns already computed statistics like the total queue lengths
 // for UDP datagrams read from /proc/net/udp6.
 func (fs FS) NetUDP6Summary() (*NetUDPSummary, error) {
-	return newNetUDPSummary(fs.proc.Path("net/udp6"))
+	n, err := newNetUDPSummary(fs.proc.Path("net/udp6"))
+	n1 := NetUDPSummary(*n)
+	return &n1, err
 }
 
 // newNetUDP creates a new NetUDP{,6} from the contents of the given file.
 func newNetUDP(file string) (NetUDP, error) {
-	f, err := os.Open(file)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-
-	netUDP := NetUDP{}
-
-	lr := io.LimitReader(f, readLimit)
-	s := bufio.NewScanner(lr)
-	s.Scan() // skip first line with headers
-	for s.Scan() {
-		fields := strings.Fields(s.Text())
-		line, err := parseNetUDPLine(fields)
-		if err != nil {
-			return nil, err
-		}
-		netUDP = append(netUDP, line)
-	}
-	if err := s.Err(); err != nil {
-		return nil, err
-	}
-	return netUDP, nil
+	n, err := newNetIPSocket(file)
+	n1 := NetUDP(n)
+	return n1, err
 }
 
-// newNetUDPSummary creates a new NetUDP{,6} from the contents of the given file.
 func newNetUDPSummary(file string) (*NetUDPSummary, error) {
-	f, err := os.Open(file)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-
-	netUDPSummary := &NetUDPSummary{}
-
-	lr := io.LimitReader(f, readLimit)
-	s := bufio.NewScanner(lr)
-	s.Scan() // skip first line with headers
-	for s.Scan() {
-		fields := strings.Fields(s.Text())
-		line, err := parseNetUDPLine(fields)
-		if err != nil {
-			return nil, err
-		}
-		netUDPSummary.TxQueueLength += line.TxQueue
-		netUDPSummary.RxQueueLength += line.RxQueue
-		netUDPSummary.UsedSockets++
-	}
-	if err := s.Err(); err != nil {
+	n, err := newNetIPSocketSummary(file)
+	if n == nil {
 		return nil, err
 	}
-	return netUDPSummary, nil
-}
-
-// parseNetUDPLine parses a single line, represented by a list of fields.
-func parseNetUDPLine(fields []string) (*netUDPLine, error) {
-	line := &netUDPLine{}
-	if len(fields) < 8 {
-		return nil, fmt.Errorf(
-			"cannot parse net udp socket line as it has less then 8 columns: %s",
-			strings.Join(fields, " "),
-		)
-	}
-	var err error // parse error
-
-	// sl
-	s := strings.Split(fields[0], ":")
-	if len(s) != 2 {
-		return nil, fmt.Errorf(
-			"cannot parse sl field in udp socket line: %s", fields[0])
-	}
-
-	if line.Sl, err = strconv.ParseUint(s[0], 0, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse sl value in udp socket line: %s", err)
-	}
-	// local_address
-	l := strings.Split(fields[1], ":")
-	if len(l) != 2 {
-		return nil, fmt.Errorf(
-			"cannot parse local_address field in udp socket line: %s", fields[1])
-	}
-	if line.LocalAddr, err = hex.DecodeString(l[0]); err != nil {
-		return nil, fmt.Errorf(
-			"cannot parse local_address value in udp socket line: %s", err)
-	}
-	if line.LocalPort, err = strconv.ParseUint(l[1], 16, 64); err != nil {
-		return nil, fmt.Errorf(
-			"cannot parse local_address port value in udp socket line: %s", err)
-	}
-
-	// remote_address
-	r := strings.Split(fields[2], ":")
-	if len(r) != 2 {
-		return nil, fmt.Errorf(
-			"cannot parse rem_address field in udp socket line: %s", fields[1])
-	}
-	if line.RemAddr, err = hex.DecodeString(r[0]); err != nil {
-		return nil, fmt.Errorf(
-			"cannot parse rem_address value in udp socket line: %s", err)
-	}
-	if line.RemPort, err = strconv.ParseUint(r[1], 16, 64); err != nil {
-		return nil, fmt.Errorf(
-			"cannot parse rem_address port value in udp socket line: %s", err)
-	}
-
-	// st
-	if line.St, err = strconv.ParseUint(fields[3], 16, 64); err != nil {
-		return nil, fmt.Errorf(
-			"cannot parse st value in udp socket line: %s", err)
-	}
-
-	// tx_queue and rx_queue
-	q := strings.Split(fields[4], ":")
-	if len(q) != 2 {
-		return nil, fmt.Errorf(
-			"cannot parse tx/rx queues in udp socket line as it has a missing colon: %s",
-			fields[4],
-		)
-	}
-	if line.TxQueue, err = strconv.ParseUint(q[0], 16, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse tx_queue value in udp socket line: %s", err)
-	}
-	if line.RxQueue, err = strconv.ParseUint(q[1], 16, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse rx_queue value in udp socket line: %s", err)
-	}
-
-	// uid
-	if line.UID, err = strconv.ParseUint(fields[7], 0, 64); err != nil {
-		return nil, fmt.Errorf(
-			"cannot parse uid value in udp socket line: %s", err)
-	}
-
-	return line, nil
+	n1 := NetUDPSummary(*n)
+	return &n1, err
 }
diff --git a/proc_cgroup.go b/proc_cgroup.go
index 4abd464..0094a13 100644
--- a/proc_cgroup.go
+++ b/proc_cgroup.go
@@ -49,7 +49,7 @@ type Cgroup struct {
 func parseCgroupString(cgroupStr string) (*Cgroup, error) {
 	var err error
 
-	fields := strings.Split(cgroupStr, ":")
+	fields := strings.SplitN(cgroupStr, ":", 3)
 	if len(fields) < 3 {
 		return nil, fmt.Errorf("at least 3 fields required, found %d fields in cgroup string: %s", len(fields), cgroupStr)
 	}


Test patch:
diff --git a/net_ip_socket_test.go b/net_ip_socket_test.go
new file mode 100644
index 0000000..a8bf91f
--- /dev/null
+++ b/net_ip_socket_test.go
@@ -0,0 +1,108 @@
+// Copyright 2020 The Prometheus 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.
+
+package procfs
+
+import (
+	"net"
+	"reflect"
+	"testing"
+)
+
+func Test_parseNetIPSocketLine(t *testing.T) {
+	tests := []struct {
+		fields  []string
+		name    string
+		want    *netIPSocketLine
+		wantErr bool
+	}{
+		{
+			name:   "reading valid lines, no issue should happened",
+			fields: []string{"11:", "00000000:0000", "00000000:0000", "0A", "00000017:0000002A", "0:0", "0", "1000"},
+			want: &netIPSocketLine{
+				Sl:        11,
+				LocalAddr: net.IP{0, 0, 0, 0},
+				LocalPort: 0,
+				RemAddr:   net.IP{0, 0, 0, 0},
+				RemPort:   0,
+				St:        10,
+				TxQueue:   23,
+				RxQueue:   42,
+				UID:       1000,
+			},
+		},
+		{
+			name:    "error case - invalid line - number of fields/columns < 8",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "0:0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse sl - not a valid uint",
+			fields:  []string{"a:", "00000000:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse local_address - not a valid hex",
+			fields:  []string{"1:", "0000000O:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse rem_address - not a valid hex",
+			fields:  []string{"1:", "00000000:0000", "0000000O:0000", "07", "00000000:00000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - cannot parse line - missing colon",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "0000000000000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse tx_queue - not a valid hex",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "DEADCODE:00000001", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse rx_queue - not a valid hex",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "00000000:FEEDCODE", "0:0", "0", "0"},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse UID - not a valid uint",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "-10"},
+			want:    nil,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseNetIPSocketLine(tt.fields)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseNetIPSocketLine() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if tt.want == nil && got != nil {
+				t.Errorf("parseNetIPSocketLine() = %v, want %v", got, tt.want)
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseNetIPSocketLine() = %#v, want %#v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/net_tcp_test.go b/net_tcp_test.go
new file mode 100644
index 0000000..f00d4b5
--- /dev/null
+++ b/net_tcp_test.go
@@ -0,0 +1,169 @@
+// Copyright 2020 The Prometheus 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.
+
+package procfs
+
+import (
+	"net"
+	"reflect"
+	"testing"
+)
+
+func Test_newNetTCP(t *testing.T) {
+	tests := []struct {
+		name    string
+		file    string
+		want    NetTCP
+		wantErr bool
+	}{
+		{
+			name: "tcp file found, no error should come up",
+			file: "fixtures/proc/net/tcp",
+			want: []*netIPSocketLine{
+				&netIPSocketLine{
+					Sl:        0,
+					LocalAddr: net.IP{10, 0, 0, 5},
+					LocalPort: 22,
+					RemAddr:   net.IP{0, 0, 0, 0},
+					RemPort:   0,
+					St:        10,
+					TxQueue:   0,
+					RxQueue:   1,
+					UID:       0,
+				},
+				&netIPSocketLine{
+					Sl:        1,
+					LocalAddr: net.IP{0, 0, 0, 0},
+					LocalPort: 22,
+					RemAddr:   net.IP{0, 0, 0, 0},
+					RemPort:   0,
+					St:        10,
+					TxQueue:   1,
+					RxQueue:   0,
+					UID:       0,
+				},
+				&netIPSocketLine{
+					Sl:        2,
+					LocalAddr: net.IP{0, 0, 0, 0},
+					LocalPort: 22,
+					RemAddr:   net.IP{0, 0, 0, 0},
+					RemPort:   0,
+					St:        10,
+					TxQueue:   1,
+					RxQueue:   1,
+					UID:       0,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "tcp6 file found, no error should come up",
+			file: "fixtures/proc/net/tcp6",
+			want: []*netIPSocketLine{
+				&netIPSocketLine{
+					Sl:        1315,
+					LocalAddr: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+					LocalPort: 5355,
+					RemAddr:   net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+					RemPort:   0,
+					St:        7,
+					TxQueue:   0,
+					RxQueue:   0,
+					UID:       981,
+				},
+				&netIPSocketLine{
+					Sl:        6073,
+					LocalAddr: net.IP{254, 128, 0, 0, 0, 0, 0, 0, 86, 225, 173, 255, 254, 124, 102, 9},
+					LocalPort: 51073,
+					RemAddr:   net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+					RemPort:   0,
+					St:        7,
+					TxQueue:   0,
+					RxQueue:   0,
+					UID:       1000,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:    "error case - file not found",
+			file:    "somewhere over the rainbow",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse error",
+			file:    "fixtures/proc/net/tcp_broken",
+			want:    nil,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := newNetTCP(tt.file)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("newNetTCP() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("newNetTCP() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_newNetTCPSummary(t *testing.T) {
+	tests := []struct {
+		name    string
+		file    string
+		want    *NetTCPSummary
+		wantErr bool
+	}{
+		{
+			name:    "tcp file found, no error should come up",
+			file:    "fixtures/proc/net/tcp",
+			want:    &NetTCPSummary{TxQueueLength: 2, RxQueueLength: 2, UsedSockets: 3},
+			wantErr: false,
+		},
+		{
+			name:    "tcp6 file found, no error should come up",
+			file:    "fixtures/proc/net/tcp6",
+			want:    &NetTCPSummary{TxQueueLength: 0, RxQueueLength: 0, UsedSockets: 2},
+			wantErr: false,
+		},
+		{
+			name:    "error case - file not found",
+			file:    "somewhere over the rainbow",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "error case - parse error",
+			file:    "fixtures/proc/net/tcp_broken",
+			want:    nil,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := newNetTCPSummary(tt.file)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("newNetTCPSummary() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("newNetTCPSummary() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/net_udp_test.go b/net_udp_test.go
index 363fdb2..f3d56d0 100644
--- a/net_udp_test.go
+++ b/net_udp_test.go
@@ -19,94 +19,6 @@ import (
 	"testing"
 )
 
-func Test_parseNetUDPLine(t *testing.T) {
-	tests := []struct {
-		fields  []string
-		name    string
-		want    *netUDPLine
-		wantErr bool
-	}{
-		{
-			name:   "reading valid lines, no issue should happened",
-			fields: []string{"11:", "00000000:0000", "00000000:0000", "0A", "00000017:0000002A", "0:0", "0", "1000"},
-			want: &netUDPLine{
-				Sl:        11,
-				LocalAddr: net.IP{0, 0, 0, 0},
-				LocalPort: 0,
-				RemAddr:   net.IP{0, 0, 0, 0},
-				RemPort:   0,
-				St:        10,
-				TxQueue:   23,
-				RxQueue:   42,
-				UID:       1000,
-			},
-		},
-		{
-			name:    "error case - invalid line - number of fields/columns < 8",
-			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "0:0", "0"},
-			want:    nil,
-			wantErr: true,
-		},
-		{
-			name:    "error case - parse sl - not a valid uint",
-			fields:  []string{"a:", "00000000:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "0"},
-			want:    nil,
-			wantErr: true,
-		},
-		{
-			name:    "error case - parse local_address - not a valid hex",
-			fields:  []string{"1:", "0000000O:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "0"},
-			want:    nil,
-			wantErr: true,
-		},
-		{
-			name:    "error case - parse rem_address - not a valid hex",
-			fields:  []string{"1:", "00000000:0000", "0000000O:0000", "07", "00000000:00000001", "0:0", "0", "0"},
-			want:    nil,
-			wantErr: true,
-		},
-		{
-			name:    "error case - cannot parse line - missing colon",
-			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "0000000000000001", "0:0", "0", "0"},
-			want:    nil,
-			wantErr: true,
-		},
-		{
-			name:    "error case - parse tx_queue - not a valid hex",
-			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "DEADCODE:00000001", "0:0", "0", "0"},
-			want:    nil,
-			wantErr: true,
-		},
-		{
-			name:    "error case - parse rx_queue - not a valid hex",
-			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "00000000:FEEDCODE", "0:0", "0", "0"},
-			want:    nil,
-			wantErr: true,
-		},
-		{
-			name:    "error case - parse UID - not a valid uint",
-			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "-10"},
-			want:    nil,
-			wantErr: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := parseNetUDPLine(tt.fields)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("parseNetUDPLine() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if tt.want == nil && got != nil {
-				t.Errorf("parseNetUDPLine() = %v, want %v", got, tt.want)
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("parseNetUDPLine() = %#v, want %#v", got, tt.want)
-			}
-		})
-	}
-}
-
 func Test_newNetUDP(t *testing.T) {
 	tests := []struct {
 		name    string
@@ -117,8 +29,8 @@ func Test_newNetUDP(t *testing.T) {
 		{
 			name: "udp file found, no error should come up",
 			file: "fixtures/proc/net/udp",
-			want: []*netUDPLine{
-				&netUDPLine{
+			want: []*netIPSocketLine{
+				&netIPSocketLine{
 					Sl:        0,
 					LocalAddr: net.IP{10, 0, 0, 5},
 					LocalPort: 22,
@@ -129,7 +41,7 @@ func Test_newNetUDP(t *testing.T) {
 					RxQueue:   1,
 					UID:       0,
 				},
-				&netUDPLine{
+				&netIPSocketLine{
 					Sl:        1,
 					LocalAddr: net.IP{0, 0, 0, 0},
 					LocalPort: 22,
@@ -140,7 +52,7 @@ func Test_newNetUDP(t *testing.T) {
 					RxQueue:   0,
 					UID:       0,
 				},
-				&netUDPLine{
+				&netIPSocketLine{
 					Sl:        2,
 					LocalAddr: net.IP{0, 0, 0, 0},
 					LocalPort: 22,
@@ -157,8 +69,8 @@ func Test_newNetUDP(t *testing.T) {
 		{
 			name: "udp6 file found, no error should come up",
 			file: "fixtures/proc/net/udp6",
-			want: []*netUDPLine{
-				&netUDPLine{
+			want: []*netIPSocketLine{
+				&netIPSocketLine{
 					Sl:        1315,
 					LocalAddr: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
 					LocalPort: 5355,
@@ -169,9 +81,9 @@ func Test_newNetUDP(t *testing.T) {
 					RxQueue:   0,
 					UID:       981,
 				},
-				&netUDPLine{
+				&netIPSocketLine{
 					Sl:        6073,
-					LocalAddr: net.IP{0, 0, 128, 254, 0, 0, 0, 0, 255, 173, 225, 86, 9, 102, 124, 254},
+					LocalAddr: net.IP{254, 128, 0, 0, 0, 0, 0, 0, 86, 225, 173, 255, 254, 124, 102, 9},
 					LocalPort: 51073,
 					RemAddr:   net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
 					RemPort:   0,
diff --git a/proc_cgroup_test.go b/proc_cgroup_test.go
index f530edb..00ab2b4 100644
--- a/proc_cgroup_test.go
+++ b/proc_cgroup_test.go
@@ -56,13 +56,13 @@ func TestParseCgroupString(t *testing.T) {
 			},
 		},
 		{
-			name:      "extra fields (such as those added by later kernel versions)",
-			s:         "0::/:foobar",
+			name:      "path containing colons",
+			s:         "0::/some/long/path:foobar",
 			shouldErr: false,
 			cgroup: &Cgroup{
 				HierarchyID: 0,
 				Controllers: nil,
-				Path:        "/",
+				Path:        "/some/long/path:foobar",
 			},
 		},
 		{