1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package tunservice
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"net/netip"
"os"
"strconv"
"strings"
"syscall"
"github.com/armanlife/firefox-vpn-client/core/internal/tunapi"
)
// ownerPath is the executable of the program whose socket has source as
// its local address: the socket's inode from /proc/net, then the process
// that holds it (the service runs as root, so it may look at every one).
func ownerPath(udp bool, source netip.AddrPort) (string, error) {
inode, uid, err := socketInode(udp, source)
if err != nil {
return "", err
}
pid, err := socketOwner(inode, uid)
if err != nil {
return "", err
}
return tunapi.ProcessPath(uint32(pid))
}
// procNet is where the kernel lists the sockets; tests replace it.
var procNet = "/proc/net"
// socketInode finds the socket bound to source in the kernel's tables:
// the IPv6 one too for an IPv4 address, which a dual-stack app's socket
// has as a mapped one.
func socketInode(udp bool, source netip.AddrPort) (inode uint64, uid uint32, err error) {
proto := "tcp"
if udp {
proto = "udp"
}
tables := []string{proto + "6"}
if source.Addr().Unmap().Is4() {
tables = []string{proto, proto + "6"}
}
for _, table := range tables {
data, err := os.ReadFile(procNet + "/" + table)
if err != nil {
continue
}
if inode, uid, ok := findSocket(data, udp, source); ok {
return inode, uid, nil
}
}
return 0, 0, errors.New("no socket has this address")
}
// findSocket reads a table of /proc/net for the socket bound to source;
// for UDP also one bound to any address on its port.
func findSocket(table []byte, udp bool, source netip.AddrPort) (inode uint64, uid uint32, ok bool) {
want := source.Addr().Unmap()
for i, line := range bytes.Split(table, []byte("\n")) {
f := strings.Fields(string(line))
if i == 0 || len(f) < 10 {
continue
}
local, ok := parseProcAddr(f[1])
if !ok || local.Port() != source.Port() {
continue
}
if addr := local.Addr().Unmap(); addr != want && !(udp && addr.IsUnspecified()) {
continue
}
u, err := strconv.ParseUint(f[7], 10, 32)
if err != nil {
continue
}
n, err := strconv.ParseUint(f[9], 10, 64)
if err != nil || n == 0 {
continue
}
return n, uint32(u), true
}
return 0, 0, false
}
// parseProcAddr reads an address as /proc/net writes it, "0100007F:1F90":
// each 32-bit word of the address in hex as this machine's byte order has
// it, then the port in hex.
func parseProcAddr(s string) (netip.AddrPort, bool) {
hexAddr, hexPort, ok := strings.Cut(s, ":")
if !ok {
return netip.AddrPort{}, false
}
port, err := strconv.ParseUint(hexPort, 16, 16)
if err != nil {
return netip.AddrPort{}, false
}
raw, err := hex.DecodeString(hexAddr)
if err != nil || (len(raw) != 4 && len(raw) != 16) {
return netip.AddrPort{}, false
}
b := make([]byte, len(raw))
for i := 0; i < len(raw); i += 4 {
binary.NativeEndian.PutUint32(b[i:], binary.BigEndian.Uint32(raw[i:]))
}
addr, _ := netip.AddrFromSlice(b)
return netip.AddrPortFrom(addr, uint16(port)), true
}
// procDir is the processes' folder; tests replace it.
var procDir = "/proc"
// socketOwner is the process with a descriptor that is the socket: first
// among the processes of the socket's user, then the others (a process
// that is not dumpable shows as root's).
func socketOwner(inode uint64, uid uint32) (int, error) {
link := "socket:[" + strconv.FormatUint(inode, 10) + "]"
entries, err := os.ReadDir(procDir)
if err != nil {
return 0, err
}
var others []string
for _, e := range entries {
if _, err := strconv.Atoi(e.Name()); err != nil {
continue
}
if info, err := e.Info(); err == nil {
if st, ok := info.Sys().(*syscall.Stat_t); ok && st.Uid != uid {
others = append(others, e.Name())
continue
}
}
if holds(e.Name(), link) {
return strconv.Atoi(e.Name())
}
}
for _, pid := range others {
if holds(pid, link) {
return strconv.Atoi(pid)
}
}
return 0, errors.New("no process holds the socket")
}
// holds reports whether process pid has a descriptor that is link.
func holds(pid, link string) bool {
dir := procDir + "/" + pid + "/fd"
fds, err := os.ReadDir(dir)
if err != nil {
return false
}
for _, fd := range fds {
if target, err := os.Readlink(dir + "/" + fd.Name()); err == nil && target == link {
return true
}
}
return false
}