diff --git a/internal/infrastructure/netbridge/infrastructure.go b/internal/infrastructure/netbridge/infrastructure.go new file mode 100644 index 0000000..7d5cca6 --- /dev/null +++ b/internal/infrastructure/netbridge/infrastructure.go @@ -0,0 +1,8 @@ +package netbridge + +type InfrastructureStatus struct { + Enabled bool `json:"enabled"` + Available []string `json:"available"` + Selected string `json:"selected"` + HasFallback bool `json:"has_fallback"` +} diff --git a/internal/infrastructure/netbridge/interface.go b/internal/infrastructure/netbridge/interface.go index 58ea6ca..fd49526 100644 --- a/internal/infrastructure/netbridge/interface.go +++ b/internal/infrastructure/netbridge/interface.go @@ -9,6 +9,9 @@ import ( type NetbridgeInterface interface { IsEnabled() bool IsAvailable() bool + DetectInfrastructure( + ctx context.Context, + ) (InfrastructureStatus, error) PublicIP( ctx context.Context, diff --git a/internal/infrastructure/netbridge/netbridge.go b/internal/infrastructure/netbridge/netbridge.go index 715d1d3..453edb8 100644 --- a/internal/infrastructure/netbridge/netbridge.go +++ b/internal/infrastructure/netbridge/netbridge.go @@ -2,7 +2,13 @@ package netbridge import ( "context" + "fmt" + "strconv" + "strings" + "github.com/rabbytesoftware/quiver/internal/core/config" + "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators" + "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators/interfaces" domain "github.com/rabbytesoftware/quiver/internal/models/networking" ) @@ -11,30 +17,94 @@ import ( // TODO: like UPnP, NAT-PMP, hole-punching, etc. type NetbridgeImpl struct { + operators *operators.OperatorContainer + operator interfaces.OperatorInterface } func NewNetbridge() NetbridgeInterface { - return &NetbridgeImpl{} + return &NetbridgeImpl{ + operators: operators.NewOperatorContainer(), + } } func (n *NetbridgeImpl) IsEnabled() bool { - return true + return config.GetNetbridge().Enabled } func (n *NetbridgeImpl) IsAvailable() bool { - return true + if !n.IsEnabled() { + return false + } + + operator := n.obtainOperator(context.Background()) + return operator != nil +} + +func (n *NetbridgeImpl) DetectInfrastructure( + ctx context.Context, +) (InfrastructureStatus, error) { + status := InfrastructureStatus{ + Enabled: n.IsEnabled(), + } + + if !status.Enabled { + return status, nil + } + + if n.operators == nil { + n.operators = operators.NewOperatorContainer() + } + + status.Available = n.operators.AvailableNames(ctx) + operator := n.obtainOperator(ctx) + if operator != nil { + status.Selected = operator.Name() + } + status.HasFallback = len(status.Available) > 1 + + return status, nil } func (n *NetbridgeImpl) PublicIP( ctx context.Context, ) (string, error) { - return "", nil + if !n.IsEnabled() { + return "", fmt.Errorf("netbridge disabled") + } + + operator := n.obtainOperator(ctx) + if operator == nil { + return "", fmt.Errorf("no netbridge operator available") + } + + if upnpOperator, ok := operator.(interface { + PublicIP(context.Context) (string, error) + }); ok { + return upnpOperator.PublicIP(ctx) + } + + return "", fmt.Errorf("operator does not support public IP lookup") } func (n *NetbridgeImpl) LocalIP( ctx context.Context, ) (string, error) { - return "", nil + if !n.IsEnabled() { + return "", fmt.Errorf("netbridge disabled") + } + + operator := n.obtainOperator(ctx) + if operator == nil { + return "", fmt.Errorf("no netbridge operator available") + } + + if upnpOperator, ok := operator.(interface { + LocalIP(context.Context) (string, error) + }); ok { + return upnpOperator.LocalIP(ctx) + } + + return "", fmt.Errorf("operator does not support local IP lookup") } func (n *NetbridgeImpl) IsPortAvailable( @@ -42,50 +112,187 @@ func (n *NetbridgeImpl) IsPortAvailable( port int, protocol domain.Protocol, ) (bool, error) { - return true, nil + if !n.IsEnabled() { + return false, nil + } + + if !n.isPortAllowed(port) { + return false, nil + } + + operator := n.obtainOperator(ctx) + if operator == nil { + return false, nil + } + + return operator.IsPortAvailable(ctx, port, protocol) } func (n *NetbridgeImpl) IsProtocolAvailable( ctx context.Context, protocol domain.Protocol, ) (bool, error) { - return true, nil + if !n.IsEnabled() { + return false, nil + } + + operator := n.obtainOperator(ctx) + if operator == nil { + return false, nil + } + + return operator.IsProtocolAvailable(ctx, protocol) } func (n *NetbridgeImpl) ForwardRule( ctx context.Context, rule domain.Rule, ) (domain.Port, error) { - return domain.Port{}, nil + if !n.IsEnabled() { + return domain.Port{}, fmt.Errorf("netbridge disabled") + } + + operator := n.obtainOperator(ctx) + if operator == nil { + return domain.Port{}, fmt.Errorf("no netbridge operator available") + } + + port, err := operator.ForwardRule(ctx, rule) + if err != nil { + return domain.Port{}, err + } + + if !n.isPortAllowed(port.StartPort) || !n.isPortAllowed(port.EndPort) { + return domain.Port{}, fmt.Errorf("port %d-%d not allowed by netbridge configuration", + port.StartPort, port.EndPort) + } + + return port, nil } func (n *NetbridgeImpl) ForwardPort( ctx context.Context, port domain.Port, ) (domain.Port, error) { - return domain.Port{ - StartPort: port.StartPort, - EndPort: port.EndPort, - Protocol: port.Protocol, - ForwardingStatus: port.ForwardingStatus, - }, nil + if !n.IsEnabled() { + return domain.Port{}, fmt.Errorf("netbridge disabled") + } + + if !n.isPortAllowed(port.StartPort) || !n.isPortAllowed(port.EndPort) { + return domain.Port{}, fmt.Errorf("port %d-%d not allowed by netbridge configuration", + port.StartPort, port.EndPort) + } + + operator := n.obtainOperator(ctx) + if operator == nil { + return domain.Port{}, fmt.Errorf("no netbridge operator available") + } + + return operator.ForwardPort(ctx, port) } func (n *NetbridgeImpl) ReversePort( ctx context.Context, port domain.Port, ) (domain.Port, error) { - return domain.Port{ - StartPort: port.StartPort, - EndPort: port.EndPort, - Protocol: port.Protocol, - ForwardingStatus: port.ForwardingStatus, - }, nil + if !n.IsEnabled() { + return domain.Port{}, fmt.Errorf("netbridge disabled") + } + + if !n.isPortAllowed(port.StartPort) || !n.isPortAllowed(port.EndPort) { + return domain.Port{}, fmt.Errorf("port %d-%d not allowed by netbridge configuration", + port.StartPort, port.EndPort) + } + + operator := n.obtainOperator(ctx) + if operator == nil { + return domain.Port{}, fmt.Errorf("no netbridge operator available") + } + + return operator.ReversePort(ctx, port) } func (n *NetbridgeImpl) GetPortForwardingStatus( ctx context.Context, port domain.Port, ) (domain.ForwardingStatus, error) { - return port.ForwardingStatus, nil + if !n.IsEnabled() { + return domain.ForwardingStatusError, fmt.Errorf("netbridge disabled") + } + + if !n.isPortAllowed(port.StartPort) || !n.isPortAllowed(port.EndPort) { + return domain.ForwardingStatusError, fmt.Errorf("port %d-%d not allowed by netbridge configuration", + port.StartPort, port.EndPort) + } + + operator := n.obtainOperator(ctx) + if operator == nil { + return domain.ForwardingStatusError, fmt.Errorf("no netbridge operator available") + } + + return operator.GetPortForwardingStatus(ctx, port) +} + +func (n *NetbridgeImpl) obtainOperator( + ctx context.Context, +) interfaces.OperatorInterface { + if n.operator != nil { + return n.operator + } + + if n.operators == nil { + n.operators = operators.NewOperatorContainer() + } + + n.operator = n.operators.Obtain(ctx) + return n.operator +} + +func (n *NetbridgeImpl) isPortAllowed(port int) bool { + minPort, maxPort, hasRange, err := parseAllowedPorts(config.GetNetbridge().AllowedPorts) + if err != nil { + return false + } + + if !hasRange { + return true + } + + return port >= minPort && port <= maxPort +} + +func parseAllowedPorts(allowed string) (int, int, bool, error) { + allowed = strings.TrimSpace(allowed) + if allowed == "" { + return 0, 0, false, nil + } + + rangeParts := strings.Split(allowed, "-") + if len(rangeParts) == 1 { + port, err := strconv.Atoi(strings.TrimSpace(rangeParts[0])) + if err != nil { + return 0, 0, false, fmt.Errorf("invalid allowed_ports value: %w", err) + } + return port, port, true, nil + } + + if len(rangeParts) != 2 { + return 0, 0, false, fmt.Errorf("invalid allowed_ports range: %s", allowed) + } + + minPort, err := strconv.Atoi(strings.TrimSpace(rangeParts[0])) + if err != nil { + return 0, 0, false, fmt.Errorf("invalid allowed_ports start: %w", err) + } + + maxPort, err := strconv.Atoi(strings.TrimSpace(rangeParts[1])) + if err != nil { + return 0, 0, false, fmt.Errorf("invalid allowed_ports end: %w", err) + } + + if minPort <= 0 || maxPort <= 0 || minPort > maxPort { + return 0, 0, false, fmt.Errorf("invalid allowed_ports range: %s", allowed) + } + + return minPort, maxPort, true, nil } diff --git a/internal/infrastructure/netbridge/netbridge_test.go b/internal/infrastructure/netbridge/netbridge_test.go index db24916..d5b6da0 100644 --- a/internal/infrastructure/netbridge/netbridge_test.go +++ b/internal/infrastructure/netbridge/netbridge_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators" "github.com/rabbytesoftware/quiver/internal/models/networking" ) @@ -24,7 +25,9 @@ func TestNetbridgeImpl_IsEnabled(t *testing.T) { } func TestNetbridgeImpl_IsAvailable(t *testing.T) { - nb := NewNetbridge() + nb := &NetbridgeImpl{ + operator: &fakeOperator{available: true}, + } available := nb.IsAvailable() if !available { @@ -33,51 +36,59 @@ func TestNetbridgeImpl_IsAvailable(t *testing.T) { } func TestNetbridgeImpl_PublicIP(t *testing.T) { - nb := NewNetbridge() + nb := &NetbridgeImpl{ + operator: &fakeOperator{publicIP: "203.0.113.1"}, + } ctx := context.Background() ip, err := nb.PublicIP(ctx) if err != nil { t.Errorf("PublicIP() returned error: %v", err) } - if ip != "" { - t.Error("PublicIP() should return empty string for unimplemented method") + if ip != "203.0.113.1" { + t.Errorf("PublicIP() returned wrong IP: got %s, want %s", ip, "203.0.113.1") } } func TestNetbridgeImpl_LocalIP(t *testing.T) { - nb := NewNetbridge() + nb := &NetbridgeImpl{ + operator: &fakeOperator{localIP: "192.168.1.100"}, + } ctx := context.Background() ip, err := nb.LocalIP(ctx) if err != nil { t.Errorf("LocalIP() returned error: %v", err) } - if ip != "" { - t.Error("LocalIP() should return empty string for unimplemented method") + if ip != "192.168.1.100" { + t.Errorf("LocalIP() returned wrong IP: got %s, want %s", ip, "192.168.1.100") } } func TestNetbridgeImpl_IsPortAvailable(t *testing.T) { - nb := NewNetbridge() + nb := &NetbridgeImpl{ + operator: &fakeOperator{available: true, portAvailable: true}, + } ctx := context.Background() - available, err := nb.IsPortAvailable(ctx, 8080, networking.ProtocolTCP) + available, err := nb.IsPortAvailable(ctx, 40128, networking.ProtocolTCP) if err != nil { t.Errorf("IsPortAvailable() returned error: %v", err) } if !available { - t.Error("IsPortAvailable() should return true for unimplemented method") + t.Error("IsPortAvailable() should return true for allowed port") } } func TestNetbridgeImpl_ForwardPort(t *testing.T) { - nb := NewNetbridge() + nb := &NetbridgeImpl{ + operator: &fakeOperator{available: true}, + } ctx := context.Background() testPort := networking.Port{ - StartPort: 8080, - EndPort: 8080, + StartPort: 40128, + EndPort: 40128, Protocol: networking.ProtocolTCP, ForwardingStatus: networking.ForwardingStatusEnabled, } @@ -88,11 +99,11 @@ func TestNetbridgeImpl_ForwardPort(t *testing.T) { } // Check that the returned port has expected values - if result.StartPort != 8080 { - t.Errorf("ForwardPort() returned wrong StartPort: got %d, want %d", result.StartPort, 8080) + if result.StartPort != 40128 { + t.Errorf("ForwardPort() returned wrong StartPort: got %d, want %d", result.StartPort, 40128) } - if result.EndPort != 8080 { - t.Errorf("ForwardPort() returned wrong EndPort: got %d, want %d", result.EndPort, 8080) + if result.EndPort != 40128 { + t.Errorf("ForwardPort() returned wrong EndPort: got %d, want %d", result.EndPort, 40128) } if result.Protocol != networking.ProtocolTCP { t.Errorf("ForwardPort() returned wrong Protocol: got %v, want %v", result.Protocol, networking.ProtocolTCP) @@ -103,12 +114,14 @@ func TestNetbridgeImpl_ForwardPort(t *testing.T) { } func TestNetbridgeImpl_ReversePort(t *testing.T) { - nb := NewNetbridge() + nb := &NetbridgeImpl{ + operator: &fakeOperator{available: true}, + } ctx := context.Background() testPort := networking.Port{ - StartPort: 8080, - EndPort: 8080, + StartPort: 40128, + EndPort: 40128, Protocol: networking.ProtocolTCP, ForwardingStatus: networking.ForwardingStatusEnabled, } @@ -119,11 +132,11 @@ func TestNetbridgeImpl_ReversePort(t *testing.T) { } // Check that the returned port has expected values - if result.StartPort != 8080 { - t.Errorf("ReversePort() returned wrong StartPort: got %d, want %d", result.StartPort, 8080) + if result.StartPort != 40128 { + t.Errorf("ReversePort() returned wrong StartPort: got %d, want %d", result.StartPort, 40128) } - if result.EndPort != 8080 { - t.Errorf("ReversePort() returned wrong EndPort: got %d, want %d", result.EndPort, 8080) + if result.EndPort != 40128 { + t.Errorf("ReversePort() returned wrong EndPort: got %d, want %d", result.EndPort, 40128) } if result.Protocol != networking.ProtocolTCP { t.Errorf("ReversePort() returned wrong Protocol: got %v, want %v", result.Protocol, networking.ProtocolTCP) @@ -134,12 +147,14 @@ func TestNetbridgeImpl_ReversePort(t *testing.T) { } func TestNetbridgeImpl_GetPortForwardingStatus(t *testing.T) { - nb := NewNetbridge() + nb := &NetbridgeImpl{ + operator: &fakeOperator{available: true}, + } ctx := context.Background() testPort := networking.Port{ - StartPort: 8080, - EndPort: 8080, + StartPort: 40128, + EndPort: 40128, Protocol: networking.ProtocolTCP, ForwardingStatus: networking.ForwardingStatusEnabled, } @@ -153,6 +168,33 @@ func TestNetbridgeImpl_GetPortForwardingStatus(t *testing.T) { } } +func TestNetbridgeImpl_DetectInfrastructure(t *testing.T) { + container := operators.NewEmptyOperatorContainer() + container.Add(&fakeOperator{name: "primary", available: true}) + container.Add(&fakeOperator{name: "secondary", available: false}) + + nb := &NetbridgeImpl{ + operators: container, + } + + status, err := nb.DetectInfrastructure(context.Background()) + if err != nil { + t.Fatalf("DetectInfrastructure() returned error: %v", err) + } + + if !status.Enabled { + t.Error("DetectInfrastructure() should report enabled netbridge") + } + + if status.Selected != "primary" { + t.Errorf("DetectInfrastructure() returned wrong selected operator: got %s, want %s", status.Selected, "primary") + } + + if len(status.Available) == 0 { + t.Error("DetectInfrastructure() should return available operators") + } +} + func TestNetbridgeImpl_InterfaceCompliance(t *testing.T) { // Test that NetbridgeImpl implements NetbridgeInterface var _ NetbridgeInterface = &NetbridgeImpl{} @@ -176,3 +218,78 @@ func TestNetbridgeImpl_MultipleInstances(t *testing.T) { t.Error("Both instances should have same IsAvailable behavior") } } + +type fakeOperator struct { + available bool + portAvailable bool + publicIP string + localIP string + name string +} + +func (f *fakeOperator) Name() string { + if f.name == "" { + return "fake" + } + return f.name +} + +func (f *fakeOperator) IsAvailable(context.Context) (bool, error) { + return f.available, nil +} + +func (f *fakeOperator) IsPortAvailable( + _ context.Context, + _ int, + _ networking.Protocol, +) (bool, error) { + return f.portAvailable, nil +} + +func (f *fakeOperator) IsProtocolAvailable( + _ context.Context, + _ networking.Protocol, +) (bool, error) { + return f.available, nil +} + +func (f *fakeOperator) ForwardRule( + _ context.Context, + rule networking.Rule, +) (networking.Port, error) { + return networking.Port{ + StartPort: 40128, + EndPort: 40128, + Protocol: rule.Protocol, + ForwardingStatus: rule.ForwardingStatus, + }, nil +} + +func (f *fakeOperator) ForwardPort( + _ context.Context, + port networking.Port, +) (networking.Port, error) { + return port, nil +} + +func (f *fakeOperator) ReversePort( + _ context.Context, + port networking.Port, +) (networking.Port, error) { + return port, nil +} + +func (f *fakeOperator) GetPortForwardingStatus( + _ context.Context, + port networking.Port, +) (networking.ForwardingStatus, error) { + return port.ForwardingStatus, nil +} + +func (f *fakeOperator) PublicIP(context.Context) (string, error) { + return f.publicIP, nil +} + +func (f *fakeOperator) LocalIP(context.Context) (string, error) { + return f.localIP, nil +} diff --git a/internal/infrastructure/netbridge/operators/holepunch/holepunch.go b/internal/infrastructure/netbridge/operators/holepunch/holepunch.go new file mode 100644 index 0000000..059554e --- /dev/null +++ b/internal/infrastructure/netbridge/operators/holepunch/holepunch.go @@ -0,0 +1,66 @@ +package holepunch + +import ( + "context" + "fmt" + + "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators/interfaces" + "github.com/rabbytesoftware/quiver/internal/models/networking" +) + +type HolePunch struct{} + +func NewHolePunch() interfaces.OperatorInterface { + return &HolePunch{} +} + +func (h *HolePunch) Name() string { + return "holepunch" +} + +func (h *HolePunch) IsAvailable(context.Context) (bool, error) { + return false, nil +} + +func (h *HolePunch) IsPortAvailable( + _ context.Context, + _ int, + _ networking.Protocol, +) (bool, error) { + return false, fmt.Errorf("hole punching not implemented") +} + +func (h *HolePunch) IsProtocolAvailable( + _ context.Context, + _ networking.Protocol, +) (bool, error) { + return false, fmt.Errorf("hole punching not implemented") +} + +func (h *HolePunch) ForwardRule( + _ context.Context, + _ networking.Rule, +) (networking.Port, error) { + return networking.Port{}, fmt.Errorf("hole punching not implemented") +} + +func (h *HolePunch) ForwardPort( + _ context.Context, + _ networking.Port, +) (networking.Port, error) { + return networking.Port{}, fmt.Errorf("hole punching not implemented") +} + +func (h *HolePunch) ReversePort( + _ context.Context, + _ networking.Port, +) (networking.Port, error) { + return networking.Port{}, fmt.Errorf("hole punching not implemented") +} + +func (h *HolePunch) GetPortForwardingStatus( + _ context.Context, + _ networking.Port, +) (networking.ForwardingStatus, error) { + return networking.ForwardingStatusError, fmt.Errorf("hole punching not implemented") +} diff --git a/internal/infrastructure/netbridge/operators/natpmp/natpmp.go b/internal/infrastructure/netbridge/operators/natpmp/natpmp.go new file mode 100644 index 0000000..f9450c7 --- /dev/null +++ b/internal/infrastructure/netbridge/operators/natpmp/natpmp.go @@ -0,0 +1,66 @@ +package natpmp + +import ( + "context" + "fmt" + + "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators/interfaces" + "github.com/rabbytesoftware/quiver/internal/models/networking" +) + +type NATPMP struct{} + +func NewNATPMP() interfaces.OperatorInterface { + return &NATPMP{} +} + +func (n *NATPMP) Name() string { + return "natpmp" +} + +func (n *NATPMP) IsAvailable(context.Context) (bool, error) { + return false, nil +} + +func (n *NATPMP) IsPortAvailable( + _ context.Context, + _ int, + _ networking.Protocol, +) (bool, error) { + return false, fmt.Errorf("NAT-PMP not implemented") +} + +func (n *NATPMP) IsProtocolAvailable( + _ context.Context, + _ networking.Protocol, +) (bool, error) { + return false, fmt.Errorf("NAT-PMP not implemented") +} + +func (n *NATPMP) ForwardRule( + _ context.Context, + _ networking.Rule, +) (networking.Port, error) { + return networking.Port{}, fmt.Errorf("NAT-PMP not implemented") +} + +func (n *NATPMP) ForwardPort( + _ context.Context, + _ networking.Port, +) (networking.Port, error) { + return networking.Port{}, fmt.Errorf("NAT-PMP not implemented") +} + +func (n *NATPMP) ReversePort( + _ context.Context, + _ networking.Port, +) (networking.Port, error) { + return networking.Port{}, fmt.Errorf("NAT-PMP not implemented") +} + +func (n *NATPMP) GetPortForwardingStatus( + _ context.Context, + _ networking.Port, +) (networking.ForwardingStatus, error) { + return networking.ForwardingStatusError, fmt.Errorf("NAT-PMP not implemented") +} diff --git a/internal/infrastructure/netbridge/operators/operators.go b/internal/infrastructure/netbridge/operators/operators.go index 7fb4dc1..7dee1c1 100644 --- a/internal/infrastructure/netbridge/operators/operators.go +++ b/internal/infrastructure/netbridge/operators/operators.go @@ -3,24 +3,39 @@ package operators import ( "context" + "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators/holepunch" "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators/interfaces" + "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators/natpmp" + "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators/pcp" "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators/upnp" ) type OperatorContainer struct { operators map[string]interfaces.OperatorInterface + order []string } func NewOperatorContainer() *OperatorContainer { operators := &OperatorContainer{ operators: make(map[string]interfaces.OperatorInterface), + order: []string{}, } operators.Add(upnp.NewUPnPOperator()) - + operators.Add(natpmp.NewNATPMP()) + operators.Add(pcp.NewPCP()) + operators.Add(holepunch.NewHolePunch()) + return operators } +func NewEmptyOperatorContainer() *OperatorContainer { + return &OperatorContainer{ + operators: make(map[string]interfaces.OperatorInterface), + order: []string{}, + } +} + func (c *OperatorContainer) All() map[string]interfaces.OperatorInterface { return c.operators } @@ -34,13 +49,27 @@ func (c *OperatorContainer) Get( func (c *OperatorContainer) Add( operator interfaces.OperatorInterface, ) { - c.operators[operator.Name()] = operator + name := operator.Name() + if _, exists := c.operators[name]; exists { + c.operators[name] = operator + return + } + + c.operators[name] = operator + c.order = append(c.order, name) } func (c *OperatorContainer) Obtain( - ctx context.Context, + ctx context.Context, ) interfaces.OperatorInterface { - for _, operator := range c.operators { + return c.ObtainFrom(ctx, nil) +} + +func (c *OperatorContainer) ObtainFrom( + ctx context.Context, + priority []string, +) interfaces.OperatorInterface { + for _, operator := range c.orderedOperators(priority) { available, err := operator.IsAvailable(ctx) if err != nil { continue @@ -53,3 +82,44 @@ func (c *OperatorContainer) Obtain( return nil } + +func (c *OperatorContainer) AvailableNames( + ctx context.Context, +) []string { + available := []string{} + for _, operator := range c.orderedOperators(nil) { + isAvailable, err := operator.IsAvailable(ctx) + if err != nil { + continue + } + + if isAvailable { + available = append(available, operator.Name()) + } + } + + return available +} + +func (c *OperatorContainer) orderedOperators( + priority []string, +) []interfaces.OperatorInterface { + if len(priority) > 0 { + ordered := make([]interfaces.OperatorInterface, 0, len(priority)) + for _, name := range priority { + if operator, ok := c.operators[name]; ok { + ordered = append(ordered, operator) + } + } + return ordered + } + + ordered := make([]interfaces.OperatorInterface, 0, len(c.order)) + for _, name := range c.order { + operator, ok := c.operators[name] + if ok { + ordered = append(ordered, operator) + } + } + return ordered +} diff --git a/internal/infrastructure/netbridge/operators/pcp/pcp.go b/internal/infrastructure/netbridge/operators/pcp/pcp.go new file mode 100644 index 0000000..e7f2aae --- /dev/null +++ b/internal/infrastructure/netbridge/operators/pcp/pcp.go @@ -0,0 +1,66 @@ +package pcp + +import ( + "context" + "fmt" + + "github.com/rabbytesoftware/quiver/internal/infrastructure/netbridge/operators/interfaces" + "github.com/rabbytesoftware/quiver/internal/models/networking" +) + +type PCP struct{} + +func NewPCP() interfaces.OperatorInterface { + return &PCP{} +} + +func (p *PCP) Name() string { + return "pcp" +} + +func (p *PCP) IsAvailable(context.Context) (bool, error) { + return false, nil +} + +func (p *PCP) IsPortAvailable( + _ context.Context, + _ int, + _ networking.Protocol, +) (bool, error) { + return false, fmt.Errorf("PCP not implemented") +} + +func (p *PCP) IsProtocolAvailable( + _ context.Context, + _ networking.Protocol, +) (bool, error) { + return false, fmt.Errorf("PCP not implemented") +} + +func (p *PCP) ForwardRule( + _ context.Context, + _ networking.Rule, +) (networking.Port, error) { + return networking.Port{}, fmt.Errorf("PCP not implemented") +} + +func (p *PCP) ForwardPort( + _ context.Context, + _ networking.Port, +) (networking.Port, error) { + return networking.Port{}, fmt.Errorf("PCP not implemented") +} + +func (p *PCP) ReversePort( + _ context.Context, + _ networking.Port, +) (networking.Port, error) { + return networking.Port{}, fmt.Errorf("PCP not implemented") +} + +func (p *PCP) GetPortForwardingStatus( + _ context.Context, + _ networking.Port, +) (networking.ForwardingStatus, error) { + return networking.ForwardingStatusError, fmt.Errorf("PCP not implemented") +} diff --git a/internal/infrastructure/netbridge/operators/upnp/upnp.go b/internal/infrastructure/netbridge/operators/upnp/upnp.go index 7e6ef20..edbd373 100644 --- a/internal/infrastructure/netbridge/operators/upnp/upnp.go +++ b/internal/infrastructure/netbridge/operators/upnp/upnp.go @@ -75,7 +75,7 @@ func (o *UPnPOperatorImpl) IsPortAvailable( protocol networking.Protocol, ) (bool, error) { address := fmt.Sprintf(":%d", port) - + var network string switch protocol { case networking.ProtocolTCP: @@ -117,19 +117,49 @@ func (o *UPnPOperatorImpl) IsProtocolAvailable( return available, nil } +func (o *UPnPOperatorImpl) PublicIP( + ctx context.Context, +) (string, error) { + if err := o.discoverUPnPDevice(ctx); err != nil { + return "", fmt.Errorf("UPnP not available: %w", err) + } + + ip, err := o.client.GetExternalIPAddress() + if err != nil { + return "", fmt.Errorf("failed to get public IP: %w", err) + } + + return ip, nil +} + +func (o *UPnPOperatorImpl) LocalIP( + ctx context.Context, +) (string, error) { + if err := o.discoverUPnPDevice(ctx); err != nil { + return "", fmt.Errorf("UPnP not available: %w", err) + } + + localIP, err := o.getLocalIP() + if err != nil { + return "", fmt.Errorf("failed to get local IP: %w", err) + } + + return localIP, nil +} + func (o *UPnPOperatorImpl) ForwardRule( ctx context.Context, rule networking.Rule, ) (networking.Port, error) { if !rule.IsValid() { - return networking.Port{}, fmt.Errorf("invalid rule: protocol=%s, status=%s", + return networking.Port{}, fmt.Errorf("invalid rule: protocol=%s, status=%s", rule.Protocol, rule.ForwardingStatus) } port := networking.Port{ ID: uuid.New(), StartPort: 8080, - EndPort: 8080, + EndPort: 8080, Protocol: rule.Protocol, ForwardingStatus: rule.ForwardingStatus, } @@ -146,7 +176,7 @@ func (o *UPnPOperatorImpl) ForwardPort( } if !port.IsStartPortValid() || !port.IsEndPortValid() { - return networking.Port{}, fmt.Errorf("invalid port range: %d-%d", + return networking.Port{}, fmt.Errorf("invalid port range: %d-%d", port.StartPort, port.EndPort) } @@ -163,7 +193,7 @@ func (o *UPnPOperatorImpl) ForwardPort( } description := fmt.Sprintf("Quiver-%s-%d", port.Protocol, port.StartPort) - + err = o.client.AddPortMapping("", uint16(port.StartPort), protocol, uint16(port.EndPort), localIP, true, description, 0) if err != nil { watcher.Warn(err.Error()) @@ -218,9 +248,9 @@ func (o *UPnPOperatorImpl) GetPortForwardingStatus( protocol = "UDP" } - _, _, enabled, _, _, err := + _, _, enabled, _, _, err := o.client.GetSpecificPortMappingEntry("", uint16(port.StartPort), protocol) - + if err != nil { watcher.Debug("Port mapping not found") return networking.ForwardingStatusDisabled, nil @@ -231,7 +261,7 @@ func (o *UPnPOperatorImpl) GetPortForwardingStatus( if enabled { return networking.ForwardingStatusEnabled, nil } - + return networking.ForwardingStatusDisabled, nil }