Skip to content
Merged
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
15 changes: 12 additions & 3 deletions command/remote_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,18 @@ func buildRemoteAccessPayload(cfg *config.Config, action string) ([]byte, error)
if cfg.IPv6Address != nil && *cfg.IPv6Address != "" {
ipv6 = *cfg.IPv6Address
}

// Validate any explicitly-provided IPs
if ipv4 != "" && !config.IsValidIPv4(ipv4) {
return nil, fmt.Errorf("invalid IPv4 address: %q", ipv4)
}
if ipv6 != "" && !config.IsValidIPv6(ipv6) {
return nil, fmt.Errorf("invalid IPv6 address: %q", ipv6)
}

if ipv4 == "" && ipv6 == "" {
var err error
ipv4, ipv6, err = cfg.GetMyIPs2()
ipv4, ipv6, err = cfg.GetMyIPs()
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -181,7 +190,7 @@ func remoteAllowCommand(ctx context.Context, cfg *config.Config) *cobra.Command
// Build payload
payload, err := buildRemoteAccessPayload(cfg, "add")
if err != nil {
out.Warn(fmt.Sprintf("Warning: %v. Aborting allow.", err))
out.ErrorFHighlight(" %s %v", "✗", err)
return nil
}

Expand Down Expand Up @@ -241,7 +250,7 @@ func remoteDenyCommand(ctx context.Context, cfg *config.Config) *cobra.Command {
// Build payload with action revoke
payload, err := buildRemoteAccessPayload(cfg, "revoke")
if err != nil {
out.Warn(fmt.Sprintf("Warning: %v. Aborting deny.", err))
out.ErrorFHighlight(" %s %v", "✗", err)
return nil
}

Expand Down
74 changes: 54 additions & 20 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import (
"fmt"
"io"
"net/http"
"net/netip"
"os"
"path/filepath"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -143,24 +143,46 @@ func Dump() ([]byte, error) {
return data, nil
}

func (cfg Config) checkGotIP(ip string) (bool, error) {
return regexp.MatchString(`^\d{1,3}(?:\.\d{1,3}){3}(?:/\d{1,2})?$`, ip)
// IsValidIPv4 validates an IPv4 address (with optional CIDR suffix) using the standard library.
func IsValidIPv4(ip string) bool {
// Try as a CIDR prefix first (e.g. "1.2.3.4/32")
if prefix, err := netip.ParsePrefix(ip); err == nil {
return prefix.Addr().Is4()
}
// Try as a plain address
if addr, err := netip.ParseAddr(ip); err == nil {
return addr.Is4()
}
return false
}

// IsValidIPv6 validates an IPv6 address (with optional CIDR suffix) using the standard library.
func IsValidIPv6(ip string) bool {
// Try as a CIDR prefix first (e.g. "2001:db8::/64")
if prefix, err := netip.ParsePrefix(ip); err == nil {
return prefix.Addr().Is6()
}
// Try as a plain address
if addr, err := netip.ParseAddr(ip); err == nil {
return addr.Is6()
}
return false
}

// GetMyIP returns first IP in: `--ip` flag, `MY_IP` env var, config file, external service
func (cfg Config) GetMyIP() (string, error) {
// flag or config-file used?
if cfg.IPAddress != nil && *cfg.IPAddress != "" {
if isValidIP, err := cfg.checkGotIP(*cfg.IPAddress); err != nil || !isValidIP {
return "", fmt.Errorf("unexpected format for IP (from --ip flag or config-file): %w", err)
if !IsValidIPv4(*cfg.IPAddress) {
return "", fmt.Errorf("unexpected format for IP (from --ip flag or config-file): %q", *cfg.IPAddress)
}
return *cfg.IPAddress, nil
}

// env var used?
if ip := os.Getenv("MY_IP"); ip != "" {
if isValidIP, err := cfg.checkGotIP(ip); err != nil || !isValidIP {
return "", fmt.Errorf("unexpected format for env var MY_IP: %w", err)
if !IsValidIPv4(ip) {
return "", fmt.Errorf("unexpected format for env var MY_IP: %q", ip)
}
return ip, nil
}
Expand Down Expand Up @@ -188,15 +210,16 @@ func (cfg Config) GetMyIP() (string, error) {
return "", err
}

if isValidIP, err := cfg.checkGotIP(string(b)); err != nil || !isValidIP {
return "", fmt.Errorf("unexpected format for IP result from IP service: %w", err)
ip := string(b)
if !IsValidIPv4(ip) {
return "", fmt.Errorf("unexpected format for IP result from IP service: %q", ip)
}

return string(b), nil
return ip, nil
}

// GetMyIPs2 returns both IPv4 and IPv6 addresses, checking config > cli > env, then external service if needed.
func (cfg Config) GetMyIPs2() (ipv4, ipv6 string, err error) {
// GetMyIPs returns both IPv4 and IPv6 addresses, checking config > env, then external service if needed.
func (cfg Config) GetMyIPs() (ipv4, ipv6 string, err error) {
// 1. Check config/env for explicit values
if cfg.IPv4Address != nil && *cfg.IPv4Address != "" {
ipv4 = *cfg.IPv4Address
Expand All @@ -212,38 +235,49 @@ func (cfg Config) GetMyIPs2() (ipv4, ipv6 string, err error) {
ipv6 = ip
}

// 2. If not set, fetch from external service
// 2. Validate any explicitly-provided values
if ipv4 != "" && !IsValidIPv4(ipv4) {
return "", "", fmt.Errorf("invalid IPv4 address from config/env: %q", ipv4)
}
if ipv6 != "" && !IsValidIPv6(ipv6) {
return "", "", fmt.Errorf("invalid IPv6 address from config/env: %q", ipv6)
}

// 3. If not set, fetch from external service
if ipv4 == "" {
req4, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://api.ipify.org", http.NoBody)
res, err4 := httpClient.Do(req4)
if err4 == nil && res.StatusCode == 200 {
b, errRead := io.ReadAll(res.Body)
res.Body.Close()
if errRead == nil {
s := string(b)
if isValidIP, _ := cfg.checkGotIP(s); isValidIP {
s := strings.TrimSpace(string(b))
if IsValidIPv4(s) {
ipv4 = s
}
}
}
}
if ipv6 == "" {
req6, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://api64.ipify.org", http.NoBody)
// api6.ipify.org is an IPv6-only endpoint (AAAA records only).
// If the user has no IPv6 connectivity, the request will fail
// (DNS resolution or connection error), which cleanly indicates
// "no IPv6 available" rather than silently returning an IPv4.
req6, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://api6.ipify.org", http.NoBody)
res, err6 := httpClient.Do(req6)
if err6 == nil && res.StatusCode == 200 {
b, errRead := io.ReadAll(res.Body)
res.Body.Close()
if errRead == nil {
s := string(b)
// TODO: improve IPv6 validation
if s != "" {
s := strings.TrimSpace(string(b))
if IsValidIPv6(s) {
ipv6 = s
}
}
}
}

// 3. Validate at least one IP found
// 4. Validate at least one IP found
if ipv4 == "" && ipv6 == "" {
err = fmt.Errorf("could not determine IPv4 or IPv6 address from config, env, or external service")
}
Expand Down