Repository: prometheus/procfs
Pull request #236 (2019-11-21 18:26:49)

procfs: clean up /proc/crypto parser and tests
I think this code could use more unit tests, but I've tidied it up a bit to make it easier to follow.

mdlayher: @pgier updated and fixed a bug. Mind doing another review?

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 438ca92..7c4ce1f 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,6 +1,4 @@
-# Run only staticcheck for now. Additional linters will be enabled one-by-one.
 linters:
   enable:
   - staticcheck
   - govet
-  disable-all: true
diff --git a/crypto.go b/crypto.go
index 19d4041..a958933 100644
--- a/crypto.go
+++ b/crypto.go
@@ -14,10 +14,10 @@
 package procfs
 
 import (
+	"bufio"
 	"bytes"
 	"fmt"
-	"io/ioutil"
-	"strconv"
+	"io"
 	"strings"
 
 	"github.com/prometheus/procfs/internal/util"
@@ -52,80 +52,102 @@ type Crypto struct {
 // structs containing the relevant info.  More information available here:
 // https://kernel.readthedocs.io/en/sphinx-samples/crypto-API.html
 func (fs FS) Crypto() ([]Crypto, error) {
-	data, err := ioutil.ReadFile(fs.proc.Path("crypto"))
+	path := fs.proc.Path("crypto")
+	b, err := util.ReadFileNoStat(path)
 	if err != nil {
-		return nil, fmt.Errorf("error parsing crypto %s: %s", fs.proc.Path("crypto"), err)
+		return nil, fmt.Errorf("error reading crypto %s: %s", path, err)
 	}
-	crypto, err := parseCrypto(data)
+
+	crypto, err := parseCrypto(bytes.NewReader(b))
 	if err != nil {
-		return nil, fmt.Errorf("error parsing crypto %s: %s", fs.proc.Path("crypto"), err)
+		return nil, fmt.Errorf("error parsing crypto %s: %s", path, err)
 	}
+
 	return crypto, nil
 }
 
-func parseCrypto(cryptoData []byte) ([]Crypto, error) {
-	crypto := []Crypto{}
-
-	cryptoBlocks := bytes.Split(cryptoData, []byte("\n\n"))
-
-	for _, block := range cryptoBlocks {
-		var newCryptoElem Crypto
-
-		lines := strings.Split(string(block), "\n")
-		for _, line := range lines {
-			if strings.TrimSpace(line) == "" || line[0] == ' ' {
-				continue
-			}
-			fields := strings.Split(line, ":")
-			key := strings.TrimSpace(fields[0])
-			value := strings.TrimSpace(fields[1])
-			vp := util.NewValueParser(value)
-
-			switch strings.TrimSpace(key) {
-			case "async":
-				b, err := strconv.ParseBool(value)
-				if err == nil {
-					newCryptoElem.Async = b
-				}
-			case "blocksize":
-				newCryptoElem.Blocksize = vp.PUInt64()
-			case "chunksize":
-				newCryptoElem.Chunksize = vp.PUInt64()
-			case "digestsize":
-				newCryptoElem.Digestsize = vp.PUInt64()
-			case "driver":
-				newCryptoElem.Driver = value
-			case "geniv":
-				newCryptoElem.Geniv = value
-			case "internal":
-				newCryptoElem.Internal = value
-			case "ivsize":
-				newCryptoElem.Ivsize = vp.PUInt64()
-			case "maxauthsize":
-				newCryptoElem.Maxauthsize = vp.PUInt64()
-			case "max keysize":
-				newCryptoElem.MaxKeysize = vp.PUInt64()
-			case "min keysize":
-				newCryptoElem.MinKeysize = vp.PUInt64()
-			case "module":
-				newCryptoElem.Module = value
-			case "name":
-				newCryptoElem.Name = value
-			case "priority":
-				newCryptoElem.Priority = vp.PInt64()
-			case "refcnt":
-				newCryptoElem.Refcnt = vp.PInt64()
-			case "seedsize":
-				newCryptoElem.Seedsize = vp.PUInt64()
-			case "selftest":
-				newCryptoElem.Selftest = value
-			case "type":
-				newCryptoElem.Type = value
-			case "walksize":
-				newCryptoElem.Walksize = vp.PUInt64()
-			}
+// parseCrypto parses a /proc/crypto stream into Crypto elements.
+func parseCrypto(r io.Reader) ([]Crypto, error) {
+	var out []Crypto
+
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		text := s.Text()
+		switch {
+		case strings.HasPrefix(text, "name"):
+			// Each crypto element begins with its name.
+			out = append(out, Crypto{})
+		case text == "":
+			continue
+		}
+
+		kv := strings.Split(text, ":")
+		if len(kv) != 2 {
+			return nil, fmt.Errorf("malformed crypto line: %q", text)
+		}
+
+		k := strings.TrimSpace(kv[0])
+		v := strings.TrimSpace(kv[1])
+
+		// Parse the key/value pair into the currently focused element.
+		c := &out[len(out)-1]
+		if err := c.parseKV(k, v); err != nil {
+			return nil, err
 		}
-		crypto = append(crypto, newCryptoElem)
 	}
-	return crypto, nil
+
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+
+	return out, nil
+}
+
+// parseKV parses a key/value pair into the appropriate field of c.
+func (c *Crypto) parseKV(k, v string) error {
+	vp := util.NewValueParser(v)
+
+	switch k {
+	case "async":
+		// Interpret literal yes as true.
+		c.Async = v == "yes"
+	case "blocksize":
+		c.Blocksize = vp.PUInt64()
+	case "chunksize":
+		c.Chunksize = vp.PUInt64()
+	case "digestsize":
+		c.Digestsize = vp.PUInt64()
+	case "driver":
+		c.Driver = v
+	case "geniv":
+		c.Geniv = v
+	case "internal":
+		c.Internal = v
+	case "ivsize":
+		c.Ivsize = vp.PUInt64()
+	case "maxauthsize":
+		c.Maxauthsize = vp.PUInt64()
+	case "max keysize":
+		c.MaxKeysize = vp.PUInt64()
+	case "min keysize":
+		c.MinKeysize = vp.PUInt64()
+	case "module":
+		c.Module = v
+	case "name":
+		c.Name = v
+	case "priority":
+		c.Priority = vp.PInt64()
+	case "refcnt":
+		c.Refcnt = vp.PInt64()
+	case "seedsize":
+		c.Seedsize = vp.PUInt64()
+	case "selftest":
+		c.Selftest = v
+	case "type":
+		c.Type = v
+	case "walksize":
+		c.Walksize = vp.PUInt64()
+	}
+
+	return vp.Err()
 }
diff --git a/fixtures.ttar b/fixtures.ttar
index 38b71fe..1beedf2 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -554,7 +554,7 @@ power management:
 Mode: 444
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/crypto
-Lines: 971
+Lines: 972
 name         : ccm(aes)
 driver       : ccm_base(ctr(aes-aesni),cbcmac(aes-aesni))
 module       : ccm
@@ -588,6 +588,7 @@ refcnt       : 1
 selftest     : passed
 internal     : no
 type         : kpp
+async        : yes
 
 name         : ecb(arc4)
 driver       : ecb(arc4)-generic
@@ -1801,6 +1802,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 68d704e..dc19026 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 dd630c0..50dab4b 100644
--- a/meminfo.go
+++ b/meminfo.go
@@ -16,6 +16,8 @@ package procfs
 import (
 	"bufio"
 	"bytes"
+	"fmt"
+	"io"
 	"strconv"
 	"strings"
 
@@ -143,311 +145,133 @@ 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) {
-	data, err := util.ReadFileNoStat(fs.proc.Path("meminfo"))
+	b, err := util.ReadFileNoStat(fs.proc.Path("meminfo"))
 	if err != nil {
 		return Meminfo{}, err
 	}
-	return parseMemInfo(data)
+
+	m, err := parseMemInfo(bytes.NewReader(b))
+	if err != nil {
+		return Meminfo{}, fmt.Errorf("failed to parse meminfo: %v", err)
+	}
+
+	return *m, nil
 }
 
-func parseMemInfo(info []byte) (m Meminfo, err error) {
-	scanner := bufio.NewScanner(bytes.NewReader(info))
+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())
+		}
 
-	var line string
-	for scanner.Scan() {
-		line = scanner.Text()
+		v, err := strconv.ParseUint(fields[1], 0, 64)
+		if err != nil {
+			return nil, err
+		}
 
-		field := strings.Fields(line)
-		switch field[0] {
+		switch fields[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..f91ef55
--- /dev/null
+++ b/net_sockstat.go
@@ -0,0 +1,163 @@
+// 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.
+//
+// If IPv6 is disabled on this kernel, the returned error can be checked with
+// os.IsNotExist.
+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 {
+		// Do not wrap this error so the caller can detect os.IsNotExist and
+		// similar conditions.
+		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 240340a..93bd58f 100644
--- a/net_unix.go
+++ b/net_unix.go
@@ -207,10 +207,6 @@ 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/crypto_test.go b/crypto_test.go
index 4d45824..a5b0478 100644
--- a/crypto_test.go
+++ b/crypto_test.go
@@ -19,12 +19,6 @@ import (
 	"github.com/google/go-cmp/cmp"
 )
 
-func newint64(i int64) *int64 {
-	return &i
-}
-func newuint64(i uint64) *uint64 {
-	return &i
-}
 func TestFS_Crypto(t *testing.T) {
 	fs := getProcFixtures(t)
 	crypto, err := fs.Crypto()
@@ -34,12 +28,86 @@ func TestFS_Crypto(t *testing.T) {
 	}
 
 	refs := []Crypto{
-		{Name: "ccm(aes)", Driver: "ccm_base(ctr(aes-aesni),cbcmac(aes-aesni))", Module: "ccm", Priority: newint64(300), Refcnt: newint64(4), Selftest: "passed", Internal: "no", Type: "aead", Async: false, Blocksize: newuint64(1), Ivsize: newuint64(16), Maxauthsize: newuint64(16), Geniv: "<none>"},
-		{Name: "cbcmac(aes)", Driver: "cbcmac(aes-aesni)", Module: "ccm", Priority: newint64(300), Refcnt: newint64(7), Selftest: "passed", Internal: "no", Type: "shash", Blocksize: newuint64(1), Digestsize: newuint64(16)},
-		{Name: "ecdh", Driver: "ecdh-generic", Module: "ecdh_generic", Priority: newint64(100), Refcnt: newint64(1), Selftest: "passed", Internal: "no", Type: "kpp"},
-		{Name: "ecb(arc4)", Driver: "ecb(arc4)-generic", Module: "arc4", Priority: newint64(100), Refcnt: newint64(1), Selftest: "passed", Internal: "no", Type: "skcipher", Async: false, Blocksize: newuint64(1), MinKeysize: newuint64(1), MaxKeysize: newuint64(256), Ivsize: newuint64(0), Chunksize: newuint64(1), Walksize: newuint64(1)},
-		{Name: "arc4", Driver: "arc4-generic", Module: "arc4", Priority: newint64(0), Refcnt: newint64(3), Selftest: "passed", Internal: "no", Type: "cipher", Blocksize: newuint64(1), MinKeysize: newuint64(1), MaxKeysize: newuint64(256)},
-		{Name: "crct10dif", Driver: "crct10dif-pclmul", Module: "crct10dif_pclmul", Priority: newint64(200), Refcnt: newint64(2), Selftest: "passed", Internal: "no", Type: "shash", Blocksize: newuint64(1), Digestsize: newuint64(2)},
+		{
+			Name:        "ccm(aes)",
+			Driver:      "ccm_base(ctr(aes-aesni),cbcmac(aes-aesni))",
+			Module:      "ccm",
+			Priority:    newint64(300),
+			Refcnt:      newint64(4),
+			Selftest:    "passed",
+			Internal:    "no",
+			Type:        "aead",
+			Async:       false,
+			Blocksize:   newuint64(1),
+			Ivsize:      newuint64(16),
+			Maxauthsize: newuint64(16),
+			Geniv:       "<none>",
+		},
+		{
+			Name:       "cbcmac(aes)",
+			Driver:     "cbcmac(aes-aesni)",
+			Module:     "ccm",
+			Priority:   newint64(300),
+			Refcnt:     newint64(7),
+			Selftest:   "passed",
+			Internal:   "no",
+			Type:       "shash",
+			Blocksize:  newuint64(1),
+			Digestsize: newuint64(16),
+		},
+		{
+			Name:     "ecdh",
+			Driver:   "ecdh-generic",
+			Module:   "ecdh_generic",
+			Priority: newint64(100),
+			Refcnt:   newint64(1),
+			Selftest: "passed",
+			Internal: "no",
+			Type:     "kpp",
+			Async:    true,
+		},
+		{
+			Name:       "ecb(arc4)",
+			Driver:     "ecb(arc4)-generic",
+			Module:     "arc4",
+			Priority:   newint64(100),
+			Refcnt:     newint64(1),
+			Selftest:   "passed",
+			Internal:   "no",
+			Type:       "skcipher",
+			Async:      false,
+			Blocksize:  newuint64(1),
+			MinKeysize: newuint64(1),
+			MaxKeysize: newuint64(256),
+			Ivsize:     newuint64(0),
+			Chunksize:  newuint64(1),
+			Walksize:   newuint64(1),
+		},
+		{
+			Name:       "arc4",
+			Driver:     "arc4-generic",
+			Module:     "arc4",
+			Priority:   newint64(0),
+			Refcnt:     newint64(3),
+			Selftest:   "passed",
+			Internal:   "no",
+			Type:       "cipher",
+			Blocksize:  newuint64(1),
+			MinKeysize: newuint64(1),
+			MaxKeysize: newuint64(256),
+		},
+		{
+			Name:       "crct10dif",
+			Driver:     "crct10dif-pclmul",
+			Module:     "crct10dif_pclmul",
+			Priority:   newint64(200),
+			Refcnt:     newint64(2),
+			Selftest:   "passed",
+			Internal:   "no",
+			Type:       "shash",
+			Blocksize:  newuint64(1),
+			Digestsize: newuint64(2),
+		},
 	}
 
 	if want, have := len(refs), len(crypto); want > have {
@@ -52,3 +120,11 @@ func TestFS_Crypto(t *testing.T) {
 		}
 	}
 }
+
+func newint64(i int64) *int64 {
+	return &i
+}
+
+func newuint64(i uint64) *uint64 {
+	return &i
+}
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 e89e169..a2c24d8 100644
--- a/iscsi/get_test.go
+++ b/iscsi/get_test.go
@@ -22,8 +22,7 @@ import (
 
 func TestGetStats(t *testing.T) {
 	tests := []struct {
-		invalid bool
-		stat    *iscsi.Stats
+		stat *iscsi.Stats
 	}{
 		{
 			stat: &iscsi.Stats{
diff --git a/net_sockstat_test.go b/net_sockstat_test.go
new file mode 100644
index 0000000..08672d7
--- /dev/null
+++ b/net_sockstat_test.go
@@ -0,0 +1,213 @@
+// 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 (
+	"os"
+	"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_readSockstatIsNotExist(t *testing.T) {
+	// On a machine with IPv6 disabled for example, we want to ensure that
+	// readSockstat returns an error that is compatible with os.IsNotExist.
+	//
+	// We can use a synthetic file path here to verify this behavior.
+	_, err := readSockstat("/does/not/exist")
+	if err == nil || !os.IsNotExist(err) {
+		t.Fatalf("error is not compatible with os.IsNotExist: %#v", err)
+	}
+}
+
+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 b4d9aad..988f9a1 100644
--- a/vm_test.go
+++ b/vm_test.go
@@ -11,6 +11,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// +build !windows
+
 package procfs
 
 import (