Skip to content

Commit 50d6692

Browse files
committed
add captive portal for directories with multiple processes
1 parent 8d85019 commit 50d6692

12 files changed

Lines changed: 938 additions & 67 deletions

File tree

cmd/localproxyd/daemon.go

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type Daemon struct {
4040
envoyMgr *envoy.Manager
4141
statsScraper *envoy.StatsScraper
4242
routeRegistry *registry.RouteRegistry
43+
store *registry.Store
4344
dashboardServer *proxy.DashboardServer
4445
processWatcher *discovery.ProcessWatcher
4546
dockerWatcher *discovery.DockerWatcher
@@ -116,12 +117,17 @@ func (d *Daemon) Stop() {
116117
if d.envoyMgr != nil {
117118
d.envoyMgr.Stop()
118119
}
120+
if d.store != nil {
121+
d.store.Close()
122+
}
119123
if d.logFile != nil {
120124
d.logFile.Close()
121125
}
122-
err := d.hostsMgr.Cleanup()
123-
if err != nil {
124-
log.Printf("warning: hosts manager cleanup failed: %v", err)
126+
if d.hostsMgr != nil {
127+
err := d.hostsMgr.Cleanup()
128+
if err != nil {
129+
log.Printf("warning: hosts manager cleanup failed: %v", err)
130+
}
125131
}
126132
log.Println("daemon stopped")
127133
}
@@ -166,9 +172,23 @@ func (d *Daemon) initEnvoy() error {
166172
}
167173

168174
func (d *Daemon) initRouting() error {
175+
storePath := filepath.Join(d.dataDir, "store.db")
176+
store, err := registry.NewStore(storePath)
177+
if err != nil {
178+
return fmt.Errorf("failed to open store: %v", err)
179+
}
180+
d.store = store
181+
169182
basePaths := d.getBasePaths()
170183
d.dashboardServer = proxy.NewDashboardServer(basePaths, d.config.TraceProcessLogs, d.statsScraper)
171-
d.routeRegistry = registry.NewRouteRegistry(d.onRoutesChanged)
184+
d.routeRegistry = registry.NewRouteRegistry(d.onRoutesChanged, d.store)
185+
186+
d.dashboardServer.SetRegistry(d.routeRegistry)
187+
d.dashboardServer.SetStore(d.store)
188+
189+
d.store.SetOnChange(func() {
190+
d.routeRegistry.RefreshRoutes()
191+
})
172192

173193
certPath, keyPath, _ := d.certMgr.GetCert("localhost")
174194
initialRoute := xds.Route{
@@ -257,20 +277,38 @@ func (d *Daemon) onRoutesChanged(routes []proxy.Route) {
257277
if certKey == "" {
258278
certKey = "localhost"
259279
}
260-
if err := d.certMgr.EnsureCert(certKey); err != nil {
261-
log.Printf("failed to generate cert for %s: %v", certKey, err)
262-
continue
280+
281+
var certPath, keyPath string
282+
useWildcard := r.HasWildcard || r.FolderGroup != ""
283+
284+
if useWildcard {
285+
wildcardKey := certKey
286+
if r.FolderGroup != "" && !r.HasWildcard {
287+
wildcardKey = r.FolderGroup
288+
}
289+
if err := d.certMgr.EnsureWildcardCert(wildcardKey); err != nil {
290+
log.Printf("failed to generate wildcard cert for %s: %v", wildcardKey, err)
291+
continue
292+
}
293+
certPath, keyPath, _ = d.certMgr.GetWildcardCert(wildcardKey)
294+
} else {
295+
if err := d.certMgr.EnsureCert(certKey); err != nil {
296+
log.Printf("failed to generate cert for %s: %v", certKey, err)
297+
continue
298+
}
299+
certPath, keyPath, _ = d.certMgr.GetCert(certKey)
263300
}
264-
certPath, keyPath, _ := d.certMgr.GetCert(certKey)
265301

266302
xdsRoute := xds.Route{
267-
Subdomain: r.Subdomain,
268-
Host: r.Endpoint.Addr().String(),
269-
Port: int(r.Endpoint.Port()),
270-
TCPPort: r.TCPPort,
271-
Protocol: xds.ProtocolHTTP,
272-
CertPath: certPath,
273-
KeyPath: keyPath,
303+
Subdomain: r.Subdomain,
304+
Host: r.Endpoint.Addr().String(),
305+
Port: int(r.Endpoint.Port()),
306+
TCPPort: r.TCPPort,
307+
Protocol: xds.ProtocolHTTP,
308+
CertPath: certPath,
309+
KeyPath: keyPath,
310+
HasWildcard: r.HasWildcard,
311+
FolderGroup: r.FolderGroup,
274312
}
275313
if r.TCPPort > 0 {
276314
xdsRoute.Protocol = xds.ProtocolTCP

dev.sh

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
1-
#!/bin/bash
2-
3-
pkill -f "go run.*localproxyd" 2>/dev/null
4-
sleep 0.5
5-
1+
#!/usr/bin/env bash
62
sudo CGO_ENABLED=0 go run ./cmd/localproxyd --watch ~/projects --https-redirect $@

internal/certs/mkcert.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,26 +42,46 @@ func (m *CertManager) Init() error {
4242
}
4343

4444
func (m *CertManager) EnsureCert(subdomain string) error {
45+
return m.ensureCertInternal(subdomain, false)
46+
}
47+
48+
func (m *CertManager) EnsureWildcardCert(subdomain string) error {
49+
return m.ensureCertInternal(subdomain, true)
50+
}
51+
52+
func (m *CertManager) ensureCertInternal(subdomain string, wildcard bool) error {
4553
m.mu.Lock()
4654
defer m.mu.Unlock()
4755

48-
if _, exists := m.certs[subdomain]; exists {
56+
cacheKey := subdomain
57+
if wildcard {
58+
cacheKey = "wildcard_" + subdomain
59+
}
60+
61+
if _, exists := m.certs[cacheKey]; exists {
4962
return nil
5063
}
5164

52-
certPath := filepath.Join(m.certsDir, subdomain+".pem")
53-
keyPath := filepath.Join(m.certsDir, subdomain+"-key.pem")
65+
certPath := filepath.Join(m.certsDir, cacheKey+".pem")
66+
keyPath := filepath.Join(m.certsDir, cacheKey+"-key.pem")
5467

5568
if _, err := os.Stat(certPath); err == nil {
5669
if _, err := os.Stat(keyPath); err == nil {
57-
m.certs[subdomain] = &CertPaths{CertPath: certPath, KeyPath: keyPath}
70+
m.certs[cacheKey] = &CertPaths{CertPath: certPath, KeyPath: keyPath}
5871
return nil
5972
}
6073
}
6174

6275
var domains []string
6376
if subdomain == "localhost" {
6477
domains = []string{"localhost", "proxy.localhost", "proxy.internal"}
78+
} else if wildcard {
79+
domains = []string{
80+
"*." + subdomain + ".localhost",
81+
"*." + subdomain + ".internal",
82+
subdomain + ".localhost",
83+
subdomain + ".internal",
84+
}
6585
} else {
6686
domains = []string{subdomain + ".localhost", subdomain + ".internal"}
6787
}
@@ -74,10 +94,22 @@ func (m *CertManager) EnsureCert(subdomain string) error {
7494
return fmt.Errorf("failed to generate cert for %v: %w", domains, err)
7595
}
7696

77-
m.certs[subdomain] = &CertPaths{CertPath: certPath, KeyPath: keyPath}
97+
m.certs[cacheKey] = &CertPaths{CertPath: certPath, KeyPath: keyPath}
7898
return nil
7999
}
80100

101+
func (m *CertManager) GetWildcardCert(subdomain string) (certPath, keyPath string, ok bool) {
102+
m.mu.RLock()
103+
defer m.mu.RUnlock()
104+
105+
cacheKey := "wildcard_" + subdomain
106+
paths, exists := m.certs[cacheKey]
107+
if !exists {
108+
return "", "", false
109+
}
110+
return paths.CertPath, paths.KeyPath, true
111+
}
112+
81113
func (m *CertManager) GetCert(subdomain string) (certPath, keyPath string, ok bool) {
82114
m.mu.RLock()
83115
defer m.mu.RUnlock()

internal/discovery/docker.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,6 @@ func (w *DockerWatcher) DiscoverServiceInfo(svc DiscoveredService, onDiscovered
352352
log.Printf("docker: starting service discovery for %s on %d ports", svc.Subdomain, len(targets))
353353
results := make(chan []ServiceInfo, 1)
354354
ProbeEndpoints(w.ctx, targets, results)
355-
onDiscovered(svc)
356355

357356
go w.onServiceDiscovered(svc, results, portToIndex, onDiscovered)
358357
}

internal/discovery/process.go

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,12 @@ func (w *ProcessWatcher) processEntry(state *scanState, entry portEntry) {
200200
return
201201
}
202202

203-
subdomain, needsCustomMapping := w.buildSubdomain(basePath, cwd, state.ignoredDirs)
204-
if subdomain == "" && !needsCustomMapping {
203+
result := w.buildSubdomain(basePath, cwd, state.ignoredDirs)
204+
if result.subdomain == "" && !result.needsCustomMapping {
205205
return
206206
}
207207

208-
w.addListeningProcess(state, pid, entry.Endpoint, subdomain, cwd, needsCustomMapping)
208+
w.addListeningProcess(state, pid, entry.Endpoint, result, cwd)
209209
state.usedPorts[entry.Endpoint.Port()] = true
210210
}
211211

@@ -247,19 +247,21 @@ func (w *ProcessWatcher) addWellKnownProcess(state *scanState, pid int, endpoint
247247
state.usedPorts[port] = true
248248
}
249249

250-
func (w *ProcessWatcher) addListeningProcess(state *scanState, pid int, endpoint netip.AddrPort, subdomain string, cwd string, needsCustomMapping bool) {
250+
func (w *ProcessWatcher) addListeningProcess(state *scanState, pid int, endpoint netip.AddrPort, result subdomainResult, cwd string) {
251251
if state.seenPID[pid] {
252252
return
253253
}
254254
state.results = append(state.results, DiscoveredService{
255-
Subdomain: subdomain,
255+
Subdomain: result.subdomain,
256256
Endpoint: endpoint,
257257
Source: RouteSourceProcess,
258258
Process: &ProcessInfo{
259259
PID: pid,
260260
Cwd: cwd,
261-
Disabled: needsCustomMapping,
262-
NeedsCustomMapping: needsCustomMapping,
261+
Disabled: result.needsCustomMapping,
262+
NeedsCustomMapping: result.needsCustomMapping,
263+
TopLevelFolder: result.topLevelFolder,
264+
RelativePath: result.relativePath,
263265
},
264266
})
265267
state.seenPID[pid] = true
@@ -273,28 +275,46 @@ func (w *ProcessWatcher) handleOutsideBasePath(state *scanState, pid int, endpoi
273275
w.addWellKnownProcess(state, pid, endpoint)
274276
}
275277

276-
func (w *ProcessWatcher) buildSubdomain(basePath string, cwd string, ignoredDirs map[string]bool) (string, bool) {
278+
type subdomainResult struct {
279+
subdomain string
280+
needsCustomMapping bool
281+
topLevelFolder string
282+
relativePath string
283+
}
284+
285+
func (w *ProcessWatcher) buildSubdomain(basePath string, cwd string, ignoredDirs map[string]bool) subdomainResult {
277286
rel, err := filepath.Rel(basePath, cwd)
278287
if err != nil {
279-
return "", false
288+
return subdomainResult{}
280289
}
281290

282291
parts := strings.Split(rel, string(filepath.Separator))
283292
if len(parts) == 0 || parts[0] == "" || parts[0] == "." {
284-
return "", false
293+
return subdomainResult{}
285294
}
286295

287296
filteredParts := w.filterPathParts(parts, ignoredDirs)
288297
if len(filteredParts) == 0 {
289-
return "", false
298+
return subdomainResult{}
290299
}
291300

301+
topLevel := filteredParts[0]
302+
292303
if len(filteredParts) == 1 {
293-
subdomain := filteredParts[0]
294-
return subdomain, false
304+
return subdomainResult{
305+
subdomain: topLevel,
306+
needsCustomMapping: false,
307+
topLevelFolder: topLevel,
308+
relativePath: topLevel,
309+
}
295310
}
296311

297-
return "", true
312+
return subdomainResult{
313+
subdomain: topLevel,
314+
needsCustomMapping: true,
315+
topLevelFolder: topLevel,
316+
relativePath: strings.Join(filteredParts, "/"),
317+
}
298318
}
299319

300320
func (w *ProcessWatcher) filterPathParts(parts []string, ignoredDirs map[string]bool) []string {

internal/discovery/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type ProcessInfo struct {
2929
NeedsCustomMapping bool
3030
IsWellKnown bool
3131
IsDocker bool
32+
TopLevelFolder string
33+
RelativePath string
3234
}
3335

3436
type DockerListener struct {

internal/proxy/route.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@ type Route struct {
2020
NeedsCustomMapping bool
2121
IsDocker bool
2222
ServiceProtocol string
23+
HasWildcard bool
24+
FolderGroup string
25+
TopLevelFolder string
26+
RelativePath string
2327
}

0 commit comments

Comments
 (0)