Functional Weave
Code in Rust

net.port

Check a TCP/UDP port number and name the IANA service registered on it, with its system/user/dynamic range.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 30 tests, run in TypeScript, Python and Rust.isPort 9 · portService 21

What it does

Two functions for TCP and UDP port numbers:

- `isPort(value)` answers whether a number is a port you can connect to: a whole number from 1 to 65535. It never throws, so it is the check to use when validating input. - `portService(port, protocol)` says which RFC 6335 range the port is in and which service IANA registered on it for `tcp` or `udp`. It throws on a port `isPort` refuses or an unknown protocol.

The functions

A group: 2 functions that work together, each in its own file, each pinned by its own tests in TypeScript, Python and Rust. A project can install only the ones it calls.

  1. is_port (value: int) -> bool
  2. port_service (port: int, protocol: Protocol) -> PortService

The types it declares, generated into your project

// Protocol is a string in Rust, one of: "tcp", "udp".
// Parameters take it as &str and results hold it as String.

// PortRange is a string in Rust, one of: "system", "user", "dynamic".
// Parameters take it as &str and results hold it as String.

/// A port, the range RFC 6335 puts it in, and the service registered on it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PortService {
    pub port: i64,
    pub protocol: String,
    pub range: String,
    /// IANA service name, null when this registry has none for it
    pub service: Option<String>,
    pub description: Option<String>,
}

Once installed, your code imports each one from the group's module.

is_port 9 tests

pub fn is_port(value: i64) -> bool
valueinttrue for 1 to 65535
returnsbool

For example

  • is_port(22) → true 22 is a port
  • is_port(1) → true 1 is the lowest port
  • is_port(65,535) → true 65535 is the highest port
fune!(net.port@^1);  // then call is_port(…)
impl/rust/is_port.rs · 17 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// Whether a number is a usable TCP or UDP port: a whole number from 1 to
/// 65535. Port 0 is reserved (RFC 6335) and asks the operating system to pick
/// one, so it is never a port to connect to.
pub fn is_port(value: i64) -> bool {
    (1..=65535).contains(&value)
}

pub fn fune_vector(args: &[Value]) -> Value {
    // A fraction or a non-number is not a port, as TypeScript and Python answer.
    match &args[0] {
        Value::Int(i) => Value::Bool(is_port(*i)),
        Value::Float(f) if f.fract() == 0.0 && f.abs() < 1e15 => Value::Bool(is_port(*f as i64)),
        _ => Value::Bool(false),
    }
}

port_service throws on bad input 21 tests

pub fn port_service(port: i64, protocol: &str) -> PortService
portint1 to 65535
protocolProtocol
returnsPortService

For example

  • port_service(22, tcp) → port 22, protocol tcp, range system, service ssh, description The Secure Shell (SSH) Protocol 22/tcp is ssh
  • port_service(53, udp) → port 53, protocol udp, range system, service domain, description Domain Name Server 53/udp is domain (DNS)
  • port_service(80, tcp) → port 80, protocol tcp, range system, service http, description World Wide Web HTTP 80/tcp is http, the primary of IANA's three names for it
fune!(net.port@^1);  // then call port_service(…)
impl/rust/port_service.rs · 58 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::net_port_data::SERVICES;  ← this capability’s own data, compiled from data/services.json into the same file by fune build
use super::net_port_is_port::is_port;  ← isPort, another function of this group · built into the same file, even by a slim install

/// The RFC 6335 range a port is in, and the IANA service registered on it for
/// a protocol. A port with nothing in this registry's table has a null service:
/// unassigned, or assigned but not common enough to ship here.
///
/// # Panics
/// Panics on a port outside 1..=65535 or a protocol other than tcp or udp.
pub fn port_service(port: i64, protocol: &str) -> PortService {
    if !is_port(port) {
        panic!("port must be 1 to 65535, received {}", port);
    }
    if protocol != "tcp" && protocol != "udp" {
        panic!("protocol must be tcp or udp, received \"{}\"", protocol);
    }
    // RFC 6335 section 6: system 0-1023, user 1024-49151, dynamic 49152-65535.
    let range = if port <= 1023 {
        "system"
    } else if port <= 49151 {
        "user"
    } else {
        "dynamic"
    };
    let row = SERVICES.iter().find(|s| s.port == port && s.protocol == protocol);
    PortService {
        port,
        protocol: protocol.to_string(),
        range: range.to_string(),
        service: row.map(|r| r.service.to_string()),
        description: row.map(|r| r.description.to_string()),
    }
}

/// Object keys are camelCase to match the shared vectors.
pub fn port_service_to_value(result: &PortService) -> Value {
    let text = |field: &Option<String>| match field {
        Some(s) => Value::str(s),
        None => Value::Null,
    };
    Value::obj(vec![
        ("port", Value::Int(result.port)),
        ("protocol", Value::str(&result.protocol)),
        ("range", Value::str(&result.range)),
        ("service", text(&result.service)),
        ("description", text(&result.description)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    // Refuse what an i64 cannot hold, with the wording TypeScript and Python use.
    let port = match &args[0] {
        Value::Int(i) => *i,
        other => panic!("port must be an integer, received {}", other),
    };
    port_service_to_value(&port_service(port, args[1].as_str()))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add net.port

That builds the whole group. To build only what you call, and whatever it uses inside the group:

fune add net.port --only isPort
Download for Rust net.port-1.0.0-rust.fune · 30,532 bytes sha256 0bc5e77a8036597ce2184a3238e938b1b264d4447baa9454afcada383df3bf80

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./net.port-1.0.0-rust.fune, or fetch it from a terminal with fune pull net.port@1.0.0:rust.

The whole function, every language, is one file too: net.port-1.0.0.fune, 33,904 bytes, sha256 b4abf0a4585a74e0ed2cb54fce546dc137d24a263d490d0705b908921f46fd33. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before net.port.isPort
// fune: before net.port.portService

after — your function gets the result and the arguments, and returns the final result.

// fune: after net.port.isPort
// fune: after net.port.portService

replace — it requires no other capability, so there is no dependency to replace.

step — your function runs at a numbered point inside a function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show net.port --steps.

// fune: step net.port.<fn> after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

isPort 9 tests

CaseArgumentsExpected
22 is a port 22 → true
1 is the lowest port 1 → true
65535 is the highest port 65,535 → true
0 is reserved, not a port to connect to 0 → false
65536 does not fit in 16 bits 65,536 → false
negative numbers are not ports -80 → false
a fraction is not a port 80.5 → false
a string of digits is not a number 80 → false
null is not a port — → false

portService 21 tests

CaseArgumentsExpected
22/tcp is ssh 22, tcp → port 22, protocol tcp, range system, service ssh, description The Secure Shell (SSH) Protocol
53/udp is domain (DNS) 53, udp → port 53, protocol udp, range system, service domain, description Domain Name Server
80/tcp is http, the primary of IANA's three names for it 80, tcp → port 80, protocol tcp, range system, service http, description World Wide Web HTTP
443/tcp is https 443, tcp → port 443, protocol tcp, range system, service https, description http protocol over TLS/SSL
1023 is the last system port 1,023, tcp → port 1,023, protocol tcp, range system, service —, description —
1024 is the first user port, and is reserved 1,024, tcp → port 1,024, protocol tcp, range user, service —, description —
5432/tcp is postgresql 5,432, tcp → port 5,432, protocol tcp, range user, service postgresql, description PostgreSQL Database
5433/tcp is registered to Pyrrho, not a second PostgreSQL 5,433, tcp → port 5,433, protocol tcp, range user, service pyrrho, description Pyrrho DBMS
6379/tcp is redis 6,379, tcp → port 6,379, protocol tcp, range user, service redis, description An advanced key-value cache and store
6379/udp is reserved, so no service 6,379, udp → port 6,379, protocol udp, range user, service —, description —
Show the other 11 tests
CaseArgumentsExpected
4874 is unassigned (IANA 4872-4875) 4,874, tcp → port 4,874, protocol tcp, range user, service —, description —
8080/tcp is http-alt 8,080, tcp → port 8,080, protocol tcp, range user, service http-alt, description HTTP Alternate (see port 80)
465/tcp is submissions (RFC 8314), not the retired urd 465, tcp → port 465, protocol tcp, range system, service submissions, description Message Submission over TLS protocol
49151 is the last user port 49,151, udp → port 49,151, protocol udp, range user, service —, description —
49152 is the first dynamic port 49,152, tcp → port 49,152, protocol tcp, range dynamic, service —, description —
65535 is dynamic 65,535, udp → port 65,535, protocol udp, range dynamic, service —, description —
port 0 is refused 0, tcp → error: port must be 1 to 65535, received 0
port 65536 is refused 65,536, tcp → error: port must be 1 to 65535, received 65536
a fractional port is refused 22.5, tcp → error: port must be an integer
a protocol other than tcp or udp is refused 22, sctp → error: protocol must be tcp or udp, received "sctp"
protocol is case-sensitive 22, TCP → error: protocol must be tcp or udp, received "TCP"

More from the author

They are a group because `portService` is only defined on what `isPort` accepts; a caller validating an inventory wants the first, one labelling a report wants the second.

## Ranges (RFC 6335 section 6)

| range | ports | | |-------|-------|--| | `system` | 0-1023 | assigned by IETF review; binding needs privilege on Unix | | `user` | 1024-49151 | assigned by IANA on request | | `dynamic` | 49152-65535 | never assigned; ephemeral client ports |

Port 0 is reserved and means "let the OS pick", so `isPort(0)` is false.

## The service table

`data/services.json` holds 144 rows (76 ports, tcp and udp) copied verbatim from the IANA Service Name and Transport Protocol Port Number Registry: the service name and description exactly as IANA writes them, including its capitalisation. It is a subset: the ports an operations team meets (ssh, dns, http/https, mail, directory, databases, message brokers, remote access, VPN). A port not in the table has `service: null`, which means "not in this table", not necessarily "unassigned".

Choices where IANA lists more than one name on a port/protocol:

- 80: `http` (IANA also lists `www` and `www-http` as aliases). - 465/tcp: `submissions` (RFC 8314), not the historical `urd`. - 2049: `nfs`, not `shilp`. - Everywhere else, the first name IANA lists. Rows IANA marks `Reserved` (e.g. 6379/udp, 993/udp) are left out, so they have no service.

Notable answers: 5433 is registered to `pyrrho` (Pyrrho DBMS), not to a second PostgreSQL, however often Postgres is run there; 4874 is unassigned (IANA lists 4872-4875 as Unassigned).

Source: IANA, "Service Name and Transport Protocol Port Number Registry", https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.csv, fetched 2026-09-26. RFC 6335 (Procedures for the Management of the Service Name and Transport Protocol Port Number Registry), section 6, for the ranges.

Files

PathBytes
README.md2,332
data/services.json14,062
impl/python/is_port.py334
impl/python/port_service.py1,261
impl/rust/is_port.rs641
impl/rust/port_service.rs2,078
impl/typescript/is_port.ts351
impl/typescript/port_service.ts1,190
vectors.json4,830