Skip to content

Commit c0e3538

Browse files
committed
add process discovery for linux
1 parent 4a3ff17 commit c0e3538

6 files changed

Lines changed: 77 additions & 8 deletions

File tree

.dockerignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.git
2+
.gitignore
3+
.github
4+
.devenv
5+
devenv.nix
6+
devenv.yaml
7+
devenv.lock
8+
.envrc
9+
.zed
10+
.claude
11+
README.md
12+
showcase.jpg
13+
bin/
14+
localproxyd
15+
*.md
16+
!README.md

Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM golang:1.25-alpine AS builder
2+
3+
RUN apk add --no-cache git ca-certificates
4+
5+
WORKDIR /app
6+
7+
COPY go.mod go.sum ./
8+
RUN go mod download
9+
10+
COPY . .
11+
12+
RUN CGO_ENABLED=0 GOOS=linux go build -o localproxyd ./cmd/localproxyd
13+
14+
FROM alpine:latest
15+
16+
RUN apk --no-cache add ca-certificates netcat-openbsd
17+
18+
WORKDIR /root
19+
20+
COPY --from=builder /app/localproxyd /usr/local/bin/
21+
22+
COPY --from=builder /app/internal/dashboard/templates /root/internal/dashboard/templates
23+
24+
CMD sh -c "while true; do nc -l -p 9999; done & localproxyd --watch /"

internal/dashboard/templates/dashboard.html

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ <h1>LocalProxy</h1>
577577
https://{{if
578578
.SelectedBackend.Subdomain}}{{.SelectedBackend.Subdomain}}.{{end}}localhost
579579
</a>
580-
<div class="metrics-grid">
580+
<!--<div class="metrics-grid">
581581
<div class="metric-card">
582582
<div class="metric-label">Traffic</div>
583583
<div class="metric-value">
@@ -654,7 +654,7 @@ <h1>LocalProxy</h1>
654654
<span class="metric-unit">disconnects</span>
655655
</div>
656656
</div>
657-
</div>
657+
</div>-->
658658
<div class="info-grid">
659659
<div class="info-item">
660660
<span class="info-label">PID:</span>
@@ -698,7 +698,8 @@ <h1>LocalProxy</h1>
698698
class="port-item{{if eq .Port (port $.SelectedBackend.Endpoint)}} active{{end}}"
699699
>
700700
<span class="port-arrow"
701-
>{{if eq .Port (port $.SelectedBackend.Endpoint)}}&gt;{{end}}</span
701+
>{{if eq .Port (port
702+
$.SelectedBackend.Endpoint)}}&gt;{{end}}</span
702703
>
703704
<span>:{{.Text}}</span>
704705
<span class="port-protocol">{{or .Protocol "?"}}</span>
@@ -715,8 +716,8 @@ <h1>LocalProxy</h1>
715716
>
716717
<div class="logs-container">
717718
<!-- Debug: Source = "{{.SelectedBackend.Source}}" -->
718-
{{if and .SelectedBackend (eq .SelectedBackend.Source "process")
719-
(not $.TraceProcessLogs)}}
719+
{{if and .SelectedBackend (eq .SelectedBackend.Source
720+
"process") (not $.TraceProcessLogs)}}
720721
<div class="log-disclaimer">
721722
<p>
722723
Logs for processes are disabled. To enable them, run

internal/discovery/process.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ func (w *ProcessWatcher) scan() ([]DiscoveredService, error) {
161161
log.Printf("process: getListeningPorts error: %v", err)
162162
return nil, err
163163
}
164+
log.Printf("process: scanning %d listeners", len(listeners))
164165

165166
state := &scanState{
166167
ignoredDirs: map[string]bool{"apps": true, "packages": true},
@@ -170,9 +171,11 @@ func (w *ProcessWatcher) scan() ([]DiscoveredService, error) {
170171
}
171172

172173
for _, entry := range listeners {
174+
log.Printf("process: processing PID=%d port=%d", entry.PID, entry.Endpoint.Port())
173175
w.processEntry(state, entry)
174176
}
175177

178+
log.Printf("process: scan complete, found %d results", len(state.results))
176179
return state.results, nil
177180
}
178181

@@ -188,19 +191,24 @@ func (w *ProcessWatcher) processEntry(state *scanState, entry portEntry) {
188191
pid := entry.PID
189192

190193
cwd := w.getOrCacheCWD(state, pid)
194+
log.Printf("process: PID=%d cwd='%s'", pid, cwd)
191195

192196
if cwd == "" {
197+
log.Printf("process: PID=%d has no CWD, trying well-known", pid)
193198
w.handleUnknownCWD(state, pid, entry.Endpoint)
194199
return
195200
}
196201

197202
basePath := w.findMatchingBasePath(cwd)
203+
log.Printf("process: PID=%d basePath='%s', basePaths=%v", pid, basePath, w.basePaths)
198204
if basePath == "" {
205+
log.Printf("process: PID=%d outside base paths, trying well-known", pid)
199206
w.handleOutsideBasePath(state, pid, entry.Endpoint)
200207
return
201208
}
202209

203210
result := w.buildSubdomain(basePath, cwd, state.ignoredDirs)
211+
log.Printf("process: PID=%d subdomain='%s', needsCustomMapping=%v", pid, result.subdomain, result.needsCustomMapping)
204212
if result.subdomain == "" && !result.needsCustomMapping {
205213
return
206214
}
@@ -231,8 +239,10 @@ func (w *ProcessWatcher) addWellKnownProcess(state *scanState, pid int, endpoint
231239
port := endpoint.Port()
232240
info, ok := WellKnownPorts[port]
233241
if !ok || state.usedPorts[port] {
242+
log.Printf("process: PID=%d port=%d not well-known or already used (ok=%v, used=%v)", pid, port, ok, state.usedPorts[port])
234243
return
235244
}
245+
log.Printf("process: adding PID=%d as well-known service: %s", pid, info.Subdomain)
236246
state.results = append(state.results, DiscoveredService{
237247
Subdomain: info.Subdomain,
238248
Endpoint: endpoint,

internal/discovery/process_linux.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ func hexByte(s string) int {
2020
func (w *ProcessWatcher) getListeningPorts() ([]portEntry, error) {
2121
entries, err := w.parseProcNet("/proc/net/tcp")
2222
if err != nil {
23+
fmt.Printf("Error parsing /proc/net/tcp: %v\n", err)
2324
return nil, err
2425
}
26+
fmt.Printf("Found %d entries from /proc/net/tcp\n", len(entries))
2527

2628
entries6, err := w.parseProcNet("/proc/net/tcp6")
2729
if err == nil {
2830
entries = append(entries, entries6...)
31+
fmt.Printf("Found %d entries from /proc/net/tcp6\n", len(entries6))
2932
}
3033

3134
return entries, nil
@@ -40,6 +43,7 @@ func (w *ProcessWatcher) parseProcNet(path string) ([]portEntry, error) {
4043

4144
inodeToPID := make(map[string]int)
4245
procDirs, _ := filepath.Glob("/proc/[0-9]*/fd/*")
46+
fmt.Printf("Found %d fd paths\n", len(procDirs))
4347
for _, fdPath := range procDirs {
4448
link, err := os.Readlink(fdPath)
4549
if err != nil {
@@ -59,6 +63,7 @@ func (w *ProcessWatcher) parseProcNet(path string) ([]portEntry, error) {
5963
}
6064
inodeToPID[inode] = pid
6165
}
66+
fmt.Printf("Mapped %d inodes to PIDs\n", len(inodeToPID))
6267

6368
var result []portEntry
6469
scanner := bufio.NewScanner(file)
@@ -68,6 +73,7 @@ func (w *ProcessWatcher) parseProcNet(path string) ([]portEntry, error) {
6873
line := scanner.Text()
6974
fields := strings.Fields(line)
7075
if len(fields) < 10 {
76+
fmt.Printf("Skipping line with %d fields: %s\n", len(fields), line)
7177
continue
7278
}
7379

@@ -79,18 +85,21 @@ func (w *ProcessWatcher) parseProcNet(path string) ([]portEntry, error) {
7985
localAddr := fields[1]
8086
addrParts := strings.Split(localAddr, ":")
8187
if len(addrParts) != 2 {
88+
fmt.Printf("Skipping bad address format: %s\n", localAddr)
8289
continue
8390
}
8491

8592
addrHex := addrParts[0]
8693
portHex := addrParts[1]
8794
port64, err := strconv.ParseInt(portHex, 16, 32)
8895
if err != nil {
96+
fmt.Printf("Skipping bad port %s: %v\n", portHex, err)
8997
continue
9098
}
9199
port := int(port64)
92100

93101
if port < 1024 {
102+
fmt.Printf("Skipping port < 1024: %d\n", port)
94103
continue
95104
}
96105

@@ -108,11 +117,13 @@ func (w *ProcessWatcher) parseProcNet(path string) ([]portEntry, error) {
108117
inode := fields[9]
109118
pid, ok := inodeToPID[inode]
110119
if !ok {
120+
fmt.Printf("No PID for inode %s, port %d, ip %s\n", inode, port, ipStr)
111121
continue
112122
}
113123

114124
addr, err := netip.ParseAddr(ipStr)
115125
if err != nil {
126+
fmt.Printf("Skipping invalid IP %s: %v\n", ipStr, err)
116127
continue
117128
}
118129
result = append(result, portEntry{PID: pid, Endpoint: netip.AddrPortFrom(addr, uint16(port))})

internal/proxy/protocol/postgres.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ import (
1010
"github.com/jackc/pgx/v5/pgproto3"
1111
)
1212

13+
type Direction int
14+
15+
const (
16+
DirectionClientToServer Direction = 0
17+
DirectionServerToClient Direction = iota
18+
)
19+
1320
type PgMessage struct {
1421
Timestamp time.Time `json:"timestamp"`
15-
Direction string `json:"direction"`
22+
Direction Direction `json:"direction"`
1623
Type string `json:"type"`
1724
Details any `json:"details,omitempty"`
1825
Raw []byte `json:"-"`
@@ -50,7 +57,7 @@ func ParseFrontendMessage(endpoint netip.AddrPort, data []byte) *PgMessage {
5057

5158
msg := &PgMessage{
5259
Timestamp: time.Now(),
53-
Direction: "client->server",
60+
Direction: DirectionClientToServer,
5461
Raw: data,
5562
}
5663

@@ -178,7 +185,7 @@ func ParseBackendMessage(endpoint netip.AddrPort, data []byte) *PgMessage {
178185

179186
msg := &PgMessage{
180187
Timestamp: time.Now(),
181-
Direction: "server->client",
188+
Direction: DirectionServerToClient,
182189
Raw: data,
183190
}
184191

0 commit comments

Comments
 (0)