// Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: BSD-3-Clause package portlist import ( "bufio" "bytes" "encoding/binary" "errors" "fmt" "io" "io/fs" "log" "os" "path/filepath" "runtime" "strings" "syscall" "time" "unsafe" "go4.org/mem" "golang.org/x/sys/unix" "tailscale.com/util/dirwalk" "tailscale.com/util/mak" ) func init() { newOSImpl = newLinuxImpl // Refresh service discovery once per second. pollInterval = 1 * time.Second } type linuxImpl struct { procNetFiles []*os.File // seeked to start & reused between calls readlinkPathBuf []byte diagBuf []byte // reused for every inet_diag receive diagPorts map[string]Port // keyed by "socket:[]", matching /proc//fd symlink targets known map[string]*portMeta // inode string => metadata br *bufio.Reader includeLocalhost bool diagFD int // -1 means not yet opened or closed diagPID uint32 diagSeq uint32 namespace netNamespace diagPermanent bool } type portMeta struct { port Port pid int keep bool needsProcName bool } func newLinuxImplBase(includeLocalhost bool) *linuxImpl { return &linuxImpl{ br: bufio.NewReader(eofReader), known: map[string]*portMeta{}, diagPorts: map[string]Port{}, includeLocalhost: includeLocalhost, diagFD: -1, } } func newLinuxImpl(includeLocalhost bool) osImpl { return newLinuxImplWithDiag(includeLocalhost, true) } // newLinuxImplWithDiag is also used by the Linux benchmarks to construct a // proc-only implementation without opening an unused diagnostic socket. func newLinuxImplWithDiag(includeLocalhost, useDiag bool) *linuxImpl { // /proc/thread-self and netlink socket creation use the calling thread's // network namespace. Stay on one thread from the namespace check through // socket creation so both operations use the same namespace. runtime.LockOSThread() defer runtime.UnlockOSThread() li := newLinuxImplBase(includeLocalhost) var procErr error li.namespace, procErr = netNamespaceID("/proc/self/ns/net") threadNS, threadErr := netNamespaceID("/proc/thread-self/ns/net") for _, name := range []string{ "/proc/net/tcp", "/proc/net/tcp6", "/proc/net/udp", "/proc/net/udp6", } { f, err := os.Open(name) if err != nil { if os.IsNotExist(err) { continue } log.Printf("portlist warning; ignoring: %v", err) continue } li.procNetFiles = append(li.procNetFiles, f) } // /proc/net is resolved through /proc/self, which denotes the process // leader. inet_diag is scoped to the calling thread's namespace. If those // views differ, use the complete proc fallback rather than mixing them. if !useDiag || runtime.GOOS == "android" || (procErr != nil || threadErr != nil || threadNS != li.namespace) { li.diagPermanent = true } else { li.initDiag() } return li } func (li *linuxImpl) initDiag() { fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_INET_DIAG) if err != nil { li.diagPermanent = isDiagCapabilityError(err) return } if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil { unix.Close(fd) li.diagPermanent = isDiagCapabilityError(err) return } sa, err := unix.Getsockname(fd) if err != nil { unix.Close(fd) li.diagPermanent = isDiagCapabilityError(err) return } li.diagPID = sa.(*unix.SockaddrNetlink).Pid li.diagFD = fd } // netNamespace identifies the namespace pinned by the proc and netlink files. type netNamespace struct{ dev, ino uint64 } func netNamespaceID(path string) (netNamespace, error) { var st unix.Stat_t if err := unix.Stat(path, &st); err != nil { return netNamespace{}, err } return netNamespace{uint64(st.Dev), uint64(st.Ino)}, nil } func (li *linuxImpl) sameNetNamespace() bool { ns, err := netNamespaceID("/proc/thread-self/ns/net") return err == nil && ns == li.namespace } func isDiagCapabilityError(err error) bool { // A missing protocol diagnostic handler (for example, CONFIG_INET_UDP_DIAG // disabled) returns ENOENT after the kernel attempts to load its module. return errors.Is(err, unix.ENOENT) || errors.Is(err, unix.EPERM) || errors.Is(err, unix.EACCES) || errors.Is(err, unix.EAFNOSUPPORT) || errors.Is(err, unix.EPROTONOSUPPORT) || errors.Is(err, unix.ENOPROTOOPT) || errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ENOSYS) } func (li *linuxImpl) Close() error { li.closeDiag() for _, f := range li.procNetFiles { f.Close() } li.procNetFiles = nil return nil } const ( v6Localhost = "00000000000000000000000001000000:" v6Any = "00000000000000000000000000000000:0000" v4Localhost = "0100007F:" v4Any = "00000000:0000" ) var eofReader = bytes.NewReader(nil) func (li *linuxImpl) AppendListeningPorts(base []Port) ([]Port, error) { if runtime.GOOS == "android" { // Android 10+ doesn't allow access to this anymore. // https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem // Ignore it rather than have the system log about our violation. return nil, nil } br := li.br defer br.Reset(eofReader) // Start by marking all previous known ports as gone. If this mark // bit is still false later, we'll remove them. for _, pm := range li.known { pm.keep = false } var err error if !li.diagPermanent { err = li.appendDiagPorts() } if err != nil || li.diagPermanent { for _, f := range li.procNetFiles { name := f.Name() _, err := f.Seek(0, io.SeekStart) if err != nil { return nil, err } br.Reset(f) err = li.parseProcNetFile(br, filepath.Base(name)) if err != nil { return nil, fmt.Errorf("parsing %q: %w", name, err) } } } // Delete ports that aren't open any longer. // And see if there are any process names we need to look for. var needProc map[string]*portMeta for inode, pm := range li.known { if !pm.keep { delete(li.known, inode) continue } if pm.needsProcName { mak.Set(&needProc, inode, pm) } } err = li.findProcessNames(needProc) if err != nil { return nil, err } ret := base for _, pm := range li.known { ret = append(ret, pm.port) } return sortAndDedup(ret), nil } // appendDiagPorts obtains the same listener set as parseProcNetFile, but asks // the kernel to do the expensive table filtering. No state is changed until // all four dumps have completed successfully; callers can safely fall back to // proc without leaving a partial result in known. func (li *linuxImpl) appendDiagPorts() error { if li.diagPermanent { return errors.New("inet_diag unavailable") } if li.diagFD < 0 { // A replacement socket must be created in the namespace where the // proc descriptors were opened. Keep the identity check and socket // construction on one OS thread; an existing socket remains pinned to // its creation namespace and needs no per-poll thread pinning. runtime.LockOSThread() defer runtime.UnlockOSThread() if !li.sameNetNamespace() { return errors.New("inet_diag: network namespace differs from proc view") } li.initDiag() if li.diagFD < 0 { return errors.New("inet_diag unavailable") } } type query struct { family uint8 proto uint8 } queries := []query{{unix.AF_INET, unix.IPPROTO_TCP}, {unix.AF_INET6, unix.IPPROTO_TCP}, {unix.AF_INET, unix.IPPROTO_UDP}, {unix.AF_INET6, unix.IPPROTO_UDP}} newPorts := li.diagPorts for inode := range newPorts { delete(newPorts, inode) } for _, q := range queries { err := li.diagDump(q.family, q.proto, newPorts) if err != nil { if isDiagCapabilityError(err) { li.diagPermanent = true } li.closeDiag() return err } } for inode, pm := range li.known { if p, ok := newPorts[inode]; ok { pm.keep = true if pm.port.Proto != p.Proto || pm.port.Port != p.Port { pm.port.Process, pm.port.Pid = "", 0 pm.needsProcName = true } pm.port.Proto, pm.port.Port = p.Proto, p.Port continue } delete(li.known, inode) } for inode, p := range newPorts { if _, ok := li.known[inode]; !ok { li.known[inode] = &portMeta{port: p, keep: true, needsProcName: true} } } return nil } func (li *linuxImpl) closeDiag() { if li.diagFD >= 0 { _ = unix.Close(li.diagFD) li.diagFD = -1 li.diagPID = 0 } } // tcpListen is Linux's TCP_LISTEN state from include/net/tcp_states.h. const tcpListen = 10 func diagRequest(family, proto uint8) []byte { // Pack these layouts from include/uapi/linux/inet_diag.h. Offsets below // are relative to the start of each struct; states uses native byte order. // // struct inet_diag_sockid { // 48 bytes // __be16 idiag_sport; // 0 // __be16 idiag_dport; // 2 // __be32 idiag_src[4]; // 4 // __be32 idiag_dst[4]; // 20 // __u32 idiag_if; // 36 // __u32 idiag_cookie[2]; // 40 // }; // struct inet_diag_req_v2 { // 56 bytes // __u8 sdiag_family; // 0 // __u8 sdiag_protocol; // 1 // __u8 idiag_ext; // 2 // __u8 pad; // 3 // __u32 idiag_states; // 4 // struct inet_diag_sockid id; // 8 // }; b := make([]byte, 56) b[0], b[1] = family, proto // Query TCP_LISTEN only. On Linux, entries with a zero remote endpoint in // /proc/net/tcp are listeners; requesting other TCP states is substantially // slower and does not add ports. UDP uses all states and retains the exact // zero-remote filtering above. if proto == unix.IPPROTO_TCP { binary.NativeEndian.PutUint32(b[4:8], 1< maxBytes-totalBytes { return errors.New("inet_diag: response byte limit exceeded") } totalBytes += n if flags&unix.MSG_TRUNC != 0 { return errors.New("inet_diag: truncated netlink datagram") } if sa, ok := from.(*unix.SockaddrNetlink); !ok || sa.Pid != 0 { return errors.New("inet_diag: unexpected netlink sender") } done, err := parseDiagDatagram(li.diagBuf[:n], seq, li.diagPID, family, func(body []byte) error { records++ if records > maxRecords { return errors.New("inet_diag: response record limit exceeded") } // inet_diag_msg has the same address layout as the request. The // remote address begins at 24 and inode is the final uint32. remote := body[24:40] if !allZero(remote) || body[6] != 0 || body[7] != 0 { return nil } local := body[8:24] if !li.includeLocalhost && (isDiagV4Localhost(local, family) || isDiagV6Localhost(local, family)) { return nil } port := uint16(body[4])<<8 | uint16(body[5]) inode := uint64(binary.NativeEndian.Uint32(body[68:72])) key := fmt.Sprintf("socket:[%d]", inode) ports[key] = Port{Proto: protocolName(proto), Port: port} return nil }) if err != nil { return err } messages++ if done { return nil } } return errors.New("inet_diag: response limit exceeded") } func protocolName(proto uint8) string { if proto == unix.IPPROTO_TCP { return "tcp" } return "udp" } // parseDiagDatagram validates one received netlink datagram for an // inet_diag transaction and invokes onRecord for each diagnostic record. // It is pure with respect to transport state: callback body slices refer to // datagram and must be consumed or copied before returning. A false result // means the multipart transaction has not received NLMSG_DONE yet; the caller // must continue receiving and reject timeout or EOF without a true result. func parseDiagDatagram(datagram []byte, seq, pid uint32, family uint8, onRecord func([]byte) error) (done bool, err error) { for off := 0; off < len(datagram); { if len(datagram)-off < unix.NLMSG_HDRLEN { return false, errors.New("inet_diag: truncated netlink header") } mlen := int(binary.NativeEndian.Uint32(datagram[off : off+4])) if mlen < unix.NLMSG_HDRLEN || mlen > len(datagram)-off { return false, errors.New("inet_diag: malformed netlink length") } aligned := (mlen + 3) &^ 3 if aligned > len(datagram)-off { return false, errors.New("inet_diag: missing netlink alignment padding") } for _, p := range datagram[off+mlen : off+aligned] { if p != 0 { return false, errors.New("inet_diag: non-zero netlink padding") } } typ := binary.NativeEndian.Uint16(datagram[off+4 : off+6]) flags := binary.NativeEndian.Uint16(datagram[off+6 : off+8]) mseq := binary.NativeEndian.Uint32(datagram[off+8 : off+12]) mpid := binary.NativeEndian.Uint32(datagram[off+12 : off+16]) if mseq != seq || mpid != pid { return false, errors.New("inet_diag: mismatched netlink response") } if flags&unix.NLM_F_DUMP_INTR != 0 { return false, errors.New("inet_diag: dump interrupted") } body := datagram[off+unix.NLMSG_HDRLEN : off+mlen] switch typ { case unix.NLMSG_DONE: if mlen != unix.NLMSG_HDRLEN && mlen != unix.NLMSG_HDRLEN+4 { return false, errors.New("inet_diag: malformed netlink completion") } if len(body) == 4 && binary.NativeEndian.Uint32(body) != 0 { return false, diagNetlinkError(int32(binary.NativeEndian.Uint32(body))) } if off+aligned != len(datagram) { return false, errors.New("inet_diag: data after netlink completion") } return true, nil case unix.NLMSG_ERROR: if len(body) < 4 { return false, errors.New("inet_diag: truncated netlink error") } code := int32(binary.NativeEndian.Uint32(body[:4])) if code != 0 { return false, diagNetlinkError(code) } return false, errors.New("inet_diag: netlink ACK without completion") default: if typ != unix.SOCK_DIAG_BY_FAMILY || flags&unix.NLM_F_MULTI == 0 { return false, errors.New("inet_diag: unexpected netlink response") } if len(body) < 72 { return false, fmt.Errorf("inet_diag: short response (%d bytes)", len(body)) } if body[0] != family { return false, errors.New("inet_diag: response family mismatch") } if onRecord != nil { if err := onRecord(body); err != nil { return false, err } } } off += aligned } return false, nil } func diagNetlinkError(code int32) error { if code > 0 { return fmt.Errorf("inet_diag: malformed netlink error code %d", code) } return fmt.Errorf("inet_diag: netlink error: %w", unix.Errno(-code)) } func allZero(b []byte) bool { for _, x := range b { if x != 0 { return false } } return true } func isDiagV4Localhost(b []byte, family uint8) bool { return family == unix.AF_INET && b[0] == 127 && b[1] == 0 && b[2] == 0 && b[3] == 1 && allZero(b[4:]) } func isDiagV6Localhost(b []byte, family uint8) bool { return family == unix.AF_INET6 && allZero(b[:15]) && b[15] == 1 } // fileBase is one of "tcp", "tcp6", "udp", "udp6". func (li *linuxImpl) parseProcNetFile(r *bufio.Reader, fileBase string) error { proto := strings.TrimSuffix(fileBase, "6") // skip header row _, err := r.ReadSlice('\n') if err != nil { return err } fields := make([]mem.RO, 0, 20) // 17 current fields + some future slop wantRemote := mem.S(v4Any) if strings.HasSuffix(fileBase, "6") { wantRemote = mem.S(v6Any) } // remoteIndex is the index within a line to the remote address field. // -1 means not yet found. remoteIndex := -1 // Add an upper bound on how many rows we'll attempt to read just // to make sure this doesn't consume too much of their CPU. // TODO(bradfitz,crawshaw): adaptively adjust polling interval as function // of open sockets. const maxRows = 1e6 rows := 0 // Scratch buffer for making inode strings. inoBuf := make([]byte, 0, 50) for { line, err := r.ReadSlice('\n') if err == io.EOF { break } if err != nil { return err } rows++ if rows >= maxRows { break } if len(line) == 0 { continue } // On the first row of output, find the index of the 3rd field (index 2), // the remote address. All the rows are aligned, at least until 4 billion open // TCP connections, per the Linux get_tcp4_sock's "%4d: " on an int i. if remoteIndex == -1 { remoteIndex = fieldIndex(line, 2) if remoteIndex == -1 { break } } if len(line) < remoteIndex || !mem.HasPrefix(mem.B(line).SliceFrom(remoteIndex), wantRemote) { // Fast path for not being a listener port. continue } // sl local rem ... inode fields = mem.AppendFields(fields[:0], mem.B(line)) local := fields[1] rem := fields[2] inode := fields[9] if !rem.Equal(wantRemote) { // not a "listener" port continue } // If a port is bound to localhost, ignore it. // TODO: localhost is bigger than 1 IP, we need to ignore // more things. if !li.includeLocalhost && (mem.HasPrefix(local, mem.S(v4Localhost)) || mem.HasPrefix(local, mem.S(v6Localhost))) { continue } // Don't use strings.Split here, because it causes // allocations significant enough to show up in profiles. i := mem.IndexByte(local, ':') if i == -1 { return fmt.Errorf("%q unexpectedly didn't have a colon", local.StringCopy()) } portv, err := mem.ParseUint(local.SliceFrom(i+1), 16, 16) if err != nil { return fmt.Errorf("%#v: %s", local.SliceFrom(9).StringCopy(), err) } inoBuf = append(inoBuf[:0], "socket:["...) inoBuf = mem.Append(inoBuf, inode) inoBuf = append(inoBuf, ']') if pm, ok := li.known[string(inoBuf)]; ok { pm.keep = true // Rest should be unchanged. } else { li.known[string(inoBuf)] = &portMeta{ needsProcName: true, keep: true, port: Port{ Proto: proto, Port: uint16(portv), }, } } } return nil } // errDone is an internal sentinel error that we found everything we were looking for. var errDone = errors.New("done") // need is keyed by inode string. func (li *linuxImpl) findProcessNames(need map[string]*portMeta) error { if len(need) == 0 { return nil } defer func() { // Anything we didn't find, give up on and don't try to look for it later. for _, pm := range need { pm.needsProcName = false } }() err := foreachPID(func(pid mem.RO) error { var procBuf [128]byte fdPath := mem.Append(procBuf[:0], mem.S("/proc/")) fdPath = mem.Append(fdPath, pid) fdPath = mem.Append(fdPath, mem.S("/fd")) // Android logs a bunch of audit violations in logcat // if we try to open things we don't have access // to. So on Android only, ask if we have permission // rather than just trying it to determine whether we // have permission. if runtime.GOOS == "android" && syscall.Access(string(fdPath), unix.R_OK) != nil { return nil } dirwalk.WalkShallow(mem.B(fdPath), func(fd mem.RO, de fs.DirEntry) error { targetBuf := make([]byte, 64) // plenty big for "socket:[165614651]" linkPath := li.readlinkPathBuf[:0] linkPath = fmt.Appendf(linkPath, "/proc/") linkPath = mem.Append(linkPath, pid) linkPath = append(linkPath, "/fd/"...) linkPath = mem.Append(linkPath, fd) linkPath = append(linkPath, 0) // terminating NUL li.readlinkPathBuf = linkPath // to reuse its buffer next time n, ok := readlink(linkPath, targetBuf) if !ok { // Not a symlink or no permission. // Skip it. return nil } pe := need[string(targetBuf[:n])] // m[string([]byte)] avoids alloc if pe == nil { return nil } bs, err := os.ReadFile(fmt.Sprintf("/proc/%s/cmdline", pid.StringCopy())) if err != nil { // Usually shouldn't happen. One possibility is // the process has gone away, so let's skip it. return nil } argv := strings.Split(strings.TrimSuffix(string(bs), "\x00"), "\x00") if p, err := mem.ParseInt(pid, 10, 0); err == nil { pe.pid = int(p) } pe.port.Process = argvSubject(argv...) pid64, _ := mem.ParseInt(pid, 10, 0) pe.port.Pid = int(pid64) pe.needsProcName = false delete(need, string(targetBuf[:n])) if len(need) == 0 { return errDone } return nil }) return nil }) if err == errDone { return nil } return err } func foreachPID(fn func(pidStr mem.RO) error) error { err := dirwalk.WalkShallow(mem.S("/proc"), func(name mem.RO, de fs.DirEntry) error { if !isNumeric(name) { return nil } return fn(name) }) if os.IsNotExist(err) { // This can happen if the directory we're // reading disappears during the run. No big // deal. return nil } return err } func isNumeric(s mem.RO) bool { for i, n := 0, s.Len(); i < n; i++ { b := s.At(i) if b < '0' || b > '9' { return false } } return s.Len() > 0 } // fieldIndex returns the offset in line where the Nth field (0-based) begins, or -1 // if there aren't that many fields. Fields are separated by 1 or more spaces. func fieldIndex(line []byte, n int) int { skip := 0 for i := 0; i <= n; i++ { // Skip spaces. for skip < len(line) && line[skip] == ' ' { skip++ } if skip == len(line) { return -1 } if i == n { break } // Skip non-space. for skip < len(line) && line[skip] != ' ' { skip++ } } return skip } // path must be null terminated. func readlink(path, buf []byte) (n int, ok bool) { if len(buf) == 0 || len(path) < 2 || path[len(path)-1] != 0 { return 0, false } var dirfd int = unix.AT_FDCWD r0, _, e1 := unix.Syscall6(unix.SYS_READLINKAT, uintptr(dirfd), uintptr(unsafe.Pointer(&path[0])), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, 0) n = int(r0) if e1 != 0 { return 0, false } return n, true }