Skip to content

Commit 3c23c7b

Browse files
committed
fix: per-service forwarder tracking, port merge on multi-container projects, socket permissions
1 parent f774e65 commit 3c23c7b

2 files changed

Lines changed: 63 additions & 13 deletions

File tree

internal/api/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ func (a *API) ListenAndServe() error {
4646
if err != nil {
4747
return err
4848
}
49+
// Allow non-root users to connect
50+
os.Chmod(a.sockPath, 0666)
4951
a.listener = ln
5052

5153
mux := http.NewServeMux()

internal/daemon/daemon.go

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,25 @@ func (d *Daemon) OnContainerStart(ctx context.Context, info watcher.ContainerInf
144144
return entries[i].service < entries[j].service
145145
})
146146

147-
// Create forwarders
148-
var portMappings []state.PortMapping
147+
// Collect existing port mappings to avoid duplicate forwarders
148+
var existingPorts []state.PortMapping
149+
if existing != nil {
150+
existingPorts = existing.Ports
151+
}
152+
153+
// Create forwarders only for NEW ports
154+
var newPortMappings []state.PortMapping
149155
for _, e := range entries {
150156
listenAddr := fmt.Sprintf("%s:%d", ip, e.containerPort)
157+
158+
cancelKey := project + ":" + e.service + ":" + listenAddr
159+
160+
// Skip if a forwarder for this listen address already exists
161+
if _, exists := d.cancelFns[cancelKey]; exists {
162+
slog.Debug("forwarder already active, skipping", "project", project, "listen", listenAddr)
163+
continue
164+
}
165+
151166
targetAddr := fmt.Sprintf("127.0.0.1:%d", e.hostPort)
152167

153168
fwdCtx, fwdCancel := context.WithCancel(ctx)
@@ -159,9 +174,9 @@ func (d *Daemon) OnContainerStart(ctx context.Context, info watcher.ContainerInf
159174
}()
160175

161176
d.forwarders[project] = append(d.forwarders[project], fwd)
162-
d.cancelFns[project+":"+listenAddr] = fwdCancel
177+
d.cancelFns[cancelKey] = fwdCancel
163178

164-
portMappings = append(portMappings, state.PortMapping{
179+
newPortMappings = append(newPortMappings, state.PortMapping{
165180
ContainerPort: e.containerPort,
166181
HostPort: e.hostPort,
167182
Service: e.service,
@@ -170,11 +185,26 @@ func (d *Daemon) OnContainerStart(ctx context.Context, info watcher.ContainerInf
170185
slog.Info("forwarding", "project", project, "listen", listenAddr, "target", targetAddr)
171186
}
172187

173-
// Update state
188+
// Merge existing + new port mappings (deduplicated by listen addr)
189+
mergedPorts := existingPorts
190+
for _, np := range newPortMappings {
191+
duplicate := false
192+
for _, ep := range existingPorts {
193+
if ep.ContainerPort == np.ContainerPort {
194+
duplicate = true
195+
break
196+
}
197+
}
198+
if !duplicate {
199+
mergedPorts = append(mergedPorts, np)
200+
}
201+
}
202+
203+
// Update state with merged ports
174204
p := &state.Project{
175205
Name: project,
176206
IP: ip,
177-
Ports: portMappings,
207+
Ports: mergedPorts,
178208
}
179209
d.state.AddProject(p)
180210

@@ -183,26 +213,44 @@ func (d *Daemon) OnContainerStart(ctx context.Context, info watcher.ContainerInf
183213

184214
func (d *Daemon) OnContainerDie(ctx context.Context, info watcher.ContainerInfo) error {
185215
project := info.Project
216+
service := info.Service
186217

187-
// Cancel all forwarders for this project
218+
// Cancel only forwarders belonging to this service
219+
prefix := project + ":" + service + ":"
188220
for key, cancel := range d.cancelFns {
189-
if len(key) > len(project) && key[:len(project)+1] == project+":" {
221+
if len(key) >= len(prefix) && key[:len(prefix)] == prefix {
190222
cancel()
191223
delete(d.cancelFns, key)
192224
}
193225
}
194-
delete(d.forwarders, project)
195226

227+
// Remove this service's ports from the project state
196228
p := d.state.GetProject(project)
197229
if p == nil {
198230
return nil
199231
}
200232

201-
d.dns.Unregister(project)
202-
d.ipman.RemoveLoopbackIP(p.IP)
203-
d.state.RemoveProject(project)
233+
var remaining []state.PortMapping
234+
for _, pm := range p.Ports {
235+
if pm.Service != service {
236+
remaining = append(remaining, pm)
237+
}
238+
}
239+
240+
if len(remaining) == 0 {
241+
// No more services in this project — full teardown
242+
delete(d.forwarders, project)
243+
d.dns.Unregister(project)
244+
d.ipman.RemoveLoopbackIP(p.IP)
245+
d.state.RemoveProject(project)
246+
slog.Info("project removed", "project", project)
247+
} else {
248+
// Update project with remaining ports
249+
p.Ports = remaining
250+
d.state.AddProject(p)
251+
slog.Info("service removed", "project", project, "service", service, "remaining", len(remaining))
252+
}
204253

205-
slog.Info("project removed", "project", project)
206254
return nil
207255
}
208256

0 commit comments

Comments
 (0)