Skip to content

Commit 207573a

Browse files
authored
fix the silent fallback to random ports issue when invalid,privileged or already in use ports specified (#3176)
Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com>
1 parent fc9b5a8 commit 207573a

3 files changed

Lines changed: 119 additions & 7 deletions

File tree

cmd/run.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,34 @@ Or if you have an existing function:
252252
// configured to build/run the language of the function.
253253
job, err := client.Run(cmd.Context(), f, fn.RunWithAddress(cfg.Address))
254254
if err != nil {
255+
// Catch port unavailable errors and provide helpful CLI guidance
256+
var portErr *fn.ErrPortUnavailableError
257+
if errors.As(err, &portErr) {
258+
if portErr.IsPermissionDenied() {
259+
return fmt.Errorf(`cannot choose port
260+
261+
Cannot bind to port %s: permission denied.
262+
263+
Port %s is a privileged port and requires administrator/root permissions.
264+
265+
Try this:
266+
sudo func run --address %s Run with elevated permissions
267+
func run --address 127.0.0.1:8080 Use non-privileged port
268+
269+
For more options, run 'func run --help'`, portErr.Port, portErr.Port, cfg.Address)
270+
}
271+
return fmt.Errorf(`cannot choose port
272+
273+
Port %s is not available.
274+
275+
The port may be in use by another process, or you may not have permission to bind to it.
276+
277+
Try this:
278+
func run --address 127.0.0.1:8080 Use a different port
279+
lsof -i :%s Check if port is in use (Linux/Mac)
280+
281+
For more options, run 'func run --help'`, portErr.Port, portErr.Port)
282+
}
255283
return
256284
}
257285
defer func() {
@@ -388,6 +416,50 @@ func (c runConfig) Validate(cmd *cobra.Command, f fn.Function) (err error) {
388416
}
389417
}
390418

419+
// Validate address port if provided
420+
if c.Address != "" {
421+
host, port, err := net.SplitHostPort(c.Address)
422+
if err != nil {
423+
// Invalid address format will be caught by runner
424+
return nil // Let runner handle address format errors
425+
}
426+
427+
// Warn about port-only addresses (missing host)
428+
if host == "" {
429+
return fmt.Errorf(`invalid address format '%s': address must include both host and port
430+
431+
Address format: host:port
432+
433+
Examples:
434+
127.0.0.1:8080 Localhost only
435+
0.0.0.0:8080 All interfaces (IPv4)
436+
[::]:8080 All interfaces (IPv6)
437+
438+
For more options, run 'func run --help'`, c.Address)
439+
}
440+
441+
// Validate port range (1-65535)
442+
portNum, err := strconv.Atoi(port)
443+
if err != nil {
444+
return fmt.Errorf(`invalid port '%s': port must be a number between 1 and 65535
445+
446+
Examples:
447+
func run --address 127.0.0.1:8080
448+
func run --address 0.0.0.0:9090
449+
450+
For more options, run 'func run --help'`, port)
451+
}
452+
if portNum < 1 || portNum > 65535 {
453+
return fmt.Errorf(`invalid port '%d': port must be between 1 and 65535
454+
455+
Examples:
456+
func run --address 127.0.0.1:8080
457+
func run --address 0.0.0.0:9090
458+
459+
For more options, run 'func run --help'`, portNum)
460+
}
461+
}
462+
391463
if f.Build.Builder == "host" && !oci.IsSupported(f.Runtime) {
392464
return fmt.Errorf("the %q runtime currently requires being run in a container", f.Runtime)
393465
}

pkg/functions/errors.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package functions
33
import (
44
"errors"
55
"fmt"
6+
"strings"
67
"time"
78
)
89

@@ -90,3 +91,31 @@ type ErrEnvNotExist struct {
9091
func (e ErrEnvNotExist) Error() string {
9192
return fmt.Sprintf("environment variable %q does not exist", e.Name)
9293
}
94+
95+
// ErrPortUnavailableError indicates that a port cannot be bound
96+
type ErrPortUnavailableError struct {
97+
Port string
98+
Err error
99+
}
100+
101+
func (e *ErrPortUnavailableError) Error() string {
102+
if e.Err != nil {
103+
return fmt.Sprintf("port %s is not available: %v", e.Port, e.Err)
104+
}
105+
return fmt.Sprintf("port %s is not available", e.Port)
106+
}
107+
108+
func (e *ErrPortUnavailableError) Unwrap() error {
109+
return e.Err
110+
}
111+
112+
// IsPermissionDenied checks if the underlying error is a permission error
113+
func (e *ErrPortUnavailableError) IsPermissionDenied() bool {
114+
if e.Err == nil {
115+
return false
116+
}
117+
errStr := strings.ToLower(e.Err.Error())
118+
return strings.Contains(errStr, "permission denied") ||
119+
strings.Contains(errStr, "access denied") ||
120+
strings.Contains(errStr, "operation not permitted")
121+
}

pkg/functions/runner.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func (r *defaultRunner) Run(ctx context.Context, f Function, address string, sta
4545
// Parse address if provided, otherwise use defaults
4646
host := defaultRunHost
4747
port := defaultRunPort
48+
explicitPort := address != ""
4849

4950
if address != "" {
5051
var err error
@@ -54,7 +55,7 @@ func (r *defaultRunner) Run(ctx context.Context, f Function, address string, sta
5455
}
5556
}
5657

57-
port, err = choosePort(host, port)
58+
port, err = choosePort(host, port, explicitPort)
5859
if err != nil {
5960
return nil, fmt.Errorf("cannot choose port: %w", err)
6061
}
@@ -332,25 +333,35 @@ func isReady(ctx context.Context, uri string, timeout time.Duration, verbose boo
332333
return true, nil
333334
}
334335

335-
// choosePort returns an unused port on the given interface (host)
336-
// Note this is not fool-proof becase of a race with any other processes
336+
// choosePort returns an unused port on the given interface (host).
337+
// If explicitPort is true and the preferred port cannot be bound, an error is returned.
338+
// If explicitPort is false (default port), it falls back to an OS-chosen port if the preferred port is unavailable.
339+
// Note this is not fool-proof because of a race with any other processes
337340
// looking for a port at the same time. If that is important, we can implement
338341
// a check-lock-check via the filesystem.
339342
// Also note that TCP is presumed.
340-
func choosePort(iface, preferredPort string) (string, error) {
343+
func choosePort(iface, preferredPort string, explicitPort bool) (string, error) {
341344
var (
342345
port = preferredPort
343346
l net.Listener
344347
err error
345348
)
346349

347-
// Try preferred
350+
// Try preferred port
348351
if l, err = net.Listen("tcp", net.JoinHostPort(iface, port)); err == nil {
349-
l.Close() // note err==nil
352+
l.Close()
350353
return port, nil
351354
}
352355

353-
// OS-chosen
356+
// If user explicitly provided a port and it's unavailable, return typed error
357+
if explicitPort {
358+
return "", &ErrPortUnavailableError{
359+
Port: port,
360+
Err: err,
361+
}
362+
}
363+
364+
// For default ports, fall back to OS-chosen port
354365
if l, err = net.Listen("tcp", net.JoinHostPort(iface, "")); err != nil {
355366
return "", fmt.Errorf("cannot bind tcp: %w", err)
356367
}

0 commit comments

Comments
 (0)