Files
cameradar/internal/scan/skip/skip.go
T
Brendan Le Glaunec e81eeb0c4d feat: v6 rewrite
2026-01-27 22:11:17 +01:00

339 lines
7.4 KiB
Go

package skip
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"github.com/Ullaakut/cameradar/v6"
)
// Scanner is a stream scanner that skips discovery and treats every target/port as a stream.
type Scanner struct {
targets []string
ports []string
}
// New builds a scanner that skips discovery and treats every target/port as a stream.
func New(targets, ports []string) *Scanner {
return &Scanner{
targets: targets,
ports: ports,
}
}
// Scan returns the precomputed list of streams.
func (s *Scanner) Scan(ctx context.Context) ([]cameradar.Stream, error) {
return buildStreamsFromTargets(ctx, s.targets, s.ports)
}
func buildStreamsFromTargets(ctx context.Context, targets, ports []string) ([]cameradar.Stream, error) {
resolvedPorts, err := parsePorts(ctx, ports)
if err != nil {
return nil, err
}
if len(resolvedPorts) == 0 {
return nil, errors.New("no valid ports provided")
}
resolvedTargets, err := expandTargets(ctx, targets)
if err != nil {
return nil, err
}
if len(resolvedTargets) == 0 {
return nil, errors.New("no valid target addresses resolved")
}
streams := make([]cameradar.Stream, 0, len(resolvedTargets)*len(resolvedPorts))
for _, addr := range resolvedTargets {
for _, port := range resolvedPorts {
streams = append(streams, cameradar.Stream{
Address: addr,
Port: port,
})
}
}
return streams, nil
}
func parsePorts(ctx context.Context, ports []string) ([]uint16, error) {
seen := make(map[uint16]struct{})
resolved := make([]uint16, 0, len(ports))
for _, entry := range ports {
for raw := range strings.SplitSeq(entry, ",") {
value := strings.TrimSpace(raw)
if value == "" {
continue
}
values, err := parsePortValue(ctx, value)
if err != nil {
return nil, err
}
for _, port := range values {
if _, exists := seen[port]; exists {
continue
}
seen[port] = struct{}{}
resolved = append(resolved, port)
}
}
}
return resolved, nil
}
func parsePortValue(ctx context.Context, value string) ([]uint16, error) {
if strings.Contains(value, "-") {
parts := strings.SplitN(value, "-", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid port range %q", value)
}
start, err := parsePortNumber(strings.TrimSpace(parts[0]))
if err != nil {
return nil, fmt.Errorf("invalid port range %q: %w", value, err)
}
end, err := parsePortNumber(strings.TrimSpace(parts[1]))
if err != nil {
return nil, fmt.Errorf("invalid port range %q: %w", value, err)
}
if start > end {
return nil, fmt.Errorf("invalid port range %q", value)
}
ports := make([]uint16, 0, end-start+1)
for port := start; port <= end; port++ {
ports = append(ports, port)
}
return ports, nil
}
port, err := parsePortNumber(value)
if err == nil {
return []uint16{port}, nil
}
servicePort, lookupErr := net.DefaultResolver.LookupPort(ctx, "tcp", value)
if lookupErr != nil {
return nil, fmt.Errorf("invalid port %q", value)
}
if servicePort < 1 || servicePort > 65535 {
return nil, fmt.Errorf("port %d out of range", servicePort)
}
return []uint16{uint16(servicePort)}, nil
}
func parsePortNumber(value string) (uint16, error) {
port, err := strconv.Atoi(value)
if err != nil {
return 0, err
}
if port < 1 || port > 65535 {
return 0, fmt.Errorf("port %d out of range", port)
}
return uint16(port), nil
}
func expandTargets(ctx context.Context, targets []string) ([]netip.Addr, error) {
seen := make(map[netip.Addr]struct{})
resolved := make([]netip.Addr, 0, len(targets))
for _, target := range targets {
value := strings.TrimSpace(target)
if value == "" {
continue
}
addrs, err := parseTargetAddrs(ctx, value)
if err != nil {
return nil, err
}
for _, addr := range addrs {
if !addr.IsValid() {
continue
}
if _, exists := seen[addr]; exists {
continue
}
seen[addr] = struct{}{}
resolved = append(resolved, addr)
}
}
return resolved, nil
}
func parseTargetAddrs(ctx context.Context, target string) ([]netip.Addr, error) {
prefix, err := netip.ParsePrefix(target)
if err == nil { // Return early.
return expandPrefix(prefix), nil
}
if strings.Contains(target, "-") {
addrs, ok, err := parseIPv4Range(target)
if ok {
return addrs, err
}
}
addr, err := netip.ParseAddr(target)
if err == nil { // Return early.
return []netip.Addr{addr}, nil
}
ips, err := net.DefaultResolver.LookupIPAddr(ctx, target)
if err != nil {
return nil, fmt.Errorf("resolving hostname %q: %w", target, err)
}
addrs := make([]netip.Addr, 0, len(ips))
for _, ip := range ips {
addr, ok := netip.AddrFromSlice(ip.IP)
if !ok {
continue
}
addrs = append(addrs, addr.Unmap())
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no ip addresses found for hostname %q", target)
}
return addrs, nil
}
func expandPrefix(prefix netip.Prefix) []netip.Addr {
if !prefix.IsValid() {
return nil
}
prefix = prefix.Masked()
addr := prefix.Addr()
addrs := make([]netip.Addr, 0, 16)
for current := addr; prefix.Contains(current); {
addrs = append(addrs, current)
next := current.Next()
if !next.IsValid() {
break
}
current = next
}
return addrs
}
type octetRange struct {
start int
end int
}
func parseIPv4Range(target string) ([]netip.Addr, bool, error) {
parts := strings.Split(target, ".")
if len(parts) != 4 {
return nil, false, nil
}
ranges := make([]octetRange, 4)
for i, part := range parts {
parsed, ok, err := parseOctetRange(part)
if err != nil {
return nil, true, err
}
if !ok {
return nil, false, nil
}
ranges[i] = parsed
}
addrs := make([]netip.Addr, 0, 16)
for first := ranges[0].start; first <= ranges[0].end; first++ {
for second := ranges[1].start; second <= ranges[1].end; second++ {
for third := ranges[2].start; third <= ranges[2].end; third++ {
for fourth := ranges[3].start; fourth <= ranges[3].end; fourth++ {
addrs = append(addrs, netip.AddrFrom4([4]byte{
byte(first),
byte(second),
byte(third),
byte(fourth),
}))
}
}
}
}
return addrs, true, nil
}
func parseOctetRange(value string) (octetRange, bool, error) {
value = strings.TrimSpace(value)
if value == "" {
return octetRange{}, false, nil
}
if strings.Contains(value, "-") {
parts := strings.SplitN(value, "-", 2)
if len(parts) != 2 {
return octetRange{}, true, fmt.Errorf("invalid range %q", value)
}
start, err := parseOctetValue(strings.TrimSpace(parts[0]))
if err != nil {
return octetRange{}, true, err
}
end, err := parseOctetValue(strings.TrimSpace(parts[1]))
if err != nil {
return octetRange{}, true, err
}
if start > end {
return octetRange{}, true, fmt.Errorf("invalid range %q", value)
}
return octetRange{start: start, end: end}, true, nil
}
if !isDigits(value) {
return octetRange{}, false, nil
}
octet, err := parseOctetValue(value)
if err != nil {
return octetRange{}, true, err
}
return octetRange{start: octet, end: octet}, true, nil
}
func parseOctetValue(value string) (int, error) {
if !isDigits(value) {
return 0, fmt.Errorf("invalid octet %q", value)
}
parsed, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("invalid octet %q", value)
}
if parsed < 0 || parsed > 255 {
return 0, fmt.Errorf("octet %d out of range", parsed)
}
return parsed, nil
}
func isDigits(value string) bool {
for _, r := range value {
if r < '0' || r > '9' {
return false
}
}
return value != ""
}