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.
- is_port (value: int) -> bool
- 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
| value | int | true for 1 to 65535 |
| returns | bool |
For example
is_port(22)→ true 22 is a portis_port(1)→ true 1 is the lowest portis_port(65,535)→ true 65535 is the highest port
fune!(net.port@^1); // then call is_port(…)
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
| port | int | 1 to 65535 |
| protocol | Protocol | |
| returns | PortService |
For example
port_service(22, tcp)→ port 22, protocol tcp, range system, service ssh, description The Secure Shell (SSH) Protocol 22/tcp is sshport_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(…)
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
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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 2,332 |
| data/services.json | 14,062 |
| impl/python/is_port.py | 334 |
| impl/python/port_service.py | 1,261 |
| impl/rust/is_port.rs | 641 |
| impl/rust/port_service.rs | 2,078 |
| impl/typescript/is_port.ts | 351 |
| impl/typescript/port_service.ts | 1,190 |
| vectors.json | 4,830 |