Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ _testmain.go
*.exe
*.test
*.prof

# Coverage
coverage.out
20 changes: 20 additions & 0 deletions .mockery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
all: false
dir: 'mocks'
filename: 'mock_{{.InterfaceName | snakecase}}.go'
force-file-write: true
formatter: goimports
generate: true
include-auto-generated: false
log-level: info
structname: 'Mock{{.InterfaceName}}'
pkgname: 'mocks'
recursive: false
require-template-schema-exists: true
template: testify
template-schema: '{{.Template}}.schema.json'
packages:
github.com/enbility/zeroconf/v3/api:
interfaces:
PacketConn:
ConnectionFactory:
InterfaceProvider:
63 changes: 55 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,30 @@ Target environments: private LAN/Wifi, small or isolated networks.
## Install
Nothing is as easy as that:
```bash
$ go get -u github.com/enbility/zeroconf/v2
$ go get -u github.com/enbility/zeroconf/v3
```

## Browse for services in your local network

```go
entries := make(chan *zeroconf.ServiceEntry)
go func(results <-chan *zeroconf.ServiceEntry) {
for entry := range results {
log.Println(entry)
removed := make(chan *zeroconf.ServiceEntry)

go func() {
for {
select {
case entry := <-entries:
log.Println("Found:", entry)
case entry := <-removed:
log.Println("Removed:", entry)
}
}
log.Println("No more entries.")
}(entries)
}()

ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
// Discover all services on the network (e.g. _workstation._tcp)
err = zeroconf.Browse(ctx, "_workstation._tcp", "local.", entries)
err := zeroconf.Browse(ctx, "_workstation._tcp", "local.", entries, removed)
if err != nil {
log.Fatalln("Failed to browse:", err.Error())
}
Expand All @@ -53,7 +59,23 @@ See https://github.com/enbility/zeroconf/blob/master/examples/resolv/client.go.
## Lookup a specific service instance

```go
// Example filled soon.
entries := make(chan *zeroconf.ServiceEntry)

go func() {
for entry := range entries {
log.Println("Found:", entry)
}
}()

ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
// Lookup a specific service instance by name
err := zeroconf.Lookup(ctx, "MyService", "_workstation._tcp", "local.", entries)
if err != nil {
log.Fatalln("Failed to lookup:", err.Error())
}

<-ctx.Done()
```

## Register a service
Expand Down Expand Up @@ -81,6 +103,29 @@ Multiple subtypes may be added to service name, separated by commas. E.g `_works

See https://github.com/enbility/zeroconf/blob/master/examples/register/server.go.

## Testing Support (v3)

Version 3 introduces interface-based abstractions for improved testability. You can inject mock connections for unit testing without requiring real network access:

```go
// Create mock connections using the provided interfaces
mockFactory := &MyMockConnectionFactory{}

// Client with mock connections
client, err := zeroconf.NewClient(zeroconf.WithClientConnFactory(mockFactory))

// Server with mock connections
server, err := zeroconf.RegisterProxy(
"MyService", "_http._tcp", "local.", 8080,
"myhost.local.", []string{"192.168.1.100"},
[]string{"txtvers=1"},
nil, // interfaces
zeroconf.WithServerConnFactory(mockFactory),
)
```

See the `api/` package for interface definitions and `mocks/` for mockery-generated mocks.

## Features and ToDo's
This list gives a quick impression about the state of this library.
See what needs to be done and submit a pull request :)
Expand All @@ -89,6 +134,8 @@ See what needs to be done and submit a pull request :)
* [x] Multiple IPv6 / IPv4 addresses support
* [x] Send multiple probes (exp. back-off) if no service answers (*)
* [x] Timestamp entries for TTL checks
* [x] Service removal notifications via `removed` channel
* [x] Interface-based abstractions for testability (v3)
* [ ] Compare new multicasts with already received services

_Notes:_
Expand Down
248 changes: 248 additions & 0 deletions V3_REFACTORING_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# ZeroConf v3 Refactoring Plan

## Goals

1. **Testability**: Enable unit testing without real network access
2. **Interfaces**: Define clear abstractions at network boundaries
3. **Dependency Injection**: Allow mock injection for testing
4. **Test Coverage**: Target 85%+ coverage with meaningful unit tests
5. **Generated Mocks**: Use mockery for maintainable mocks
6. **Remove Global State**: Move package-level vars into config structs

## Key Insight: ControlMessage Simplification

Analysis of the codebase shows that only `IfIndex` is ever used from `ipv4.ControlMessage` and `ipv6.ControlMessage`. This allows us to create a unified `PacketConn` interface that works for both IPv4 and IPv6:

```go
// Instead of exposing ControlMessage, we just expose ifIndex
ReadFrom(b []byte) (n int, ifIndex int, src net.Addr, err error)
WriteTo(b []byte, ifIndex int, dst net.Addr) (n int, err error)
```

## Architecture Overview

```
┌─────────────────────────────────────────────────────────────┐
│ Public API │
│ Browse() / Lookup() / Register() / RegisterProxy() │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Client / Server │
│ - Use api.PacketConn interface (not concrete types) │
│ - Accept ConnectionFactory via options │
│ - Use InterfaceProvider internally for default interfaces │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ api/ Package │
│ PacketConn / ConnectionFactory / InterfaceProvider │
└─────────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ Real Implementations │ │ mocks/ Package │
│ - ipv4PacketConn │ │ - MockPacketConn │
│ - ipv6PacketConn │ │ - MockConnectionFactory │
│ - defaultConnFactory │ │ - MockInterfaceProvider │
│ - defaultIfaceProvider │ │ (generated by mockery) │
└──────────────────────────┘ └──────────────────────────┘
```

## Package Structure

```
zeroconf/v3/
├── api/ # Pure interfaces (no internal deps)
│ └── interfaces.go # PacketConn, ConnectionFactory, InterfaceProvider
├── mocks/ # Generated mocks (mockery)
│ ├── mock_packet_conn.go
│ ├── mock_connection_factory.go
│ └── mock_interface_provider.go
├── .mockery.yml # Mockery configuration
├── client.go # Client implementation
├── server.go # Server implementation
├── conn_ipv4.go # ipv4PacketConn wrapper
├── conn_ipv6.go # ipv6PacketConn wrapper
├── conn_factory.go # defaultConnectionFactory
├── conn_provider.go # defaultInterfaceProvider
├── mdns.go # Network constants (mDNS addresses)
├── service.go # ServiceEntry, ServiceRecord
├── utils.go # Helper functions
├── doc.go # Package documentation
├── *_test.go # Tests
└── examples/ # Example applications
```

## Interface Definitions (in api/interfaces.go)

### PacketConn

```go
type PacketConn interface {
ReadFrom(b []byte) (n int, ifIndex int, src net.Addr, err error)
WriteTo(b []byte, ifIndex int, dst net.Addr) (n int, err error)
Close() error
JoinGroup(ifi *net.Interface, group net.Addr) error
LeaveGroup(ifi *net.Interface, group net.Addr) error
SetMulticastTTL(ttl int) error
SetMulticastHopLimit(hopLimit int) error
SetMulticastInterface(ifi *net.Interface) error
}
```

### ConnectionFactory

```go
type ConnectionFactory interface {
CreateIPv4Conn(ifaces []net.Interface) (PacketConn, error)
CreateIPv6Conn(ifaces []net.Interface) (PacketConn, error)
}
```

### InterfaceProvider

```go
type InterfaceProvider interface {
MulticastInterfaces() []net.Interface
}
```

## Implementation Phases

### Phase 1: Package Structure & Interfaces ✓

**Completed:**
- [x] Create `api/` package with interface definitions
- [x] Configure mockery (`.mockery.yml`)
- [x] Generate mocks in `mocks/` package
- [x] Update implementation to import `api/`
- [x] Update tests to use `mocks/`

---

### Phase 2: Connection Wrappers (File Split) ✓

**Completed:**
- [x] `conn_ipv4.go` - ipv4PacketConn wrapper
- [x] `conn_ipv6.go` - ipv6PacketConn wrapper
- [x] `conn_factory.go` - defaultConnectionFactory
- [x] `conn_provider.go` - defaultInterfaceProvider with MulticastInterfaces()
- [x] Removed `conn_wrapper.go` (split into above files)

---

### Phase 3: InterfaceProvider Implementation ✓

**Completed:**
- [x] Created `defaultInterfaceProvider` implementing `api.InterfaceProvider`
- [x] Moved `listMulticastInterfaces()` into `defaultInterfaceProvider.MulticastInterfaces()`
- [x] Used internally via `NewInterfaceProvider().MulticastInterfaces()` in client/server

**Design Decision:** Removed `WithIfaceProvider` options after review - they added complexity without clear benefit since:
- `Register()` already accepts `ifaces []net.Interface` directly
- `SelectIfaces()` option exists for client
- Interface selection is simpler as a direct parameter than an injected provider

---

### Phase 4: Server Improvements ✓

**Changes to `server.go`:**
- [x] Change `Server.ipv4conn` to use `api.PacketConn`
- [x] Change `Server.ipv6conn` to use `api.PacketConn`
- [x] Add `WithServerConnFactory()` option
- [x] Remove deprecated `Server.TTL()` method

---

### Phase 5: Client Improvements ✓

**Changes to `client.go`:**
- [x] Change connection fields to use `api.PacketConn`
- [x] Add `WithClientConnFactory()` option
- [x] Export `Client` type (renamed `client` -> `Client`)
- [x] Add `NewClient()` constructor

---

### Phase 6: Coverage & Cleanup

1. Run coverage report, identify gaps
2. Add tests for untested functions
3. Update doc.go for v3
4. Final integration test pass

---

## File Changes Summary

| File | Action | Description |
|------|--------|-------------|
| `api/interfaces.go` | DONE | Interface definitions |
| `mocks/*.go` | DONE | Generated mocks (mockery) |
| `.mockery.yml` | DONE | Mockery configuration |
| `conn_ipv4.go` | NEW | IPv4 PacketConn wrapper |
| `conn_ipv6.go` | NEW | IPv6 PacketConn wrapper |
| `conn_factory.go` | NEW | defaultConnectionFactory |
| `conn_provider.go` | NEW | defaultInterfaceProvider + listMulticastInterfaces |
| `conn_wrapper.go` | DELETE | Split into above files |
| `mdns.go` | RENAME | Network constants (was connection.go) |
| `server.go` | MODIFY | Add WithServerConnFactory, remove TTL() |
| `client.go` | MODIFY | Export Client, add NewClient, add WithClientConnFactory |
| `server_unit_test.go` | DONE | Unit tests with mocks |
| `client_unit_test.go` | DONE | Unit tests with mocks |

---

## Breaking Changes

1. **Module path**: `github.com/enbility/zeroconf/v3`
2. **Exported `Client` type**: New public API
3. **`NewClient()` function**: New constructor
4. **Removed**: `Server.TTL()` method (was deprecated)

## Backward Compatibility

Main API functions remain compatible:
- `Browse(ctx, service, domain, entries, removed, opts...)` - unchanged
- `Lookup(ctx, instance, service, domain, entries, opts...)` - unchanged
- `Register(instance, service, domain, port, text, ifaces, opts...)` - unchanged
- `RegisterProxy(...)` - unchanged

New optional features via options:
- `WithClientConnFactory(factory)` / `WithServerConnFactory(factory)` - for injecting mock connections in tests

---

## Mock Generation

Using mockery v3. Configuration in `.mockery.yml`:

```yaml
packages:
github.com/enbility/zeroconf/v3/api:
interfaces:
PacketConn:
ConnectionFactory:
InterfaceProvider:
```

Regenerate mocks:
```bash
mockery
```

---

## Success Criteria

- [x] All existing tests pass
- [ ] Test coverage > 85% (currently 72.7%)
- [x] All network I/O behind interfaces
- [x] Unit tests run without network access
- [x] Mocks generated automatically
- [ ] Documentation updated
Loading