Repository: prometheus/procfs (go)

Issue:
Add parsing of sockstat
We currently parse `/proc/net/sockstat` in the node_exporter, it would be good to extract this functionality.

* [ ] Parse `/proc/net/sockstat`
* [ ] Parse `/proc/net/sockstat6`

Sadly, it looks like these files are not documented well in the kernel docs.

Pull request:
procfs: implement parsing of NetSockstat{,6}
Signed-off-by: Matt Layher <mdlayher@gmail.com>

Fixes #229.

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 7c4ce1f..438ca92 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,4 +1,6 @@
+# Run only staticcheck for now. Additional linters will be enabled one-by-one.
 linters:
   enable:
   - staticcheck
   - govet
+  disable-all: true
diff --git a/fixtures.ttar b/fixtures.ttar
index 38b71fe..c50a18a 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -1801,6 +1801,25 @@ proc4 2 2 10853
 proc4ops 72 0 0 0 1098 2 0 0 0 0 8179 5896 0 0 0 0 5900 0 0 2 0 2 0 9609 0 2 150 1272 0 0 0 1236 0 0 0 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/sockstat
+Lines: 6
+sockets: used 1602
+TCP: inuse 35 orphan 0 tw 4 alloc 59 mem 22
+UDP: inuse 12 mem 62
+UDPLITE: inuse 0
+RAW: inuse 0
+FRAG: inuse 0 memory 0
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/sockstat6
+Lines: 5
+TCP6: inuse 17
+UDP6: inuse 9
+UDPLITE6: inuse 0
+RAW6: inuse 1
+FRAG6: inuse 0 memory 0
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/net/softnet_stat
 Lines: 1
 00015c73 00020e76 F0000769 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
diff --git a/internal/util/valueparser.go b/internal/util/valueparser.go
index ac93cb4..fe2355d 100644
--- a/internal/util/valueparser.go
+++ b/internal/util/valueparser.go
@@ -33,6 +33,9 @@ func NewValueParser(v string) *ValueParser {
 	return &ValueParser{v: v}
 }
 
+// Int interprets the underlying value as an int and returns that value.
+func (vp *ValueParser) Int() int { return int(vp.int64()) }
+
 // PInt64 interprets the underlying value as an int64 and returns a pointer to
 // that value.
 func (vp *ValueParser) PInt64() *int64 {
@@ -40,16 +43,27 @@ func (vp *ValueParser) PInt64() *int64 {
 		return nil
 	}
 
+	v := vp.int64()
+	return &v
+}
+
+// int64 interprets the underlying value as an int64 and returns that value.
+// TODO: export if/when necessary.
+func (vp *ValueParser) int64() int64 {
+	if vp.err != nil {
+		return 0
+	}
+
 	// A base value of zero makes ParseInt infer the correct base using the
 	// string's prefix, if any.
 	const base = 0
 	v, err := strconv.ParseInt(vp.v, base, 64)
 	if err != nil {
 		vp.err = err
-		return nil
+		return 0
 	}
 
-	return &v
+	return v
 }
 
 // PUInt64 interprets the underlying value as an uint64 and returns a pointer to
diff --git a/iscsi/get.go b/iscsi/get.go
index dc19026..68d704e 100644
--- a/iscsi/get.go
+++ b/iscsi/get.go
@@ -192,7 +192,7 @@ func (fs FS) GetRBDMatch(rbdNumber string, poolImage string) (*RBD, error) {
 	}
 
 	for systemRbdNumber, systemRbdPath := range systemRbds {
-		var systemPool, systemImage string
+		var systemPool, systemImage string = "", ""
 		systemPoolPath := filepath.Join(systemRbdPath, "pool")
 		if _, err := os.Stat(systemPoolPath); os.IsNotExist(err) {
 			continue
diff --git a/meminfo.go b/meminfo.go
index 50dab4b..dd630c0 100644
--- a/meminfo.go
+++ b/meminfo.go
@@ -16,8 +16,6 @@ package procfs
 import (
 	"bufio"
 	"bytes"
-	"fmt"
-	"io"
 	"strconv"
 	"strings"
 
@@ -145,133 +143,311 @@ type Meminfo struct {
 // Meminfo returns an information about current kernel/system memory statistics.
 // See https://www.kernel.org/doc/Documentation/filesystems/proc.txt
 func (fs FS) Meminfo() (Meminfo, error) {
-	b, err := util.ReadFileNoStat(fs.proc.Path("meminfo"))
+	data, err := util.ReadFileNoStat(fs.proc.Path("meminfo"))
 	if err != nil {
 		return Meminfo{}, err
 	}
-
-	m, err := parseMemInfo(bytes.NewReader(b))
-	if err != nil {
-		return Meminfo{}, fmt.Errorf("failed to parse meminfo: %v", err)
-	}
-
-	return *m, nil
+	return parseMemInfo(data)
 }
 
-func parseMemInfo(r io.Reader) (*Meminfo, error) {
-	var m Meminfo
-	s := bufio.NewScanner(r)
-	for s.Scan() {
-		// Each line has at least a name and value; we ignore the unit.
-		fields := strings.Fields(s.Text())
-		if len(fields) < 2 {
-			return nil, fmt.Errorf("malformed meminfo line: %q", s.Text())
-		}
+func parseMemInfo(info []byte) (m Meminfo, err error) {
+	scanner := bufio.NewScanner(bytes.NewReader(info))
 
-		v, err := strconv.ParseUint(fields[1], 0, 64)
-		if err != nil {
-			return nil, err
-		}
+	var line string
+	for scanner.Scan() {
+		line = scanner.Text()
 
-		switch fields[0] {
+		field := strings.Fields(line)
+		switch field[0] {
 		case "MemTotal:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.MemTotal = v
 		case "MemFree:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.MemFree = v
 		case "MemAvailable:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.MemAvailable = v
 		case "Buffers:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Buffers = v
 		case "Cached:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Cached = v
 		case "SwapCached:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.SwapCached = v
 		case "Active:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Active = v
 		case "Inactive:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Inactive = v
 		case "Active(anon):":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.ActiveAnon = v
 		case "Inactive(anon):":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.InactiveAnon = v
 		case "Active(file):":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.ActiveFile = v
 		case "Inactive(file):":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.InactiveFile = v
 		case "Unevictable:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Unevictable = v
 		case "Mlocked:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Mlocked = v
 		case "SwapTotal:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.SwapTotal = v
 		case "SwapFree:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.SwapFree = v
 		case "Dirty:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Dirty = v
 		case "Writeback:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Writeback = v
 		case "AnonPages:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.AnonPages = v
 		case "Mapped:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Mapped = v
 		case "Shmem:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Shmem = v
 		case "Slab:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Slab = v
 		case "SReclaimable:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.SReclaimable = v
 		case "SUnreclaim:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.SUnreclaim = v
 		case "KernelStack:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.KernelStack = v
 		case "PageTables:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.PageTables = v
 		case "NFS_Unstable:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.NFSUnstable = v
 		case "Bounce:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Bounce = v
 		case "WritebackTmp:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.WritebackTmp = v
 		case "CommitLimit:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.CommitLimit = v
 		case "Committed_AS:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.CommittedAS = v
 		case "VmallocTotal:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.VmallocTotal = v
 		case "VmallocUsed:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.VmallocUsed = v
 		case "VmallocChunk:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.VmallocChunk = v
 		case "HardwareCorrupted:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.HardwareCorrupted = v
 		case "AnonHugePages:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.AnonHugePages = v
 		case "ShmemHugePages:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.ShmemHugePages = v
 		case "ShmemPmdMapped:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.ShmemPmdMapped = v
 		case "CmaTotal:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.CmaTotal = v
 		case "CmaFree:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.CmaFree = v
 		case "HugePages_Total:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.HugePagesTotal = v
 		case "HugePages_Free:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.HugePagesFree = v
 		case "HugePages_Rsvd:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.HugePagesRsvd = v
 		case "HugePages_Surp:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.HugePagesSurp = v
 		case "Hugepagesize:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.Hugepagesize = v
 		case "DirectMap4k:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.DirectMap4k = v
 		case "DirectMap2M:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.DirectMap2M = v
 		case "DirectMap1G:":
+			v, err := strconv.ParseUint(field[1], 0, 64)
+			if err != nil {
+				return Meminfo{}, err
+			}
 			m.DirectMap1G = v
 		}
 	}
-
-	return &m, nil
+	return m, nil
 }
diff --git a/net_sockstat.go b/net_sockstat.go
new file mode 100644
index 0000000..a34c9b8
--- /dev/null
+++ b/net_sockstat.go
@@ -0,0 +1,158 @@
+// Copyright 2019 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"
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+// A NetSockstat contains the output of /proc/net/sockstat{,6} for IPv4 or IPv6,
+// respectively.
+type NetSockstat struct {
+	// Used is non-nil for IPv4 sockstat results, but nil for IPv6.
+	Used      *int
+	Protocols []NetSockstatProtocol
+}
+
+// A NetSockstatProtocol contains statistics about a given socket protocol.
+// Pointer fields indicate that the value may or may not be present on any
+// given protocol.
+type NetSockstatProtocol struct {
+	Protocol string
+	InUse    int
+	Orphan   *int
+	TW       *int
+	Alloc    *int
+	Mem      *int
+	Memory   *int
+}
+
+// NetSockstat retrieves IPv4 socket statistics.
+func (fs FS) NetSockstat() (*NetSockstat, error) {
+	return readSockstat(fs.proc.Path("net", "sockstat"))
+}
+
+// NetSockstat6 retrieves IPv6 socket statistics.
+func (fs FS) NetSockstat6() (*NetSockstat, error) {
+	return readSockstat(fs.proc.Path("net", "sockstat6"))
+}
+
+// readSockstat opens and parses a NetSockstat from the input file.
+func readSockstat(name string) (*NetSockstat, error) {
+	// This file is small and can be read with one syscall.
+	b, err := util.ReadFileNoStat(name)
+	if err != nil {
+		return nil, err
+	}
+
+	stat, err := parseSockstat(bytes.NewReader(b))
+	if err != nil {
+		return nil, fmt.Errorf("failed to read sockstats from %q: %v", name, err)
+	}
+
+	return stat, nil
+}
+
+// parseSockstat reads the contents of a sockstat file and parses a NetSockstat.
+func parseSockstat(r io.Reader) (*NetSockstat, error) {
+	var stat NetSockstat
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		// Expect a minimum of a protocol and one key/value pair.
+		fields := strings.Split(s.Text(), " ")
+		if len(fields) < 3 {
+			return nil, fmt.Errorf("malformed sockstat line: %q", s.Text())
+		}
+
+		// The remaining fields are key/value pairs.
+		kvs, err := parseSockstatKVs(fields[1:])
+		if err != nil {
+			return nil, fmt.Errorf("error parsing sockstat key/value pairs from %q: %v", s.Text(), err)
+		}
+
+		// The first field is the protocol. We must trim its colon suffix.
+		proto := strings.TrimSuffix(fields[0], ":")
+		switch proto {
+		case "sockets":
+			// Special case: IPv4 has a sockets "used" key/value pair that we
+			// embed at the top level of the structure.
+			used := kvs["used"]
+			stat.Used = &used
+		default:
+			// Parse all other lines as individual protocols.
+			nsp := parseSockstatProtocol(kvs)
+			nsp.Protocol = proto
+			stat.Protocols = append(stat.Protocols, nsp)
+		}
+	}
+
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+
+	return &stat, nil
+}
+
+// parseSockstatKVs parses a string slice into a map of key/value pairs.
+func parseSockstatKVs(kvs []string) (map[string]int, error) {
+	if len(kvs)%2 != 0 {
+		return nil, errors.New("odd number of fields in key/value pairs")
+	}
+
+	// Iterate two values at a time to gather key/value pairs.
+	out := make(map[string]int, len(kvs)/2)
+	for i := 0; i < len(kvs); i += 2 {
+		vp := util.NewValueParser(kvs[i+1])
+		out[kvs[i]] = vp.Int()
+
+		if err := vp.Err(); err != nil {
+			return nil, err
+		}
+	}
+
+	return out, nil
+}
+
+// parseSockstatProtocol parses a NetSockstatProtocol from the input kvs map.
+func parseSockstatProtocol(kvs map[string]int) NetSockstatProtocol {
+	var nsp NetSockstatProtocol
+	for k, v := range kvs {
+		// Capture the range variable to ensure we get unique pointers for
+		// each of the optional fields.
+		v := v
+		switch k {
+		case "inuse":
+			nsp.InUse = v
+		case "orphan":
+			nsp.Orphan = &v
+		case "tw":
+			nsp.TW = &v
+		case "alloc":
+			nsp.Alloc = &v
+		case "mem":
+			nsp.Mem = &v
+		case "memory":
+			nsp.Memory = &v
+		}
+	}
+
+	return nsp
+}
diff --git a/net_unix.go b/net_unix.go
index 93bd58f..240340a 100644
--- a/net_unix.go
+++ b/net_unix.go
@@ -207,6 +207,10 @@ func (u NetUnix) parseUsers(hexStr string) (uint64, error) {
 	return strconv.ParseUint(hexStr, 16, 32)
 }
 
+func (u NetUnix) parseProtocol(hexStr string) (uint64, error) {
+	return strconv.ParseUint(hexStr, 16, 32)
+}
+
 func (u NetUnix) parseType(hexStr string) (NetUnixType, error) {
 	typ, err := strconv.ParseUint(hexStr, 16, 16)
 	if err != nil {


Test patch:
diff --git a/internal/util/valueparser_test.go b/internal/util/valueparser_test.go
index d95745a..fccdf63 100644
--- a/internal/util/valueparser_test.go
+++ b/internal/util/valueparser_test.go
@@ -27,6 +27,19 @@ func TestValueParser(t *testing.T) {
 		ok   bool
 		fn   func(t *testing.T, vp *util.ValueParser)
 	}{
+		{
+			name: "ok Int",
+			v:    "10",
+			ok:   true,
+			fn: func(t *testing.T, vp *util.ValueParser) {
+				want := 10
+				got := vp.Int()
+
+				if diff := cmp.Diff(want, got); diff != "" {
+					t.Fatalf("unexpected integer (-want +got):\n%s", diff)
+				}
+			},
+		},
 		{
 			name: "bad PInt64",
 			v:    "hello",
diff --git a/iscsi/get_test.go b/iscsi/get_test.go
index a2c24d8..e89e169 100644
--- a/iscsi/get_test.go
+++ b/iscsi/get_test.go
@@ -22,7 +22,8 @@ import (
 
 func TestGetStats(t *testing.T) {
 	tests := []struct {
-		stat *iscsi.Stats
+		invalid bool
+		stat    *iscsi.Stats
 	}{
 		{
 			stat: &iscsi.Stats{
diff --git a/net_sockstat_test.go b/net_sockstat_test.go
new file mode 100644
index 0000000..657d61e
--- /dev/null
+++ b/net_sockstat_test.go
@@ -0,0 +1,201 @@
+// Copyright 2019 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 (
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+// The majority of test cases are covered in Test_parseSockstat. The top level
+// tests just check the basics for integration test purposes.
+
+func TestNetSockstat(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatalf("failed to open procfs: %v", err)
+	}
+
+	stat, err := fs.NetSockstat()
+	if err != nil {
+		t.Fatalf("failed to get sockstat: %v", err)
+	}
+
+	// IPv4 stats should include Used.
+	if stat.Used == nil {
+		t.Fatalf("IPv4 sockstat used value was nil")
+	}
+	if diff := cmp.Diff(1602, *stat.Used); diff != "" {
+		t.Fatalf("unexpected IPv4 used sockets (-want +got):\n%s", diff)
+	}
+
+	// TCP occurs first; do a basic sanity check.
+	if diff := cmp.Diff("TCP", stat.Protocols[0].Protocol); diff != "" {
+		t.Fatalf("unexpected socket protocol (-want +got):\n%s", diff)
+	}
+	if diff := cmp.Diff(35, stat.Protocols[0].InUse); diff != "" {
+		t.Fatalf("unexpected number of TCP sockets (-want +got):\n%s", diff)
+	}
+}
+
+func TestNetSockstat6(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatalf("failed to open procfs: %v", err)
+	}
+
+	stat, err := fs.NetSockstat6()
+	if err != nil {
+		t.Fatalf("failed to get sockstat: %v", err)
+	}
+
+	// IPv6 stats should not include Used.
+	if stat.Used != nil {
+		t.Fatalf("IPv6 sockstat used value was not nil")
+	}
+
+	// TCP6 occurs first; do a basic sanity check.
+	if diff := cmp.Diff("TCP6", stat.Protocols[0].Protocol); diff != "" {
+		t.Fatalf("unexpected socket protocol (-want +got):\n%s", diff)
+	}
+	if diff := cmp.Diff(17, stat.Protocols[0].InUse); diff != "" {
+		t.Fatalf("unexpected number of TCP sockets (-want +got):\n%s", diff)
+	}
+}
+
+func Test_parseSockstat(t *testing.T) {
+	tests := []struct {
+		name string
+		s    string
+		ok   bool
+		stat *NetSockstat
+	}{
+		{
+			name: "empty",
+			ok:   true,
+			stat: &NetSockstat{},
+		},
+		{
+			name: "bad line",
+			s: `
+sockets: used
+`,
+		},
+		{
+			name: "bad key/value pairs",
+			s: `
+TCP: inuse 32 orphan
+`,
+		},
+		{
+			name: "IPv4",
+			s: `
+sockets: used 1591
+TCP: inuse 32 orphan 0 tw 0 alloc 58 mem 13
+UDP: inuse 8 mem 115
+UDPLITE: inuse 0
+RAW: inuse 0
+FRAG: inuse 0 memory 0
+			`,
+			ok: true,
+			stat: &NetSockstat{
+				Used: intp(1591),
+				Protocols: []NetSockstatProtocol{
+					{
+						Protocol: "TCP",
+						InUse:    32,
+						Orphan:   intp(0),
+						TW:       intp(0),
+						Alloc:    intp(58),
+						Mem:      intp(13),
+					},
+					{
+						Protocol: "UDP",
+						InUse:    8,
+						Mem:      intp(115),
+					},
+					{
+						Protocol: "UDPLITE",
+					},
+					{
+						Protocol: "RAW",
+					},
+					{
+						Protocol: "FRAG",
+						Memory:   intp(0),
+					},
+				},
+			},
+		},
+		{
+			name: "IPv6",
+			s: `
+TCP6: inuse 24
+UDP6: inuse 9
+UDPLITE6: inuse 0
+RAW6: inuse 1
+FRAG6: inuse 0 memory 0
+			`,
+			ok: true,
+			stat: &NetSockstat{
+				Protocols: []NetSockstatProtocol{
+					{
+						Protocol: "TCP6",
+						InUse:    24,
+					},
+					{
+						Protocol: "UDP6",
+						InUse:    9,
+					},
+					{
+						Protocol: "UDPLITE6",
+					},
+					{
+						Protocol: "RAW6",
+						InUse:    1,
+					},
+					{
+						Protocol: "FRAG6",
+						Memory:   intp(0),
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			stat, err := parseSockstat(strings.NewReader(strings.TrimSpace(tt.s)))
+			if err != nil {
+				if tt.ok {
+					t.Fatalf("failed to parse sockstats: %v", err)
+				}
+
+				t.Logf("OK error: %v", err)
+				return
+			}
+			if !tt.ok {
+				t.Fatal("expected an error, but none occurred")
+			}
+
+			if diff := cmp.Diff(tt.stat, stat); diff != "" {
+				t.Errorf("unexpected sockstats (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func intp(i int) *int { return &i }
diff --git a/vm_test.go b/vm_test.go
index 988f9a1..b4d9aad 100644
--- a/vm_test.go
+++ b/vm_test.go
@@ -11,8 +11,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// +build !windows
-
 package procfs
 
 import (