Skip to content

Instantly share code, notes, and snippets.

@paigeadelethompson
Last active July 28, 2026 21:18
Show Gist options
  • Select an option

  • Save paigeadelethompson/990007c39f27a76fa9733fa9fdc96be6 to your computer and use it in GitHub Desktop.

Select an option

Save paigeadelethompson/990007c39f27a76fa9733fa9fdc96be6 to your computer and use it in GitHub Desktop.
diff --git a/share/man/man4/Makefile b/share/man/man4/Makefile
index 65e59368a8e..e0129373093 100644
--- a/share/man/man4/Makefile
+++ b/share/man/man4/Makefile
@@ -398,6 +398,12 @@ MAN= aac.4 \
ng_l2tp.4 \
ng_lmi.4 \
ng_macfilter.4 \
+ ng_mpls.4 \
+ ng_mpls_fec.4 \
+ ng_mpls_lsp.4 \
+ ng_mpls_pw.4 \
+ ng_mpls_vpls.4 \
+ ng_mpls_vrf.4 \
ng_mppc.4 \
ng_nat.4 \
ng_netflow.4 \
diff --git a/share/man/man4/ng_mpls.4 b/share/man/man4/ng_mpls.4
new file mode 100644
index 00000000000..88ad4422670
--- /dev/null
+++ b/share/man/man4/ng_mpls.4
@@ -0,0 +1,173 @@
+.\" Copyright (c) 2026 The FreeBSD Foundation
+.\" All rights reserved.
+.\"
+.Dd July 28, 2026
+.Dt NG_MPLS 4
+.Os
+.Sh NAME
+.Nm ng_mpls
+.Nd MPLS label switching router netgraph node type
+.Sh SYNOPSIS
+.In netgraph/ng_mpls.h
+.Sh DESCRIPTION
+The
+.Nm mpls
+node type implements an MPLS label switching router (LSR) as described
+in RFC 3031 and RFC 3032.
+It receives MPLS-encapsulated packets on the
+.Dq downstream
+hook, looks up the top label in the NHLFE (Next Hop Label Forwarding Entry)
+table, and performs the configured label operation.
+.Pp
+The following label operations are supported:
+.Bl -tag -width ".Dv NG_MPLS_ACTION_SWAP_AND_PUSH"
+.It Dv NG_MPLS_ACTION_SWAP
+Replace the top label with a new label and forward to the output hook.
+.It Dv NG_MPLS_ACTION_POP
+Remove the top label and forward the inner packet.
+.It Dv NG_MPLS_ACTION_PHP
+Penultimate Hop Popping: remove the top label, check the S-bit.
+If it was the bottom of stack, extract the inner IP packet; otherwise
+forward the remaining label stack.
+.It Dv NG_MPLS_ACTION_SWAP_AND_PUSH
+Replace the top label and then push additional labels.
+.Pp
+.El
+The node walks the full MPLS label stack from outer to inner, using
+the S-bit (bottom-of-stack indicator) to detect the inner boundary.
+.Pp
+Reserved label values are handled according to RFC 3031:
+Pq Dv MPLS_LABEL_IPV4_EXPLICIT_NULL , MPLS_LABEL_IPV6_EXPLICIT_NULL ,
+.Dv MPLS_LABEL_IMPLICIT_NULL ,
+.Dv MPLS_LABEL_ROUTER_ALERT ,
+.Dv MPLS_LABEL_OAM_ALERT .
+.Ss Label stack encoding
+The push label array in NHLFE entries is indexed innermost-first:
+.Sq Li push_labels[0]
+carries the bottom-of-stack label (S=1),
+.Sq Li push_labels[N-1]
+the outermost (S=0).
+The node applies labels by iterating forward and calling
+.Fn M_PREPEND
+for each, which produces the correct wire order.
+.Ss TTL handling
+Two TTL modes are supported:
+.Bl -tag -width ".Dv NG_MPLS_TTL_UNIFORM"
+.It Dv NG_MPLS_TTL_UNIFORM
+Copy TTL from the incoming label to the outgoing label
+(pipe model variant).
+.It Dv NG_MPLS_TTL_PIPE
+Use a fixed TTL for outgoing labels.
+.El
+.Sh HOOKS
+This node type supports the following hooks:
+.Bl -tag -width indent
+.It Va downstream
+Receives MPLS-encapsulated packets from the network.
+.It Va nomatch
+Receives packets whose incoming label did not match any NHLFE entry.
+.It Va hook Ns Ar N
+Dynamically created output hooks, one per NHLFE entry.
+Naming is automatic; each NHLFE entry specifies its output hook name.
+.El
+.Sh CONTROL MESSAGES
+This node type supports the generic netgraph control messages,
+plus the following:
+.Bl -tag -width indent
+.It Dv NGM_MPLS_ADD_NHLFE Pq Ic add_nhlfe
+Add an NHLFE entry with the following structure:
+.Bd -literal -offset 0n
+struct ng_mpls_label {
+ char hook_name[NG_HOOKSIZ]; /* output hook name */
+ uint32_t label; /* incoming label */
+ uint32_t action; /* NG_MPLS_ACTION_* */
+ uint32_t swap_label; /* for SWAP/SWAP_AND_PUSH */
+ uint8_t exp; /* EXP bits */
+ uint8_t ttl; /* TTL value/initial */
+ uint8_t ttl_handling; /* NG_MPLS_TTL_* */
+ uint8_t push_count; /* labels to push */
+ uint32_t push_labels[8]; /* push stack (innermost first) */
+};
+.Ed
+.Pp
+The
+.Va label
+field identifies the incoming (top) label to match.
+The
+.Va action
+field must be one of
+.Dv NG_MPLS_ACTION_SWAP ,
+.Dv NG_MPLS_ACTION_POP ,
+.Dv NG_MPLS_ACTION_PHP ,
+or
+.Dv NG_MPLS_ACTION_SWAP_AND_PUSH .
+For
+.Dv NG_MPLS_ACTION_SWAP_AND_PUSH ,
+.Va push_count
+indicates how many labels to push (up to 8).
+.Pp
+.It Dv NGM_MPLS_DEL_NHLFE Pq Ic del_nhlfe
+Remove an NHLFE entry.
+The argument is a 32-bit incoming label value.
+.It Dv NGM_MPLS_GET_NHLFE_TABLE Pq Ic get_nhlfe_table
+Returns the NHLFE table:
+.Bd -literal -offset 0n
+struct ng_mpls_nhlfe_table {
+ uint32_t n;
+ struct ng_mpls_nhlfe_entry entry[];
+};
+
+struct ng_mpls_nhlfe_entry {
+ char hook_name[NG_HOOKSIZ];
+ uint32_t action;
+ uint32_t swap_label;
+ uint8_t push_count;
+ uint32_t push_labels[8];
+ uint64_t packets;
+ uint64_t bytes;
+};
+.Ed
+.It Dv NGM_MPLS_GET_STATS Pq Ic get_stats
+Returns aggregate statistics:
+.Bd -literal -offset 0n
+struct ng_mpls_stats {
+ uint64_t packets_in;
+ uint64_t bytes_in;
+ uint64_t packets_out;
+ uint64_t bytes_out;
+ uint64_t packets_dropped;
+ uint64_t packets_nomatch;
+};
+.Ed
+.It Dv NGM_MPLS_CLR_STATS Pq Ic clr_stats
+Clear all statistics counters.
+.El
+.Sh SHUTDOWN
+This node shuts down upon receipt of a
+.Dv NGM_SHUTDOWN
+control message or when all hooks have been disconnected.
+The NHLFE table and label table are freed during shutdown.
+.Sh EXAMPLES
+Load the module and create a swap NHLFE entry for label 100:
+.Bd -literal -offset indent
+kldload ng_mpls
+ngctl mkpeer mpls lwr downstream downstream
+ngctl msg mpls: add_nhlfe { hook="lwr" label=100 action=2 \\
+ swap_label=200 }
+.Ed
+.Sh SEE ALSO
+.Xr netgraph 4 ,
+.Xr ng_mpls_fec 4 ,
+.Xr ng_mpls_lsp 4 ,
+.Xr ng_mpls_pw 4 ,
+.Xr ng_mpls_vpls 4 ,
+.Xr ng_mpls_vrf 4 ,
+.Xr ngctl 8
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+node type was written as part of the FreeBSD MPLS stack.
+.Sh BUGS
+The node does not currently implement ECN or entropy label
+(RFC 6790) support.
diff --git a/share/man/man4/ng_mpls_fec.4 b/share/man/man4/ng_mpls_fec.4
new file mode 100644
index 00000000000..3f0e4353648
--- /dev/null
+++ b/share/man/man4/ng_mpls_fec.4
@@ -0,0 +1,152 @@
+.\" Copyright (c) 2026 The FreeBSD Foundation
+.\" All rights reserved.
+.\"
+.Dd July 28, 2026
+.Dt NG_MPLS_FEC 4
+.Os
+.Sh NAME
+.Nm ng_mpls_fec
+.Nd MPLS forwarding equivalence class classification netgraph node type
+.Sh SYNOPSIS
+.In netgraph/ng_mpls_fec.h
+.Sh DESCRIPTION
+The
+.Nm mpls_fec
+node type implements an ingress LER (Label Edge Router) FEC classifier.
+It receives IP packets (IPv4 or IPv6) on the
+.Dq downstream
+hook, performs a longest-prefix-match (LPM) lookup against the FEC table,
+pushes the configured MPLS label stack, and forwards the labeled packet to
+the
+.Dq mpls
+hook.
+.Pp
+Packets that do not match any FEC entry are forwarded to the
+.Dq nomatch
+hook for alternate processing (e.g., normal IP forwarding through the
+kernel's FIB).
+.Pp
+The push label stack is ordered innermost first:
+.Sq Li push_labels[0]
+is the bottom of stack (S=1), and the node pushes labels in forward index
+order so that the last element in the array becomes the outer label on the
+wire.
+.Pp
+IPv6 FEC entries use the same structure;
+.Va family
+is set to
+.Dv AF_INET6
+and
+.Va prefix
+holds the 32-bit IPv4 address or the first 32 bits of an IPv6 address
+(in network byte order) when the prefix length allows representation in a
+single 32-bit field; for full IPv6 support the VRF node provides longer
+prefixes.
+.Sh HOOKS
+This node type supports the following hooks:
+.Bl -tag -width indent
+.It Va downstream
+Receives raw IP packets (IPv4 or IPv6) for classification.
+.It Va mpls
+Outputs MPLS-encapsulated packets to the MPLS domain.
+Typically connected to the
+.Va downstream
+hook of an
+.Xr ng_mpls 4
+LSR node.
+.It Va nomatch
+Receives unclassified IP packets.
+.El
+.Sh CONTROL MESSAGES
+This node type supports the generic netgraph control messages,
+plus the following:
+.Bl -tag -width indent
+.It Dv NGM_MPLS_FEC_ADD_FEC Pq Ic add_fec
+Add a FEC entry with the following structure:
+.Bd -literal -offset 0n
+struct ng_mpls_fec_entry {
+ char hook_name[NG_HOOKSIZ]; /* output hook (mpls:) */
+ uint8_t family; /* AF_INET or AF_INET6 */
+ uint32_t prefix; /* IP prefix (network order) */
+ uint8_t prefix_len; /* prefix length in bits */
+ uint8_t push_count; /* labels to push */
+ uint32_t push_labels[8]; /* push stack (innermost first) */
+ uint8_t ttl; /* TTL value for pushed labels */
+};
+.Ed
+.Pp
+The
+.Va family
+field selects the address family for LPM matching.
+The
+.Va prefix
+and
+.Va prefix_len
+define the destination prefix.
+.Va hook_name
+specifies the output hook (typically
+.Dq mpls ) .
+The label stack in
+.Va push_labels
+is pushed onto matching packets, innermost first.
+.Pp
+.It Dv NGM_MPLS_FEC_DEL_FEC Pq Ic del_fec
+Remove a FEC entry.
+The argument is a
+.Vt "struct ng_mpls_fec_entry"
+with
+.Va family ,
+.Va prefix ,
+and
+.Va prefix_len
+set to identify the entry to remove.
+.It Dv NGM_MPLS_FEC_GET_TABLE Pq Ic get_table
+Returns the entire FEC table:
+.Bd -literal -offset 0n
+struct ng_mpls_fec_table {
+ uint32_t n;
+ struct ng_mpls_fec_entry entry[];
+};
+.Ed
+.It Dv NGM_MPLS_FEC_GET_STATS Pq Ic get_stats
+Returns classification statistics:
+.Bd -literal -offset 0n
+struct ng_mpls_fec_stats {
+ uint64_t classify_ok;
+ uint64_t classify_fail;
+ uint64_t packets_out;
+ uint64_t bytes_out;
+};
+.Ed
+.It Dv NGM_MPLS_FEC_CLR_STATS Pq Ic clr_stats
+Clear all statistics counters.
+.El
+.Sh SHUTDOWN
+This node shuts down upon receipt of a
+.Dv NGM_SHUTDOWN
+control message or when all hooks have been disconnected.
+All FEC entries are freed during shutdown.
+.Sh EXAMPLES
+Create an FEC node and add a FEC entry for 10.0.0.0/8 that pushes
+label 200:
+.Bd -literal -offset indent
+ngctl mkpeer fec: mpls_fec downstream downstream
+ngctl msg fec: add_fec { hook="mpls" family=2 prefix=0x0a000000 \\
+ prefix_len=8 push_count=1 push_labels[0]=200 }
+.Ed
+.Sh SEE ALSO
+.Xr netgraph 4 ,
+.Xr ng_mpls 4 ,
+.Xr ng_mpls_lsp 4 ,
+.Xr ng_mpls_pw 4 ,
+.Xr ng_mpls_vpls 4 ,
+.Xr ng_mpls_vrf 4 ,
+.Xr ngctl 8
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+node type was written as part of the FreeBSD MPLS stack.
+.Sh BUGS
+Full IPv6 prefix matching (128-bit) is not supported in the FEC node;
+use the VRF node for IPv6 L3VPN.
diff --git a/share/man/man4/ng_mpls_lsp.4 b/share/man/man4/ng_mpls_lsp.4
new file mode 100644
index 00000000000..e49191c9431
--- /dev/null
+++ b/share/man/man4/ng_mpls_lsp.4
@@ -0,0 +1,142 @@
+.\" Copyright (c) 2026 The FreeBSD Foundation
+.\" All rights reserved.
+.\"
+.Dd July 28, 2026
+.Dt NG_MPLS_LSP 4
+.Os
+.Sh NAME
+.Nm ng_mpls_lsp
+.Nd MPLS label switched path tunnel interface netgraph node type
+.Sh SYNOPSIS
+.In netgraph/ng_mpls_lsp.h
+.Sh DESCRIPTION
+The
+.Nm mpls_lsp
+node type implements an MPLS LSP (Label Switched Path) tunnel interface.
+It creates a virtual network interface
+.Pa mpls_lsp Ns Ar N
+for each configured LSP, which appears in the kernel's routing table
+and can be used as a next-hop for routed traffic.
+.Pp
+When the kernel routes a packet through the tunnel interface, the
+node pushes the configured MPLS label stack onto the packet and
+forwards it to the
+.Va lower
+hook (typically connected to an
+.Xr ng_mpls 4
+LSR node).
+.Pp
+In the reverse direction, MPLS-encapsulated packets received on the
+.Va lower
+hook have their outermost label popped.
+If the popped label's S-bit (bottom of stack) is set, the inner IP packet
+is delivered to the kernel via the tunnel interface as if it arrived on
+a physical link.
+If more labels remain after popping, the packet is forwarded to the
+.Va upper
+hook for further processing by another LSP or VPLS node.
+.Pp
+This node enables BGP-free core, MPLS-TE tunnels, and
+LDP-signaled LSPs.
+It is typically paired with an
+.Xr ng_mpls 4
+LSR node that performs label switching at each hop.
+.Sh HOOKS
+This node type supports the following hooks:
+.Bl -tag -width indent
+.It Va lower
+Connects to the MPLS network (typically an
+.Xr ng_mpls 4
+LSR node).
+.It Va upper
+For multi-label stacks: receives packets that still carry labels after
+the outer label was popped.
+.El
+.Sh CONTROL MESSAGES
+This node type supports the generic netgraph control messages,
+plus the following:
+.Bl -tag -width indent
+.It Dv NGM_MPLS_LSP_ADD_LSP Pq Ic add_lsp
+Create an LSP tunnel with the following configuration:
+.Bd -literal -offset 0n
+struct ng_mpls_lsp_config {
+ uint32_t lsp_id; /* LSP identifier */
+ uint32_t push_count; /* labels to push */
+ uint32_t push_labels[8]; /* push stack */
+ uint32_t flags; /* NG_MPLS_LSP_F_* */
+ uint32_t mtu; /* tunnel MTU */
+};
+.Ed
+.Pp
+The
+.Va lsp_id
+identifies the tunnel; an interface
+.Pa mpls_lsp Ns Ar N
+is created where
+.Ar N
+equals
+.Va lsp_id .
+The
+.Va push_labels
+array holds the label stack to push on outgoing packets,
+innermost-first.
+The
+.Va flags
+field currently supports
+.Dv NG_MPLS_LSP_F_ACTIVE
+to mark the interface as administratively up.
+.Pp
+.It Dv NGM_MPLS_LSP_DEL_LSP Pq Ic del_lsp
+Remove an LSP tunnel by ID.
+The argument is a 32-bit LSP identifier.
+The associated tunnel interface is destroyed.
+.It Dv NGM_MPLS_LSP_GET_CONFIG Pq Ic get_config
+Returns the configuration of an LSP tunnel.
+The argument is a 32-bit LSP identifier; the return value is a
+.Vt "struct ng_mpls_lsp_config" .
+.It Dv NGM_MPLS_LSP_GET_STATS Pq Ic get_stats
+Returns per-LSP statistics:
+.Bd -literal -offset 0n
+struct ng_mpls_lsp_stats {
+ uint64_t tx_packets;
+ uint64_t tx_bytes;
+ uint64_t rx_packets;
+ uint64_t rx_bytes;
+ uint64_t tx_errors;
+ uint64_t rx_errors;
+};
+.Ed
+.It Dv NGM_MPLS_LSP_CLR_STATS Pq Ic clr_stats
+Clear LSP statistics.
+.El
+.Sh SHUTDOWN
+This node shuts down upon receipt of a
+.Dv NGM_SHUTDOWN
+control message or when all hooks have been disconnected.
+All LSP tunnel interfaces are detached and freed during shutdown.
+.Sh EXAMPLES
+Load the module, create an LSP tunnel with label 100, and connect
+it to an LSR node:
+.Bd -literal -offset indent
+kldload ng_mpls_lsp
+ngctl mkpeer lsp: mpls_lsp lower downstream
+ngctl msg lsp: add_lsp { lsp_id=1 push_count=1 \\
+ push_labels[0]=100 flags=1 mtu=1500 }
+route add 10.0.0.0/8 -interface mpls_lsp1
+.Ed
+.Sh SEE ALSO
+.Xr netgraph 4 ,
+.Xr ng_mpls 4 ,
+.Xr ng_mpls_fec 4 ,
+.Xr ng_mpls_pw 4 ,
+.Xr ng_mpls_vpls 4 ,
+.Xr ng_mpls_vrf 4 ,
+.Xr ngctl 8
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+node type was written as part of the FreeBSD MPLS stack.
+.Sh BUGS
+The node does not automatically probe interface MTU as described
+in RFC 1191; the MTU must be configured manually.
diff --git a/share/man/man4/ng_mpls_pw.4 b/share/man/man4/ng_mpls_pw.4
new file mode 100644
index 00000000000..32dce922ef7
--- /dev/null
+++ b/share/man/man4/ng_mpls_pw.4
@@ -0,0 +1,189 @@
+.\" Copyright (c) 2026 The FreeBSD Foundation
+.\" All rights reserved.
+.\"
+.Dd July 28, 2026
+.Dt NG_MPLS_PW 4
+.Os
+.Sh NAME
+.Nm ng_mpls_pw
+.Nd MPLS pseudowire netgraph node type
+.Sh SYNOPSIS
+.In netgraph/ng_mpls_pw.h
+.Sh DESCRIPTION
+The
+.Nm mpls_pw
+node type implements an MPLS pseudowire (RFC 4448) for point-to-point
+Ethernet-over-MPLS transport. It is used as the transport layer by
+.Xr ng_mpls_vpls 4
+for multipoint L2VPN services, and can also be used standalone for
+point-to-point L2 circuits.
+.Pp
+In the CE-facing direction, raw Ethernet frames received on a
+.Va ce Ns Ar N
+hook are MPLS-encapsulated and forwarded to the
+.Va downstream
+hook.
+The encapsulation consists of an optional control word (CW), a
+VC label, and an optional transport label stack.
+.Pp
+In the MPLS-facing direction, labeled packets from the
+.Va downstream
+hook have their label stack walked to find a matching VC label.
+The matching VC label and any outer transport labels are popped,
+the control word (if present) is stripped, and the inner Ethernet
+frame is delivered to the appropriate
+.Va ce Ns Ar N
+hook.
+.Pp
+The push label stack is configured innermost first:
+.Sq Li push_labels[0]
+should be the VC label (bottom of stack, S=1),
+and subsequent entries are transport labels placed above it.
+.Pp
+Two pseudowire types are defined:
+.Bl -tag -width ".Dv NG_MPLS_PW_TYPE_ETHERNET"
+.It Dv NG_MPLS_PW_TYPE_ETHERNET Pq 0x0005
+Ethernet port-to-port (RFC 4448).
+.It Dv NG_MPLS_PW_TYPE_VLAN Pq 0x0004
+VLAN-transparent pseudowire.
+.El
+.Pp
+When
+.Dv NG_MPLS_PW_F_CW
+is set in the flags field, a 4-byte control word is inserted between
+the label stack and the Ethernet frame, following the format defined
+in RFC 4385.
+.Sh HOOKS
+This node type supports the following hooks:
+.Bl -tag -width indent
+.It Va downstream
+Connects to the MPLS network (typically an
+.Xr ng_mpls 4
+LSR node).
+.It Va ce Ns Ar N
+Pseudowire endpoint ports, one per PW.
+.El
+.Sh CONTROL MESSAGES
+This node type supports the generic netgraph control messages,
+plus the following:
+.Bl -tag -width indent
+.It Dv NGM_MPLS_PW_ADD_PW Pq Ic add_pw
+Add a pseudowire, auto-deriving the push stack from
+.Va tunnel_label
+and
+.Va vc_label :
+.Bd -literal -offset 0n
+struct ng_mpls_pw_config {
+ char ce_hook[NG_HOOKSIZ]; /* CE hook name */
+ uint32_t vc_label; /* VC label */
+ uint32_t tunnel_label; /* tunnel/transport label */
+ uint16_t pw_type; /* NG_MPLS_PW_TYPE_* */
+ uint8_t expect_cw; /* expect control word */
+ uint8_t flags; /* NG_MPLS_PW_F_* */
+ uint8_t push_count; /* explicit push count */
+ uint32_t push_labels[8]; /* explicit push stack */
+};
+.Ed
+.Pp
+When
+.Va push_count
+is zero (as used by
+.Dv NGM_MPLS_PW_ADD_PW ) ,
+the push stack is derived from
+.Va tunnel_label
+and
+.Va vc_label .
+When
+.Va push_count
+is non-zero (as used by
+.Dv NGM_MPLS_PW_ADD_PW2 ) ,
+the explicit
+.Va push_labels
+array is used.
+.Pp
+.It Dv NGM_MPLS_PW_ADD_PW2 Pq Ic add_pw2
+Add a pseudowire with an explicit push label stack.
+Uses the same
+.Vt "struct ng_mpls_pw_config"
+as
+.Dv NGM_MPLS_PW_ADD_PW ,
+but requires
+.Va push_count
+to be set to the number of labels in
+.Va push_labels .
+.Pp
+.It Dv NGM_MPLS_PW_DEL_PW Pq Ic del_pw
+Remove a pseudowire by CE hook name.
+The argument is a
+.Vt "struct ng_mpls_pw_config"
+with
+.Va ce_hook
+set.
+.It Dv NGM_MPLS_PW_GET_TABLE Pq Ic get_table
+Returns the PW table:
+.Bd -literal -offset 0n
+struct ng_mpls_pw_table {
+ uint32_t n;
+ struct ng_mpls_pw_config pw[];
+};
+.Ed
+.It Dv NGM_MPLS_PW_GET_VC_TABLE Pq Ic get_vc_table
+Returns the table of VC labels currently in use.
+The format is an array of
+.Va { vc_label, hook_name }
+pairs.
+.It Dv NGM_MPLS_PW_GET_CONFIG Pq Ic get_config
+Returns the configuration for a specific PW, identified by
+CE hook name in a
+.Vt "struct ng_mpls_pw_config" .
+.It Dv NGM_MPLS_PW_SET_CONFIG Pq Ic set_config
+Update PW configuration parameters.
+Only the
+.Va flags
+and
+.Va expect_cw
+fields may be changed after creation.
+.It Dv NGM_MPLS_PW_GET_STATS Pq Ic get_stats
+Returns aggregate statistics:
+.Bd -literal -offset 0n
+struct ng_mpls_pw_stats {
+ uint64_t rx_packets;
+ uint64_t rx_bytes;
+ uint64_t tx_packets;
+ uint64_t tx_bytes;
+ uint64_t rx_errors;
+ uint64_t tx_errors;
+};
+.Ed
+.It Dv NGM_MPLS_PW_CLR_STATS Pq Ic clr_stats
+Clear all statistics counters.
+.El
+.Sh SHUTDOWN
+This node shuts down upon receipt of a
+.Dv NGM_SHUTDOWN
+control message or when all hooks have been disconnected.
+All pseudowire entries are freed during shutdown.
+.Sh EXAMPLES
+Create a PW node with a double-label pseudowire:
+.Bd -literal -offset indent
+ngctl mkpeer pw: mpls_pw downstream downstream
+ngctl msg pw: add_pw2 { ce_hook="ce1" push_count=2 \\
+ push_labels[0]=400 push_labels[1]=300 }
+ngctl connect pw: ce1 em0: lower
+.Ed
+.Sh SEE ALSO
+.Xr netgraph 4 ,
+.Xr ng_mpls 4 ,
+.Xr ng_mpls_fec 4 ,
+.Xr ng_mpls_lsp 4 ,
+.Xr ng_mpls_vpls 4 ,
+.Xr ng_mpls_vrf 4 ,
+.Xr ngctl 8
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+node type was written as part of the FreeBSD MPLS stack.
+.Sh BUGS
+Only Ethernet and VLAN pseudowire types are supported.
+AToM over MPLS (RFC 5086) is not implemented.
diff --git a/share/man/man4/ng_mpls_vpls.4 b/share/man/man4/ng_mpls_vpls.4
new file mode 100644
index 00000000000..012eb7e0cb2
--- /dev/null
+++ b/share/man/man4/ng_mpls_vpls.4
@@ -0,0 +1,144 @@
+.\" Copyright (c) 2026 The FreeBSD Foundation
+.\" All rights reserved.
+.\"
+.Dd July 28, 2026
+.Dt NG_MPLS_VPLS 4
+.Os
+.Sh NAME
+.Nm ng_mpls_vpls
+.Nd MPLS virtual private LAN service netgraph node type
+.Sh SYNOPSIS
+.In netgraph/ng_mpls_vpls.h
+.Sh DESCRIPTION
+The
+.Nm mpls_vpls
+node type implements VPLS (Virtual Private LAN Service, RFC 4762),
+providing multipoint Ethernet bridging over MPLS pseudowires.
+It is the L2VPN counterpart to the L3VPN functionality provided by
+.Xr ng_mpls_vrf 4 .
+.Pp
+The node maintains a MAC address table, learning source MAC-to-port
+associations from incoming traffic on all ports (CE and PW).
+Forwarding decisions are made by looking up the destination MAC:
+.Bl -bullet -compact
+.It
+Known unicast is forwarded only to the learned port.
+.It
+Unknown unicast, broadcast, and multicast are flooded to all ports.
+.El
+.Pp
+Two port types are supported:
+.Bl -tag -width indent
+.It CE ports ( Va ce Ns Ar N )
+Customer-facing ports that receive and send raw Ethernet frames
+directly to/from customer equipment.
+.It PW ports ( Va pw Ns Ar N )
+Pseudowire-facing ports that connect to
+.Xr ng_mpls_pw 4
+nodes for MPLS transport across the provider network.
+.El
+.Pp
+Split-horizon forwarding is enforced:
+traffic received from a PW port is never forwarded to another PW port.
+This prevents loops when the PEs are connected in a full mesh
+of pseudowires.
+Traffic received from a CE port is forwarded to all other ports
+(PW and CE) based on MAC lookup.
+.Pp
+MAC entries age out after a configurable staleness period.
+The aging callout periodically scans the hash table and removes
+entries whose staleness exceeds the configured threshold.
+.Pp
+The MAC hash table has
+.Dv NG_MPLS_VPLS_HASH_SIZE
+(default 256) buckets.
+.Sh HOOKS
+This node type supports an unlimited number of hooks.
+.Bl -tag -width indent
+.It Va ce Ns Ar N
+Customer Edge ports, one per connected CE device.
+.It Va pw Ns Ar N
+Pseudowire ports, one per pseudowire to a remote PE.
+.El
+.Sh CONTROL MESSAGES
+This node type supports the generic netgraph control messages,
+plus the following:
+.Bl -tag -width indent
+.It Dv NGM_MPLS_VPLS_SET_CONFIG Pq Ic set_config
+Set VPLS configuration parameters:
+.Bd -literal -offset 0n
+struct ng_mpls_vpls_config {
+ uint32_t debug_level;
+ uint32_t max_staleness; /* max age before removal (sec) */
+ uint32_t loop_timeout; /* loop detection mute (sec) */
+ uint32_t min_stable_age; /* min stable host age (sec) */
+};
+.Ed
+.It Dv NGM_MPLS_VPLS_GET_CONFIG Pq Ic get_config
+Returns the current configuration as a
+.Vt "struct ng_mpls_vpls_config" .
+.It Dv NGM_MPLS_VPLS_GET_TABLE Pq Ic get_table
+Returns the MAC address table:
+.Bd -literal -offset 0n
+struct ng_mpls_vpls_host_ary {
+ uint32_t num_hosts;
+ struct ng_mpls_vpls_host hosts[];
+};
+
+struct ng_mpls_vpls_host {
+ u_char addr[ETHER_ADDR_LEN]; /* MAC address */
+ char hook[NG_HOOKSIZ]; /* learned port */
+ uint16_t age; /* age in seconds */
+ uint16_t staleness; /* staleness counter */
+};
+.Ed
+.It Dv NGM_MPLS_VPLS_GET_STATS Pq Ic get_stats
+Returns aggregate statistics.
+.It Dv NGM_MPLS_VPLS_GET_PORT_STATS Pq Ic get_port_stats
+Returns per-port statistics:
+.Bd -literal -offset 0n
+struct ng_mpls_vpls_port_stats {
+ uint64_t rx_packets;
+ uint64_t rx_bytes;
+ uint64_t rx_unknown;
+ uint64_t tx_packets;
+ uint64_t tx_bytes;
+ uint64_t loop_drops;
+};
+.Ed
+.It Dv NGM_MPLS_VPLS_CLR_STATS Pq Ic clr_stats
+Clear all statistics counters.
+.It Dv NGM_MPLS_VPLS_RESET Pq Ic reset
+Flush the MAC address table and unmute any looped ports.
+The node configuration is not changed.
+.El
+.Sh SHUTDOWN
+This node shuts down upon receipt of a
+.Dv NGM_SHUTDOWN
+control message or when all hooks have been disconnected.
+The MAC address table, port list, and aging callout are all
+cleaned up during shutdown.
+.Sh EXAMPLES
+Create a VPLS instance and connect a CE port:
+.Bd -literal -offset indent
+ngctl mkpeer vpls: mpls_vpls ce0 ce0
+ngctl connect vpls: ce1 em0: lower
+ngctl msg vpls: set_config { max_staleness=900 }
+.Ed
+.Sh SEE ALSO
+.Xr netgraph 4 ,
+.Xr ng_mpls 4 ,
+.Xr ng_mpls_fec 4 ,
+.Xr ng_mpls_lsp 4 ,
+.Xr ng_mpls_pw 4 ,
+.Xr ng_mpls_vrf 4 ,
+.Xr ngctl 8
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+node type was written as part of the FreeBSD MPLS stack.
+.Sh BUGS
+The MAC hash table size is fixed at 256 buckets.
+No spanning tree protocol (STP) is implemented; loop prevention
+relies on split-horizon and simple loop detection.
diff --git a/share/man/man4/ng_mpls_vrf.4 b/share/man/man4/ng_mpls_vrf.4
new file mode 100644
index 00000000000..0d64c63f7da
--- /dev/null
+++ b/share/man/man4/ng_mpls_vrf.4
@@ -0,0 +1,198 @@
+.\" Copyright (c) 2026 The FreeBSD Foundation
+.\" All rights reserved.
+.\"
+.Dd July 28, 2026
+.Dt NG_MPLS_VRF 4
+.Os
+.Sh NAME
+.Nm ng_mpls_vrf
+.Nd MPLS L3VPN VRF netgraph node type
+.Sh SYNOPSIS
+.In netgraph/ng_mpls_vrf.h
+.Sh DESCRIPTION
+The
+.Nm mpls_vrf
+node type implements a VRF (Virtual Routing and Forwarding) instance for
+MPLS L3VPN as described in RFC 4364.
+It creates a virtual network interface
+.Pa vrf Ns Ar N
+for each VRF instance, which appears in the system's routing table with its
+own dedicated FIB (Forwarding Information Base).
+.Pp
+Traffic flow:
+.Bl -enum -compact
+.It
+Traffic arriving from a CE port
+.Pq Va ce Ns Ar N
+is injected into the kernel's network stack via the VRF interface,
+where it is routed using the VRF's dedicated FIB.
+.It
+If the destination is a local IP in the VRF, the packet is forwarded
+out the appropriate CE port.
+.It
+If the destination is remote, the packet is MPLS-encapsulated with a VPN
+label and optional transport label stack, and sent to the
+.Va downstream
+hook.
+.It
+MPLS traffic arriving on the
+.Va downstream
+hook has its VPN label looked up in the VPN label table.
+The label is popped and the inner IP packet is delivered to the kernel
+via the VRF interface, which routes it to the correct CE port.
+.El
+.Pp
+Each VRF instance is identified by a
+.Va vrf_id
+and optionally configured with a Route Distinguisher (RD) for BGP/MPLS
+IP VPN operation.
+The
+.Va flags
+field controls import/export behavior:
+.Bl -tag -width ".Dv NG_MPLS_VRF_F_BOTH"
+.It Dv NG_MPLS_VRF_F_IMPORT
+Accept VPN routes into the VRF.
+.It Dv NG_MPLS_VRF_F_EXPORT
+Advertise VRF routes via MP-BGP.
+.It Dv NG_MPLS_VRF_F_BOTH
+Import and export routes.
+.El
+.Pp
+Both IPv4 and IPv6 VPN label entries are supported.
+IPv6 entries use
+.Dv NGM_MPLS_VRF_ADD_VPN_LABEL6
+with a 128-bit prefix array.
+.Pp
+The push label stack for VPN entries is limited to
+.Dv NG_MPLS_VRF_MAX_LABELS
+(currently 2) labels: the VPN label and an optional transport label.
+.Sh HOOKS
+This node type supports the following hooks:
+.Bl -tag -width indent
+.It Va downstream
+Connects to the MPLS network (typically an
+.Xr ng_mpls 4
+LSR node).
+.It Va ce Ns Ar N
+Customer Edge ports, one per connected CE router.
+.El
+.Sh CONTROL MESSAGES
+This node type supports the generic netgraph control messages,
+plus the following:
+.Bl -tag -width indent
+.It Dv NGM_MPLS_VRF_ADD_VRF Pq Ic add_vrf
+Create a VRF instance:
+.Bd -literal -offset 0n
+struct ng_mpls_vrf_config {
+ uint32_t vrf_id; /* VRF identifier */
+ uint32_t fib_num; /* FIB number */
+ uint32_t rd_high; /* Route Distinguisher (high) */
+ uint32_t rd_low; /* Route Distinguisher (low) */
+ uint32_t flags; /* NG_MPLS_VRF_F_* */
+};
+.Ed
+.Pp
+An interface
+.Pa vrf Ns Ar N
+is created where
+.Ar N
+equals
+.Va vrf_id .
+The
+.Va fib_num
+field selects the FIB table to use for this VRF (0 for the default FIB).
+.Pp
+.It Dv NGM_MPLS_VRF_DEL_VRF Pq Ic del_vrf
+Remove a VRF instance by ID.
+The associated VRF interface is destroyed.
+.It Dv NGM_MPLS_VRF_ADD_VPN_LABEL Pq Ic add_vpn_label
+Add an IPv4 VPN label entry:
+.Bd -literal -offset 0n
+struct ng_mpls_vrf_vpn_label {
+ uint32_t vpn_label;
+ uint32_t vrf_id;
+ uint32_t prefix; /* IPv4 prefix (network order) */
+ uint8_t prefix_len;
+ uint8_t family; /* AF_INET */
+ uint8_t push_count; /* 1 or 2 */
+ uint32_t push_labels[2]; /* VPN label + optional transport */
+};
+.Ed
+.It Dv NGM_MPLS_VRF_ADD_VPN_LABEL6 Pq Ic add_vpn_label6
+Add an IPv6 VPN label entry:
+.Bd -literal -offset 0n
+struct ng_mpls_vrf_vpn_label6 {
+ uint32_t vpn_label;
+ uint32_t vrf_id;
+ uint32_t prefix[4]; /* IPv6 prefix (network order) */
+ uint8_t prefix_len;
+ uint8_t push_count; /* 1 or 2 */
+ uint32_t push_labels[2];
+};
+.Ed
+.It Dv NGM_MPLS_VRF_DEL_VPN_LABEL Pq Ic del_vpn_label
+Remove a VPN label entry by label value.
+.It Dv NGM_MPLS_VRF_DEL_VPN_LABEL6 Pq Ic del_vpn_label6
+Remove an IPv6 VPN label entry by label value.
+.It Dv NGM_MPLS_VRF_GET_CONFIG Pq Ic get_config
+Returns the configuration for a VRF instance (identified by VRF ID).
+.It Dv NGM_MPLS_VRF_GET_STATS Pq Ic get_stats
+Returns VRF statistics:
+.Bd -literal -offset 0n
+struct ng_mpls_vrf_stats {
+ uint64_t rx_ce_packets;
+ uint64_t rx_ce_bytes;
+ uint64_t tx_ce_packets;
+ uint64_t tx_ce_bytes;
+ uint64_t rx_mpls_packets;
+ uint64_t rx_mpls_bytes;
+ uint64_t tx_mpls_packets;
+ uint64_t tx_mpls_bytes;
+ uint64_t vpn_label_added;
+ uint64_t vpn_label_removed;
+ uint64_t no_route;
+ /* IPv6 counters: */
+ uint64_t rx6_ce_packets;
+ uint64_t rx6_ce_bytes;
+ uint64_t tx6_ce_packets;
+ uint64_t tx6_ce_bytes;
+ uint64_t rx6_mpls_packets;
+ uint64_t rx6_mpls_bytes;
+ uint64_t tx6_mpls_packets;
+ uint64_t tx6_mpls_bytes;
+};
+.Ed
+.It Dv NGM_MPLS_VRF_CLR_STATS Pq Ic clr_stats
+Clear all VRF statistics counters.
+.El
+.Sh SHUTDOWN
+This node shuts down upon receipt of a
+.Dv NGM_SHUTDOWN
+control message or when all hooks have been disconnected.
+All VRF instances, CE ports, and VPN label entries are freed during
+shutdown; VRF interfaces are detached from the kernel.
+.Sh EXAMPLES
+Create a VRF instance and attach a CE port:
+.Bd -literal -offset indent
+ngctl mkpeer vrf: mpls_vrf ce0 ce0
+ngctl msg vrf: add_vrf { vrf_id=100 fib_num=1 flags=1 }
+ngctl msg vrf: add_vpn_label { vpn_label=500 vrf_id=100 \\
+ prefix=0x0a000000 prefix_len=8 push_count=1 \\
+ push_labels[0]=500 }
+.Ed
+.Sh SEE ALSO
+.Xr netgraph 4 ,
+.Xr ng_mpls 4 ,
+.Xr ng_mpls_fec 4 ,
+.Xr ng_mpls_lsp 4 ,
+.Xr ng_mpls_pw 4 ,
+.Xr ng_mpls_vpls 4 ,
+.Xr ngctl 8
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+node type was written as part of the FreeBSD MPLS stack.
+.Sh BUGS
+The push label stack is limited to 2 labels (VPN + transport).
+IPv6 NDP offload is not supported for CE ports.
diff --git a/sys/conf/files b/sys/conf/files
index 8c9993d8f57..97df65b1d2e 100644
--- a/sys/conf/files
+++ b/sys/conf/files
@@ -4358,6 +4358,12 @@ netgraph/ng_tty.c optional netgraph_tty
netgraph/ng_vjc.c optional netgraph_vjc
netgraph/ng_vlan.c optional netgraph_vlan
netgraph/ng_vlan_rotate.c optional netgraph_vlan_rotate
+netgraph/ng_mpls.c optional netgraph_mpls
+netgraph/ng_mpls_vpls.c optional netgraph_mpls_vpls
+netgraph/ng_mpls_lsp.c optional netgraph_mpls_lsp
+netgraph/ng_mpls_fec.c optional netgraph_mpls_fec
+netgraph/ng_mpls_pw.c optional netgraph_mpls_pw
+netgraph/ng_mpls_vrf.c optional netgraph_mpls_vrf inet
netinet/accf_data.c optional accept_filter_data inet
netinet/accf_dns.c optional accept_filter_dns inet
netinet/accf_http.c optional accept_filter_http inet
diff --git a/sys/modules/netgraph/Makefile b/sys/modules/netgraph/Makefile
index 94560d5c51d..c265c97d4b4 100644
--- a/sys/modules/netgraph/Makefile
+++ b/sys/modules/netgraph/Makefile
@@ -29,6 +29,12 @@ SUBDIR= async \
l2tp \
lmi \
macfilter \
+ mpls \
+ mpls_fec \
+ mpls_lsp \
+ mpls_vpls \
+ mpls_pw \
+ mpls_vrf \
${_mppc} \
nat \
netflow \
diff --git a/sys/modules/netgraph/mpls/Makefile b/sys/modules/netgraph/mpls/Makefile
new file mode 100644
index 00000000000..6a883e30b29
--- /dev/null
+++ b/sys/modules/netgraph/mpls/Makefile
@@ -0,0 +1,4 @@
+KMOD= ng_mpls
+SRCS= ng_mpls.c
+
+.include <bsd.kmod.mk>
diff --git a/sys/modules/netgraph/mpls_fec/Makefile b/sys/modules/netgraph/mpls_fec/Makefile
new file mode 100644
index 00000000000..94d4d5e92a9
--- /dev/null
+++ b/sys/modules/netgraph/mpls_fec/Makefile
@@ -0,0 +1,4 @@
+KMOD= ng_mpls_fec
+SRCS= ng_mpls_fec.c
+
+.include <bsd.kmod.mk>
diff --git a/sys/modules/netgraph/mpls_lsp/Makefile b/sys/modules/netgraph/mpls_lsp/Makefile
new file mode 100644
index 00000000000..40f446eaabf
--- /dev/null
+++ b/sys/modules/netgraph/mpls_lsp/Makefile
@@ -0,0 +1,4 @@
+KMOD= ng_mpls_lsp
+SRCS= ng_mpls_lsp.c
+
+.include <bsd.kmod.mk>
diff --git a/sys/modules/netgraph/mpls_pw/Makefile b/sys/modules/netgraph/mpls_pw/Makefile
new file mode 100644
index 00000000000..021f613047c
--- /dev/null
+++ b/sys/modules/netgraph/mpls_pw/Makefile
@@ -0,0 +1,4 @@
+KMOD= ng_mpls_pw
+SRCS= ng_mpls_pw.c
+
+.include <bsd.kmod.mk>
diff --git a/sys/modules/netgraph/mpls_vpls/Makefile b/sys/modules/netgraph/mpls_vpls/Makefile
new file mode 100644
index 00000000000..4675053625e
--- /dev/null
+++ b/sys/modules/netgraph/mpls_vpls/Makefile
@@ -0,0 +1,4 @@
+KMOD= ng_mpls_vpls
+SRCS= ng_mpls_vpls.c
+
+.include <bsd.kmod.mk>
diff --git a/sys/modules/netgraph/mpls_vrf/Makefile b/sys/modules/netgraph/mpls_vrf/Makefile
new file mode 100644
index 00000000000..e6bd792da1b
--- /dev/null
+++ b/sys/modules/netgraph/mpls_vrf/Makefile
@@ -0,0 +1,4 @@
+KMOD= ng_mpls_vrf
+SRCS= ng_mpls_vrf.c
+
+.include <bsd.kmod.mk>
diff --git a/sys/netgraph/ng_mpls.c b/sys/netgraph/ng_mpls.c
new file mode 100644
index 00000000000..c3b9346a1f3
--- /dev/null
+++ b/sys/netgraph/ng_mpls.c
@@ -0,0 +1,1184 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/errno.h>
+#include <sys/kernel.h>
+#include <sys/malloc.h>
+#include <sys/mbuf.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/systm.h>
+
+#include <net/ethernet.h>
+#include <net/if.h>
+
+#include <netgraph/ng_message.h>
+#include <netgraph/ng_parse.h>
+#include <netgraph/ng_mpls.h>
+#include <netgraph/netgraph.h>
+
+struct mpls_label_entry {
+ LIST_ENTRY(mpls_label_entry) entries;
+ hook_p hook;
+ uint32_t label;
+ uint32_t action;
+ uint32_t swap_label;
+ uint8_t push_count;
+ uint32_t push_labels[MPLS_MAX_LABEL_STACK];
+ uint8_t ttl_handling;
+ uint8_t ttl;
+ uint64_t packets;
+ uint64_t bytes;
+};
+
+struct mpls_nhlfe_entry {
+ LIST_ENTRY(mpls_nhlfe_entry) entries;
+ uint32_t id;
+ hook_p hook;
+ uint32_t action;
+ uint32_t swap_label;
+ uint8_t push_count;
+ uint32_t push_labels[MPLS_MAX_LABEL_STACK];
+ uint64_t packets;
+ uint64_t bytes;
+};
+
+struct ng_mpls_private {
+ hook_p downstream_hook;
+ hook_p nomatch_hook;
+ uint32_t decap_enable;
+ uint32_t encap_enable;
+ uint64_t packets_in;
+ uint64_t bytes_in;
+ uint64_t packets_out;
+ uint64_t bytes_out;
+ uint64_t packets_dropped;
+ uint64_t packets_nomatch;
+ uint32_t next_nhlfe_id;
+ LIST_HEAD(, mpls_label_entry) label_list;
+ LIST_HEAD(, mpls_nhlfe_entry) nhlfe_list;
+};
+typedef struct ng_mpls_private *priv_p;
+
+static ng_constructor_t ng_mpls_constructor;
+static ng_rcvmsg_t ng_mpls_rcvmsg;
+static ng_shutdown_t ng_mpls_shutdown;
+static ng_newhook_t ng_mpls_newhook;
+static ng_rcvdata_t ng_mpls_rcvdata;
+static ng_disconnect_t ng_mpls_disconnect;
+
+static struct mpls_label_entry *ng_mpls_find_label(priv_p, uint32_t);
+static struct mpls_nhlfe_entry *ng_mpls_find_nhlfe(priv_p, uint32_t);
+static int ng_mpls_getTableLength(const struct ng_parse_type *,
+ const u_char *, const u_char *);
+static int ng_mpls_getNhlfeTableLength(const struct ng_parse_type *,
+ const u_char *, const u_char *);
+static int ng_mpls_remove_shim(struct mbuf **, int);
+static void ng_mpls_fix_ether_type(struct mbuf **);
+static int ng_mpls_push_label(struct mbuf **, uint32_t, int);
+static int ng_mpls_push_label_stack(struct mbuf **,
+ const uint32_t *, int);
+
+static int
+ng_mpls_getPushStackLength(const struct ng_parse_type *type __unused,
+ const u_char *start __unused, const u_char *buf __unused)
+{
+ return (MPLS_MAX_LABEL_STACK);
+}
+
+/* Parse type for the fixed-length push_labels array. */
+static const struct ng_parse_array_info ng_mpls_push_stack_info = {
+ &ng_parse_uint32_type,
+ ng_mpls_getPushStackLength
+};
+static const struct ng_parse_type ng_mpls_push_stack_type = {
+ &ng_parse_array_type,
+ &ng_mpls_push_stack_info
+};
+
+static const struct ng_parse_struct_field ng_mpls_label_fields[] =
+ NG_MPLS_LABEL_FIELDS;
+static const struct ng_parse_type ng_mpls_label_type = {
+ &ng_parse_struct_type,
+ &ng_mpls_label_fields
+};
+
+static const struct ng_parse_array_info ng_mpls_table_array_info = {
+ &ng_mpls_label_type,
+ ng_mpls_getTableLength
+};
+static const struct ng_parse_type ng_mpls_table_array_type = {
+ &ng_parse_array_type,
+ &ng_mpls_table_array_info
+};
+
+static const struct ng_parse_struct_field ng_mpls_table_fields[] =
+ NG_MPLS_TABLE_FIELDS;
+static const struct ng_parse_type ng_mpls_table_type = {
+ &ng_parse_struct_type,
+ &ng_mpls_table_fields
+};
+
+static const struct ng_parse_struct_field ng_mpls_nhlfe_entry_fields[] =
+ NG_MPLS_NHLFE_ENTRY_FIELDS;
+static const struct ng_parse_type ng_mpls_nhlfe_entry_type = {
+ &ng_parse_struct_type,
+ &ng_mpls_nhlfe_entry_fields
+};
+
+static const struct ng_parse_array_info ng_mpls_nhlfe_table_array_info = {
+ &ng_mpls_nhlfe_entry_type,
+ ng_mpls_getNhlfeTableLength
+};
+static const struct ng_parse_type ng_mpls_nhlfe_table_array_type = {
+ &ng_parse_array_type,
+ &ng_mpls_nhlfe_table_array_info
+};
+
+static const struct ng_parse_struct_field ng_mpls_nhlfe_table_fields[] =
+ NG_MPLS_NHLFE_TABLE_FIELDS;
+static const struct ng_parse_type ng_mpls_nhlfe_table_type = {
+ &ng_parse_struct_type,
+ &ng_mpls_nhlfe_table_fields
+};
+
+static const struct ng_parse_struct_field ng_mpls_stats_fields[] =
+ NG_MPLS_STATS_FIELDS;
+static const struct ng_parse_type ng_mpls_stats_type = {
+ &ng_parse_struct_type,
+ &ng_mpls_stats_fields
+};
+
+static const struct ng_cmdlist ng_mpls_cmdlist[] = {
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_ADD_LABEL,
+ "addlabel",
+ &ng_mpls_label_type,
+ NULL
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_DEL_LABEL,
+ "dellabel",
+ &ng_parse_hookbuf_type,
+ NULL
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_GET_TABLE,
+ "gettable",
+ NULL,
+ &ng_mpls_table_type
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_DEL_LABEL_VAL,
+ "dellabelval",
+ &ng_parse_uint32_type,
+ NULL
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_GET_DECAP,
+ "getdecap",
+ NULL,
+ &ng_parse_hint32_type
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_SET_DECAP,
+ "setdecap",
+ &ng_parse_hint32_type,
+ NULL
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_GET_ENCAP,
+ "getencap",
+ NULL,
+ &ng_parse_hint32_type
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_SET_ENCAP,
+ "setencap",
+ &ng_parse_hint32_type,
+ NULL
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_GET_STATS,
+ "getstats",
+ NULL,
+ &ng_mpls_stats_type
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_CLR_STATS,
+ "clrstats",
+ NULL,
+ NULL
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_ADD_NHLFE,
+ "addnhlfe",
+ &ng_mpls_label_type,
+ NULL
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_GET_NHLFE_TABLE,
+ "getnhlfetable",
+ NULL,
+ &ng_mpls_nhlfe_table_type
+ },
+ {
+ NGM_MPLS_COOKIE,
+ NGM_MPLS_DEL_NHLFE,
+ "delnhlfe",
+ &ng_parse_uint32_type,
+ NULL
+ },
+ { 0 }
+};
+
+static struct ng_type ng_mpls_typestruct = {
+ .version = NG_ABI_VERSION,
+ .name = NG_MPLS_NODE_TYPE,
+ .constructor = ng_mpls_constructor,
+ .rcvmsg = ng_mpls_rcvmsg,
+ .shutdown = ng_mpls_shutdown,
+ .newhook = ng_mpls_newhook,
+ .rcvdata = ng_mpls_rcvdata,
+ .disconnect = ng_mpls_disconnect,
+ .cmdlist = ng_mpls_cmdlist,
+};
+NETGRAPH_INIT(mpls, &ng_mpls_typestruct);
+
+static struct mpls_label_entry *
+ng_mpls_find_label(priv_p priv, uint32_t label)
+{
+ struct mpls_label_entry *le;
+
+ LIST_FOREACH(le, &priv->label_list, entries) {
+ if (le->label == label)
+ return (le);
+ }
+ return (NULL);
+}
+
+static struct mpls_nhlfe_entry *
+ng_mpls_find_nhlfe(priv_p priv, uint32_t id)
+{
+ struct mpls_nhlfe_entry *ne;
+
+ LIST_FOREACH(ne, &priv->nhlfe_list, entries) {
+ if (ne->id == id)
+ return (ne);
+ }
+ return (NULL);
+}
+
+static int
+ng_mpls_getTableLength(const struct ng_parse_type *type __unused,
+ const u_char *start __unused, const u_char *buf)
+{
+ const struct ng_mpls_table *const table =
+ (const struct ng_mpls_table *)(buf - sizeof(uint32_t));
+
+ return (table->n);
+}
+
+static int
+ng_mpls_getNhlfeTableLength(const struct ng_parse_type *type __unused,
+ const u_char *start __unused, const u_char *buf)
+{
+ const struct ng_mpls_nhlfe_table *const table =
+ (const struct ng_mpls_nhlfe_table *)(buf - sizeof(uint32_t));
+
+ return (table->n);
+}
+
+/*
+ * Remove the MPLS shim at the given offset from the Ethernet header.
+ * *m may be updated by m_pullup.
+ * Returns 0 on success, ENOBUFS if pullup fails.
+ */
+static int
+ng_mpls_remove_shim(struct mbuf **mp, int offset)
+{
+ struct mbuf *m = *mp;
+ int remainder;
+
+ if (m->m_len < offset + MPLS_SHIM_LEN &&
+ (m = m_pullup(m, offset + MPLS_SHIM_LEN)) == NULL)
+ return (ENOBUFS);
+
+ remainder = m->m_len - offset - MPLS_SHIM_LEN;
+ if (remainder > 0)
+ ovbcopy(mtod(m, char *) + offset + MPLS_SHIM_LEN,
+ mtod(m, char *) + offset, remainder);
+ m->m_len -= MPLS_SHIM_LEN;
+ m->m_pkthdr.len -= MPLS_SHIM_LEN;
+ *mp = m;
+ return (0);
+}
+
+/*
+ * Fix the Ethernet type after popping the bottom MPLS label.
+ * Determines the payload type from the first nibble after the Ethernet header.
+ * *m may be updated by m_pullup. Sets *mp to NULL on pullup failure.
+ */
+static void
+ng_mpls_fix_ether_type(struct mbuf **mp)
+{
+ struct mbuf *m = *mp;
+ struct ether_header *eh;
+ uint8_t *next;
+
+ if (m->m_pkthdr.len < sizeof(struct ether_header) + 1)
+ return;
+ if (m->m_len < sizeof(struct ether_header) + 1 &&
+ (m = m_pullup(m, sizeof(struct ether_header) + 1)) == NULL) {
+ *mp = NULL;
+ return;
+ }
+
+ eh = mtod(m, struct ether_header *);
+ next = (uint8_t *)(eh + 1);
+
+ if ((next[0] >> 4) == 4)
+ eh->ether_type = htons(ETHERTYPE_IP);
+ else if ((next[0] >> 4) == 6)
+ eh->ether_type = htons(ETHERTYPE_IPV6);
+ *mp = m;
+}
+
+/*
+ * Prepend an MPLS shim to the packet.
+ * This inserts 4 bytes between the Ethernet header and the payload.
+ * The S bit of the shim is set by the caller; if this is the bottom
+ * (innermost) label, pass s_bit=1.
+ * Returns 0 on success, non-zero on failure.
+ */
+static int
+ng_mpls_push_label(struct mbuf **m, uint32_t label_value, int s_bit)
+{
+ struct ether_header *eh;
+ uint8_t dmac[ETHER_ADDR_LEN];
+ uint8_t smac[ETHER_ADDR_LEN];
+ uint32_t shim;
+
+ if ((*m)->m_pkthdr.len < sizeof(struct ether_header))
+ return (EINVAL);
+ if ((*m)->m_len < sizeof(struct ether_header) &&
+ (*m = m_pullup(*m, sizeof(struct ether_header))) == NULL)
+ return (ENOBUFS);
+
+ eh = mtod(*m, struct ether_header *);
+ memcpy(dmac, eh->ether_dhost, ETHER_ADDR_LEN);
+ memcpy(smac, eh->ether_shost, ETHER_ADDR_LEN);
+
+ M_PREPEND((*m), MPLS_SHIM_LEN, M_NOWAIT);
+ if ((*m) == NULL)
+ return (ENOMEM);
+
+ if ((*m)->m_len < sizeof(struct ether_header) + MPLS_SHIM_LEN &&
+ (*m = m_pullup(*m, sizeof(struct ether_header) +
+ MPLS_SHIM_LEN)) == NULL)
+ return (ENOBUFS);
+
+ eh = mtod(*m, struct ether_header *);
+ memcpy(eh->ether_dhost, dmac, ETHER_ADDR_LEN);
+ memcpy(eh->ether_shost, smac, ETHER_ADDR_LEN);
+ eh->ether_type = htons(ETHERTYPE_MPLS);
+
+ shim = MPLS_MAKE(label_value, 0, s_bit, 64);
+ *(uint32_t *)(eh + 1) = shim;
+
+ return (0);
+}
+
+/*
+ * Push a stack of MPLS labels onto the packet.
+ * Labels are pushed from innermost (last in array, S=1) to
+ * outermost (first in array after the swap label).
+ */
+static int
+ng_mpls_push_label_stack(struct mbuf **m,
+ const uint32_t *labels, int count)
+{
+ int i;
+ int error;
+
+ for (i = count - 1; i >= 0; i--) {
+ int s = (i == count - 1) ? 1 : 0;
+ error = ng_mpls_push_label(m, labels[i], s);
+ if (error)
+ return (error);
+ }
+ return (0);
+}
+
+static int
+ng_mpls_constructor(node_p node)
+{
+ priv_p priv;
+
+ priv = malloc(sizeof(*priv), M_NETGRAPH, M_WAITOK | M_ZERO);
+ priv->decap_enable = 1;
+ priv->encap_enable = MPLS_ENCAP_FROM_FILTER;
+ priv->next_nhlfe_id = 1;
+ LIST_INIT(&priv->label_list);
+ LIST_INIT(&priv->nhlfe_list);
+ NG_NODE_SET_PRIVATE(node, priv);
+ return (0);
+}
+
+static int
+ng_mpls_newhook(node_p node, hook_p hook, const char *name)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+
+ if (strcmp(name, NG_MPLS_HOOK_DOWNSTREAM) == 0)
+ priv->downstream_hook = hook;
+ else if (strcmp(name, NG_MPLS_HOOK_NOMATCH) == 0)
+ priv->nomatch_hook = hook;
+ NG_HOOK_SET_PRIVATE(hook, NULL);
+ return (0);
+}
+
+static int
+ng_mpls_rcvmsg(node_p node, item_p item, hook_p lasthook)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct ng_mesg *msg, *resp = NULL;
+ struct ng_mpls_label *ml;
+ struct mpls_label_entry *le;
+ struct mpls_nhlfe_entry *ne;
+ hook_p hook;
+ struct ng_mpls_table *t;
+ struct ng_mpls_nhlfe_table *nt;
+ struct ng_mpls_nhlfe_entry *nte;
+ int label_count;
+ uint32_t label;
+ int error = 0;
+
+ NGI_GET_MSG(item, msg);
+ switch (msg->header.typecookie) {
+ case NGM_MPLS_COOKIE:
+ switch (msg->header.cmd) {
+ case NGM_MPLS_ADD_LABEL:
+ if (msg->header.arglen < sizeof(*ml)) {
+ error = EINVAL;
+ break;
+ }
+ ml = (struct ng_mpls_label *)msg->data;
+
+ if (ml->label > MPLS_LABEL_MAX) {
+ error = EINVAL;
+ break;
+ }
+ if (ml->action != NG_MPLS_ACTION_NONE &&
+ ml->action != NG_MPLS_ACTION_POP &&
+ ml->action != NG_MPLS_ACTION_SWAP &&
+ ml->action != NG_MPLS_ACTION_PHP &&
+ ml->action != NG_MPLS_ACTION_SWAP_AND_PUSH) {
+ error = EINVAL;
+ break;
+ }
+ if (ml->push_count > MPLS_MAX_LABEL_STACK) {
+ error = EINVAL;
+ break;
+ }
+
+ hook = ng_findhook(node, ml->hook_name);
+ if (hook == NULL) {
+ error = ENOENT;
+ break;
+ }
+ if (hook == priv->downstream_hook ||
+ hook == priv->nomatch_hook) {
+ error = EINVAL;
+ break;
+ }
+ if (NG_HOOK_PRIVATE(hook) != NULL) {
+ error = EEXIST;
+ break;
+ }
+ if (ng_mpls_find_label(priv, ml->label) != NULL) {
+ error = EEXIST;
+ break;
+ }
+
+ le = malloc(sizeof(*le), M_NETGRAPH,
+ M_NOWAIT | M_ZERO);
+ if (le == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ le->hook = hook;
+ le->label = ml->label;
+ le->action = ml->action;
+ le->swap_label = ml->swap_label;
+ le->push_count = ml->push_count;
+ le->ttl_handling = ml->ttl_handling;
+ le->ttl = ml->ttl;
+ memcpy(le->push_labels, ml->push_labels,
+ ml->push_count * sizeof(uint32_t));
+ LIST_INSERT_HEAD(&priv->label_list, le, entries);
+ NG_HOOK_SET_PRIVATE(hook, le);
+ break;
+
+ case NGM_MPLS_DEL_LABEL:
+ if (msg->header.arglen != NG_HOOKSIZ) {
+ error = EINVAL;
+ break;
+ }
+ hook = ng_findhook(node, (char *)msg->data);
+ if (hook == NULL) {
+ error = ENOENT;
+ break;
+ }
+ le = NG_HOOK_PRIVATE(hook);
+ if (le == NULL) {
+ error = ENOENT;
+ break;
+ }
+ LIST_REMOVE(le, entries);
+ NG_HOOK_SET_PRIVATE(hook, NULL);
+ free(le, M_NETGRAPH);
+ break;
+
+ case NGM_MPLS_DEL_LABEL_VAL:
+ if (msg->header.arglen != sizeof(uint32_t)) {
+ error = EINVAL;
+ break;
+ }
+ label = *((uint32_t *)msg->data);
+ if (label > MPLS_LABEL_MAX) {
+ error = EINVAL;
+ break;
+ }
+ le = ng_mpls_find_label(priv, label);
+ if (le == NULL) {
+ error = ENOENT;
+ break;
+ }
+ LIST_REMOVE(le, entries);
+ NG_HOOK_SET_PRIVATE(le->hook, NULL);
+ free(le, M_NETGRAPH);
+ break;
+
+ case NGM_MPLS_GET_TABLE:
+ label_count = 0;
+ LIST_FOREACH(le, &priv->label_list, entries) {
+ if (NG_HOOK_IS_VALID(le->hook))
+ label_count++;
+ }
+
+ NG_MKRESPONSE(resp, msg, sizeof(*t) +
+ label_count * sizeof(*t->label), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+
+ t = (struct ng_mpls_table *)resp->data;
+ t->n = 0;
+ ml = &t->label[0];
+ LIST_FOREACH(le, &priv->label_list, entries) {
+ if (NG_HOOK_NOT_VALID(le->hook))
+ continue;
+
+ ml->label = le->label;
+ ml->action = le->action;
+ ml->swap_label = le->swap_label;
+ ml->exp = 0;
+ ml->ttl = le->ttl;
+ ml->ttl_handling = le->ttl_handling;
+ ml->push_count = le->push_count;
+ memcpy(ml->push_labels, le->push_labels,
+ le->push_count * sizeof(uint32_t));
+ memset(ml->push_labels + le->push_count, 0,
+ (MPLS_MAX_LABEL_STACK - le->push_count) *
+ sizeof(uint32_t));
+ strncpy(ml->hook_name,
+ NG_HOOK_NAME(le->hook), NG_HOOKSIZ);
+ ml++;
+ t->n++;
+ }
+ break;
+
+ case NGM_MPLS_GET_DECAP:
+ NG_MKRESPONSE(resp, msg, sizeof(uint32_t), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ *((uint32_t *)resp->data) = priv->decap_enable;
+ break;
+
+ case NGM_MPLS_SET_DECAP:
+ if (msg->header.arglen != sizeof(uint32_t)) {
+ error = EINVAL;
+ break;
+ }
+ priv->decap_enable = *((uint32_t *)msg->data);
+ break;
+
+ case NGM_MPLS_GET_ENCAP:
+ NG_MKRESPONSE(resp, msg, sizeof(uint32_t), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ *((uint32_t *)resp->data) = priv->encap_enable;
+ break;
+
+ case NGM_MPLS_SET_ENCAP:
+ if (msg->header.arglen != sizeof(uint32_t)) {
+ error = EINVAL;
+ break;
+ }
+ priv->encap_enable = *((uint32_t *)msg->data);
+ break;
+
+ case NGM_MPLS_GET_STATS:
+ {
+ struct ng_mpls_stats *stats;
+
+ NG_MKRESPONSE(resp, msg, sizeof(*stats), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ stats = (struct ng_mpls_stats *)resp->data;
+ stats->packets_in = priv->packets_in;
+ stats->bytes_in = priv->bytes_in;
+ stats->packets_out = priv->packets_out;
+ stats->bytes_out = priv->bytes_out;
+ stats->packets_dropped = priv->packets_dropped;
+ stats->packets_nomatch = priv->packets_nomatch;
+ break;
+ }
+
+ case NGM_MPLS_CLR_STATS:
+ priv->packets_in = 0;
+ priv->bytes_in = 0;
+ priv->packets_out = 0;
+ priv->bytes_out = 0;
+ priv->packets_dropped = 0;
+ priv->packets_nomatch = 0;
+ LIST_FOREACH(le, &priv->label_list, entries) {
+ le->packets = 0;
+ le->bytes = 0;
+ }
+ LIST_FOREACH(ne, &priv->nhlfe_list, entries) {
+ ne->packets = 0;
+ ne->bytes = 0;
+ }
+ break;
+
+ case NGM_MPLS_ADD_NHLFE:
+ if (msg->header.arglen < sizeof(*ml)) {
+ error = EINVAL;
+ break;
+ }
+ ml = (struct ng_mpls_label *)msg->data;
+
+ if (ml->push_count > MPLS_MAX_LABEL_STACK) {
+ error = EINVAL;
+ break;
+ }
+
+ hook = ng_findhook(node, ml->hook_name);
+ if (hook == NULL) {
+ error = ENOENT;
+ break;
+ }
+
+ ne = malloc(sizeof(*ne), M_NETGRAPH,
+ M_NOWAIT | M_ZERO);
+ if (ne == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ ne->id = priv->next_nhlfe_id++;
+ ne->hook = hook;
+ ne->action = ml->action;
+ ne->swap_label = ml->swap_label;
+ ne->push_count = ml->push_count;
+ memcpy(ne->push_labels, ml->push_labels,
+ ml->push_count * sizeof(uint32_t));
+ LIST_INSERT_HEAD(&priv->nhlfe_list, ne, entries);
+ break;
+
+ case NGM_MPLS_GET_NHLFE_TABLE:
+ {
+ int nhlfe_count = 0;
+
+ LIST_FOREACH(ne, &priv->nhlfe_list, entries)
+ nhlfe_count++;
+
+ NG_MKRESPONSE(resp, msg, sizeof(*nt) +
+ nhlfe_count * sizeof(*nt->entry), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+
+ nt = (struct ng_mpls_nhlfe_table *)resp->data;
+ nt->n = 0;
+ nte = &nt->entry[0];
+ LIST_FOREACH(ne, &priv->nhlfe_list, entries) {
+ strncpy(nte->hook_name,
+ NG_HOOK_NAME(ne->hook), NG_HOOKSIZ);
+ nte->action = ne->action;
+ nte->swap_label = ne->swap_label;
+ nte->push_count = ne->push_count;
+ memcpy(nte->push_labels, ne->push_labels,
+ ne->push_count * sizeof(uint32_t));
+ nte->packets = ne->packets;
+ nte->bytes = ne->bytes;
+ nte++;
+ nt->n++;
+ }
+ break;
+ }
+
+ case NGM_MPLS_DEL_NHLFE:
+ if (msg->header.arglen != sizeof(uint32_t)) {
+ error = EINVAL;
+ break;
+ }
+ label = *((uint32_t *)msg->data);
+ ne = ng_mpls_find_nhlfe(priv, label);
+ if (ne == NULL) {
+ error = ENOENT;
+ break;
+ }
+ LIST_REMOVE(ne, entries);
+ free(ne, M_NETGRAPH);
+ break;
+
+ default:
+ error = EINVAL;
+ break;
+ }
+ break;
+
+ default:
+ error = EINVAL;
+ break;
+ }
+ NG_RESPOND_MSG(error, node, item, resp);
+ NG_FREE_MSG(msg);
+ return (error);
+}
+
+/*
+ * Process MPLS packets arriving from the downstream hook.
+ * Walks the label stack applying ILM entries for each label.
+ */
+ static int
+ng_mpls_rcvdata_downstream(priv_p priv, item_p item, struct mbuf *m)
+{
+ struct ether_header *eh;
+ struct mpls_label_entry *le;
+ hook_p dst_hook;
+ int offset;
+ int error;
+ uint32_t shim;
+ uint32_t label;
+ int s_bit;
+ int remaining_labels;
+
+ if (m->m_pkthdr.len < sizeof(struct ether_header)) {
+ priv->packets_dropped++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (EINVAL);
+ }
+ if (m->m_len < sizeof(struct ether_header) &&
+ (m = m_pullup(m, sizeof(struct ether_header))) == NULL) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+
+ eh = mtod(m, struct ether_header *);
+
+ if (ntohs(eh->ether_type) != ETHERTYPE_MPLS &&
+ ntohs(eh->ether_type) != ETHERTYPE_MPLS_MCAST) {
+ dst_hook = priv->nomatch_hook;
+ priv->packets_nomatch++;
+ if (dst_hook == NULL) {
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+
+ priv->packets_in++;
+ priv->bytes_in += m->m_pkthdr.len;
+ dst_hook = priv->nomatch_hook;
+
+ offset = sizeof(struct ether_header);
+ remaining_labels = MPLS_MAX_LABEL_STACK;
+
+ while (m->m_pkthdr.len >= offset + MPLS_SHIM_LEN &&
+ remaining_labels > 0) {
+ if (m->m_len < offset + MPLS_SHIM_LEN &&
+ (m = m_pullup(m, offset + MPLS_SHIM_LEN)) == NULL) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+ eh = mtod(m, struct ether_header *);
+
+ shim = *(uint32_t *)((char *)eh + offset);
+ label = MPLS_LABEL(shim);
+ s_bit = MPLS_S(shim);
+ remaining_labels--;
+
+ if (label == MPLS_LABEL_IMPLICIT_NULL) {
+ if (ng_mpls_remove_shim(&m, offset)) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+ if (s_bit) {
+ ng_mpls_fix_ether_type(&m);
+ dst_hook = priv->nomatch_hook;
+ priv->packets_nomatch++;
+ if (dst_hook == NULL) {
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+ continue;
+ }
+
+ if (label == MPLS_LABEL_IPV4_EXPLICIT_NULL ||
+ label == MPLS_LABEL_IPV6_EXPLICIT_NULL) {
+ if (ng_mpls_remove_shim(&m, offset)) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+ ng_mpls_fix_ether_type(&m);
+ dst_hook = priv->nomatch_hook;
+ priv->packets_nomatch++;
+ if (dst_hook == NULL) {
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+
+ if (label == MPLS_LABEL_ROUTER_ALERT ||
+ label == MPLS_LABEL_OAM_ALERT) {
+ if (ng_mpls_remove_shim(&m, offset)) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+ continue;
+ }
+
+ le = ng_mpls_find_label(priv, label);
+ if (le == NULL || !NG_HOOK_IS_VALID(le->hook)) {
+ dst_hook = priv->nomatch_hook;
+ priv->packets_nomatch++;
+ if (dst_hook == NULL) {
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+
+ dst_hook = le->hook;
+ le->packets++;
+ le->bytes += m->m_pkthdr.len;
+
+ switch (le->action) {
+ case NG_MPLS_ACTION_POP:
+ if (ng_mpls_remove_shim(&m, offset)) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+ if (s_bit) {
+ ng_mpls_fix_ether_type(&m);
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+ continue;
+
+ case NG_MPLS_ACTION_SWAP: {
+ uint32_t new_shim;
+
+ new_shim = MPLS_MAKE(le->swap_label,
+ MPLS_TC(shim), s_bit,
+ MPLS_TTL(shim) - 1);
+ *(uint32_t *)((char *)eh + offset) = new_shim;
+
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+
+ case NG_MPLS_ACTION_PHP:
+ if (ng_mpls_remove_shim(&m, offset)) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+ if (!s_bit &&
+ m->m_pkthdr.len >= offset + MPLS_SHIM_LEN &&
+ m->m_len < offset + MPLS_SHIM_LEN &&
+ (m = m_pullup(m, offset +
+ MPLS_SHIM_LEN)) == NULL) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+ if (s_bit)
+ ng_mpls_fix_ether_type(&m);
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+
+ case NG_MPLS_ACTION_SWAP_AND_PUSH: {
+ struct ether_header eh_save;
+ int inner_off = offset + MPLS_SHIM_LEN;
+ int i;
+
+ memcpy(&eh_save, eh, sizeof(struct ether_header));
+ m_adj(m, inner_off);
+
+ for (i = le->push_count - 1; i >= 0; i--) {
+ int in_s = (i == le->push_count - 1) ?
+ s_bit : 0;
+ M_PREPEND(m, MPLS_SHIM_LEN, M_NOWAIT);
+ if (m == NULL) {
+ NG_FREE_ITEM(item);
+ return (ENOMEM);
+ }
+ *(uint32_t *)mtod(m, uint32_t *) =
+ MPLS_MAKE(le->push_labels[i],
+ 0, in_s, le->ttl);
+ }
+
+ M_PREPEND(m, MPLS_SHIM_LEN, M_NOWAIT);
+ if (m == NULL) {
+ NG_FREE_ITEM(item);
+ return (ENOMEM);
+ }
+ *(uint32_t *)mtod(m, uint32_t *) =
+ MPLS_MAKE(le->swap_label,
+ MPLS_TC(shim), 0, MPLS_TTL(shim) - 1);
+
+ M_PREPEND(m, sizeof(struct ether_header), M_NOWAIT);
+ if (m == NULL) {
+ NG_FREE_ITEM(item);
+ return (ENOMEM);
+ }
+ memcpy(mtod(m, struct ether_header *),
+ &eh_save, sizeof(struct ether_header));
+ mtod(m, struct ether_header *)->ether_type =
+ htons(ETHERTYPE_MPLS);
+
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+
+ case NG_MPLS_ACTION_NONE:
+ default:
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+ }
+
+ if (remaining_labels == 0) {
+ priv->packets_dropped++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (E2BIG);
+ }
+
+ priv->packets_nomatch++;
+ if (dst_hook == NULL) {
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+}
+
+/*
+ * Process packets arriving from filter hooks (encap path).
+ * Pushes the label stack and forwards to downstream.
+ */
+static int
+ng_mpls_rcvdata_encap(priv_p priv, hook_p hook, item_p item, struct mbuf *m)
+{
+ struct mpls_label_entry *le;
+ hook_p dst_hook;
+ int error;
+
+ dst_hook = priv->downstream_hook;
+ if (dst_hook == NULL) {
+ priv->packets_dropped++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (ENETDOWN);
+ }
+
+ priv->packets_out++;
+ priv->bytes_out += m->m_pkthdr.len;
+
+ if (hook == priv->nomatch_hook) {
+ if ((priv->encap_enable & MPLS_ENCAP_FROM_NOMATCH) == 0) {
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+
+ le = NG_HOOK_PRIVATE(hook);
+ if (le == NULL) {
+ priv->packets_dropped++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (EOPNOTSUPP);
+ }
+
+ if ((priv->encap_enable & MPLS_ENCAP_FROM_FILTER) == 0) {
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+
+ le->packets++;
+ le->bytes += m->m_pkthdr.len;
+
+ if (le->push_count > 0) {
+ error = ng_mpls_push_label_stack(&m,
+ le->push_labels, le->push_count);
+ if (error) {
+ NG_FREE_ITEM(item);
+ return (error);
+ }
+ } else {
+ error = ng_mpls_push_label(&m, le->label, 1);
+ if (error) {
+ NG_FREE_ITEM(item);
+ return (error);
+ }
+ }
+
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+}
+
+static int
+ng_mpls_rcvdata(hook_p hook, item_p item)
+{
+ const priv_p priv = NG_NODE_PRIVATE(NG_HOOK_NODE(hook));
+ struct mbuf *m;
+
+ NGI_GET_M(item, m);
+
+ if (hook == priv->downstream_hook)
+ return (ng_mpls_rcvdata_downstream(priv, item, m));
+
+ return (ng_mpls_rcvdata_encap(priv, hook, item, m));
+}
+
+static int
+ng_mpls_shutdown(node_p node)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct mpls_label_entry *le;
+ struct mpls_nhlfe_entry *ne;
+
+ while (!LIST_EMPTY(&priv->label_list)) {
+ le = LIST_FIRST(&priv->label_list);
+ LIST_REMOVE(le, entries);
+ free(le, M_NETGRAPH);
+ }
+
+ while (!LIST_EMPTY(&priv->nhlfe_list)) {
+ ne = LIST_FIRST(&priv->nhlfe_list);
+ LIST_REMOVE(ne, entries);
+ free(ne, M_NETGRAPH);
+ }
+
+ NG_NODE_SET_PRIVATE(node, NULL);
+ NG_NODE_UNREF(node);
+ free(priv, M_NETGRAPH);
+ return (0);
+}
+
+static int
+ng_mpls_disconnect(hook_p hook)
+{
+ const priv_p priv = NG_NODE_PRIVATE(NG_HOOK_NODE(hook));
+ struct mpls_label_entry *le;
+ struct mpls_nhlfe_entry *ne, *ne_tmp;
+
+ if (hook == priv->downstream_hook) {
+ priv->downstream_hook = NULL;
+ } else if (hook == priv->nomatch_hook) {
+ priv->nomatch_hook = NULL;
+ } else {
+ le = NG_HOOK_PRIVATE(hook);
+ if (le != NULL) {
+ LIST_REMOVE(le, entries);
+ free(le, M_NETGRAPH);
+ }
+ }
+
+ LIST_FOREACH_SAFE(ne, &priv->nhlfe_list, entries, ne_tmp) {
+ if (ne->hook == hook) {
+ LIST_REMOVE(ne, entries);
+ free(ne, M_NETGRAPH);
+ }
+ }
+
+ NG_HOOK_SET_PRIVATE(hook, NULL);
+ if ((NG_NODE_NUMHOOKS(NG_HOOK_NODE(hook)) == 0) &&
+ (NG_NODE_IS_VALID(NG_HOOK_NODE(hook))))
+ ng_rmnode_self(NG_HOOK_NODE(hook));
+ return (0);
+}
diff --git a/sys/netgraph/ng_mpls.h b/sys/netgraph/ng_mpls.h
new file mode 100644
index 00000000000..d02385ec62c
--- /dev/null
+++ b/sys/netgraph/ng_mpls.h
@@ -0,0 +1,199 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef _NETGRAPH_NG_MPLS_H_
+#define _NETGRAPH_NG_MPLS_H_
+
+/* Node type name and magic cookie. */
+#define NG_MPLS_NODE_TYPE "mpls"
+#define NGM_MPLS_COOKIE 1794513802
+
+/* Hook names. */
+#define NG_MPLS_HOOK_DOWNSTREAM "downstream"
+#define NG_MPLS_HOOK_NOMATCH "nomatch"
+
+/* MPLS shim header macros (big-endian encoding). */
+#define MPLS_LABEL_MASK 0xfffff000
+#define MPLS_LABEL_SHIFT 12
+#define MPLS_TC_MASK 0x00000e00
+#define MPLS_TC_SHIFT 9
+#define MPLS_S_MASK 0x00000100
+#define MPLS_S_SHIFT 8
+#define MPLS_TTL_MASK 0x000000ff
+#define MPLS_TTL_SHIFT 0
+
+#define MPLS_LABEL(val) ((ntohl(val) >> MPLS_LABEL_SHIFT) & 0xfffff)
+#define MPLS_TC(val) ((ntohl(val) >> MPLS_TC_SHIFT) & 0x7)
+#define MPLS_S(val) ((ntohl(val) >> MPLS_S_SHIFT) & 0x1)
+#define MPLS_TTL(val) (ntohl(val) & MPLS_TTL_MASK)
+#define MPLS_MAKE(l, tc, s, ttl) \
+ htonl((((uint32_t)(l) & 0xfffff) << MPLS_LABEL_SHIFT) | \
+ (((uint32_t)(tc) & 0x7) << MPLS_TC_SHIFT) | \
+ (((uint32_t)(s) & 0x1) << MPLS_S_SHIFT) | \
+ ((uint32_t)(ttl) & MPLS_TTL_MASK))
+
+/* MPLS shim header size (4 bytes). */
+#define MPLS_SHIM_LEN 4
+
+/* Maximum MPLS label value (20 bits). */
+#define MPLS_LABEL_MAX 0xfffff
+
+/* Well-known reserved MPLS label values. */
+#define MPLS_LABEL_IPV4_EXPLICIT_NULL 0
+#define MPLS_LABEL_ROUTER_ALERT 1
+#define MPLS_LABEL_IPV6_EXPLICIT_NULL 2
+#define MPLS_LABEL_IMPLICIT_NULL 3
+#define MPLS_LABEL_OAM_ALERT 14
+
+/* Maximum label stack depth we support. */
+#define MPLS_MAX_LABEL_STACK 8
+
+/* Label actions. */
+#define NG_MPLS_ACTION_NONE 0
+#define NG_MPLS_ACTION_POP 1
+#define NG_MPLS_ACTION_SWAP 2
+#define NG_MPLS_ACTION_PHP 3
+#define NG_MPLS_ACTION_SWAP_AND_PUSH 4
+
+/* TTL handling modes. */
+#define NG_MPLS_TTL_UNIFORM 0
+#define NG_MPLS_TTL_PIPE 1
+
+/* Encapsulation modes. */
+#define MPLS_ENCAP_FROM_FILTER 0x00000001
+#define MPLS_ENCAP_FROM_NOMATCH 0x00000002
+
+/* Netgraph commands. */
+enum {
+ NGM_MPLS_ADD_LABEL = 1,
+ NGM_MPLS_DEL_LABEL,
+ NGM_MPLS_GET_TABLE,
+ NGM_MPLS_DEL_LABEL_VAL,
+ NGM_MPLS_GET_DECAP,
+ NGM_MPLS_SET_DECAP,
+ NGM_MPLS_GET_ENCAP,
+ NGM_MPLS_SET_ENCAP,
+ NGM_MPLS_GET_STATS,
+ NGM_MPLS_CLR_STATS,
+ NGM_MPLS_ADD_NHLFE,
+ NGM_MPLS_GET_NHLFE_TABLE,
+ NGM_MPLS_DEL_NHLFE,
+};
+
+/* For NGM_MPLS_ADD_LABEL and NGM_MPLS_ADD_NHLFE control messages. */
+struct ng_mpls_label {
+ char hook_name[NG_HOOKSIZ];
+ uint32_t label;
+ uint32_t action;
+ uint32_t swap_label;
+ uint8_t exp;
+ uint8_t ttl;
+ uint8_t ttl_handling;
+ uint8_t push_count;
+ uint32_t push_labels[MPLS_MAX_LABEL_STACK];
+};
+
+#define NG_MPLS_LABEL_FIELDS { \
+ { "hook", &ng_parse_hookbuf_type }, \
+ { "label", &ng_parse_uint32_type }, \
+ { "action", &ng_parse_uint32_type }, \
+ { "swap_label", &ng_parse_uint32_type }, \
+ { "exp", &ng_parse_uint8_type }, \
+ { "ttl", &ng_parse_uint8_type }, \
+ { "ttl_handling", &ng_parse_uint8_type }, \
+ { "push_count", &ng_parse_uint8_type }, \
+ { "push_labels", &ng_mpls_push_stack_type }, \
+ { NULL } \
+}
+
+/* Structure returned by NGM_MPLS_GET_TABLE. */
+struct ng_mpls_table {
+ uint32_t n;
+ struct ng_mpls_label label[];
+};
+
+#define NG_MPLS_TABLE_FIELDS { \
+ { "n", &ng_parse_uint32_type }, \
+ { "label", &ng_mpls_table_array_type }, \
+ { NULL } \
+}
+
+/* NHLFE entry returned by NGM_MPLS_GET_NHLFE_TABLE. */
+struct ng_mpls_nhlfe_entry {
+ char hook_name[NG_HOOKSIZ];
+ uint32_t action;
+ uint32_t swap_label;
+ uint8_t push_count;
+ uint32_t push_labels[MPLS_MAX_LABEL_STACK];
+ uint64_t packets;
+ uint64_t bytes;
+};
+
+#define NG_MPLS_NHLFE_ENTRY_FIELDS { \
+ { "hook", &ng_parse_hookbuf_type }, \
+ { "action", &ng_parse_uint32_type }, \
+ { "swap_label", &ng_parse_uint32_type }, \
+ { "push_count", &ng_parse_uint8_type }, \
+ { "push_labels", &ng_mpls_push_stack_type }, \
+ { "packets", &ng_parse_uint64_type }, \
+ { "bytes", &ng_parse_uint64_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_nhlfe_table {
+ uint32_t n;
+ struct ng_mpls_nhlfe_entry entry[];
+};
+
+#define NG_MPLS_NHLFE_TABLE_FIELDS { \
+ { "n", &ng_parse_uint32_type }, \
+ { "entry", &ng_mpls_nhlfe_table_array_type }, \
+ { NULL } \
+}
+
+/* Structure returned by NGM_MPLS_GET_STATS. */
+struct ng_mpls_stats {
+ uint64_t packets_in;
+ uint64_t bytes_in;
+ uint64_t packets_out;
+ uint64_t bytes_out;
+ uint64_t packets_dropped;
+ uint64_t packets_nomatch;
+};
+
+#define NG_MPLS_STATS_FIELDS { \
+ { "packets_in", &ng_parse_uint64_type }, \
+ { "bytes_in", &ng_parse_uint64_type }, \
+ { "packets_out", &ng_parse_uint64_type }, \
+ { "bytes_out", &ng_parse_uint64_type }, \
+ { "packets_dropped", &ng_parse_uint64_type }, \
+ { "packets_nomatch", &ng_parse_uint64_type }, \
+ { NULL } \
+}
+
+#endif /* _NETGRAPH_NG_MPLS_H_ */
diff --git a/sys/netgraph/ng_mpls_fec.c b/sys/netgraph/ng_mpls_fec.c
new file mode 100644
index 00000000000..0e8588bd7ab
--- /dev/null
+++ b/sys/netgraph/ng_mpls_fec.c
@@ -0,0 +1,736 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/errno.h>
+#include <sys/kernel.h>
+#include <sys/malloc.h>
+#include <sys/mbuf.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/systm.h>
+
+#include <net/ethernet.h>
+#include <net/if.h>
+#include <net/if_var.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+
+#include <netgraph/ng_message.h>
+#include <netgraph/ng_parse.h>
+#include <netgraph/ng_mpls.h>
+#include <netgraph/ng_mpls_fec.h>
+#include <netgraph/netgraph.h>
+
+struct fec_entry {
+ LIST_ENTRY(fec_entry) entries;
+ hook_p output_hook;
+ uint8_t family;
+ uint32_t prefix[4];
+ uint8_t prefix_len;
+ uint8_t push_count;
+ uint32_t push_labels[MPLS_FEC_MAX_LABELS];
+ uint8_t ttl;
+ uint64_t packets;
+ uint64_t bytes;
+};
+
+struct ng_mpls_fec_private {
+ hook_p downstream_hook;
+ hook_p nomatch_hook;
+ hook_p mpls_hook;
+ uint64_t classify_ok;
+ uint64_t classify_fail;
+ uint64_t packets_out;
+ uint64_t bytes_out;
+ LIST_HEAD(, fec_entry) fec_list;
+};
+typedef struct ng_mpls_fec_private *priv_p;
+
+static MALLOC_DEFINE(M_NETGRAPH_MPLS_FEC, "ng_mpls_fec",
+ "netgraph mpls fec node");
+
+static ng_constructor_t ng_mpls_fec_constructor;
+static ng_rcvmsg_t ng_mpls_fec_rcvmsg;
+static ng_shutdown_t ng_mpls_fec_shutdown;
+static ng_newhook_t ng_mpls_fec_newhook;
+static ng_rcvdata_t ng_mpls_fec_rcvdata;
+static ng_disconnect_t ng_mpls_fec_disconnect;
+
+static struct fec_entry *ng_mpls_fec_lookup(priv_p, uint8_t,
+ const uint32_t *);
+static int ng_mpls_fec_add(priv_p,
+ const struct ng_mpls_fec_entry *,
+ hook_p);
+static int ng_mpls_fec_del(priv_p, uint8_t,
+ const uint32_t *, uint8_t);
+static struct fec_entry *ng_mpls_fec_classify(priv_p, struct mbuf **,
+ int *);
+static int ng_mpls_fec_getTableLength(
+ const struct ng_parse_type *,
+ const u_char *, const u_char *);
+
+static int
+ng_mpls_fec_getPushStackLength(const struct ng_parse_type *type __unused,
+ const u_char *start __unused, const u_char *buf __unused)
+{
+ return (MPLS_FEC_MAX_LABELS);
+}
+
+/* Parse type for the fixed-length push_labels array. */
+static const struct ng_parse_array_info ng_mpls_fec_push_stack_info = {
+ &ng_parse_uint32_type,
+ ng_mpls_fec_getPushStackLength
+};
+static const struct ng_parse_type ng_mpls_fec_push_stack_type = {
+ &ng_parse_array_type,
+ &ng_mpls_fec_push_stack_info
+};
+
+static const struct ng_parse_struct_field fec_entry_fields[] =
+ NG_MPLS_FEC_ENTRY_FIELDS;
+static const struct ng_parse_type fec_entry_type = {
+ &ng_parse_struct_type,
+ &fec_entry_fields
+};
+
+static const struct ng_parse_array_info fec_table_array_info = {
+ &fec_entry_type,
+ ng_mpls_fec_getTableLength
+};
+static const struct ng_parse_type ng_mpls_fec_table_array_type = {
+ &ng_parse_array_type,
+ &fec_table_array_info
+};
+
+static const struct ng_parse_struct_field fec_table_fields[] =
+ NG_MPLS_FEC_TABLE_FIELDS;
+static const struct ng_parse_type fec_table_type = {
+ &ng_parse_struct_type,
+ &fec_table_fields
+};
+
+static const struct ng_parse_struct_field fec_stats_fields[] =
+ NG_MPLS_FEC_STATS_FIELDS;
+static const struct ng_parse_type fec_stats_type = {
+ &ng_parse_struct_type,
+ &fec_stats_fields
+};
+
+static const struct ng_cmdlist ng_mpls_fec_cmdlist[] = {
+ {
+ NGM_MPLS_FEC_COOKIE,
+ NGM_MPLS_FEC_ADD_FEC,
+ "addfec",
+ &fec_entry_type,
+ NULL
+ },
+ {
+ NGM_MPLS_FEC_COOKIE,
+ NGM_MPLS_FEC_DEL_FEC,
+ "delfec",
+ &fec_entry_type,
+ NULL
+ },
+ {
+ NGM_MPLS_FEC_COOKIE,
+ NGM_MPLS_FEC_GET_TABLE,
+ "gettable",
+ NULL,
+ &fec_table_type
+ },
+ {
+ NGM_MPLS_FEC_COOKIE,
+ NGM_MPLS_FEC_GET_STATS,
+ "getstats",
+ NULL,
+ &fec_stats_type
+ },
+ {
+ NGM_MPLS_FEC_COOKIE,
+ NGM_MPLS_FEC_CLR_STATS,
+ "clrstats",
+ NULL,
+ NULL
+ },
+ { 0 }
+};
+
+static struct ng_type ng_mpls_fec_typestruct = {
+ .version = NG_ABI_VERSION,
+ .name = NG_MPLS_FEC_NODE_TYPE,
+ .constructor = ng_mpls_fec_constructor,
+ .rcvmsg = ng_mpls_fec_rcvmsg,
+ .shutdown = ng_mpls_fec_shutdown,
+ .newhook = ng_mpls_fec_newhook,
+ .rcvdata = ng_mpls_fec_rcvdata,
+ .disconnect = ng_mpls_fec_disconnect,
+ .cmdlist = ng_mpls_fec_cmdlist,
+};
+NETGRAPH_INIT(mpls_fec, &ng_mpls_fec_typestruct);
+
+/*
+ * Check whether a prefix matches a given address.
+ * prefix and addr are both in network byte order.
+ */
+static int
+fec_prefix_match(const uint32_t *prefix, int prefix_len,
+ const uint32_t *addr, int family)
+{
+ int words;
+ int i;
+
+ if (family == AF_INET)
+ words = 1;
+ else
+ words = 4;
+
+ for (i = 0; i < words && prefix_len > 0; i++) {
+ uint32_t mask;
+
+ if (prefix_len >= 32) {
+ mask = 0xffffffff;
+ prefix_len -= 32;
+ } else {
+ mask = htonl(0xffffffff << (32 - prefix_len));
+ prefix_len = 0;
+ }
+
+ if ((ntohl(prefix[i]) & ntohl(mask)) !=
+ (ntohl(addr[i]) & ntohl(mask)))
+ return (0);
+ }
+ return (1);
+}
+
+/*
+ * Longest-prefix-match lookup.
+ * Returns the best matching FEC entry, or NULL.
+ */
+static struct fec_entry *
+ng_mpls_fec_lookup(priv_p priv, uint8_t family, const uint32_t *addr)
+{
+ struct fec_entry *fe, *best = NULL;
+
+ LIST_FOREACH(fe, &priv->fec_list, entries) {
+ if (fe->family != family)
+ continue;
+ if (fec_prefix_match(fe->prefix, fe->prefix_len,
+ addr, family)) {
+ if (best == NULL ||
+ fe->prefix_len > best->prefix_len)
+ best = fe;
+ }
+ }
+ return (best);
+}
+
+static int
+ng_mpls_fec_add(priv_p priv, const struct ng_mpls_fec_entry *ent, hook_p hook)
+{
+ struct fec_entry *fe;
+
+ fe = malloc(sizeof(*fe), M_NETGRAPH_MPLS_FEC, M_NOWAIT | M_ZERO);
+ if (fe == NULL)
+ return (ENOMEM);
+
+ fe->output_hook = hook;
+ fe->family = ent->family;
+ fe->prefix_len = ent->prefix_len;
+ fe->push_count = ent->push_count;
+ fe->ttl = ent->ttl;
+ memcpy(fe->prefix, &ent->prefix, sizeof(uint32_t));
+ memcpy(fe->push_labels, ent->push_labels,
+ ent->push_count * sizeof(uint32_t));
+
+ LIST_INSERT_HEAD(&priv->fec_list, fe, entries);
+ return (0);
+}
+
+static int
+ng_mpls_fec_del(priv_p priv, uint8_t family,
+ const uint32_t *prefix, uint8_t prefix_len)
+{
+ struct fec_entry *fe;
+
+ LIST_FOREACH(fe, &priv->fec_list, entries) {
+ if (fe->family == family &&
+ fe->prefix_len == prefix_len &&
+ fe->prefix[0] == prefix[0]) {
+ LIST_REMOVE(fe, entries);
+ free(fe, M_NETGRAPH_MPLS_FEC);
+ return (0);
+ }
+ }
+ return (ENOENT);
+}
+
+/*
+ * Classify an mbuf against FEC entries.
+ * Parses the L3 header, extracts destination address, performs LPM lookup.
+ * Returns matching fec_entry, or NULL on no match / error.
+ * On error, *error_out is set (ENOENT, ENOBUFS, etc.).
+ * *m may be updated if m_pullup reallocates the mbuf.
+ */
+static struct fec_entry *
+ng_mpls_fec_classify(priv_p priv, struct mbuf **mp, int *error_out)
+{
+ struct mbuf *m = *mp;
+ struct ether_header *eh;
+ uint16_t etype;
+ uint32_t addr[4];
+ uint8_t family;
+ struct fec_entry *fe;
+
+ *error_out = 0;
+
+ if (m->m_pkthdr.len < sizeof(struct ether_header)) {
+ *error_out = ENOENT;
+ return (NULL);
+ }
+ if (m->m_len < sizeof(struct ether_header) &&
+ ((*mp = m = m_pullup(m, sizeof(struct ether_header))) == NULL)) {
+ *error_out = ENOBUFS;
+ return (NULL);
+ }
+
+ eh = mtod(m, struct ether_header *);
+ etype = ntohs(eh->ether_type);
+
+ switch (etype) {
+#ifdef INET
+ case ETHERTYPE_IP:
+ {
+ struct ip *ip;
+
+ if (m->m_pkthdr.len < sizeof(struct ether_header) +
+ sizeof(struct ip)) {
+ *error_out = ENOENT;
+ return (NULL);
+ }
+ if (m->m_len < sizeof(struct ether_header) + sizeof(struct ip) &&
+ ((*mp = m = m_pullup(m, sizeof(struct ether_header) +
+ sizeof(struct ip))) == NULL)) {
+ *error_out = ENOBUFS;
+ return (NULL);
+ }
+
+ eh = mtod(m, struct ether_header *);
+ ip = (struct ip *)(eh + 1);
+ addr[0] = ip->ip_dst.s_addr;
+ addr[1] = addr[2] = addr[3] = 0;
+ family = AF_INET;
+ break;
+ }
+#endif
+#ifdef INET6
+ case ETHERTYPE_IPV6:
+ {
+ struct ip6_hdr *ip6;
+
+ if (m->m_pkthdr.len < sizeof(struct ether_header) +
+ sizeof(struct ip6_hdr)) {
+ *error_out = ENOENT;
+ return (NULL);
+ }
+ if (m->m_len < sizeof(struct ether_header) +
+ sizeof(struct ip6_hdr) &&
+ ((*mp = m = m_pullup(m, sizeof(struct ether_header) +
+ sizeof(struct ip6_hdr))) == NULL)) {
+ *error_out = ENOBUFS;
+ return (NULL);
+ }
+
+ eh = mtod(m, struct ether_header *);
+ ip6 = (struct ip6_hdr *)(eh + 1);
+ memcpy(addr, &ip6->ip6_dst, sizeof(addr));
+ family = AF_INET6;
+ break;
+ }
+#endif
+ default:
+ *error_out = ENOENT;
+ return (NULL);
+ }
+
+ fe = ng_mpls_fec_lookup(priv, family, addr);
+ if (fe == NULL) {
+ *error_out = ENOENT;
+ return (NULL);
+ }
+
+ fe->packets++;
+ fe->bytes += m->m_pkthdr.len;
+ priv->classify_ok++;
+ return (fe);
+}
+
+static int
+ng_mpls_fec_getTableLength(const struct ng_parse_type *type __unused,
+ const u_char *start __unused, const u_char *buf)
+{
+ const struct ng_mpls_fec_table *const table =
+ (const struct ng_mpls_fec_table *)(buf - sizeof(uint32_t));
+
+ return (table->n);
+}
+
+static int
+ng_mpls_fec_constructor(node_p node)
+{
+ priv_p priv;
+
+ priv = malloc(sizeof(*priv), M_NETGRAPH_MPLS_FEC, M_WAITOK | M_ZERO);
+ LIST_INIT(&priv->fec_list);
+ NG_NODE_SET_PRIVATE(node, priv);
+ return (0);
+}
+
+static int
+ng_mpls_fec_newhook(node_p node, hook_p hook, const char *name)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+
+ if (strcmp(name, NG_MPLS_FEC_HOOK_DOWNSTREAM) == 0)
+ priv->downstream_hook = hook;
+ else if (strcmp(name, NG_MPLS_FEC_HOOK_NOMATCH) == 0)
+ priv->nomatch_hook = hook;
+ else if (strcmp(name, NG_MPLS_FEC_HOOK_MPLS) == 0)
+ priv->mpls_hook = hook;
+ else
+ return (EINVAL);
+
+ NG_HOOK_SET_PRIVATE(hook, NULL);
+ return (0);
+}
+
+static int
+ng_mpls_fec_rcvmsg(node_p node, item_p item, hook_p lasthook)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct ng_mesg *msg, *resp = NULL;
+ int error = 0;
+
+ NGI_GET_MSG(item, msg);
+ switch (msg->header.typecookie) {
+ case NGM_MPLS_FEC_COOKIE:
+ switch (msg->header.cmd) {
+ case NGM_MPLS_FEC_ADD_FEC:
+ {
+ struct ng_mpls_fec_entry *ent;
+ hook_p hook;
+
+ if (msg->header.arglen < sizeof(*ent)) {
+ error = EINVAL;
+ break;
+ }
+ ent = (struct ng_mpls_fec_entry *)msg->data;
+
+ if (ent->push_count > MPLS_FEC_MAX_LABELS) {
+ error = EINVAL;
+ break;
+ }
+ if (ent->family != AF_INET &&
+ ent->family != AF_INET6) {
+ error = EAFNOSUPPORT;
+ break;
+ }
+
+ hook = ng_findhook(node, ent->hook_name);
+ if (hook == NULL) {
+ error = ENOENT;
+ break;
+ }
+
+ error = ng_mpls_fec_add(priv, ent, hook);
+ break;
+ }
+
+ case NGM_MPLS_FEC_DEL_FEC:
+ {
+ struct ng_mpls_fec_entry *ent;
+
+ if (msg->header.arglen < sizeof(*ent)) {
+ error = EINVAL;
+ break;
+ }
+ ent = (struct ng_mpls_fec_entry *)msg->data;
+
+ error = ng_mpls_fec_del(priv, ent->family,
+ &ent->prefix, ent->prefix_len);
+ break;
+ }
+
+ case NGM_MPLS_FEC_GET_TABLE:
+ {
+ struct ng_mpls_fec_table *table;
+ struct ng_mpls_fec_entry *fe_out;
+ struct fec_entry *fe;
+ int count = 0;
+
+ LIST_FOREACH(fe, &priv->fec_list, entries)
+ count++;
+
+ NG_MKRESPONSE(resp, msg, sizeof(*table) +
+ count * sizeof(*table->entry), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+
+ table = (struct ng_mpls_fec_table *)resp->data;
+ table->n = 0;
+ fe_out = &table->entry[0];
+ LIST_FOREACH(fe, &priv->fec_list, entries) {
+ strncpy(fe_out->hook_name,
+ NG_HOOK_NAME(fe->output_hook),
+ NG_HOOKSIZ);
+ fe_out->family = fe->family;
+ fe_out->prefix_len = fe->prefix_len;
+ fe_out->push_count = fe->push_count;
+ fe_out->ttl = fe->ttl;
+ memcpy(&fe_out->prefix, fe->prefix,
+ sizeof(uint32_t));
+ memcpy(fe_out->push_labels, fe->push_labels,
+ fe->push_count * sizeof(uint32_t));
+ fe_out++;
+ table->n++;
+ }
+ break;
+ }
+
+ case NGM_MPLS_FEC_GET_STATS:
+ {
+ struct ng_mpls_fec_stats *stats;
+
+ NG_MKRESPONSE(resp, msg, sizeof(*stats), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ stats = (struct ng_mpls_fec_stats *)resp->data;
+ stats->classify_ok = priv->classify_ok;
+ stats->classify_fail = priv->classify_fail;
+ stats->packets_out = priv->packets_out;
+ stats->bytes_out = priv->bytes_out;
+ break;
+ }
+
+ case NGM_MPLS_FEC_CLR_STATS:
+ priv->classify_ok = 0;
+ priv->classify_fail = 0;
+ priv->packets_out = 0;
+ priv->bytes_out = 0;
+ break;
+
+ default:
+ error = EINVAL;
+ break;
+ }
+ break;
+
+ default:
+ error = EINVAL;
+ break;
+ }
+ NG_RESPOND_MSG(error, node, item, resp);
+ NG_FREE_MSG(msg);
+ return (error);
+}
+
+/*
+ * Prepend an MPLS shim to the packet, preserving the Ethernet header.
+ */
+static int
+ng_mpls_fec_push_label(struct mbuf **m, uint32_t label_value, int s_bit)
+{
+ struct ether_header *eh;
+ uint8_t dmac[ETHER_ADDR_LEN];
+ uint8_t smac[ETHER_ADDR_LEN];
+ uint32_t shim;
+
+ if ((*m)->m_pkthdr.len < sizeof(struct ether_header))
+ return (EINVAL);
+ if ((*m)->m_len < sizeof(struct ether_header) &&
+ (*m = m_pullup(*m, sizeof(struct ether_header))) == NULL)
+ return (ENOBUFS);
+
+ eh = mtod(*m, struct ether_header *);
+ memcpy(dmac, eh->ether_dhost, ETHER_ADDR_LEN);
+ memcpy(smac, eh->ether_shost, ETHER_ADDR_LEN);
+
+ M_PREPEND((*m), MPLS_SHIM_LEN, M_NOWAIT);
+ if ((*m) == NULL)
+ return (ENOMEM);
+
+ if ((*m)->m_len < sizeof(struct ether_header) + MPLS_SHIM_LEN &&
+ (*m = m_pullup(*m, sizeof(struct ether_header) +
+ MPLS_SHIM_LEN)) == NULL)
+ return (ENOBUFS);
+
+ eh = mtod(*m, struct ether_header *);
+ memcpy(eh->ether_dhost, dmac, ETHER_ADDR_LEN);
+ memcpy(eh->ether_shost, smac, ETHER_ADDR_LEN);
+ eh->ether_type = htons(ETHERTYPE_MPLS);
+
+ shim = MPLS_MAKE(label_value, 0, s_bit, 64);
+ *(uint32_t *)(eh + 1) = shim;
+
+ return (0);
+}
+
+/*
+ * Push a label stack onto the packet, innermost first.
+ */
+static int
+ng_mpls_fec_push_stack(struct mbuf **m, const uint32_t *labels,
+ int count, uint8_t ttl)
+{
+ int i;
+ int error;
+
+ for (i = 0; i < count; i++) {
+ int s = (i == 0) ? 1 : 0;
+ error = ng_mpls_fec_push_label(m, labels[i], s);
+ if (error)
+ return (error);
+ }
+ return (0);
+}
+
+/*
+ * Data from the downstream (CE-facing) hook.
+ * Classify, push MPLS label stack, forward to mpls hook.
+ */
+static int
+ng_mpls_fec_rcvdata_downstream(priv_p priv, item_p item, struct mbuf *m)
+{
+ struct fec_entry *fe;
+ int error;
+
+ fe = ng_mpls_fec_classify(priv, &m, &error);
+ if (fe == NULL) {
+ priv->classify_fail++;
+ if (error == ENOENT) {
+ if (priv->nomatch_hook != NULL) {
+ NG_FWD_NEW_DATA(error, item,
+ priv->nomatch_hook, m);
+ return (error);
+ }
+ }
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+
+ if (fe->output_hook == priv->mpls_hook && fe->push_count > 0) {
+ error = ng_mpls_fec_push_stack(&m,
+ fe->push_labels, fe->push_count, fe->ttl);
+ if (error) {
+ NG_FREE_ITEM(item);
+ return (error);
+ }
+ }
+
+ priv->packets_out++;
+ priv->bytes_out += m->m_pkthdr.len;
+
+ NG_FWD_NEW_DATA(error, item, fe->output_hook, m);
+ return (error);
+}
+
+static int
+ng_mpls_fec_rcvdata(hook_p hook, item_p item)
+{
+ const node_p node = NG_HOOK_NODE(hook);
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct mbuf *m;
+
+ NGI_GET_M(item, m);
+
+ if (hook == priv->downstream_hook)
+ return (ng_mpls_fec_rcvdata_downstream(priv, item, m));
+
+ if (hook == priv->nomatch_hook || hook == priv->mpls_hook) {
+ priv->classify_fail++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+
+ /* Data from a FEC-bound output hook (e.g., from mpls_fec -> mpls return). */
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+}
+
+static int
+ng_mpls_fec_shutdown(node_p node)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct fec_entry *fe;
+
+ while (!LIST_EMPTY(&priv->fec_list)) {
+ fe = LIST_FIRST(&priv->fec_list);
+ LIST_REMOVE(fe, entries);
+ free(fe, M_NETGRAPH_MPLS_FEC);
+ }
+
+ NG_NODE_SET_PRIVATE(node, NULL);
+ NG_NODE_UNREF(node);
+ free(priv, M_NETGRAPH_MPLS_FEC);
+ return (0);
+}
+
+static int
+ng_mpls_fec_disconnect(hook_p hook)
+{
+ const node_p node = NG_HOOK_NODE(hook);
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct fec_entry *fe, *fe_tmp;
+
+ if (hook == priv->downstream_hook)
+ priv->downstream_hook = NULL;
+ else if (hook == priv->nomatch_hook)
+ priv->nomatch_hook = NULL;
+ else if (hook == priv->mpls_hook)
+ priv->mpls_hook = NULL;
+ else {
+ LIST_FOREACH_SAFE(fe, &priv->fec_list, entries, fe_tmp) {
+ if (fe->output_hook == hook) {
+ LIST_REMOVE(fe, entries);
+ free(fe, M_NETGRAPH_MPLS_FEC);
+ }
+ }
+ }
+
+ if (NG_NODE_NUMHOOKS(node) == 0 && NG_NODE_IS_VALID(node))
+ ng_rmnode_self(node);
+ return (0);
+}
diff --git a/sys/netgraph/ng_mpls_fec.h b/sys/netgraph/ng_mpls_fec.h
new file mode 100644
index 00000000000..e2ba90fb164
--- /dev/null
+++ b/sys/netgraph/ng_mpls_fec.h
@@ -0,0 +1,96 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef _NETGRAPH_NG_MPLS_FEC_H_
+#define _NETGRAPH_NG_MPLS_FEC_H_
+
+#define NG_MPLS_FEC_NODE_TYPE "mpls_fec"
+#define NGM_MPLS_FEC_COOKIE 2837460195
+
+#define NG_MPLS_FEC_HOOK_DOWNSTREAM "downstream"
+#define NG_MPLS_FEC_HOOK_NOMATCH "nomatch"
+#define NG_MPLS_FEC_HOOK_MPLS "mpls"
+
+#define MPLS_FEC_MAX_LABELS 8
+
+struct ng_mpls_fec_entry {
+ char hook_name[NG_HOOKSIZ];
+ uint8_t family;
+ uint32_t prefix;
+ uint8_t prefix_len;
+ uint8_t push_count;
+ uint32_t push_labels[MPLS_FEC_MAX_LABELS];
+ uint8_t ttl;
+};
+
+#define NG_MPLS_FEC_ENTRY_FIELDS { \
+ { "hook", &ng_parse_hookbuf_type }, \
+ { "family", &ng_parse_uint8_type }, \
+ { "prefix", &ng_parse_ipaddr_type }, \
+ { "prefix_len", &ng_parse_uint8_type }, \
+ { "push_count", &ng_parse_uint8_type }, \
+ { "push_labels", &ng_mpls_fec_push_stack_type }, \
+ { "ttl", &ng_parse_uint8_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_fec_table {
+ uint32_t n;
+ struct ng_mpls_fec_entry entry[];
+};
+
+#define NG_MPLS_FEC_TABLE_FIELDS { \
+ { "n", &ng_parse_uint32_type }, \
+ { "entry", &ng_mpls_fec_table_array_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_fec_stats {
+ uint64_t classify_ok;
+ uint64_t classify_fail;
+ uint64_t packets_out;
+ uint64_t bytes_out;
+};
+
+#define NG_MPLS_FEC_STATS_FIELDS { \
+ { "classify_ok", &ng_parse_uint64_type }, \
+ { "classify_fail", &ng_parse_uint64_type }, \
+ { "packets_out", &ng_parse_uint64_type }, \
+ { "bytes_out", &ng_parse_uint64_type }, \
+ { NULL } \
+}
+
+enum {
+ NGM_MPLS_FEC_ADD_FEC = 1,
+ NGM_MPLS_FEC_DEL_FEC,
+ NGM_MPLS_FEC_GET_TABLE,
+ NGM_MPLS_FEC_GET_STATS,
+ NGM_MPLS_FEC_CLR_STATS,
+};
+
+#endif /* _NETGRAPH_NG_MPLS_FEC_H_ */
diff --git a/sys/netgraph/ng_mpls_lsp.c b/sys/netgraph/ng_mpls_lsp.c
new file mode 100644
index 00000000000..896b40d5b00
--- /dev/null
+++ b/sys/netgraph/ng_mpls_lsp.c
@@ -0,0 +1,531 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/kernel.h>
+#include <sys/malloc.h>
+#include <sys/mbuf.h>
+#include <sys/errno.h>
+#include <sys/socket.h>
+#include <sys/sockio.h>
+#include <sys/epoch.h>
+
+#include <net/if.h>
+#include <net/if_types.h>
+#include <net/if_var.h>
+#include <net/if_private.h>
+#include <net/if_dl.h>
+#include <net/ethernet.h>
+#include <net/bpf.h>
+#include <net/vnet.h>
+#include <net/netisr.h>
+#include <net/route.h>
+
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+
+#include <netgraph/ng_message.h>
+#include <netgraph/netgraph.h>
+#include <netgraph/ng_parse.h>
+#include <netgraph/ng_mpls.h>
+#include <netgraph/ng_mpls_lsp.h>
+
+struct ng_mpls_lsp_private;
+
+struct lsp_instance {
+ uint32_t lsp_id;
+ struct ifnet *ifp;
+ struct ng_mpls_lsp_private *priv;
+ uint32_t push_count;
+ uint32_t push_labels[NG_MPLS_LSP_MAX_LABELS];
+ uint32_t flags;
+ struct ng_mpls_lsp_stats stats;
+ LIST_ENTRY(lsp_instance) entries;
+};
+
+struct ng_mpls_lsp_private {
+ node_p node;
+ hook_p upper_hook;
+ hook_p lower_hook;
+ LIST_HEAD(, lsp_instance) lsp_list;
+ int unit;
+};
+
+typedef struct ng_mpls_lsp_private *priv_p;
+
+static MALLOC_DEFINE(M_NETGRAPH_MPLS_LSP, "ng_mpls_lsp",
+ "netgraph mpls lsp node");
+
+static ng_constructor_t ng_mpls_lsp_constructor;
+static ng_rcvmsg_t ng_mpls_lsp_rcvmsg;
+static ng_shutdown_t ng_mpls_lsp_shutdown;
+static ng_newhook_t ng_mpls_lsp_newhook;
+static ng_rcvdata_t ng_mpls_lsp_rcvdata;
+static ng_disconnect_t ng_mpls_lsp_disconnect;
+
+static int ng_mpls_lsp_output(struct ifnet *, struct mbuf *,
+ const struct sockaddr *, struct route *);
+static int ng_mpls_lsp_ioctl(struct ifnet *, u_long, caddr_t);
+static struct lsp_instance *ng_mpls_lsp_find(priv_p, uint32_t);
+
+static const struct ng_parse_struct_field lsp_config_fields[] =
+ NG_MPLS_LSP_CONFIG_FIELDS;
+static const struct ng_parse_type lsp_config_type = {
+ &ng_parse_struct_type,
+ &lsp_config_fields
+};
+
+static const struct ng_parse_struct_field lsp_stats_fields[] =
+ NG_MPLS_LSP_STATS_FIELDS;
+static const struct ng_parse_type lsp_stats_type = {
+ &ng_parse_struct_type,
+ &lsp_stats_fields
+};
+
+static const struct ng_cmdlist ng_mpls_lsp_cmdlist[] = {
+ {
+ NGM_MPLS_LSP_COOKIE,
+ NGM_MPLS_LSP_ADD_LSP,
+ "addlsp",
+ &lsp_config_type,
+ NULL
+ },
+ {
+ NGM_MPLS_LSP_COOKIE,
+ NGM_MPLS_LSP_DEL_LSP,
+ "dellsp",
+ &ng_parse_uint32_type,
+ NULL
+ },
+ {
+ NGM_MPLS_LSP_COOKIE,
+ NGM_MPLS_LSP_GET_CONFIG,
+ "getconfig",
+ &ng_parse_uint32_type,
+ &lsp_config_type
+ },
+ {
+ NGM_MPLS_LSP_COOKIE,
+ NGM_MPLS_LSP_GET_STATS,
+ "getstats",
+ &ng_parse_uint32_type,
+ &lsp_stats_type
+ },
+ {
+ NGM_MPLS_LSP_COOKIE,
+ NGM_MPLS_LSP_CLR_STATS,
+ "clrstats",
+ &ng_parse_uint32_type,
+ NULL
+ },
+ { 0 }
+};
+
+static struct ng_type ng_mpls_lsp_typestruct = {
+ .version = NG_ABI_VERSION,
+ .name = NG_MPLS_LSP_NODE_TYPE,
+ .constructor = ng_mpls_lsp_constructor,
+ .rcvmsg = ng_mpls_lsp_rcvmsg,
+ .shutdown = ng_mpls_lsp_shutdown,
+ .newhook = ng_mpls_lsp_newhook,
+ .rcvdata = ng_mpls_lsp_rcvdata,
+ .disconnect = ng_mpls_lsp_disconnect,
+ .cmdlist = ng_mpls_lsp_cmdlist,
+};
+NETGRAPH_INIT(mpls_lsp, &ng_mpls_lsp_typestruct);
+
+static int
+ng_mpls_lsp_constructor(node_p node)
+{
+ priv_p priv;
+
+ priv = malloc(sizeof(*priv), M_NETGRAPH_MPLS_LSP, M_WAITOK | M_ZERO);
+ priv->node = node;
+ LIST_INIT(&priv->lsp_list);
+ priv->unit = alloc_unr(NULL);
+ NG_NODE_SET_PRIVATE(node, priv);
+ return (0);
+}
+
+static int
+ng_mpls_lsp_newhook(node_p node, hook_p hook, const char *name)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+
+ if (strcmp(name, NG_MPLS_LSP_HOOK_UPPER) == 0) {
+ priv->upper_hook = hook;
+ return (0);
+ }
+ if (strcmp(name, NG_MPLS_LSP_HOOK_LOWER) == 0) {
+ priv->lower_hook = hook;
+ return (0);
+ }
+ return (EINVAL);
+}
+
+static int
+ng_mpls_lsp_rcvmsg(node_p node, item_p item, hook_p lasthook)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct ng_mesg *resp = NULL;
+ int error = 0;
+ struct ng_mesg *msg;
+
+ NGI_GET_MSG(item, msg);
+ switch (msg->header.typecookie) {
+ case NGM_MPLS_LSP_COOKIE:
+ switch (msg->header.cmd) {
+ case NGM_MPLS_LSP_ADD_LSP:
+ {
+ struct ng_mpls_lsp_config *conf;
+ struct lsp_instance *lsp;
+ struct ifnet *ifp;
+
+ if (msg->header.arglen < sizeof(*conf)) {
+ error = EINVAL;
+ break;
+ }
+ conf = (struct ng_mpls_lsp_config *)msg->data;
+
+ if (conf->push_count > NG_MPLS_LSP_MAX_LABELS) {
+ error = EINVAL;
+ break;
+ }
+
+ lsp = ng_mpls_lsp_find(priv, conf->lsp_id);
+ if (lsp != NULL) {
+ error = EEXIST;
+ break;
+ }
+
+ lsp = malloc(sizeof(*lsp), M_NETGRAPH_MPLS_LSP,
+ M_WAITOK | M_ZERO);
+ lsp->lsp_id = conf->lsp_id;
+ lsp->priv = priv;
+ lsp->push_count = conf->push_count;
+ memcpy(lsp->push_labels, conf->push_labels,
+ conf->push_count * sizeof(uint32_t));
+ lsp->flags = conf->flags;
+
+ ifp = if_alloc(IFT_PROPVIRTUAL);
+ if (ifp == NULL) {
+ free(lsp, M_NETGRAPH_MPLS_LSP);
+ error = ENOMEM;
+ break;
+ }
+ ifp->if_softc = lsp;
+ lsp->ifp = ifp;
+
+ if_initname(ifp, NG_MPLS_LSP_IFACE_NAME,
+ conf->lsp_id);
+ ifp->if_output = ng_mpls_lsp_output;
+ ifp->if_ioctl = ng_mpls_lsp_ioctl;
+ ifp->if_mtu = (conf->mtu != 0) ? conf->mtu :
+ ETHERMTU;
+ ifp->if_flags = IFF_SIMPLEX | IFF_POINTOPOINT |
+ IFF_MULTICAST;
+ ifp->if_fib = RT_DEFAULT_FIB;
+ ifp->if_addrlen = 0;
+ ifp->if_hdrlen = 0;
+ IFQ_SET_MAXLEN(&ifp->if_snd, ifqmaxlen);
+ ifp->if_snd.ifq_drv_maxlen = ifqmaxlen;
+ IFQ_SET_READY(&ifp->if_snd);
+
+ if_attach(ifp);
+ bpfattach(ifp, DLT_RAW, 0);
+
+ LIST_INSERT_HEAD(&priv->lsp_list, lsp, entries);
+ break;
+ }
+ case NGM_MPLS_LSP_DEL_LSP:
+ {
+ uint32_t lsp_id;
+ struct lsp_instance *lsp;
+
+ if (msg->header.arglen < sizeof(lsp_id)) {
+ error = EINVAL;
+ break;
+ }
+ lsp_id = *(uint32_t *)msg->data;
+ lsp = ng_mpls_lsp_find(priv, lsp_id);
+ if (lsp == NULL) {
+ error = ENOENT;
+ break;
+ }
+ LIST_REMOVE(lsp, entries);
+ bpfdetach(lsp->ifp);
+ if_detach(lsp->ifp);
+ if_free(lsp->ifp);
+ free(lsp, M_NETGRAPH_MPLS_LSP);
+ break;
+ }
+ case NGM_MPLS_LSP_GET_CONFIG:
+ {
+ uint32_t lsp_id;
+ struct lsp_instance *lsp;
+ struct ng_mpls_lsp_config *conf;
+
+ if (msg->header.arglen < sizeof(lsp_id)) {
+ error = EINVAL;
+ break;
+ }
+ lsp_id = *(uint32_t *)msg->data;
+ lsp = ng_mpls_lsp_find(priv, lsp_id);
+ if (lsp == NULL) {
+ error = ENOENT;
+ break;
+ }
+ NG_MKRESPONSE(resp, msg, sizeof(*conf), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ conf = (struct ng_mpls_lsp_config *)resp->data;
+ conf->lsp_id = lsp->lsp_id;
+ conf->push_count = lsp->push_count;
+ memcpy(conf->push_labels, lsp->push_labels,
+ lsp->push_count * sizeof(uint32_t));
+ conf->flags = lsp->flags;
+ conf->mtu = lsp->ifp->if_mtu;
+ break;
+ }
+ case NGM_MPLS_LSP_GET_STATS:
+ case NGM_MPLS_LSP_CLR_STATS:
+ {
+ uint32_t lsp_id;
+ struct lsp_instance *lsp;
+
+ if (msg->header.arglen < sizeof(lsp_id)) {
+ error = EINVAL;
+ break;
+ }
+ lsp_id = *(uint32_t *)msg->data;
+ lsp = ng_mpls_lsp_find(priv, lsp_id);
+ if (lsp == NULL) {
+ error = ENOENT;
+ break;
+ }
+ if (msg->header.cmd != NGM_MPLS_LSP_CLR_STATS) {
+ NG_MKRESPONSE(resp, msg, sizeof(lsp->stats),
+ M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ memcpy(resp->data, &lsp->stats,
+ sizeof(lsp->stats));
+ }
+ if (msg->header.cmd != NGM_MPLS_LSP_GET_STATS)
+ memset(&lsp->stats, 0, sizeof(lsp->stats));
+ break;
+ }
+ default:
+ error = EINVAL;
+ break;
+ }
+ break;
+ default:
+ error = EINVAL;
+ break;
+ }
+ NG_RESPOND_MSG(error, node, item, resp);
+ NG_FREE_MSG(msg);
+ return (error);
+}
+
+static int
+ng_mpls_lsp_rcvdata(hook_p hook, item_p item)
+{
+ const node_p node = NG_HOOK_NODE(hook);
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct mbuf *m;
+ struct epoch_tracker et;
+ int error;
+
+ NGI_GET_M(item, m);
+ NG_FREE_ITEM(item);
+
+ if (hook == priv->lower_hook) {
+ struct lsp_instance *lsp;
+ uint32_t shim_val;
+ int s_bit;
+
+ if (m->m_pkthdr.len < MPLS_SHIM_LEN) {
+ lsp = LIST_FIRST(&priv->lsp_list);
+ if (lsp != NULL)
+ lsp->stats.rx_errors++;
+ NG_FREE_M(m);
+ return (EINVAL);
+ }
+ if (m->m_len < MPLS_SHIM_LEN &&
+ (m = m_pullup(m, MPLS_SHIM_LEN)) == NULL)
+ return (ENOBUFS);
+
+ shim_val = *mtod(m, uint32_t *);
+ s_bit = MPLS_S(shim_val);
+ m_adj(m, MPLS_SHIM_LEN);
+
+ lsp = LIST_FIRST(&priv->lsp_list);
+ if (lsp == NULL || lsp->ifp == NULL) {
+ NG_FREE_M(m);
+ return (ENETDOWN);
+ }
+
+ lsp->stats.rx_packets++;
+ lsp->stats.rx_bytes += m->m_pkthdr.len;
+
+ if (s_bit) {
+ uint8_t version;
+
+ if (m->m_pkthdr.len < 1) {
+ lsp->stats.rx_errors++;
+ NG_FREE_M(m);
+ return (0);
+ }
+ if (m->m_len < 1 &&
+ (m = m_pullup(m, 1)) == NULL)
+ return (ENOBUFS);
+ version = *mtod(m, uint8_t *) >> 4;
+ m->m_pkthdr.rcvif = lsp->ifp;
+ M_SETFIB(m, lsp->ifp->if_fib);
+ CURVNET_SET(lsp->ifp->if_vnet);
+ NET_EPOCH_ENTER(et);
+ netisr_dispatch(
+ version == 6 ? NETISR_IPV6 : NETISR_IP, m);
+ NET_EPOCH_EXIT(et);
+ CURVNET_RESTORE();
+ } else {
+ /* More labels remain; forward to upper for processing. */
+ if (priv->upper_hook != NULL)
+ NG_SEND_DATA_ONLY(error, priv->upper_hook, m);
+ else
+ NG_FREE_M(m);
+ }
+ return (0);
+ }
+
+ /* Data from upper hook: not expected (if_output handles tx). */
+ NG_FREE_M(m);
+ return (0);
+}
+
+static int
+ng_mpls_lsp_output(struct ifnet *ifp, struct mbuf *m,
+ const struct sockaddr *dst, struct route *ro __unused)
+{
+ struct lsp_instance *lsp = ifp->if_softc;
+ uint32_t af;
+ int i, error;
+ uint32_t shim;
+
+ if (!((ifp->if_flags & IFF_UP) &&
+ (ifp->if_drv_flags & IFF_DRV_RUNNING))) {
+ m_freem(m);
+ return (ENETDOWN);
+ }
+
+ if (dst->sa_family == AF_UNSPEC || dst->sa_family == pseudo_AF_HDRCMPLT)
+ bcopy(dst->sa_data, &af, sizeof(af));
+ else
+ af = dst->sa_family;
+
+ if (af != AF_INET && af != AF_INET6) {
+ m_freem(m);
+ return (EAFNOSUPPORT);
+ }
+
+ /* Push label stack (innermost first, outermost last). */
+ for (i = 0; i < (int)lsp->push_count; i++) {
+ int s_bit = (i == 0) ? 1 : 0;
+ shim = MPLS_MAKE(lsp->push_labels[i], 0, s_bit, 255);
+ M_PREPEND(m, MPLS_SHIM_LEN, M_NOWAIT);
+ if (m == NULL)
+ return (ENOBUFS);
+ *mtod(m, uint32_t *) = shim;
+ }
+
+ lsp->stats.tx_packets++;
+ lsp->stats.tx_bytes += m->m_pkthdr.len;
+
+ NG_SEND_DATA_ONLY(error, lsp->priv->lower_hook, m);
+ return (error);
+}
+
+static int
+ng_mpls_lsp_ioctl(struct ifnet *ifp, u_long cmd, caddr_t data)
+{
+ struct ifreq *ifr = (struct ifreq *)data;
+ int error = 0;
+
+ switch (cmd) {
+ case SIOCSIFADDR:
+ case SIOCGIFADDR:
+ break;
+ case SIOCSIFMTU:
+ if (ifr->ifr_mtu < 128)
+ ifr->ifr_mtu = 128;
+ if (ifr->ifr_mtu > 65535)
+ ifr->ifr_mtu = 65535;
+ ifp->if_mtu = ifr->ifr_mtu;
+ break;
+ case SIOCSIFFLAGS:
+ break;
+ default:
+ error = EINVAL;
+ }
+ return (error);
+}
+
+static int
+ng_mpls_lsp_shutdown(node_p node)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+
+ while (!LIST_EMPTY(&priv->lsp_list)) {
+ struct lsp_instance *lsp = LIST_FIRST(&priv->lsp_list);
+ LIST_REMOVE(lsp, entries);
+ bpfdetach(lsp->ifp);
+ if_detach(lsp->ifp);
+ if_free(lsp->ifp);
+ free(lsp, M_NETGRAPH_MPLS_LSP);
+ }
+
+ NG_NODE_SET_PRIVATE(node, NULL);
+ NG_NODE_UNREF(node);
+ free(priv, M_NETGRAPH_MPLS_LSP);
+ return (0);
+}
+
+static int
+ng_mpls_lsp_disconnect(hook_p hook)
+{
+ const node_p node = NG_HOOK_NODE(hook);
+ const priv_p priv = NG_NODE_PRIVATE(node);
+
+ if (hook == priv->upper_hook)
+ priv->upper_hook = NULL;
+ else if (hook == priv->lower_hook)
+ priv->lower_hook = NULL;
+
+ if (NG_NODE_NUMHOOKS(node) == 0 && NG_NODE_IS_VALID(node))
+ ng_rmnode_self(node);
+ return (0);
+}
+
+static struct lsp_instance *
+ng_mpls_lsp_find(priv_p priv, uint32_t lsp_id)
+{
+ struct lsp_instance *lsp;
+
+ LIST_FOREACH(lsp, &priv->lsp_list, entries) {
+ if (lsp->lsp_id == lsp_id)
+ return (lsp);
+ }
+ return (NULL);
+}
diff --git a/sys/netgraph/ng_mpls_lsp.h b/sys/netgraph/ng_mpls_lsp.h
new file mode 100644
index 00000000000..f0c6bd93bba
--- /dev/null
+++ b/sys/netgraph/ng_mpls_lsp.h
@@ -0,0 +1,74 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ */
+
+#ifndef _NETGRAPH_NG_MPLS_LSP_H_
+#define _NETGRAPH_NG_MPLS_LSP_H_
+
+#define NG_MPLS_LSP_NODE_TYPE "mpls_lsp"
+#define NGM_MPLS_LSP_COOKIE 3138914159
+
+#define NG_MPLS_LSP_IFACE_NAME "mpls_lsp"
+
+#define NG_MPLS_LSP_HOOK_UPPER "upper"
+#define NG_MPLS_LSP_HOOK_LOWER "lower"
+
+#define NG_MPLS_LSP_MAX_LABELS 8
+
+#define NG_MPLS_LSP_F_ACTIVE 0x01
+
+struct ng_mpls_lsp_config {
+ uint32_t lsp_id;
+ uint32_t push_count;
+ uint32_t push_labels[NG_MPLS_LSP_MAX_LABELS];
+ uint32_t flags;
+ uint32_t mtu;
+};
+
+struct ng_mpls_lsp_stats {
+ uint64_t tx_packets;
+ uint64_t tx_bytes;
+ uint64_t rx_packets;
+ uint64_t rx_bytes;
+ uint64_t tx_errors;
+ uint64_t rx_errors;
+};
+
+#define NG_MPLS_LSP_CONFIG_FIELDS { \
+ { "lsp_id", &ng_parse_uint32_type }, \
+ { "push_count", &ng_parse_uint32_type }, \
+ { "push_labels[0]", &ng_parse_uint32_type }, \
+ { "push_labels[1]", &ng_parse_uint32_type }, \
+ { "push_labels[2]", &ng_parse_uint32_type }, \
+ { "push_labels[3]", &ng_parse_uint32_type }, \
+ { "push_labels[4]", &ng_parse_uint32_type }, \
+ { "push_labels[5]", &ng_parse_uint32_type }, \
+ { "push_labels[6]", &ng_parse_uint32_type }, \
+ { "push_labels[7]", &ng_parse_uint32_type }, \
+ { "flags", &ng_parse_uint32_type }, \
+ { "mtu", &ng_parse_uint32_type }, \
+ { NULL } \
+}
+
+#define NG_MPLS_LSP_STATS_FIELDS { \
+ { "tx_packets", &ng_parse_uint64_type }, \
+ { "tx_bytes", &ng_parse_uint64_type }, \
+ { "rx_packets", &ng_parse_uint64_type }, \
+ { "rx_bytes", &ng_parse_uint64_type }, \
+ { "tx_errors", &ng_parse_uint64_type }, \
+ { "rx_errors", &ng_parse_uint64_type }, \
+ { NULL } \
+}
+
+enum {
+ NGM_MPLS_LSP_ADD_LSP = 1,
+ NGM_MPLS_LSP_DEL_LSP,
+ NGM_MPLS_LSP_GET_CONFIG,
+ NGM_MPLS_LSP_GET_STATS,
+ NGM_MPLS_LSP_CLR_STATS,
+};
+
+#endif /* _NETGRAPH_NG_MPLS_LSP_H_ */
diff --git a/sys/netgraph/ng_mpls_pw.c b/sys/netgraph/ng_mpls_pw.c
new file mode 100644
index 00000000000..c4f32cd445f
--- /dev/null
+++ b/sys/netgraph/ng_mpls_pw.c
@@ -0,0 +1,696 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ */
+
+#include <sys/param.h>
+#include <sys/errno.h>
+#include <sys/kernel.h>
+#include <sys/malloc.h>
+#include <sys/mbuf.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/systm.h>
+
+#include <net/ethernet.h>
+#include <net/if.h>
+
+#include <netgraph/ng_message.h>
+#include <netgraph/ng_parse.h>
+#include <netgraph/ng_mpls.h>
+#include <netgraph/ng_mpls_pw.h>
+#include <netgraph/netgraph.h>
+
+struct mpls_pw_entry {
+ LIST_ENTRY(mpls_pw_entry) entries;
+ hook_p ce_hook;
+ uint32_t vc_label;
+ uint32_t tunnel_label;
+ uint16_t pw_type;
+ uint8_t flags;
+ uint8_t push_count;
+ uint32_t push_labels[NG_MPLS_PW_MAX_LABELS];
+ uint64_t rx_packets;
+ uint64_t rx_bytes;
+ uint64_t tx_packets;
+ uint64_t tx_bytes;
+};
+
+struct ng_mpls_pw_private {
+ hook_p downstream_hook;
+ uint64_t rx_errors;
+ uint64_t tx_errors;
+ LIST_HEAD(, mpls_pw_entry) pw_list;
+};
+typedef struct ng_mpls_pw_private *priv_p;
+
+#define PW_MODE_RAW 0
+#define PW_MODE_TAGGED 1
+
+static ng_constructor_t ng_mpls_pw_constructor;
+static ng_rcvmsg_t ng_mpls_pw_rcvmsg;
+static ng_shutdown_t ng_mpls_pw_shutdown;
+static ng_newhook_t ng_mpls_pw_newhook;
+static ng_rcvdata_t ng_mpls_pw_rcvdata;
+static ng_disconnect_t ng_mpls_pw_disconnect;
+
+static struct mpls_pw_entry *ng_mpls_pw_find_by_vc(priv_p, uint32_t);
+static struct mpls_pw_entry *ng_mpls_pw_find_by_hook(priv_p, hook_p);
+static int ng_mpls_pw_getTableLength(const struct ng_parse_type *,
+ const u_char *, const u_char *);
+
+static const struct ng_parse_struct_field ng_mpls_pw_config_fields[] =
+ NG_MPLS_PW_CONFIG_FIELDS;
+static const struct ng_parse_type ng_mpls_pw_config_type = {
+ &ng_parse_struct_type,
+ &ng_mpls_pw_config_fields
+};
+
+static const struct ng_parse_array_info ng_mpls_pw_table_array_info = {
+ &ng_mpls_pw_config_type,
+ ng_mpls_pw_getTableLength
+};
+static const struct ng_parse_type ng_mpls_pw_table_array_type = {
+ &ng_parse_array_type,
+ &ng_mpls_pw_table_array_info
+};
+
+static const struct ng_parse_struct_field ng_mpls_pw_table_fields[] =
+ NG_MPLS_PW_TABLE_FIELDS;
+static const struct ng_parse_type ng_mpls_pw_table_type = {
+ &ng_parse_struct_type,
+ &ng_mpls_pw_table_fields
+};
+
+static const struct ng_parse_struct_field ng_mpls_pw_stats_fields[] =
+ NG_MPLS_PW_STATS_FIELDS;
+static const struct ng_parse_type ng_mpls_pw_stats_type = {
+ &ng_parse_struct_type,
+ &ng_mpls_pw_stats_fields
+};
+
+static const struct ng_cmdlist ng_mpls_pw_cmdlist[] = {
+ {
+ NGM_MPLS_PW_COOKIE,
+ NGM_MPLS_PW_ADD_PW,
+ "addpw",
+ &ng_mpls_pw_config_type,
+ NULL
+ },
+ {
+ NGM_MPLS_PW_COOKIE,
+ NGM_MPLS_PW_ADD_PW2,
+ "addpw2",
+ &ng_mpls_pw_config_type,
+ NULL
+ },
+ {
+ NGM_MPLS_PW_COOKIE,
+ NGM_MPLS_PW_DEL_PW,
+ "delpw",
+ &ng_parse_hookbuf_type,
+ NULL
+ },
+ {
+ NGM_MPLS_PW_COOKIE,
+ NGM_MPLS_PW_GET_TABLE,
+ "gettable",
+ NULL,
+ &ng_mpls_pw_table_type
+ },
+ {
+ NGM_MPLS_PW_COOKIE,
+ NGM_MPLS_PW_GET_STATS,
+ "getstats",
+ NULL,
+ &ng_mpls_pw_stats_type
+ },
+ {
+ NGM_MPLS_PW_COOKIE,
+ NGM_MPLS_PW_CLR_STATS,
+ "clrstats",
+ NULL,
+ NULL
+ },
+ {
+ NGM_MPLS_PW_COOKIE,
+ NGM_MPLS_PW_GET_CONFIG,
+ "getconfig",
+ NULL,
+ &ng_mpls_pw_config_type
+ },
+ {
+ NGM_MPLS_PW_COOKIE,
+ NGM_MPLS_PW_SET_CONFIG,
+ "setconfig",
+ &ng_mpls_pw_config_type,
+ NULL
+ },
+ {
+ NGM_MPLS_PW_COOKIE,
+ NGM_MPLS_PW_GET_VC_TABLE,
+ "getvctable",
+ NULL,
+ &ng_mpls_pw_table_type
+ },
+ { 0 }
+};
+
+static struct ng_type ng_mpls_pw_typestruct = {
+ .version = NG_ABI_VERSION,
+ .name = NG_MPLS_PW_NODE_TYPE,
+ .constructor = ng_mpls_pw_constructor,
+ .rcvmsg = ng_mpls_pw_rcvmsg,
+ .shutdown = ng_mpls_pw_shutdown,
+ .newhook = ng_mpls_pw_newhook,
+ .rcvdata = ng_mpls_pw_rcvdata,
+ .disconnect = ng_mpls_pw_disconnect,
+ .cmdlist = ng_mpls_pw_cmdlist,
+};
+NETGRAPH_INIT(mpls_pw, &ng_mpls_pw_typestruct);
+
+static struct mpls_pw_entry *
+ng_mpls_pw_find_by_vc(priv_p priv, uint32_t label)
+{
+ struct mpls_pw_entry *pw;
+
+ LIST_FOREACH(pw, &priv->pw_list, entries)
+ if (pw->vc_label == label)
+ return (pw);
+ return (NULL);
+}
+
+static struct mpls_pw_entry *
+ng_mpls_pw_find_by_hook(priv_p priv, hook_p hook)
+{
+ struct mpls_pw_entry *pw;
+
+ LIST_FOREACH(pw, &priv->pw_list, entries)
+ if (pw->ce_hook == hook)
+ return (pw);
+ return (NULL);
+}
+
+static int
+ng_mpls_pw_getTableLength(const struct ng_parse_type *type __unused,
+ const u_char *start, const u_char *buf)
+{
+ const struct ng_mpls_pw_table *const table =
+ (const struct ng_mpls_pw_table *)(buf - sizeof(uint32_t));
+ return (table->n);
+}
+
+static int
+ng_mpls_pw_constructor(node_p node)
+{
+ priv_p priv;
+
+ priv = malloc(sizeof(*priv), M_NETGRAPH, M_WAITOK | M_ZERO);
+ LIST_INIT(&priv->pw_list);
+ NG_NODE_SET_PRIVATE(node, priv);
+ return (0);
+}
+
+static int
+ng_mpls_pw_newhook(node_p node, hook_p hook, const char *name)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ const size_t plen = sizeof(NG_MPLS_PW_HOOK_CE_PREFIX) - 1;
+
+ if (strcmp(name, NG_MPLS_PW_HOOK_DOWNSTREAM) == 0) {
+ priv->downstream_hook = hook;
+ } else if (strncmp(name, NG_MPLS_PW_HOOK_CE_PREFIX, plen) == 0) {
+ char *eptr;
+
+ strtoul(name + plen, &eptr, 10);
+ if (*eptr != '\0')
+ return (EINVAL);
+ } else
+ return (EINVAL);
+
+ NG_HOOK_SET_PRIVATE(hook, NULL);
+ return (0);
+}
+
+static int
+ng_mpls_pw_add_pw_entry(priv_p priv, struct ng_mpls_pw_config *pwc,
+ hook_p hook, int is_pw2)
+{
+ struct mpls_pw_entry *pw;
+
+ if (ng_mpls_pw_find_by_vc(priv, pwc->vc_label) != NULL)
+ return (EEXIST);
+
+ pw = malloc(sizeof(*pw), M_NETGRAPH, M_NOWAIT | M_ZERO);
+ if (pw == NULL)
+ return (ENOMEM);
+
+ pw->ce_hook = hook;
+ pw->vc_label = pwc->vc_label;
+ pw->tunnel_label = pwc->tunnel_label;
+ pw->pw_type = pwc->pw_type;
+ pw->flags = pwc->flags;
+
+ if (is_pw2 || pwc->push_count > 0) {
+ pw->push_count = pwc->push_count;
+ memcpy(pw->push_labels, pwc->push_labels,
+ pwc->push_count * sizeof(uint32_t));
+ } else {
+ uint32_t lbls[2];
+ int n = 0;
+
+ if (pwc->tunnel_label != 0 && pwc->tunnel_label <= MPLS_LABEL_MAX)
+ lbls[n++] = pwc->tunnel_label;
+ lbls[n++] = pwc->vc_label;
+
+ pw->push_count = n;
+ memcpy(pw->push_labels, lbls, n * sizeof(uint32_t));
+ }
+
+ LIST_INSERT_HEAD(&priv->pw_list, pw, entries);
+ NG_HOOK_SET_PRIVATE(hook, pw);
+ return (0);
+}
+
+static int
+ng_mpls_pw_rcvmsg(node_p node, item_p item, hook_p lasthook)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct ng_mesg *msg, *resp = NULL;
+ struct ng_mpls_pw_config *pwc;
+ struct mpls_pw_entry *pw;
+ hook_p hook;
+ struct ng_mpls_pw_table *t;
+ struct ng_mpls_pw_stats *ps;
+ int error = 0;
+ int is_pw2;
+
+ NGI_GET_MSG(item, msg);
+ switch (msg->header.typecookie) {
+ case NGM_MPLS_PW_COOKIE:
+ switch (msg->header.cmd) {
+ case NGM_MPLS_PW_ADD_PW:
+ case NGM_MPLS_PW_ADD_PW2:
+ case NGM_MPLS_PW_SET_CONFIG:
+ is_pw2 = (msg->header.cmd == NGM_MPLS_PW_ADD_PW2);
+ if (msg->header.arglen < sizeof(*pwc)) {
+ error = EINVAL;
+ break;
+ }
+ pwc = (struct ng_mpls_pw_config *)msg->data;
+
+ if (pwc->vc_label > MPLS_LABEL_MAX) {
+ error = EINVAL;
+ break;
+ }
+ if (pwc->tunnel_label > MPLS_LABEL_MAX) {
+ error = EINVAL;
+ break;
+ }
+ if (pwc->push_count > NG_MPLS_PW_MAX_LABELS) {
+ error = EINVAL;
+ break;
+ }
+
+ hook = ng_findhook(node, pwc->ce_hook);
+ if (hook == NULL || hook == priv->downstream_hook) {
+ error = ENOENT;
+ break;
+ }
+
+ if (msg->header.cmd == NGM_MPLS_PW_SET_CONFIG) {
+ pw = NG_HOOK_PRIVATE(hook);
+ if (pw != NULL) {
+ pw->tunnel_label = pwc->tunnel_label;
+ pw->flags = pwc->flags;
+ pw->push_count = pwc->push_count;
+ memcpy(pw->push_labels, pwc->push_labels,
+ pwc->push_count * sizeof(uint32_t));
+ }
+ break;
+ }
+
+ error = ng_mpls_pw_add_pw_entry(priv, pwc, hook, is_pw2);
+ break;
+
+ case NGM_MPLS_PW_DEL_PW:
+ if (msg->header.arglen != NG_HOOKSIZ) {
+ error = EINVAL;
+ break;
+ }
+ hook = ng_findhook(node, (char *)msg->data);
+ if (hook == NULL) {
+ error = ENOENT;
+ break;
+ }
+ pw = NG_HOOK_PRIVATE(hook);
+ if (pw == NULL) {
+ error = ENOENT;
+ break;
+ }
+ LIST_REMOVE(pw, entries);
+ NG_HOOK_SET_PRIVATE(hook, NULL);
+ free(pw, M_NETGRAPH);
+ break;
+
+ case NGM_MPLS_PW_GET_TABLE:
+ case NGM_MPLS_PW_GET_VC_TABLE:
+ {
+ int npw = 0;
+
+ LIST_FOREACH(pw, &priv->pw_list, entries)
+ npw++;
+
+ NG_MKRESPONSE(resp, msg, sizeof(*t) +
+ npw * sizeof(*t->pw), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ t = (struct ng_mpls_pw_table *)resp->data;
+ t->n = 0;
+ pwc = &t->pw[0];
+ LIST_FOREACH(pw, &priv->pw_list, entries) {
+ strncpy(pwc->ce_hook,
+ NG_HOOK_NAME(pw->ce_hook), NG_HOOKSIZ);
+ pwc->vc_label = pw->vc_label;
+ pwc->tunnel_label = pw->tunnel_label;
+ pwc->pw_type = pw->pw_type;
+ pwc->flags = pw->flags;
+ pwc->push_count = pw->push_count;
+ memcpy(pwc->push_labels, pw->push_labels,
+ pw->push_count * sizeof(uint32_t));
+ pwc++;
+ t->n++;
+ }
+ break;
+ }
+
+ case NGM_MPLS_PW_GET_STATS:
+ NG_MKRESPONSE(resp, msg, sizeof(*ps), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ ps = (struct ng_mpls_pw_stats *)resp->data;
+ ps->rx_errors = priv->rx_errors;
+ ps->tx_errors = priv->tx_errors;
+ LIST_FOREACH(pw, &priv->pw_list, entries) {
+ ps->rx_packets += pw->rx_packets;
+ ps->rx_bytes += pw->rx_bytes;
+ ps->tx_packets += pw->tx_packets;
+ ps->tx_bytes += pw->tx_bytes;
+ }
+ break;
+
+ case NGM_MPLS_PW_CLR_STATS:
+ priv->rx_errors = 0;
+ priv->tx_errors = 0;
+ LIST_FOREACH(pw, &priv->pw_list, entries) {
+ pw->rx_packets = 0;
+ pw->rx_bytes = 0;
+ pw->tx_packets = 0;
+ pw->tx_bytes = 0;
+ }
+ break;
+
+ case NGM_MPLS_PW_GET_CONFIG:
+ {
+ if (msg->header.arglen != NG_HOOKSIZ) {
+ error = EINVAL;
+ break;
+ }
+ hook = ng_findhook(node, (char *)msg->data);
+ if (hook == NULL) {
+ error = ENOENT;
+ break;
+ }
+ pw = NG_HOOK_PRIVATE(hook);
+ if (pw == NULL) {
+ error = ENOENT;
+ break;
+ }
+ NG_MKRESPONSE(resp, msg, sizeof(*pwc), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ pwc = (struct ng_mpls_pw_config *)resp->data;
+ strncpy(pwc->ce_hook, NG_HOOK_NAME(pw->ce_hook),
+ NG_HOOKSIZ);
+ pwc->vc_label = pw->vc_label;
+ pwc->tunnel_label = pw->tunnel_label;
+ pwc->pw_type = pw->pw_type;
+ pwc->flags = pw->flags;
+ pwc->push_count = pw->push_count;
+ memcpy(pwc->push_labels, pw->push_labels,
+ pw->push_count * sizeof(uint32_t));
+ break;
+ }
+
+ default:
+ error = EINVAL;
+ break;
+ }
+ break;
+ default:
+ error = EINVAL;
+ break;
+ }
+ NG_RESPOND_MSG(error, node, item, resp);
+ NG_FREE_MSG(msg);
+ return (error);
+}
+
+/*
+ * Push MPLS label stack onto an Ethernet frame.
+ * M_PREPEND inserts space before the Ethernet header; we save the DMAC/SMAC
+ * from their new offset (shifted right by total_bytes), then write them back
+ * to the correct header positions with MPLS EtherType.
+ */
+static int
+ng_mpls_pw_push_labels(struct mbuf **mp, const uint32_t *labels, int count)
+{
+ int total_bytes = count * MPLS_SHIM_LEN;
+ uint8_t dmac[ETHER_ADDR_LEN];
+ uint8_t smac[ETHER_ADDR_LEN];
+ struct ether_header *eh;
+ int i;
+
+ if (total_bytes == 0)
+ return (0);
+
+ M_PREPEND((*mp), total_bytes, M_NOWAIT);
+ if ((*mp) == NULL)
+ return (ENOMEM);
+
+ if ((*mp)->m_len < sizeof(struct ether_header) + total_bytes &&
+ (*mp = m_pullup((*mp), sizeof(struct ether_header) +
+ total_bytes)) == NULL)
+ return (ENOBUFS);
+
+ eh = mtod((*mp), struct ether_header *);
+
+ memcpy(dmac, (uint8_t *)eh + total_bytes, ETHER_ADDR_LEN);
+ memcpy(smac, (uint8_t *)eh + total_bytes + ETHER_ADDR_LEN,
+ ETHER_ADDR_LEN);
+
+ memcpy(eh->ether_dhost, dmac, ETHER_ADDR_LEN);
+ memcpy(eh->ether_shost, smac, ETHER_ADDR_LEN);
+
+ eh->ether_type = htons(ETHERTYPE_MPLS);
+
+ for (i = 0; i < count; i++) {
+ int s_bit = (i == 0) ? 1 : 0;
+ uint32_t shim = MPLS_MAKE(labels[i], 0, s_bit, 64);
+ *(uint32_t *)((uint8_t *)(eh + 1) + i * MPLS_SHIM_LEN) = shim;
+ }
+
+ return (0);
+}
+
+/*
+ * Pop MPLS label stack from an Ethernet-encapsulated MPLS packet.
+ * m_adj removes the labels; we save DMAC/SMAC first, then restore them
+ * in the correct position and fix the EtherType from the inner payload.
+ */
+static int
+ng_mpls_pw_pop_labels(struct mbuf **mp, int count)
+{
+ struct ether_header *eh;
+ uint8_t dmac[ETHER_ADDR_LEN];
+ uint8_t smac[ETHER_ADDR_LEN];
+ int total_bytes = count * MPLS_SHIM_LEN;
+
+ if (total_bytes == 0)
+ return (0);
+ if ((*mp)->m_pkthdr.len < sizeof(struct ether_header) + total_bytes)
+ return (EINVAL);
+
+ if ((*mp)->m_len < sizeof(struct ether_header) + total_bytes &&
+ (*mp = m_pullup((*mp), sizeof(struct ether_header) +
+ total_bytes)) == NULL)
+ return (ENOBUFS);
+
+ eh = mtod((*mp), struct ether_header *);
+ memcpy(dmac, eh->ether_dhost, ETHER_ADDR_LEN);
+ memcpy(smac, eh->ether_shost, ETHER_ADDR_LEN);
+
+ m_adj((*mp), total_bytes);
+
+ if ((*mp)->m_len < sizeof(struct ether_header) &&
+ (*mp = m_pullup((*mp), sizeof(struct ether_header))) == NULL)
+ return (ENOBUFS);
+
+ eh = mtod((*mp), struct ether_header *);
+ memcpy(eh->ether_dhost, dmac, ETHER_ADDR_LEN);
+ memcpy(eh->ether_shost, smac, ETHER_ADDR_LEN);
+
+ {
+ uint8_t *ptr = (uint8_t *)&eh->ether_type;
+ if ((ptr[0] >> 4) == 4)
+ eh->ether_type = htons(ETHERTYPE_IP);
+ else if ((ptr[0] >> 4) == 6)
+ eh->ether_type = htons(ETHERTYPE_IPV6);
+ else
+ eh->ether_type = htons(ETHERTYPE_IP);
+ }
+
+ return (0);
+}
+
+static int
+ng_mpls_pw_rcvdata(hook_p hook, item_p item)
+{
+ const priv_p priv = NG_NODE_PRIVATE(NG_HOOK_NODE(hook));
+ struct mpls_pw_entry *pw;
+ struct mbuf *m;
+ hook_p dst_hook;
+ int error;
+
+ NGI_GET_M(item, m);
+
+ if (hook == priv->downstream_hook) {
+ int pop_count;
+ int i;
+ uint32_t shim;
+
+ if (m->m_pkthdr.len < sizeof(struct ether_header) +
+ MPLS_SHIM_LEN) {
+ priv->rx_errors++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (EINVAL);
+ }
+
+ pw = NULL;
+ for (i = 0; i < NG_MPLS_PW_MAX_LABELS; i++) {
+ int offset = sizeof(struct ether_header) +
+ i * MPLS_SHIM_LEN;
+ if (m->m_pkthdr.len < offset + MPLS_SHIM_LEN)
+ break;
+ if (m->m_len < offset + MPLS_SHIM_LEN &&
+ (m = m_pullup(m, offset + MPLS_SHIM_LEN)) == NULL) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+ shim = *(uint32_t *)
+ ((uint8_t *)mtod(m, struct ether_header *) +
+ sizeof(struct ether_header) + i * MPLS_SHIM_LEN);
+ pw = ng_mpls_pw_find_by_vc(priv, MPLS_LABEL(shim));
+ if (pw != NULL)
+ break;
+ }
+ if (pw == NULL) {
+ priv->rx_errors++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+
+ dst_hook = pw->ce_hook;
+ pop_count = (pw->push_count > 0) ? pw->push_count : (i + 1);
+
+ if (ng_mpls_pw_pop_labels(&m, pop_count)) {
+ priv->rx_errors++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (EINVAL);
+ }
+
+ pw->rx_packets++;
+ pw->rx_bytes += m->m_pkthdr.len;
+
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+ }
+
+ dst_hook = priv->downstream_hook;
+ if (dst_hook == NULL) {
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (ENETDOWN);
+ }
+
+ pw = NG_HOOK_PRIVATE(hook);
+ if (pw == NULL) {
+ priv->tx_errors++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (EOPNOTSUPP);
+ }
+
+ if (ng_mpls_pw_push_labels(&m, pw->push_labels, pw->push_count)) {
+ priv->tx_errors++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (ENOMEM);
+ }
+
+ pw->tx_packets++;
+ pw->tx_bytes += m->m_pkthdr.len;
+
+ NG_FWD_NEW_DATA(error, item, dst_hook, m);
+ return (error);
+}
+
+static int
+ng_mpls_pw_shutdown(node_p node)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct mpls_pw_entry *pw;
+
+ while (!LIST_EMPTY(&priv->pw_list)) {
+ pw = LIST_FIRST(&priv->pw_list);
+ LIST_REMOVE(pw, entries);
+ free(pw, M_NETGRAPH);
+ }
+ NG_NODE_SET_PRIVATE(node, NULL);
+ NG_NODE_UNREF(node);
+ free(priv, M_NETGRAPH);
+ return (0);
+}
+
+static int
+ng_mpls_pw_disconnect(hook_p hook)
+{
+ const priv_p priv = NG_NODE_PRIVATE(NG_HOOK_NODE(hook));
+ struct mpls_pw_entry *pw;
+
+ if (hook == priv->downstream_hook) {
+ priv->downstream_hook = NULL;
+ } else {
+ pw = NG_HOOK_PRIVATE(hook);
+ if (pw != NULL) {
+ LIST_REMOVE(pw, entries);
+ free(pw, M_NETGRAPH);
+ }
+ }
+ NG_HOOK_SET_PRIVATE(hook, NULL);
+ if ((NG_NODE_NUMHOOKS(NG_HOOK_NODE(hook)) == 0) &&
+ (NG_NODE_IS_VALID(NG_HOOK_NODE(hook))))
+ ng_rmnode_self(NG_HOOK_NODE(hook));
+ return (0);
+}
diff --git a/sys/netgraph/ng_mpls_pw.h b/sys/netgraph/ng_mpls_pw.h
new file mode 100644
index 00000000000..36c6f6d5702
--- /dev/null
+++ b/sys/netgraph/ng_mpls_pw.h
@@ -0,0 +1,97 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ */
+
+#ifndef _NETGRAPH_NG_MPLS_PW_H_
+#define _NETGRAPH_NG_MPLS_PW_H_
+
+#define NG_MPLS_PW_NODE_TYPE "mpls_pw"
+#define NGM_MPLS_PW_COOKIE 2034615892
+
+#define NG_MPLS_PW_HOOK_DOWNSTREAM "downstream"
+#define NG_MPLS_PW_HOOK_CE_PREFIX "ce"
+#define NG_MPLS_PW_HOOK_CE_FMT "ce%u"
+
+#define NG_MPLS_PW_MAX_LABELS 8
+
+#define NG_MPLS_PW_F_CW 0x01
+
+#define NG_MPLS_PW_TYPE_ETHERNET 0x0005
+#define NG_MPLS_PW_TYPE_VLAN 0x0004
+
+enum {
+ NGM_MPLS_PW_ADD_PW = 1,
+ NGM_MPLS_PW_DEL_PW,
+ NGM_MPLS_PW_GET_TABLE,
+ NGM_MPLS_PW_GET_STATS,
+ NGM_MPLS_PW_CLR_STATS,
+ NGM_MPLS_PW_GET_CONFIG,
+ NGM_MPLS_PW_SET_CONFIG,
+ NGM_MPLS_PW_ADD_PW2,
+ NGM_MPLS_PW_GET_VC_TABLE,
+};
+
+struct ng_mpls_pw_config {
+ char ce_hook[NG_HOOKSIZ];
+ uint32_t vc_label;
+ uint32_t tunnel_label;
+ uint16_t pw_type;
+ uint8_t expect_cw;
+ uint8_t flags;
+ uint8_t push_count;
+ uint32_t push_labels[NG_MPLS_PW_MAX_LABELS];
+};
+
+#define NG_MPLS_PW_CONFIG_FIELDS { \
+ { "ce_hook", &ng_parse_hookbuf_type }, \
+ { "vc_label", &ng_parse_uint32_type }, \
+ { "tunnel_label", &ng_parse_uint32_type }, \
+ { "pw_type", &ng_parse_uint16_type }, \
+ { "expect_cw", &ng_parse_uint8_type }, \
+ { "flags", &ng_parse_uint8_type }, \
+ { "push_count", &ng_parse_uint8_type }, \
+ { "push_labels[0]", &ng_parse_uint32_type }, \
+ { "push_labels[1]", &ng_parse_uint32_type }, \
+ { "push_labels[2]", &ng_parse_uint32_type }, \
+ { "push_labels[3]", &ng_parse_uint32_type }, \
+ { "push_labels[4]", &ng_parse_uint32_type }, \
+ { "push_labels[5]", &ng_parse_uint32_type }, \
+ { "push_labels[6]", &ng_parse_uint32_type }, \
+ { "push_labels[7]", &ng_parse_uint32_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_pw_table {
+ uint32_t n;
+ struct ng_mpls_pw_config pw[];
+};
+
+#define NG_MPLS_PW_TABLE_FIELDS { \
+ { "n", &ng_parse_uint32_type }, \
+ { "pw", &ng_mpls_pw_table_array_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_pw_stats {
+ uint64_t rx_packets;
+ uint64_t rx_bytes;
+ uint64_t tx_packets;
+ uint64_t tx_bytes;
+ uint64_t rx_errors;
+ uint64_t tx_errors;
+};
+
+#define NG_MPLS_PW_STATS_FIELDS { \
+ { "rx_packets", &ng_parse_uint64_type }, \
+ { "rx_bytes", &ng_parse_uint64_type }, \
+ { "tx_packets", &ng_parse_uint64_type }, \
+ { "tx_bytes", &ng_parse_uint64_type }, \
+ { "rx_errors", &ng_parse_uint64_type }, \
+ { "tx_errors", &ng_parse_uint64_type }, \
+ { NULL } \
+}
+
+#endif /* _NETGRAPH_NG_MPLS_PW_H_ */
diff --git a/sys/netgraph/ng_mpls_vpls.c b/sys/netgraph/ng_mpls_vpls.c
new file mode 100644
index 00000000000..3c82681e02f
--- /dev/null
+++ b/sys/netgraph/ng_mpls_vpls.c
@@ -0,0 +1,671 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * VPLS — Virtual Private LAN Service over pseudowires.
+ * Connects CE ports and PW ports (each PW connects to ng_mpls_pw), performs
+ * MAC learning, flooding, and split-horizon (PW→CE only) to prevent loops.
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/kernel.h>
+#include <sys/lock.h>
+#include <sys/malloc.h>
+#include <sys/mbuf.h>
+#include <sys/errno.h>
+#include <sys/socket.h>
+#include <sys/syslog.h>
+#include <sys/callout.h>
+
+#include <net/if.h>
+#include <net/if_var.h>
+#include <net/ethernet.h>
+
+#include <netgraph/ng_message.h>
+#include <netgraph/netgraph.h>
+#include <netgraph/ng_parse.h>
+#include <netgraph/ng_mpls_vpls.h>
+
+struct vpls_port;
+SLIST_HEAD(vpls_bucket, vpls_host);
+
+struct vpls_port {
+ hook_p hook;
+ int is_pw;
+ uint64_t rx_packets;
+ uint64_t rx_bytes;
+ uint64_t rx_unknown;
+ uint64_t tx_packets;
+ uint64_t tx_bytes;
+ uint64_t loop_drops;
+ LIST_ENTRY(vpls_port) entries;
+};
+
+struct vpls_host {
+ u_char addr[ETHER_ADDR_LEN];
+ struct vpls_port *port;
+ uint16_t age;
+ uint16_t staleness;
+ SLIST_ENTRY(vpls_host) next;
+};
+
+struct ng_mpls_vpls_private {
+ struct vpls_bucket *buckets;
+ struct ng_mpls_vpls_config conf;
+ node_p node;
+ uint32_t num_hosts;
+ LIST_HEAD(, vpls_port) port_list;
+ int num_ports;
+ struct callout timer;
+};
+typedef struct ng_mpls_vpls_private *priv_p;
+
+static MALLOC_DEFINE(M_NETGRAPH_MPLS_VPLS, "ng_mpls_vpls",
+ "netgraph mpls vpls node");
+
+static ng_constructor_t ng_mpls_vpls_constructor;
+static ng_rcvmsg_t ng_mpls_vpls_rcvmsg;
+static ng_shutdown_t ng_mpls_vpls_shutdown;
+static ng_newhook_t ng_mpls_vpls_newhook;
+static ng_rcvdata_t ng_mpls_vpls_rcvdata;
+static ng_disconnect_t ng_mpls_vpls_disconnect;
+
+static void ng_mpls_vpls_timer(void *);
+static uint32_t ng_mpls_vpls_hash(const u_char *);
+static struct vpls_host *ng_mpls_vpls_lookup(priv_p, const u_char *);
+static void ng_mpls_vpls_learn(priv_p, const u_char *, struct vpls_port *);
+static struct vpls_port *ng_mpls_vpls_find_port(priv_p, hook_p);
+static int ng_mpls_vpls_getTableLength(const struct ng_parse_type *,
+ const u_char *, const u_char *);
+
+static const struct ng_parse_struct_field vpls_config_fields[] =
+ NG_MPLS_VPLS_CONFIG_FIELDS;
+static const struct ng_parse_type vpls_config_type = {
+ &ng_parse_struct_type,
+ &vpls_config_fields
+};
+
+static const struct ng_parse_struct_field vpls_host_fields[] =
+ NG_MPLS_VPLS_HOST_FIELDS;
+static const struct ng_parse_type vpls_host_type = {
+ &ng_parse_struct_type,
+ &vpls_host_fields
+};
+
+static int
+ng_mpls_vpls_getTableLength(const struct ng_parse_type *type __unused,
+ const u_char *start __unused, const u_char *buf)
+{
+ const struct ng_mpls_vpls_host_ary *const ary =
+ (const struct ng_mpls_vpls_host_ary *)(buf - sizeof(uint32_t));
+ return (ary->num_hosts);
+}
+
+static const struct ng_parse_array_info vpls_host_ary_info = {
+ &vpls_host_type,
+ ng_mpls_vpls_getTableLength
+};
+static const struct ng_parse_type vpls_host_ary_type = {
+ &ng_parse_array_type,
+ &vpls_host_ary_info
+};
+
+static const struct ng_parse_struct_field vpls_host_ary_fields[] =
+ NG_MPLS_VPLS_HOST_ARY_FIELDS(&vpls_host_ary_type);
+static const struct ng_parse_type vpls_host_ary_outer_type = {
+ &ng_parse_struct_type,
+ &vpls_host_ary_fields
+};
+
+static const struct ng_parse_struct_field vpls_port_stats_fields[] =
+ NG_MPLS_VPLS_PORT_STATS_FIELDS;
+static const struct ng_parse_type vpls_port_stats_type = {
+ &ng_parse_struct_type,
+ &vpls_port_stats_fields
+};
+
+static const struct ng_cmdlist ng_mpls_vpls_cmdlist[] = {
+ {
+ NGM_MPLS_VPLS_COOKIE,
+ NGM_MPLS_VPLS_SET_CONFIG,
+ "setconfig",
+ &vpls_config_type,
+ NULL
+ },
+ {
+ NGM_MPLS_VPLS_COOKIE,
+ NGM_MPLS_VPLS_GET_CONFIG,
+ "getconfig",
+ NULL,
+ &vpls_config_type
+ },
+ {
+ NGM_MPLS_VPLS_COOKIE,
+ NGM_MPLS_VPLS_GET_TABLE,
+ "gettable",
+ NULL,
+ &vpls_host_ary_outer_type
+ },
+ {
+ NGM_MPLS_VPLS_COOKIE,
+ NGM_MPLS_VPLS_GET_STATS,
+ "getstats",
+ NULL,
+ &vpls_port_stats_type
+ },
+ {
+ NGM_MPLS_VPLS_COOKIE,
+ NGM_MPLS_VPLS_CLR_STATS,
+ "clrstats",
+ NULL,
+ NULL
+ },
+ {
+ NGM_MPLS_VPLS_COOKIE,
+ NGM_MPLS_VPLS_RESET,
+ "reset",
+ NULL,
+ NULL
+ },
+ {
+ NGM_MPLS_VPLS_COOKIE,
+ NGM_MPLS_VPLS_GET_PORT_STATS,
+ "getportstats",
+ &ng_parse_hookbuf_type,
+ &vpls_port_stats_type
+ },
+ { 0 }
+};
+
+static struct ng_type ng_mpls_vpls_typestruct = {
+ .version = NG_ABI_VERSION,
+ .name = NG_MPLS_VPLS_NODE_TYPE,
+ .constructor = ng_mpls_vpls_constructor,
+ .rcvmsg = ng_mpls_vpls_rcvmsg,
+ .shutdown = ng_mpls_vpls_shutdown,
+ .newhook = ng_mpls_vpls_newhook,
+ .rcvdata = ng_mpls_vpls_rcvdata,
+ .disconnect = ng_mpls_vpls_disconnect,
+ .cmdlist = ng_mpls_vpls_cmdlist,
+};
+NETGRAPH_INIT(mpls_vpls, &ng_mpls_vpls_typestruct);
+
+static uint32_t
+ng_mpls_vpls_hash(const u_char *addr)
+{
+ return (addr[0] ^ addr[1] ^ addr[2] ^ addr[3] ^ addr[4] ^ addr[5]) &
+ (NG_MPLS_VPLS_HASH_SIZE - 1);
+}
+
+static struct vpls_host *
+ng_mpls_vpls_lookup(priv_p priv, const u_char *addr)
+{
+ struct vpls_bucket *bucket;
+ struct vpls_host *h;
+
+ bucket = &priv->buckets[ng_mpls_vpls_hash(addr)];
+ SLIST_FOREACH(h, bucket, next) {
+ if (bcmp(h->addr, addr, ETHER_ADDR_LEN) == 0)
+ return (h);
+ }
+ return (NULL);
+}
+
+static void
+ng_mpls_vpls_learn(priv_p priv, const u_char *addr, struct vpls_port *port)
+{
+ struct vpls_host *h;
+
+ h = ng_mpls_vpls_lookup(priv, addr);
+ if (h != NULL) {
+ h->port = port;
+ h->staleness = 0;
+ return;
+ }
+
+ h = malloc(sizeof(*h), M_NETGRAPH_MPLS_VPLS, M_NOWAIT);
+ if (h == NULL)
+ return;
+
+ memcpy(h->addr, addr, ETHER_ADDR_LEN);
+ h->port = port;
+ h->age = 0;
+ h->staleness = 0;
+
+ struct vpls_bucket *bucket =
+ &priv->buckets[ng_mpls_vpls_hash(addr)];
+ SLIST_INSERT_HEAD(bucket, h, next);
+ priv->num_hosts++;
+}
+
+static struct vpls_port *
+ng_mpls_vpls_find_port(priv_p priv, hook_p hook)
+{
+ struct vpls_port *p;
+
+ LIST_FOREACH(p, &priv->port_list, entries) {
+ if (p->hook == hook)
+ return (p);
+ }
+ return (NULL);
+}
+
+static void
+ng_mpls_vpls_timer(void *arg)
+{
+ priv_p priv = arg;
+ struct vpls_bucket *bucket;
+ struct vpls_host *h, *h_tmp;
+ int i;
+
+ for (i = 0; i < NG_MPLS_VPLS_HASH_SIZE; i++) {
+ bucket = &priv->buckets[i];
+ SLIST_FOREACH_SAFE(h, bucket, next, h_tmp) {
+ h->age++;
+ h->staleness++;
+ if (h->staleness >= priv->conf.max_staleness) {
+ SLIST_REMOVE(bucket, h, vpls_host, next);
+ free(h, M_NETGRAPH_MPLS_VPLS);
+ priv->num_hosts--;
+ }
+ }
+ }
+
+ callout_reset(&priv->timer, hz, ng_mpls_vpls_timer, priv);
+}
+
+static int
+ng_mpls_vpls_constructor(node_p node)
+{
+ priv_p priv;
+
+ priv = malloc(sizeof(*priv), M_NETGRAPH_MPLS_VPLS, M_WAITOK | M_ZERO);
+ priv->node = node;
+ priv->buckets = malloc(sizeof(struct vpls_bucket) *
+ NG_MPLS_VPLS_HASH_SIZE, M_NETGRAPH_MPLS_VPLS, M_WAITOK | M_ZERO);
+ for (int i = 0; i < NG_MPLS_VPLS_HASH_SIZE; i++)
+ SLIST_INIT(&priv->buckets[i]);
+
+ LIST_INIT(&priv->port_list);
+ priv->conf.max_staleness = 300;
+ priv->conf.debug_level = 0;
+
+ callout_init(&priv->timer, 1);
+ callout_reset(&priv->timer, hz, ng_mpls_vpls_timer, priv);
+
+ NG_NODE_SET_PRIVATE(node, priv);
+ return (0);
+}
+
+static int
+ng_mpls_vpls_newhook(node_p node, hook_p hook, const char *name)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct vpls_port *port;
+ size_t plen;
+
+ plen = sizeof(NG_MPLS_VPLS_HOOK_CE_PREFIX) - 1;
+ if (strncmp(name, NG_MPLS_VPLS_HOOK_CE_PREFIX, plen) == 0) {
+ char *eptr;
+ strtoul(name + plen, &eptr, 10);
+ if (*eptr != '\0')
+ return (EINVAL);
+
+ port = malloc(sizeof(*port), M_NETGRAPH_MPLS_VPLS,
+ M_NOWAIT | M_ZERO);
+ if (port == NULL)
+ return (ENOMEM);
+ port->hook = hook;
+ port->is_pw = 0;
+ NG_HOOK_SET_PRIVATE(hook, port);
+ LIST_INSERT_HEAD(&priv->port_list, port, entries);
+ priv->num_ports++;
+ return (0);
+ }
+
+ plen = sizeof(NG_MPLS_VPLS_HOOK_PW_PREFIX) - 1;
+ if (strncmp(name, NG_MPLS_VPLS_HOOK_PW_PREFIX, plen) == 0) {
+ char *eptr;
+ strtoul(name + plen, &eptr, 10);
+ if (*eptr != '\0')
+ return (EINVAL);
+
+ port = malloc(sizeof(*port), M_NETGRAPH_MPLS_VPLS,
+ M_NOWAIT | M_ZERO);
+ if (port == NULL)
+ return (ENOMEM);
+ port->hook = hook;
+ port->is_pw = 1;
+ NG_HOOK_SET_PRIVATE(hook, port);
+ LIST_INSERT_HEAD(&priv->port_list, port, entries);
+ priv->num_ports++;
+ return (0);
+ }
+
+ return (EINVAL);
+}
+
+static int
+ng_mpls_vpls_rcvmsg(node_p node, item_p item, hook_p lasthook)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct ng_mesg *msg, *resp = NULL;
+ int error = 0;
+
+ NGI_GET_MSG(item, msg);
+ switch (msg->header.typecookie) {
+ case NGM_MPLS_VPLS_COOKIE:
+ switch (msg->header.cmd) {
+ case NGM_MPLS_VPLS_SET_CONFIG:
+ {
+ struct ng_mpls_vpls_config *conf =
+ (struct ng_mpls_vpls_config *)msg->data;
+ if (msg->header.arglen < sizeof(*conf)) {
+ error = EINVAL;
+ break;
+ }
+ priv->conf = *conf;
+ break;
+ }
+ case NGM_MPLS_VPLS_GET_CONFIG:
+ {
+ NG_MKRESPONSE(resp, msg, sizeof(priv->conf), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ memcpy(resp->data, &priv->conf, sizeof(priv->conf));
+ break;
+ }
+ case NGM_MPLS_VPLS_GET_TABLE:
+ {
+ struct ng_mpls_vpls_host_ary *ary;
+ struct ng_mpls_vpls_host *out;
+ struct vpls_host *h;
+ int i, count = 0;
+
+ for (i = 0; i < NG_MPLS_VPLS_HASH_SIZE; i++) {
+ SLIST_FOREACH(h, &priv->buckets[i], next)
+ count++;
+ }
+
+ NG_MKRESPONSE(resp, msg, sizeof(*ary) +
+ count * sizeof(*out), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ ary = (struct ng_mpls_vpls_host_ary *)resp->data;
+ ary->num_hosts = 0;
+ out = &ary->hosts[0];
+ for (i = 0; i < NG_MPLS_VPLS_HASH_SIZE; i++) {
+ SLIST_FOREACH(h, &priv->buckets[i], next) {
+ memcpy(out->addr, h->addr,
+ ETHER_ADDR_LEN);
+ strncpy(out->hook,
+ NG_HOOK_NAME(h->port->hook),
+ NG_HOOKSIZ);
+ out->age = h->age;
+ out->staleness = h->staleness;
+ out++;
+ ary->num_hosts++;
+ }
+ }
+ break;
+ }
+ case NGM_MPLS_VPLS_GET_STATS:
+ {
+ struct ng_mpls_vpls_port_stats *ps;
+ struct vpls_port *p;
+
+ NG_MKRESPONSE(resp, msg, sizeof(*ps), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ ps = (struct ng_mpls_vpls_port_stats *)resp->data;
+ memset(ps, 0, sizeof(*ps));
+ LIST_FOREACH(p, &priv->port_list, entries) {
+ ps->rx_packets += p->rx_packets;
+ ps->rx_bytes += p->rx_bytes;
+ ps->rx_unknown += p->rx_unknown;
+ ps->tx_packets += p->tx_packets;
+ ps->tx_bytes += p->tx_bytes;
+ ps->loop_drops += p->loop_drops;
+ }
+ break;
+ }
+ case NGM_MPLS_VPLS_GET_PORT_STATS:
+ {
+ struct ng_mpls_vpls_port_stats *ps;
+ hook_p hook;
+ struct vpls_port *p;
+
+ if (msg->header.arglen != NG_HOOKSIZ) {
+ error = EINVAL;
+ break;
+ }
+ hook = ng_findhook(node, (char *)msg->data);
+ if (hook == NULL) {
+ error = ENOENT;
+ break;
+ }
+ p = NG_HOOK_PRIVATE(hook);
+ if (p == NULL) {
+ error = ENOENT;
+ break;
+ }
+ NG_MKRESPONSE(resp, msg, sizeof(*ps), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ ps = (struct ng_mpls_vpls_port_stats *)resp->data;
+ ps->rx_packets = p->rx_packets;
+ ps->rx_bytes = p->rx_bytes;
+ ps->rx_unknown = p->rx_unknown;
+ ps->tx_packets = p->tx_packets;
+ ps->tx_bytes = p->tx_bytes;
+ ps->loop_drops = p->loop_drops;
+ break;
+ }
+ case NGM_MPLS_VPLS_CLR_STATS:
+ {
+ struct vpls_port *p;
+ LIST_FOREACH(p, &priv->port_list, entries) {
+ p->rx_packets = 0;
+ p->rx_bytes = 0;
+ p->rx_unknown = 0;
+ p->tx_packets = 0;
+ p->tx_bytes = 0;
+ p->loop_drops = 0;
+ }
+ break;
+ }
+ case NGM_MPLS_VPLS_RESET:
+ {
+ struct vpls_host *h, *h_tmp;
+ int i;
+ for (i = 0; i < NG_MPLS_VPLS_HASH_SIZE; i++) {
+ SLIST_FOREACH_SAFE(h, &priv->buckets[i],
+ next, h_tmp) {
+ SLIST_REMOVE(&priv->buckets[i], h,
+ vpls_host, next);
+ free(h, M_NETGRAPH_MPLS_VPLS);
+ }
+ }
+ priv->num_hosts = 0;
+ break;
+ }
+ default:
+ error = EINVAL;
+ break;
+ }
+ break;
+ default:
+ error = EINVAL;
+ break;
+ }
+ NG_RESPOND_MSG(error, node, item, resp);
+ NG_FREE_MSG(msg);
+ return (error);
+}
+
+static int
+ng_mpls_vpls_forward(priv_p priv, struct vpls_port *ingress,
+ struct mbuf *m, item_p item)
+{
+ struct ether_header *eh;
+ struct vpls_host *dst;
+ struct vpls_port *p, *flood_targets[64];
+ int n_flood = 0;
+ int i, bw_dmac, error;
+
+ if (m->m_pkthdr.len < sizeof(struct ether_header)) {
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (EINVAL);
+ }
+ if (m->m_len < sizeof(struct ether_header) &&
+ (m = m_pullup(m, sizeof(struct ether_header))) == NULL) {
+ NG_FREE_ITEM(item);
+ return (ENOBUFS);
+ }
+
+ eh = mtod(m, struct ether_header *);
+
+ ng_mpls_vpls_learn(priv, eh->ether_shost, ingress);
+
+ bw_dmac = (eh->ether_dhost[0] & 1) ||
+ bcmp(eh->ether_dhost, "\xff\xff\xff\xff\xff\xff", ETHER_ADDR_LEN)
+ == 0;
+
+ dst = ng_mpls_vpls_lookup(priv, eh->ether_dhost);
+
+ if (dst != NULL && !bw_dmac) {
+ if (dst->port == ingress) {
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+ ingress->tx_packets++;
+ ingress->tx_bytes += m->m_pkthdr.len;
+ NG_FWD_NEW_DATA(error, item, dst->port->hook, m);
+ return (error);
+ }
+
+ /* Flood: collect all eligible ports. */
+ LIST_FOREACH(p, &priv->port_list, entries) {
+ if (p->hook == ingress->hook)
+ continue;
+ if (ingress->is_pw && p->is_pw)
+ continue;
+ if (n_flood < 64)
+ flood_targets[n_flood++] = p;
+ }
+
+ if (n_flood == 0) {
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+
+ /* Send copies to all but the last target. */
+ for (i = 0; i < n_flood - 1; i++) {
+ struct mbuf *m_copy;
+
+ m_copy = m_dup(m, M_NOWAIT);
+ if (m_copy == NULL)
+ continue;
+ flood_targets[i]->tx_packets++;
+ flood_targets[i]->tx_bytes += m_copy->m_pkthdr.len;
+ NG_SEND_DATA_ONLY(error, flood_targets[i]->hook, m_copy);
+ }
+
+ /* Send the original to the last target. */
+ flood_targets[n_flood - 1]->tx_packets++;
+ flood_targets[n_flood - 1]->tx_bytes += m->m_pkthdr.len;
+ NG_FWD_NEW_DATA(error, item, flood_targets[n_flood - 1]->hook, m);
+ return (error);
+}
+
+static int
+ng_mpls_vpls_rcvdata(hook_p hook, item_p item)
+{
+ const node_p node = NG_HOOK_NODE(hook);
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct vpls_port *ingress;
+ struct mbuf *m;
+
+ ingress = NG_HOOK_PRIVATE(hook);
+ if (ingress == NULL) {
+ NG_FREE_ITEM(item);
+ return (0);
+ }
+
+ NGI_GET_M(item, m);
+
+ if (m->m_pkthdr.len < sizeof(struct ether_header)) {
+ ingress->rx_unknown++;
+ NG_FREE_M(m);
+ NG_FREE_ITEM(item);
+ return (EINVAL);
+ }
+
+ ingress->rx_packets++;
+ ingress->rx_bytes += m->m_pkthdr.len;
+
+ ng_mpls_vpls_forward(priv, ingress, m, item);
+ return (0);
+}
+
+static int
+ng_mpls_vpls_shutdown(node_p node)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct vpls_port *p, *p_tmp;
+ struct vpls_host *h, *h_tmp;
+ int i;
+
+ callout_stop(&priv->timer);
+
+ LIST_FOREACH_SAFE(p, &priv->port_list, entries, p_tmp) {
+ LIST_REMOVE(p, entries);
+ free(p, M_NETGRAPH_MPLS_VPLS);
+ }
+
+ for (i = 0; i < NG_MPLS_VPLS_HASH_SIZE; i++) {
+ SLIST_FOREACH_SAFE(h, &priv->buckets[i], next, h_tmp) {
+ SLIST_REMOVE(&priv->buckets[i], h, vpls_host, next);
+ free(h, M_NETGRAPH_MPLS_VPLS);
+ }
+ }
+ free(priv->buckets, M_NETGRAPH_MPLS_VPLS);
+
+ NG_NODE_SET_PRIVATE(node, NULL);
+ NG_NODE_UNREF(node);
+ free(priv, M_NETGRAPH_MPLS_VPLS);
+ return (0);
+}
+
+static int
+ng_mpls_vpls_disconnect(hook_p hook)
+{
+ const node_p node = NG_HOOK_NODE(hook);
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct vpls_port *p;
+
+ p = NG_HOOK_PRIVATE(hook);
+ if (p != NULL) {
+ LIST_REMOVE(p, entries);
+ free(p, M_NETGRAPH_MPLS_VPLS);
+ priv->num_ports--;
+ }
+
+ if (NG_NODE_NUMHOOKS(node) == 0 && NG_NODE_IS_VALID(node))
+ ng_rmnode_self(node);
+ return (0);
+}
diff --git a/sys/netgraph/ng_mpls_vpls.h b/sys/netgraph/ng_mpls_vpls.h
new file mode 100644
index 00000000000..7c2af9bb012
--- /dev/null
+++ b/sys/netgraph/ng_mpls_vpls.h
@@ -0,0 +1,91 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ */
+
+#ifndef _NETGRAPH_NG_MPLS_VPLS_H_
+#define _NETGRAPH_NG_MPLS_VPLS_H_
+
+#define NG_MPLS_VPLS_NODE_TYPE "mpls_vpls"
+#define NGM_MPLS_VPLS_COOKIE 2275046183
+
+#define NG_MPLS_VPLS_HOOK_CE_PREFIX "ce"
+#define NG_MPLS_VPLS_HOOK_CE_FMT "ce%u"
+#define NG_MPLS_VPLS_HOOK_PW_PREFIX "pw"
+#define NG_MPLS_VPLS_HOOK_PW_FMT "pw%u"
+
+#define NG_MPLS_VPLS_HASH_SIZE 256
+
+struct ng_mpls_vpls_config {
+ uint32_t debug_level;
+ uint32_t max_staleness;
+ uint32_t loop_timeout;
+ uint32_t min_stable_age;
+};
+
+#define NG_MPLS_VPLS_CONFIG_FIELDS { \
+ { "debug_level", &ng_parse_uint32_type }, \
+ { "max_staleness", &ng_parse_uint32_type }, \
+ { "loop_timeout", &ng_parse_uint32_type }, \
+ { "min_stable_age", &ng_parse_uint32_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_vpls_host {
+ u_char addr[ETHER_ADDR_LEN];
+ char hook[NG_HOOKSIZ];
+ uint16_t age;
+ uint16_t staleness;
+};
+
+#define NG_MPLS_VPLS_HOST_FIELDS { \
+ { "addr", &ng_parse_enaddr_type }, \
+ { "hook", &ng_parse_hookbuf_type }, \
+ { "age", &ng_parse_uint16_type }, \
+ { "staleness", &ng_parse_uint16_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_vpls_host_ary {
+ uint32_t num_hosts;
+ struct ng_mpls_vpls_host hosts[];
+};
+
+#define NG_MPLS_VPLS_HOST_ARY_FIELDS(harytype) { \
+ { "num_hosts", &ng_parse_uint32_type }, \
+ { "hosts", (harytype) }, \
+ { NULL } \
+}
+
+struct ng_mpls_vpls_port_stats {
+ uint64_t rx_packets;
+ uint64_t rx_bytes;
+ uint64_t rx_unknown;
+ uint64_t tx_packets;
+ uint64_t tx_bytes;
+ uint64_t loop_drops;
+};
+
+#define NG_MPLS_VPLS_PORT_STATS_FIELDS { \
+ { "rx_packets", &ng_parse_uint64_type }, \
+ { "rx_bytes", &ng_parse_uint64_type }, \
+ { "rx_unknown", &ng_parse_uint64_type }, \
+ { "tx_packets", &ng_parse_uint64_type }, \
+ { "tx_bytes", &ng_parse_uint64_type }, \
+ { "loop_drops", &ng_parse_uint64_type }, \
+ { NULL } \
+}
+
+enum {
+ NGM_MPLS_VPLS_SET_CONFIG = 1,
+ NGM_MPLS_VPLS_GET_CONFIG,
+ NGM_MPLS_VPLS_GET_TABLE,
+ NGM_MPLS_VPLS_GET_STATS,
+ NGM_MPLS_VPLS_CLR_STATS,
+ NGM_MPLS_VPLS_RESET,
+ NGM_MPLS_VPLS_GET_PORT_STATS,
+};
+
+#endif /* _NETGRAPH_NG_MPLS_VPLS_H_ */
diff --git a/sys/netgraph/ng_mpls_vrf.c b/sys/netgraph/ng_mpls_vrf.c
new file mode 100644
index 00000000000..9235152eabc
--- /dev/null
+++ b/sys/netgraph/ng_mpls_vrf.c
@@ -0,0 +1,1146 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/kernel.h>
+#include <sys/lock.h>
+#include <sys/malloc.h>
+#include <sys/mbuf.h>
+#include <sys/errno.h>
+#include <sys/socket.h>
+#include <sys/sockio.h>
+#include <sys/syslog.h>
+#include <sys/epoch.h>
+
+#include <net/if.h>
+#include <net/if_types.h>
+#include <net/if_var.h>
+#include <net/if_private.h>
+#include <net/if_dl.h>
+#include <net/ethernet.h>
+#include <net/bpf.h>
+#include <net/vnet.h>
+#include <net/netisr.h>
+#include <net/route.h>
+
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/in_fib.h>
+#include <netinet6/in6_fib.h>
+#include <netinet/in_var.h>
+#include <netinet/if_ether.h>
+#include <net/if_llatbl.h>
+#include <netinet6/nd6.h>
+
+#include <netgraph/ng_message.h>
+#include <netgraph/netgraph.h>
+#include <netgraph/ng_parse.h>
+#include <netgraph/ng_ether.h>
+#include <netgraph/ng_mpls.h>
+#include <netgraph/ng_mpls_vrf.h>
+
+struct ng_mpls_vrf_private;
+
+struct vrf_instance {
+ uint32_t vrf_id;
+ uint32_t fib_num;
+ struct ifnet *ifp;
+ struct ng_mpls_vrf_private *priv;
+ uint32_t rd_high;
+ uint32_t rd_low;
+ uint32_t flags;
+ struct ng_mpls_vrf_stats stats;
+ LIST_ENTRY(vrf_instance) entries;
+};
+
+struct ce_port {
+ hook_p hook;
+ uint32_t vrf_id;
+ uint8_t mac[ETHER_ADDR_LEN];
+ LIST_ENTRY(ce_port) entries;
+};
+
+struct vpn_label_entry {
+ uint32_t label;
+ uint32_t vrf_id;
+ uint32_t addr[4];
+ uint8_t prefix_len;
+ uint8_t push_count;
+ uint32_t push_labels[NG_MPLS_VRF_MAX_LABELS];
+ uint8_t family;
+ LIST_ENTRY(vpn_label_entry) entries;
+};
+
+struct ng_mpls_vrf_private {
+ node_p node;
+ hook_p downstream_hook;
+ LIST_HEAD(, vrf_instance) vrf_list;
+ LIST_HEAD(, ce_port) ce_list;
+ LIST_HEAD(, vpn_label_entry) vpn_label_list;
+ uint32_t vrf_count;
+ uint32_t ce_count;
+ int unit;
+};
+
+typedef struct ng_mpls_vrf_private *priv_p;
+
+static MALLOC_DEFINE(M_NETGRAPH_MPLS_VRF, "ng_mpls_vrf", "netgraph mpls vrf node");
+
+static ng_constructor_t ng_mpls_vrf_constructor;
+static ng_rcvmsg_t ng_mpls_vrf_rcvmsg;
+static ng_shutdown_t ng_mpls_vrf_shutdown;
+static ng_newhook_t ng_mpls_vrf_newhook;
+static ng_rcvdata_t ng_mpls_vrf_rcvdata;
+static ng_disconnect_t ng_mpls_vrf_disconnect;
+
+static int ng_mpls_vrf_output(struct ifnet *, struct mbuf *,
+ const struct sockaddr *, struct route *);
+static int ng_mpls_vrf_ioctl(struct ifnet *, u_long, caddr_t);
+static int ng_mpls_vrf_ce_input(hook_p, struct mbuf *);
+static int ng_mpls_vrf_mpls_input(hook_p, struct mbuf *);
+static struct vrf_instance *ng_mpls_vrf_find(priv_p, uint32_t);
+static struct ce_port *ng_mpls_vrf_find_ce(priv_p, uint32_t);
+static struct vpn_label_entry *ng_mpls_vrf_find_label(priv_p, uint32_t);
+
+static const struct ng_parse_struct_field vrf_config_fields[] =
+ NG_MPLS_VRF_CONFIG_FIELDS;
+static const struct ng_parse_type vrf_config_type = {
+ &ng_parse_struct_type,
+ &vrf_config_fields
+};
+
+static const struct ng_parse_struct_field vrf_vpn_label_fields[] =
+ NG_MPLS_VRF_VPN_LABEL_FIELDS;
+static const struct ng_parse_type vrf_vpn_label_type = {
+ &ng_parse_struct_type,
+ &vrf_vpn_label_fields
+};
+
+static const struct ng_parse_struct_field vrf_vpn_label6_fields[] =
+ NG_MPLS_VRF_VPN_LABEL6_FIELDS;
+static const struct ng_parse_type vrf_vpn_label6_type = {
+ &ng_parse_struct_type,
+ &vrf_vpn_label6_fields
+};
+
+static const struct ng_parse_struct_field vrf_stats_fields[] =
+ NG_MPLS_VRF_STATS_FIELDS;
+static const struct ng_parse_type vrf_stats_type = {
+ &ng_parse_struct_type,
+ &vrf_stats_fields
+};
+
+static const struct ng_cmdlist ng_mpls_vrf_cmdlist[] = {
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_ADD_VRF,
+ "addvrf",
+ &vrf_config_type,
+ NULL
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_DEL_VRF,
+ "delvrf",
+ &ng_parse_uint32_type,
+ NULL
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_ADD_VPN_LABEL,
+ "addvpnlabel",
+ &vrf_vpn_label_type,
+ NULL
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_DEL_VPN_LABEL,
+ "delvpnlabel",
+ &ng_parse_uint32_type,
+ NULL
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_ADD_VPN_LABEL6,
+ "addvpnlabel6",
+ &vrf_vpn_label6_type,
+ NULL
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_DEL_VPN_LABEL6,
+ "delvpnlabel6",
+ &ng_parse_uint32_type,
+ NULL
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_GET_STATS,
+ "getstats",
+ &ng_parse_uint32_type,
+ &vrf_stats_type
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_CLR_STATS,
+ "clrstats",
+ &ng_parse_uint32_type,
+ NULL
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_GET_CONFIG,
+ "getconfig",
+ &ng_parse_uint32_type,
+ &vrf_config_type
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_GET_VRF_TABLE,
+ "getvrftable",
+ NULL,
+ &ng_parse_uint32_type
+ },
+ {
+ NGM_MPLS_VRF_COOKIE,
+ NGM_MPLS_VRF_GET_CE_TABLE,
+ "getcetable",
+ NULL,
+ &ng_parse_uint32_type
+ },
+ { 0 }
+};
+
+static struct ng_type ng_mpls_vrf_typestruct = {
+ .version = NG_ABI_VERSION,
+ .name = NG_MPLS_VRF_NODE_TYPE,
+ .constructor = ng_mpls_vrf_constructor,
+ .rcvmsg = ng_mpls_vrf_rcvmsg,
+ .shutdown = ng_mpls_vrf_shutdown,
+ .newhook = ng_mpls_vrf_newhook,
+ .rcvdata = ng_mpls_vrf_rcvdata,
+ .disconnect = ng_mpls_vrf_disconnect,
+ .cmdlist = ng_mpls_vrf_cmdlist,
+};
+NETGRAPH_INIT(mpls_vrf, &ng_mpls_vrf_typestruct);
+
+static int
+ng_mpls_vrf_constructor(node_p node)
+{
+ priv_p priv;
+
+ priv = malloc(sizeof(*priv), M_NETGRAPH_MPLS_VRF, M_WAITOK | M_ZERO);
+ priv->node = node;
+ LIST_INIT(&priv->vrf_list);
+ LIST_INIT(&priv->ce_list);
+ LIST_INIT(&priv->vpn_label_list);
+ priv->unit = alloc_unr(NULL);
+ NG_NODE_SET_PRIVATE(node, priv);
+ return (0);
+}
+
+static int
+ng_mpls_vrf_newhook(node_p node, hook_p hook, const char *name)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ size_t ceplen = sizeof(NG_MPLS_VRF_HOOK_CE_PREFIX) - 1;
+
+ if (strcmp(name, NG_MPLS_VRF_HOOK_DOWNSTREAM) == 0) {
+ if (priv->downstream_hook != NULL)
+ return (EISCONN);
+ priv->downstream_hook = hook;
+ NG_HOOK_SET_TO_INBOUND(hook);
+ return (0);
+ }
+
+ if (strncmp(name, NG_MPLS_VRF_HOOK_CE_PREFIX, ceplen) == 0) {
+ struct ce_port *cp;
+ unsigned long id;
+ char *end;
+
+ id = strtoul(name + ceplen, &end, 10);
+ if (*end != '\0' || id > 65535)
+ return (EINVAL);
+
+ cp = malloc(sizeof(*cp), M_NETGRAPH_MPLS_VRF, M_NOWAIT | M_ZERO);
+ if (cp == NULL)
+ return (ENOMEM);
+ cp->hook = hook;
+ cp->vrf_id = NG_MPLS_VRF_FIB_DEFAULT;
+ NG_HOOK_SET_PRIVATE(hook, cp);
+ NG_HOOK_SET_TO_INBOUND(hook);
+ LIST_INSERT_HEAD(&priv->ce_list, cp, entries);
+ priv->ce_count++;
+ return (0);
+ }
+
+ return (EINVAL);
+}
+
+static int
+ng_mpls_vrf_rcvmsg(node_p node, item_p item, hook_p lasthook)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct ng_mesg *resp = NULL;
+ int error = 0;
+ struct ng_mesg *msg;
+
+ NGI_GET_MSG(item, msg);
+ switch (msg->header.typecookie) {
+ case NGM_MPLS_VRF_COOKIE:
+ switch (msg->header.cmd) {
+ case NGM_MPLS_VRF_ADD_VRF:
+ {
+ struct ng_mpls_vrf_config *conf;
+ struct vrf_instance *vi;
+ struct ifnet *ifp;
+ char ifname[IFNAMSIZ];
+
+ if (msg->header.arglen < sizeof(*conf)) {
+ error = EINVAL;
+ break;
+ }
+ conf = (struct ng_mpls_vrf_config *)msg->data;
+
+ vi = ng_mpls_vrf_find(priv, conf->vrf_id);
+ if (vi != NULL) {
+ error = EEXIST;
+ break;
+ }
+
+ vi = malloc(sizeof(*vi), M_NETGRAPH_MPLS_VRF,
+ M_WAITOK | M_ZERO);
+ vi->vrf_id = conf->vrf_id;
+ vi->fib_num = conf->fib_num;
+ vi->priv = priv;
+ vi->rd_high = conf->rd_high;
+ vi->rd_low = conf->rd_low;
+ vi->flags = conf->flags;
+
+ ifp = if_alloc(IFT_PROPVIRTUAL);
+ if (ifp == NULL) {
+ free(vi, M_NETGRAPH_MPLS_VRF);
+ error = ENOMEM;
+ break;
+ }
+ ifp->if_softc = vi;
+ vi->ifp = ifp;
+
+ snprintf(ifname, sizeof(ifname), "%s%u",
+ NG_MPLS_VRF_IFACE_NAME, conf->vrf_id);
+ if_initname(ifp, NG_MPLS_VRF_IFACE_NAME, conf->vrf_id);
+ ifp->if_output = ng_mpls_vrf_output;
+ ifp->if_ioctl = ng_mpls_vrf_ioctl;
+ ifp->if_mtu = ETHERMTU;
+ ifp->if_flags = IFF_SIMPLEX | IFF_BROADCAST |
+ IFF_MULTICAST;
+ ifp->if_fib = conf->fib_num;
+ ifp->if_addrlen = ETHER_ADDR_LEN;
+ ifp->if_hdrlen = ETHER_HDR_LEN;
+ IFQ_SET_MAXLEN(&ifp->if_snd, ifqmaxlen);
+ ifp->if_snd.ifq_drv_maxlen = ifqmaxlen;
+ IFQ_SET_READY(&ifp->if_snd);
+
+ if_attach(ifp);
+ bpfattach(ifp, DLT_EN10MB, ETHER_HDR_LEN);
+
+ LIST_INSERT_HEAD(&priv->vrf_list, vi, entries);
+ priv->vrf_count++;
+ break;
+ }
+ case NGM_MPLS_VRF_DEL_VRF:
+ {
+ uint32_t vrf_id;
+ struct vrf_instance *vi;
+
+ if (msg->header.arglen < sizeof(vrf_id)) {
+ error = EINVAL;
+ break;
+ }
+ vrf_id = *(uint32_t *)msg->data;
+ vi = ng_mpls_vrf_find(priv, vrf_id);
+ if (vi == NULL) {
+ error = ENOENT;
+ break;
+ }
+ LIST_REMOVE(vi, entries);
+ bpfdetach(vi->ifp);
+ if_detach(vi->ifp);
+ if_free(vi->ifp);
+ free(vi, M_NETGRAPH_MPLS_VRF);
+ priv->vrf_count--;
+ break;
+ }
+ case NGM_MPLS_VRF_ADD_VPN_LABEL:
+ case NGM_MPLS_VRF_ADD_VPN_LABEL6:
+ {
+ uint32_t vpn_label;
+ uint32_t vrf_id;
+ uint32_t *prefix;
+ uint8_t prefix_len;
+ uint8_t family;
+ uint8_t push_count;
+ uint32_t *push_labels;
+ struct vpn_label_entry *vle;
+
+ if (msg->header.cmd == NGM_MPLS_VRF_ADD_VPN_LABEL) {
+ struct ng_mpls_vrf_vpn_label *nl =
+ (struct ng_mpls_vrf_vpn_label *)msg->data;
+ if (msg->header.arglen < sizeof(*nl)) {
+ error = EINVAL;
+ break;
+ }
+ vpn_label = nl->vpn_label;
+ vrf_id = nl->vrf_id;
+ prefix = &nl->prefix;
+ prefix_len = nl->prefix_len;
+ family = nl->family;
+ push_count = nl->push_count;
+ push_labels = nl->push_labels;
+ } else {
+ struct ng_mpls_vrf_vpn_label6 *nl =
+ (struct ng_mpls_vrf_vpn_label6 *)msg->data;
+ if (msg->header.arglen < sizeof(*nl)) {
+ error = EINVAL;
+ break;
+ }
+ vpn_label = nl->vpn_label;
+ vrf_id = nl->vrf_id;
+ prefix = nl->prefix;
+ prefix_len = nl->prefix_len;
+ family = AF_INET6;
+ push_count = nl->push_count;
+ push_labels = nl->push_labels;
+ }
+
+ if (push_count > NG_MPLS_VRF_MAX_LABELS) {
+ error = EINVAL;
+ break;
+ }
+
+ vle = malloc(sizeof(*vle), M_NETGRAPH_MPLS_VRF,
+ M_NOWAIT);
+ if (vle == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ vle->label = vpn_label;
+ vle->vrf_id = vrf_id;
+ memcpy(vle->addr, prefix, (family == AF_INET) ?
+ sizeof(uint32_t) : 16);
+ vle->prefix_len = prefix_len;
+ vle->push_count = push_count;
+ memcpy(vle->push_labels, push_labels,
+ push_count * sizeof(uint32_t));
+ vle->family = family;
+ LIST_INSERT_HEAD(&priv->vpn_label_list, vle, entries);
+ {
+ struct vrf_instance *vi =
+ ng_mpls_vrf_find(priv, vrf_id);
+ if (vi != NULL)
+ vi->stats.vpn_label_added++;
+ }
+ break;
+ }
+ case NGM_MPLS_VRF_DEL_VPN_LABEL:
+ case NGM_MPLS_VRF_DEL_VPN_LABEL6:
+ {
+ uint32_t label;
+ struct vpn_label_entry *vle;
+
+ if (msg->header.arglen < sizeof(label)) {
+ error = EINVAL;
+ break;
+ }
+ label = *(uint32_t *)msg->data;
+ LIST_FOREACH(vle, &priv->vpn_label_list, entries) {
+ if (vle->label == label) {
+ struct vrf_instance *vi =
+ ng_mpls_vrf_find(priv,
+ vle->vrf_id);
+ if (vi != NULL)
+ vi->stats.vpn_label_removed++;
+ LIST_REMOVE(vle, entries);
+ free(vle, M_NETGRAPH_MPLS_VRF);
+ break;
+ }
+ }
+ break;
+ }
+ case NGM_MPLS_VRF_GET_STATS:
+ case NGM_MPLS_VRF_CLR_STATS:
+ {
+ uint32_t vrf_id;
+ struct vrf_instance *vi;
+
+ if (msg->header.arglen < sizeof(vrf_id)) {
+ error = EINVAL;
+ break;
+ }
+ vrf_id = *(uint32_t *)msg->data;
+ vi = ng_mpls_vrf_find(priv, vrf_id);
+ if (vi == NULL) {
+ error = ENOENT;
+ break;
+ }
+ if (msg->header.cmd != NGM_MPLS_VRF_CLR_STATS) {
+ NG_MKRESPONSE(resp, msg, sizeof(vi->stats),
+ M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ memcpy(resp->data, &vi->stats,
+ sizeof(vi->stats));
+ }
+ if (msg->header.cmd != NGM_MPLS_VRF_GET_STATS)
+ memset(&vi->stats, 0, sizeof(vi->stats));
+ break;
+ }
+ case NGM_MPLS_VRF_GET_CONFIG:
+ {
+ uint32_t vrf_id;
+ struct vrf_instance *vi;
+ struct ng_mpls_vrf_config *conf;
+
+ if (msg->header.arglen < sizeof(vrf_id)) {
+ error = EINVAL;
+ break;
+ }
+ vrf_id = *(uint32_t *)msg->data;
+ vi = ng_mpls_vrf_find(priv, vrf_id);
+ if (vi == NULL) {
+ error = ENOENT;
+ break;
+ }
+ NG_MKRESPONSE(resp, msg, sizeof(*conf), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ conf = (struct ng_mpls_vrf_config *)resp->data;
+ conf->vrf_id = vi->vrf_id;
+ conf->fib_num = vi->fib_num;
+ conf->rd_high = vi->rd_high;
+ conf->rd_low = vi->rd_low;
+ conf->flags = vi->flags;
+ break;
+ }
+ case NGM_MPLS_VRF_GET_VRF_TABLE:
+ {
+ uint32_t count = 0;
+ struct vrf_instance *vi;
+
+ LIST_FOREACH(vi, &priv->vrf_list, entries)
+ count++;
+ NG_MKRESPONSE(resp, msg, sizeof(count), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ *(uint32_t *)resp->data = count;
+ break;
+ }
+ case NGM_MPLS_VRF_GET_CE_TABLE:
+ {
+ uint32_t count = 0;
+ struct ce_port *cp;
+
+ LIST_FOREACH(cp, &priv->ce_list, entries)
+ count++;
+ NG_MKRESPONSE(resp, msg, sizeof(count), M_NOWAIT);
+ if (resp == NULL) {
+ error = ENOMEM;
+ break;
+ }
+ *(uint32_t *)resp->data = count;
+ break;
+ }
+ default:
+ error = EINVAL;
+ break;
+ }
+ break;
+ default:
+ error = EINVAL;
+ break;
+ }
+ NG_RESPOND_MSG(error, node, item, resp);
+ NG_FREE_MSG(msg);
+ return (error);
+}
+
+static int
+ng_mpls_vrf_rcvdata(hook_p hook, item_p item)
+{
+ struct mbuf *m;
+
+ NGI_GET_M(item, m);
+ NG_FREE_ITEM(item);
+
+ if (strcmp(NG_HOOK_NAME(hook), NG_MPLS_VRF_HOOK_DOWNSTREAM) == 0)
+ return (ng_mpls_vrf_mpls_input(hook, m));
+
+ return (ng_mpls_vrf_ce_input(hook, m));
+}
+
+static int
+ng_mpls_vrf_ce_input(hook_p hook, struct mbuf *m)
+{
+ const node_p node = NG_HOOK_NODE(hook);
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct ce_port *cp = NG_HOOK_PRIVATE(hook);
+ struct vrf_instance *vi;
+ struct ether_header *eh;
+ struct epoch_tracker et;
+ int isr;
+
+ vi = ng_mpls_vrf_find(priv, cp->vrf_id);
+ if (vi == NULL || vi->ifp == NULL) {
+ NG_FREE_M(m);
+ return (ENETDOWN);
+ }
+
+ if (m->m_pkthdr.len < ETHER_HDR_LEN) {
+ NG_FREE_M(m);
+ return (EINVAL);
+ }
+
+ if (m->m_len < ETHER_HDR_LEN &&
+ (m = m_pullup(m, ETHER_HDR_LEN)) == NULL)
+ return (ENOBUFS);
+
+ eh = mtod(m, struct ether_header *);
+ m_adj(m, ETHER_HDR_LEN);
+
+ switch (ntohs(eh->ether_type)) {
+#ifdef INET
+ case ETHERTYPE_IP:
+ isr = NETISR_IP;
+ vi->stats.rx_ce_packets++;
+ vi->stats.rx_ce_bytes += m->m_pkthdr.len;
+ break;
+#endif
+#ifdef INET6
+ case ETHERTYPE_IPV6:
+ isr = NETISR_IPV6;
+ vi->stats.rx6_ce_packets++;
+ vi->stats.rx6_ce_bytes += m->m_pkthdr.len;
+ break;
+#endif
+ default:
+ NG_FREE_M(m);
+ return (0);
+ }
+
+ m->m_pkthdr.rcvif = vi->ifp;
+ M_SETFIB(m, vi->fib_num);
+ CURVNET_SET(vi->ifp->if_vnet);
+ NET_EPOCH_ENTER(et);
+ netisr_dispatch(isr, m);
+ NET_EPOCH_EXIT(et);
+ CURVNET_RESTORE();
+
+ return (0);
+}
+
+static int
+ng_mpls_vrf_mpls_input(hook_p hook, struct mbuf *m)
+{
+ const node_p node = NG_HOOK_NODE(hook);
+ const priv_p priv = NG_NODE_PRIVATE(node);
+ struct vpn_label_entry *vle;
+ uint32_t vpn_label;
+ uint32_t *shim;
+ struct vrf_instance *vi;
+ struct epoch_tracker et;
+ int isr;
+
+ if (m->m_pkthdr.len < MPLS_SHIM_LEN) {
+ NG_FREE_M(m);
+ return (EINVAL);
+ }
+
+ if (m->m_len < MPLS_SHIM_LEN &&
+ (m = m_pullup(m, MPLS_SHIM_LEN)) == NULL)
+ return (ENOBUFS);
+
+ shim = mtod(m, uint32_t *);
+ vpn_label = MPLS_LABEL(*shim);
+ m_adj(m, MPLS_SHIM_LEN);
+
+ vle = ng_mpls_vrf_find_label(priv, vpn_label);
+ if (vle == NULL) {
+ NG_FREE_M(m);
+ return (ENOENT);
+ }
+
+ vi = ng_mpls_vrf_find(priv, vle->vrf_id);
+ if (vi == NULL || vi->ifp == NULL) {
+ NG_FREE_M(m);
+ return (ENETDOWN);
+ }
+
+ if (vle->family == AF_INET) {
+ isr = NETISR_IP;
+ vi->stats.rx_mpls_packets++;
+ vi->stats.rx_mpls_bytes += m->m_pkthdr.len;
+ } else {
+ isr = NETISR_IPV6;
+ vi->stats.rx6_mpls_packets++;
+ vi->stats.rx6_mpls_bytes += m->m_pkthdr.len;
+ }
+
+ m->m_pkthdr.rcvif = vi->ifp;
+ M_SETFIB(m, vi->fib_num);
+
+ CURVNET_SET(vi->ifp->if_vnet);
+ NET_EPOCH_ENTER(et);
+ netisr_dispatch(isr, m);
+ NET_EPOCH_EXIT(et);
+ CURVNET_RESTORE();
+
+ return (0);
+}
+
+static int
+ng_mpls_vrf_encap_ethernet(struct mbuf *m, const u_char *dst_mac,
+ const u_char *src_mac, uint16_t ethertype)
+{
+ struct ether_header *eh;
+
+ M_PREPEND(m, ETHER_HDR_LEN, M_NOWAIT);
+ if (m == NULL)
+ return (ENOBUFS);
+ eh = mtod(m, struct ether_header *);
+ memcpy(eh->ether_dhost, dst_mac, ETHER_ADDR_LEN);
+ memcpy(eh->ether_shost, src_mac, ETHER_ADDR_LEN);
+ eh->ether_type = htons(ethertype);
+ return (0);
+}
+
+static int
+ng_mpls_vrf_arp_resolve(struct vrf_instance *vi, struct in_addr dest,
+ u_char *mac)
+{
+ struct llentry *lle;
+
+ lle = lla_lookup(LLTABLE(vi->ifp), LLE_EXCLUSIVE, (struct sockaddr *)
+ &(struct sockaddr_in){ .sin_family = AF_INET,
+ .sin_addr = dest });
+ if (lle == NULL)
+ return (EAGAIN);
+
+ if (lle->la_flags & LLE_VALID) {
+ memcpy(mac, &lle->ll_addr, ETHER_ADDR_LEN);
+ LLE_WUNLOCK(lle);
+ return (0);
+ }
+ LLE_WUNLOCK(lle);
+
+ arprequest(vi->ifp, &((struct sockaddr_in *)
+ &vi->ifp->if_addr->ifa_addr)->sin_addr, &dest, NULL);
+ return (EAGAIN);
+}
+
+#ifdef INET6
+static int
+ng_mpls_vrf_ndp_resolve(struct vrf_instance *vi, const struct in6_addr *dest,
+ u_char *mac)
+{
+ struct llentry *lle;
+ struct sockaddr_in6 sin6 = {
+ .sin6_family = AF_INET6,
+ .sin6_len = sizeof(sin6),
+ };
+ sin6.sin6_addr = *dest;
+
+ lle = lla_lookup(LLTABLE(vi->ifp), LLE_EXCLUSIVE,
+ (struct sockaddr *)&sin6);
+ if (lle == NULL)
+ return (EAGAIN);
+
+ if ((lle->la_flags & LLE_VALID) && !LLE_LINK(lle)) {
+ memcpy(mac, &lle->ll_addr, ETHER_ADDR_LEN);
+ LLE_WUNLOCK(lle);
+ return (0);
+ }
+ LLE_WUNLOCK(lle);
+
+ nd6_ns_output(vi->ifp, NULL, NULL, dest, NULL);
+ return (EAGAIN);
+}
+#endif
+
+static int
+ng_mpls_vrf_prepend_label_stack(struct mbuf **mp, struct vpn_label_entry *vle)
+{
+ int i;
+ uint32_t shim;
+
+ for (i = 0; i < vle->push_count; i++) {
+ int s_bit = (i == 0) ? 1 : 0;
+ shim = MPLS_MAKE(vle->push_labels[i], 0, s_bit, 255);
+ M_PREPEND((*mp), MPLS_SHIM_LEN, M_NOWAIT);
+ if ((*mp) == NULL)
+ return (ENOBUFS);
+ *mtod((*mp), uint32_t *) = shim;
+ }
+ return (0);
+}
+
+static int
+ng_mpls_vrf_output(struct ifnet *ifp, struct mbuf *m,
+ const struct sockaddr *dst, struct route *ro __unused)
+{
+ uint32_t af;
+
+ if (!((ifp->if_flags & IFF_UP) &&
+ (ifp->if_drv_flags & IFF_DRV_RUNNING))) {
+ m_freem(m);
+ return (ENETDOWN);
+ }
+
+ if (dst->sa_family == AF_UNSPEC || dst->sa_family == pseudo_AF_HDRCMPLT)
+ bcopy(dst->sa_data, &af, sizeof(af));
+ else
+ af = dst->sa_family;
+
+ switch (af) {
+#ifdef INET
+ case AF_INET:
+ {
+ struct vrf_instance *vi = ifp->if_softc;
+ struct nhop_object *nh;
+ struct in_addr dest;
+ struct vpn_label_entry *vle;
+ const struct sockaddr_in *sin;
+ const struct ng_mpls_vrf_private *priv;
+ struct ce_port *cp;
+ u_char mac[ETHER_ADDR_LEN];
+ uint32_t shim;
+ int error;
+
+ dest = ((struct sockaddr_in *)dst)->sin_addr;
+
+ nh = fib4_lookup(vi->fib_num, dest, 0, NHR_NONE, 0);
+ if (nh == NULL) {
+ vi->stats.no_route++;
+ m_freem(m);
+ return (EHOSTUNREACH);
+ }
+
+ priv = vi->priv;
+
+ if (nh->nh_ifp != vi->ifp &&
+ ng_mpls_vrf_find(priv, 0) != NULL) {
+ cp = ng_mpls_vrf_find_ce(priv, 0);
+ if (cp != NULL && cp->hook != NULL) {
+ if (ng_mpls_vrf_arp_resolve(vi, dest, mac)
+ == 0) {
+ u_char *src_mac;
+ src_mac = IF_LLADDR(vi->ifp);
+ error = ng_mpls_vrf_encap_ethernet(m,
+ mac, src_mac, ETHERTYPE_IP);
+ if (error) {
+ m_freem(m);
+ return (error);
+ }
+ vi->stats.tx_ce_packets++;
+ vi->stats.tx_ce_bytes +=
+ m->m_pkthdr.len;
+ NG_SEND_DATA_ONLY(error, cp->hook, m);
+ return (error);
+ }
+ }
+ }
+
+ sin = (const struct sockaddr_in *)
+ nh->nh_addr_info;
+ if (sin == NULL || sin->sin_family != AF_INET) {
+ m_freem(m);
+ return (EHOSTUNREACH);
+ }
+
+ cp = ng_mpls_vrf_find_ce(priv, 0);
+ if (cp != NULL && cp->hook != NULL) {
+ if (ng_mpls_vrf_arp_resolve(vi, dest, mac) == 0) {
+ error = ng_mpls_vrf_encap_ethernet(m,
+ mac, cp->mac, ETHERTYPE_IP);
+ if (error) {
+ m_freem(m);
+ return (error);
+ }
+ vi->stats.tx_ce_packets++;
+ vi->stats.tx_ce_bytes += m->m_pkthdr.len;
+ NG_SEND_DATA_ONLY(error, cp->hook, m);
+ return (error);
+ }
+ }
+
+ LIST_FOREACH(vle, &priv->vpn_label_list, entries) {
+ if (vle->vrf_id == vi->vrf_id &&
+ vle->family == AF_INET &&
+ vle->prefix_len == 0) {
+ if (vle->push_count > 0) {
+ error = ng_mpls_vrf_prepend_label_stack(
+ &m, vle);
+ if (error) {
+ m_freem(m);
+ return (error);
+ }
+ } else {
+ shim = MPLS_MAKE(vle->label, 0, 1, 255);
+ M_PREPEND(m, MPLS_SHIM_LEN, M_NOWAIT);
+ if (m == NULL)
+ return (ENOBUFS);
+ *mtod(m, uint32_t *) = shim;
+ }
+
+ if (priv->downstream_hook != NULL) {
+ vi->stats.tx_mpls_packets++;
+ vi->stats.tx_mpls_bytes +=
+ m->m_pkthdr.len;
+ NG_SEND_DATA_ONLY(error,
+ priv->downstream_hook, m);
+ return (error);
+ }
+ break;
+ }
+ }
+
+ m_freem(m);
+ return (ENETUNREACH);
+ }
+#endif
+#ifdef INET6
+ case AF_INET6:
+ {
+ struct vrf_instance *vi = ifp->if_softc;
+ struct nhop_object *nh;
+ struct in6_addr dest;
+ struct vpn_label_entry *vle;
+ const struct sockaddr_in6 *sin6;
+ const struct ng_mpls_vrf_private *priv;
+ struct ce_port *cp;
+ u_char mac[ETHER_ADDR_LEN];
+ uint32_t shim;
+ int error;
+
+ dest = ((const struct sockaddr_in6 *)dst)->sin6_addr;
+
+ nh = fib6_lookup(vi->fib_num, &dest, 0, NHR_NONE, 0);
+ if (nh == NULL) {
+ vi->stats.no_route++;
+ m_freem(m);
+ return (EHOSTUNREACH);
+ }
+
+ priv = vi->priv;
+
+ if (nh->nh_ifp != vi->ifp &&
+ ng_mpls_vrf_find(priv, 0) != NULL) {
+ cp = ng_mpls_vrf_find_ce(priv, 0);
+ if (cp != NULL && cp->hook != NULL) {
+ if (ng_mpls_vrf_ndp_resolve(vi, &dest, mac)
+ == 0) {
+ u_char *src_mac;
+ src_mac = IF_LLADDR(vi->ifp);
+ error = ng_mpls_vrf_encap_ethernet(m,
+ mac, src_mac, ETHERTYPE_IPV6);
+ if (error) {
+ m_freem(m);
+ return (error);
+ }
+ vi->stats.tx6_ce_packets++;
+ vi->stats.tx6_ce_bytes +=
+ m->m_pkthdr.len;
+ NG_SEND_DATA_ONLY(error, cp->hook, m);
+ return (error);
+ }
+ }
+ }
+
+ sin6 = (const struct sockaddr_in6 *)
+ nh->nh_addr_info;
+ if (sin6 == NULL || sin6->sin6_family != AF_INET6) {
+ m_freem(m);
+ return (EHOSTUNREACH);
+ }
+
+ cp = ng_mpls_vrf_find_ce(priv, 0);
+ if (cp != NULL && cp->hook != NULL) {
+ if (ng_mpls_vrf_ndp_resolve(vi, &dest, mac) == 0) {
+ error = ng_mpls_vrf_encap_ethernet(m,
+ mac, cp->mac, ETHERTYPE_IPV6);
+ if (error) {
+ m_freem(m);
+ return (error);
+ }
+ vi->stats.tx6_ce_packets++;
+ vi->stats.tx6_ce_bytes += m->m_pkthdr.len;
+ NG_SEND_DATA_ONLY(error, cp->hook, m);
+ return (error);
+ }
+ }
+
+ LIST_FOREACH(vle, &priv->vpn_label_list, entries) {
+ if (vle->vrf_id == vi->vrf_id &&
+ vle->family == AF_INET6 &&
+ vle->prefix_len == 0) {
+ if (vle->push_count > 0) {
+ error = ng_mpls_vrf_prepend_label_stack(
+ &m, vle);
+ if (error) {
+ m_freem(m);
+ return (error);
+ }
+ } else {
+ shim = MPLS_MAKE(vle->label, 0, 1, 255);
+ M_PREPEND(m, MPLS_SHIM_LEN, M_NOWAIT);
+ if (m == NULL)
+ return (ENOBUFS);
+ *mtod(m, uint32_t *) = shim;
+ }
+
+ if (priv->downstream_hook != NULL) {
+ vi->stats.tx6_mpls_packets++;
+ vi->stats.tx6_mpls_bytes +=
+ m->m_pkthdr.len;
+ NG_SEND_DATA_ONLY(error,
+ priv->downstream_hook, m);
+ return (error);
+ }
+ break;
+ }
+ }
+
+ m_freem(m);
+ return (ENETUNREACH);
+ }
+#endif
+ default:
+ m_freem(m);
+ return (EAFNOSUPPORT);
+ }
+}
+
+static int
+ng_mpls_vrf_ioctl(struct ifnet *ifp, u_long cmd, caddr_t data)
+{
+ struct ifreq *ifr = (struct ifreq *)data;
+ int error = 0;
+
+ switch (cmd) {
+ case SIOCSIFADDR:
+ case SIOCGIFADDR:
+ case SIOCSIFMTU:
+ if (ifr->ifr_mtu < 576)
+ ifr->ifr_mtu = 576;
+ if (ifr->ifr_mtu > 65535)
+ ifr->ifr_mtu = 65535;
+ ifp->if_mtu = ifr->ifr_mtu;
+ break;
+ case SIOCSIFFLAGS:
+ break;
+ default:
+ error = EINVAL;
+ }
+ return (error);
+}
+
+static int
+ng_mpls_vrf_shutdown(node_p node)
+{
+ const priv_p priv = NG_NODE_PRIVATE(node);
+
+ while (!LIST_EMPTY(&priv->vrf_list)) {
+ struct vrf_instance *vi = LIST_FIRST(&priv->vrf_list);
+ LIST_REMOVE(vi, entries);
+ bpfdetach(vi->ifp);
+ if_detach(vi->ifp);
+ if_free(vi->ifp);
+ free(vi, M_NETGRAPH_MPLS_VRF);
+ }
+
+ while (!LIST_EMPTY(&priv->ce_list)) {
+ struct ce_port *cp = LIST_FIRST(&priv->ce_list);
+ LIST_REMOVE(cp, entries);
+ free(cp, M_NETGRAPH_MPLS_VRF);
+ }
+
+ while (!LIST_EMPTY(&priv->vpn_label_list)) {
+ struct vpn_label_entry *vle = LIST_FIRST(&priv->vpn_label_list);
+ LIST_REMOVE(vle, entries);
+ free(vle, M_NETGRAPH_MPLS_VRF);
+ }
+
+ NG_NODE_SET_PRIVATE(node, NULL);
+ NG_NODE_UNREF(node);
+ free(priv, M_NETGRAPH_MPLS_VRF);
+ return (0);
+}
+
+static int
+ng_mpls_vrf_disconnect(hook_p hook)
+{
+ const node_p node = NG_HOOK_NODE(hook);
+ const priv_p priv = NG_NODE_PRIVATE(node);
+
+ if (hook == priv->downstream_hook) {
+ priv->downstream_hook = NULL;
+ } else {
+ struct ce_port *cp = NG_HOOK_PRIVATE(hook);
+ if (cp != NULL) {
+ LIST_REMOVE(cp, entries);
+ free(cp, M_NETGRAPH_MPLS_VRF);
+ priv->ce_count--;
+ }
+ }
+
+ if (NG_NODE_NUMHOOKS(node) == 0 && NG_NODE_IS_VALID(node))
+ ng_rmnode_self(node);
+ return (0);
+}
+
+static struct vrf_instance *
+ng_mpls_vrf_find(priv_p priv, uint32_t vrf_id)
+{
+ struct vrf_instance *vi;
+
+ LIST_FOREACH(vi, &priv->vrf_list, entries) {
+ if (vi->vrf_id == vrf_id)
+ return (vi);
+ }
+ return (NULL);
+}
+
+static struct ce_port *
+ng_mpls_vrf_find_ce(priv_p priv, uint32_t vrf_id)
+{
+ struct ce_port *cp;
+
+ LIST_FOREACH(cp, &priv->ce_list, entries) {
+ if (cp->vrf_id == vrf_id)
+ return (cp);
+ }
+ return (NULL);
+}
+
+static struct vpn_label_entry *
+ng_mpls_vrf_find_label(priv_p priv, uint32_t label)
+{
+ struct vpn_label_entry *vle;
+
+ LIST_FOREACH(vle, &priv->vpn_label_list, entries) {
+ if (vle->label == label)
+ return (vle);
+ }
+ return (NULL);
+}
diff --git a/sys/netgraph/ng_mpls_vrf.h b/sys/netgraph/ng_mpls_vrf.h
new file mode 100644
index 00000000000..cc8acd227ea
--- /dev/null
+++ b/sys/netgraph/ng_mpls_vrf.h
@@ -0,0 +1,151 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ */
+
+#ifndef _NETGRAPH_NG_MPLS_VRF_H_
+#define _NETGRAPH_NG_MPLS_VRF_H_
+
+#define NG_MPLS_VRF_NODE_TYPE "mpls_vrf"
+#define NGM_MPLS_VRF_COOKIE 2108574401
+
+#define NG_MPLS_VRF_IFACE_NAME "vrf"
+
+#define NG_MPLS_VRF_HOOK_CE_PREFIX "ce"
+#define NG_MPLS_VRF_HOOK_CE_FMT "ce%u"
+#define NG_MPLS_VRF_HOOK_DOWNSTREAM "downstream"
+#define NG_MPLS_VRF_HOOK_VRF_PREFIX "vrf"
+#define NG_MPLS_VRF_HOOK_VRF_FMT "vrf%u_mpls"
+
+#define NG_MPLS_VRF_FIB_DEFAULT 0
+#define NG_MPLS_VRF_VPN_LABEL_MIN 16
+#define NG_MPLS_VRF_VPN_LABEL_MAX 0xfffff
+#define NG_MPLS_VRF_MAX_LABELS 2
+
+struct ng_mpls_vrf_config {
+ uint32_t vrf_id;
+ uint32_t fib_num;
+ uint32_t rd_high;
+ uint32_t rd_low;
+ uint32_t flags;
+#define NG_MPLS_VRF_F_IMPORT 0x01
+#define NG_MPLS_VRF_F_EXPORT 0x02
+#define NG_MPLS_VRF_F_BOTH 0x03
+};
+
+#define NG_MPLS_VRF_CONFIG_FIELDS { \
+ { "vrf_id", &ng_parse_uint32_type }, \
+ { "fib_num", &ng_parse_uint32_type }, \
+ { "rd_high", &ng_parse_uint32_type }, \
+ { "rd_low", &ng_parse_uint32_type }, \
+ { "flags", &ng_parse_uint32_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_vrf_vpn_label {
+ uint32_t vpn_label;
+ uint32_t vrf_id;
+ uint32_t prefix;
+ uint8_t prefix_len;
+ uint8_t family;
+ uint8_t push_count;
+ uint32_t push_labels[NG_MPLS_VRF_MAX_LABELS];
+};
+
+#define NG_MPLS_VRF_VPN_LABEL_FIELDS { \
+ { "vpn_label", &ng_parse_uint32_type }, \
+ { "vrf_id", &ng_parse_uint32_type }, \
+ { "prefix", &ng_parse_ipaddr_type }, \
+ { "prefix_len", &ng_parse_uint8_type }, \
+ { "family", &ng_parse_uint8_type }, \
+ { "push_count", &ng_parse_uint8_type }, \
+ { "push_labels[0]", &ng_parse_uint32_type }, \
+ { "push_labels[1]", &ng_parse_uint32_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_vrf_vpn_label6 {
+ uint32_t vpn_label;
+ uint32_t vrf_id;
+ uint32_t prefix[4];
+ uint8_t prefix_len;
+ uint8_t push_count;
+ uint32_t push_labels[NG_MPLS_VRF_MAX_LABELS];
+};
+
+#define NG_MPLS_VRF_VPN_LABEL6_FIELDS { \
+ { "vpn_label", &ng_parse_uint32_type }, \
+ { "vrf_id", &ng_parse_uint32_type }, \
+ { "prefix[0]", &ng_parse_uint32_type }, \
+ { "prefix[1]", &ng_parse_uint32_type }, \
+ { "prefix[2]", &ng_parse_uint32_type }, \
+ { "prefix[3]", &ng_parse_uint32_type }, \
+ { "prefix_len", &ng_parse_uint8_type }, \
+ { "push_count", &ng_parse_uint8_type }, \
+ { "push_labels[0]", &ng_parse_uint32_type }, \
+ { "push_labels[1]", &ng_parse_uint32_type }, \
+ { NULL } \
+}
+
+struct ng_mpls_vrf_stats {
+ uint64_t rx_ce_packets;
+ uint64_t rx_ce_bytes;
+ uint64_t tx_ce_packets;
+ uint64_t tx_ce_bytes;
+ uint64_t rx_mpls_packets;
+ uint64_t rx_mpls_bytes;
+ uint64_t tx_mpls_packets;
+ uint64_t tx_mpls_bytes;
+ uint64_t vpn_label_added;
+ uint64_t vpn_label_removed;
+ uint64_t no_route;
+ uint64_t rx6_ce_packets;
+ uint64_t rx6_ce_bytes;
+ uint64_t tx6_ce_packets;
+ uint64_t tx6_ce_bytes;
+ uint64_t rx6_mpls_packets;
+ uint64_t rx6_mpls_bytes;
+ uint64_t tx6_mpls_packets;
+ uint64_t tx6_mpls_bytes;
+};
+
+#define NG_MPLS_VRF_STATS_FIELDS { \
+ { "rx_ce_packets", &ng_parse_uint64_type }, \
+ { "rx_ce_bytes", &ng_parse_uint64_type }, \
+ { "tx_ce_packets", &ng_parse_uint64_type }, \
+ { "tx_ce_bytes", &ng_parse_uint64_type }, \
+ { "rx_mpls_packets", &ng_parse_uint64_type }, \
+ { "rx_mpls_bytes", &ng_parse_uint64_type }, \
+ { "tx_mpls_packets", &ng_parse_uint64_type }, \
+ { "tx_mpls_bytes", &ng_parse_uint64_type }, \
+ { "vpn_label_added", &ng_parse_uint64_type }, \
+ { "vpn_label_removed", &ng_parse_uint64_type }, \
+ { "no_route", &ng_parse_uint64_type }, \
+ { "rx6_ce_packets", &ng_parse_uint64_type }, \
+ { "rx6_ce_bytes", &ng_parse_uint64_type }, \
+ { "tx6_ce_packets", &ng_parse_uint64_type }, \
+ { "tx6_ce_bytes", &ng_parse_uint64_type }, \
+ { "rx6_mpls_packets", &ng_parse_uint64_type }, \
+ { "rx6_mpls_bytes", &ng_parse_uint64_type }, \
+ { "tx6_mpls_packets", &ng_parse_uint64_type }, \
+ { "tx6_mpls_bytes", &ng_parse_uint64_type }, \
+ { NULL } \
+}
+
+enum {
+ NGM_MPLS_VRF_ADD_VRF = 1,
+ NGM_MPLS_VRF_DEL_VRF,
+ NGM_MPLS_VRF_ADD_VPN_LABEL,
+ NGM_MPLS_VRF_DEL_VPN_LABEL,
+ NGM_MPLS_VRF_GET_STATS,
+ NGM_MPLS_VRF_CLR_STATS,
+ NGM_MPLS_VRF_GET_CONFIG,
+ NGM_MPLS_VRF_ADD_VPN_LABEL6,
+ NGM_MPLS_VRF_DEL_VPN_LABEL6,
+ NGM_MPLS_VRF_GET_VRF_TABLE,
+ NGM_MPLS_VRF_GET_CE_TABLE,
+};
+
+#endif /* _NETGRAPH_NG_MPLS_VRF_H_ */
diff --git a/tools/build/test-includes/badfiles.inc b/tools/build/test-includes/badfiles.inc
index 9feb73edc5d..a42abcb3c4c 100644
--- a/tools/build/test-includes/badfiles.inc
+++ b/tools/build/test-includes/badfiles.inc
@@ -211,6 +211,12 @@ BADHDRS= \
netgraph/ng_lmi.h \
netgraph/ng_macfilter.h \
netgraph/ng_message.h \
+ netgraph/ng_mpls.h \
+ netgraph/ng_mpls_fec.h \
+ netgraph/ng_mpls_lsp.h \
+ netgraph/ng_mpls_pw.h \
+ netgraph/ng_mpls_vpls.h \
+ netgraph/ng_mpls_vrf.h \
netgraph/ng_mppc.h \
netgraph/ng_nat.h \
netgraph/ng_one2many.h \
diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile
index 44679ca290a..ba84339491a 100644
--- a/usr.sbin/Makefile
+++ b/usr.sbin/Makefile
@@ -173,7 +173,9 @@ SUBDIR.${MK_MAN_UTILS}+= manctl
SUBDIR.${MK_MLX5TOOL}+= mlx5tool
SUBDIR.${MK_NETGRAPH}+= flowctl
SUBDIR.${MK_NETGRAPH}+= ngctl
+SUBDIR.${MK_NETGRAPH}+= ngbpgd
SUBDIR.${MK_NETGRAPH}+= nghook
+SUBDIR.${MK_NETGRAPH}+= ngldpd
SUBDIR.${MK_NIS}+= rpc.yppasswdd
SUBDIR.${MK_NIS}+= rpc.ypupdated
SUBDIR.${MK_NIS}+= rpc.ypxfrd
diff --git a/usr.sbin/ldpd/Makefile b/usr.sbin/ldpd/Makefile
new file mode 100644
index 00000000000..18ec97550b8
--- /dev/null
+++ b/usr.sbin/ldpd/Makefile
@@ -0,0 +1,11 @@
+PACKAGE=ldp
+PROG= ldpd
+MAN= ldpd.8
+SRCS= main.c hello.c session.c label.c ng.c
+
+WARNS?= 3
+LIBADD= netgraph
+
+CFLAGS+= -I${.CURDIR}/../../sys
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/ldpd/hello.c b/usr.sbin/ldpd/hello.c
new file mode 100644
index 00000000000..0d4902c2917
--- /dev/null
+++ b/usr.sbin/ldpd/hello.c
@@ -0,0 +1,218 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+
+#include <net/if.h>
+#include <net/route.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+
+#include <err.h>
+#include <errno.h>
+#include <ifaddrs.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ldpd.h"
+
+int hello_sock = -1;
+
+static struct sockaddr_in all_routers;
+
+int
+hello_init(void)
+{
+ struct sockaddr_in sin;
+ int on = 1;
+ int ret;
+
+ if (hello_sock != -1)
+ close(hello_sock);
+
+ hello_sock = socket(AF_INET, SOCK_DGRAM, 0);
+ if (hello_sock == -1) {
+ warn("hello socket");
+ return (-1);
+ }
+
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_addr.s_addr = htonl(INADDR_ANY);
+ sin.sin_port = htons(LDP_UDP_PORT);
+
+ ret = bind(hello_sock, (struct sockaddr *)&sin, sizeof(sin));
+ if (ret == -1) {
+ warn("bind hello");
+ close(hello_sock);
+ return (-1);
+ }
+
+ ret = setsockopt(hello_sock, IPPROTO_IP, IP_MULTICAST_LOOP,
+ &on, sizeof(on));
+ if (ret == -1)
+ warn("IP_MULTICAST_LOOP");
+
+ memset(&all_routers, 0, sizeof(all_routers));
+ all_routers.sin_family = AF_INET;
+ all_routers.sin_addr.s_addr = inet_addr(LDP_ALL_ROUTERS);
+ all_routers.sin_port = htons(LDP_UDP_PORT);
+
+ return (0);
+}
+
+void
+hello_recv_peers(void)
+{
+ struct ifaddrs *ifap, *ifa;
+
+ if (getifaddrs(&ifap) == -1)
+ return;
+
+ for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
+ struct ip_mreq mreq;
+
+ if (ifa->ifa_addr == NULL ||
+ ifa->ifa_addr->sa_family != AF_INET)
+ continue;
+ if (!(ifa->ifa_flags & IFF_UP) ||
+ (ifa->ifa_flags & IFF_LOOPBACK))
+ continue;
+
+ mreq.imr_multiaddr.s_addr = all_routers.sin_addr.s_addr;
+ mreq.imr_interface =
+ ((struct sockaddr_in *)ifa->ifa_addr)->sin_addr;
+
+ if (setsockopt(hello_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,
+ &mreq, sizeof(mreq)) == -1)
+ warn("IP_ADD_MEMBERSHIP %s", ifa->ifa_name);
+ }
+ freeifaddrs(ifap);
+}
+
+int
+hello_send(struct ldp_peer *peer)
+{
+ struct sockaddr_in sin;
+ uint8_t buf[256];
+ struct ldp_pdu_header *pdu;
+ struct ldp_msg_header *msg;
+ struct ldp_tlv_header *tlv;
+ struct ldp_hello_params *params;
+ uint16_t pdu_len;
+ int ret;
+
+ memset(buf, 0, sizeof(buf));
+ pdu = (struct ldp_pdu_header *)buf;
+ pdu->version = htons(LDP_VERSION);
+ pdu->lsr_id = htonl(0);
+ pdu->lspace_id = 0;
+
+ msg = (struct ldp_msg_header *)(pdu + 1);
+ msg->type = htons(LDP_MSG_HELLO);
+ msg->msg_id = htonl(1);
+
+ tlv = (struct ldp_tlv_header *)(msg + 1);
+ tlv->type = htons(0x0400);
+ tlv->length = htons(sizeof(*params));
+ params = (struct ldp_hello_params *)(tlv + 1);
+ params->hold_time = htons(15);
+ params->flags = 0;
+
+ msg->length = htons(sizeof(*tlv) + sizeof(*params));
+ pdu_len = sizeof(*pdu) + sizeof(*msg) + sizeof(*tlv) + sizeof(*params);
+ pdu->pdu_length = htons(pdu_len - sizeof(pdu->version) -
+ sizeof(pdu->pdu_length));
+
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_port = htons(LDP_UDP_PORT);
+ if (peer != NULL)
+ sin.sin_addr = ((struct sockaddr_in *)&peer->addr)->sin_addr;
+ else
+ sin.sin_addr.s_addr = all_routers.sin_addr.s_addr;
+
+ ret = sendto(hello_sock, buf, pdu_len, 0,
+ (struct sockaddr *)&sin, sizeof(sin));
+ if (ret == -1)
+ warn("send hello");
+ return (ret);
+}
+
+void
+hello_recv_peer(int fd __unused)
+{
+ uint8_t buf[1500];
+ struct sockaddr_in sin;
+ socklen_t sinlen = sizeof(sin);
+ struct ldp_pdu_header *pdu;
+ struct ldp_msg_header *msg;
+ struct ldp_tlv_header *tlv;
+ ssize_t n;
+ struct ldp_peer *peer;
+ uint16_t hold_time;
+
+ n = recvfrom(fd, buf, sizeof(buf), 0,
+ (struct sockaddr *)&sin, &sinlen);
+ if (n == -1) {
+ warn("hello recv");
+ return;
+ }
+ if (n < sizeof(*pdu) + sizeof(*msg))
+ return;
+
+ pdu = (struct ldp_pdu_header *)buf;
+ if (ntohs(pdu->version) != LDP_VERSION)
+ return;
+
+ peer = session_find((struct sockaddr *)&sin);
+ if (peer == NULL) {
+ peer = session_new((struct sockaddr *)&sin, sinlen, 0);
+ if (peer == NULL)
+ return;
+ }
+
+ msg = (struct ldp_msg_header *)(pdu + 1);
+ if (ntohs(msg->type) != LDP_MSG_HELLO)
+ return;
+
+ tlv = (struct ldp_tlv_header *)(msg + 1);
+ if (ntohs(tlv->type) != 0x0400)
+ return;
+ hold_time = ntohs(((struct ldp_hello_params *)(tlv + 1))->hold_time);
+ peer->hold_time = hold_time;
+
+ if (peer->state == LDP_S_NONEXISTENT) {
+ hello_send(peer);
+ session_open(peer);
+ }
+}
diff --git a/usr.sbin/ldpd/label.c b/usr.sbin/ldpd/label.c
new file mode 100644
index 00000000000..dcccdf72779
--- /dev/null
+++ b/usr.sbin/ldpd/label.c
@@ -0,0 +1,146 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/queue.h>
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "ldpd.h"
+
+static uint32_t next_local_label = 16;
+static TAILQ_HEAD(, ldp_label_mapping) local_labels;
+
+static int label_db_initialized;
+
+static void
+label_db_init(void)
+{
+ if (label_db_initialized)
+ return;
+ TAILQ_INIT(&local_labels);
+ label_db_initialized = 1;
+}
+
+uint32_t
+label_alloc_local(void)
+{
+ uint32_t label;
+
+ label_db_init();
+ label = next_local_label++;
+ return (label);
+}
+
+static int
+label_prefix_match(const struct fec_prefix *a, const struct fec_prefix *b)
+{
+ uint8_t mask[16];
+ int i, full_bytes, bits;
+
+ if (a->family != b->family || a->prefix_len != b->prefix_len)
+ return (0);
+
+ full_bytes = a->prefix_len / 8;
+ bits = a->prefix_len % 8;
+
+ if (memcmp(a->prefix, b->prefix, full_bytes) != 0)
+ return (0);
+
+ if (bits > 0) {
+ mask[0] = 0xff << (8 - bits);
+ if ((a->prefix[full_bytes] & mask[0]) !=
+ (b->prefix[full_bytes] & mask[0]))
+ return (0);
+ }
+ return (1);
+}
+
+struct ldp_label_mapping *
+label_find(uint32_t label)
+{
+ struct ldp_label_mapping *lm;
+
+ label_db_init();
+ TAILQ_FOREACH(lm, &local_labels, entry) {
+ if (lm->label == label)
+ return (lm);
+ }
+ return (NULL);
+}
+
+int
+label_assign(struct ldp_peer *peer, struct ldp_fec *fec)
+{
+ struct ldp_label_mapping *lm;
+ uint32_t label;
+
+ label_db_init();
+ lm = calloc(1, sizeof(*lm));
+ if (lm == NULL)
+ return (-1);
+
+ label = label_alloc_local();
+ lm->label = label;
+ lm->fec_type = fec->type;
+
+ if (fec->type == LDP_FEC_PREFIX)
+ memcpy(&lm->fec, &fec->u.prefix, sizeof(lm->fec));
+
+ TAILQ_INSERT_TAIL(&local_labels, lm, entry);
+ TAILQ_INSERT_TAIL(&peer->label_out, lm, entry);
+
+ lm->flags |= LMF_INSTALLED;
+
+ return (0);
+}
+
+int
+label_withdraw(struct ldp_peer *peer, struct ldp_fec *fec)
+{
+ struct ldp_label_mapping *lm, *tlm;
+
+ TAILQ_FOREACH_SAFE(lm, &peer->label_out, entry, tlm) {
+ if (fec != NULL && fec->type == LDP_FEC_PREFIX &&
+ label_prefix_match(&lm->fec, &fec->u.prefix))
+ goto remove;
+ if (fec == NULL || fec->type == LDP_FEC_WILDCARD) {
+ goto remove;
+ }
+ continue;
+remove:
+ TAILQ_REMOVE(&peer->label_out, lm, entry);
+ TAILQ_REMOVE(&local_labels, lm, entry);
+ ng_del_label(lm->label);
+ free(lm);
+ }
+ return (0);
+}
diff --git a/usr.sbin/ldpd/ldpd.h b/usr.sbin/ldpd/ldpd.h
new file mode 100644
index 00000000000..b0ecea15f17
--- /dev/null
+++ b/usr.sbin/ldpd/ldpd.h
@@ -0,0 +1,222 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef _LDPD_H_
+#define _LDPD_H_
+
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <sys/queue.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include <net/if.h>
+#include <net/if_dl.h>
+#include <net/route.h>
+#include <netinet/in.h>
+
+#include <err.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define LDP_PORT 646
+#define LDP_UDP_PORT 646
+#define LDP_ALL_ROUTERS "224.0.0.2"
+#define LDP_ALL_ROUTERS_V6 "ff02::2"
+
+#define LDP_VERSION 1
+
+#define LDP_PDU_HEADER_LEN 10
+#define LDP_MSG_HEADER_LEN 2
+
+/* LDP message types */
+#define LDP_MSG_NOTIFICATION 0x0001
+#define LDP_MSG_HELLO 0x0100
+#define LDP_MSG_INIT 0x0200
+#define LDP_MSG_KEEPALIVE 0x0201
+#define LDP_MSG_ADDRESS 0x0300
+#define LDP_MSG_ADDRESS_WITHDRAW 0x0301
+#define LDP_MSG_LABEL_MAPPING 0x0400
+#define LDP_MSG_LABEL_REQUEST 0x0401
+#define LDP_MSG_LABEL_WITHDRAW 0x0402
+#define LDP_MSG_LABEL_RELEASE 0x0403
+#define LDP_MSG_LABEL_ABORT 0x0404
+
+/* FEC element types */
+#define LDP_FEC_WILDCARD 0x01
+#define LDP_FEC_PREFIX 0x02
+#define LDP_FEC_PWID 0x80
+
+/* Status codes */
+#define LDP_STATUS_SUCCESS 0x00000000
+#define LDP_STATUS_NO_RESOURCES 0x00000002
+
+/* Session roles */
+#define LDP_ROLE_PASSIVE 0
+#define LDP_ROLE_ACTIVE 1
+
+/* Session state machine */
+enum ldp_session_state {
+ LDP_S_NONEXISTENT,
+ LDP_S_INITIALIZED,
+ LDP_S_OPENRECV,
+ LDP_S_OPENSENT,
+ LDP_S_OPERATIONAL
+};
+
+/* LDP PDU header (wire format) */
+struct ldp_pdu_header {
+ uint16_t version;
+ uint16_t pdu_length;
+ uint32_t lsr_id;
+ uint16_t lspace_id;
+} __attribute__((packed));
+
+/* LDP message header (wire format) */
+struct ldp_msg_header {
+ uint16_t type;
+ uint16_t length;
+ uint32_t msg_id;
+} __attribute__((packed));
+
+/* LDP TLV header (wire format) */
+struct ldp_tlv_header {
+ uint16_t type;
+ uint16_t length;
+} __attribute__((packed));
+
+/* LDP Hello TLV parameters */
+struct ldp_hello_params {
+ uint16_t hold_time;
+ uint16_t flags;
+} __attribute__((packed));
+
+/* FEC TLV - prefix element */
+struct ldp_fec_prefix {
+ uint8_t fec_type;
+ uint8_t addr_family;
+ uint8_t prefix_len;
+ uint8_t prefix[];
+} __attribute__((packed));
+
+/* Label TLV */
+struct ldp_label_generic {
+ uint16_t type;
+ uint16_t length;
+ uint32_t label;
+} __attribute__((packed));
+
+struct fec_prefix {
+ uint8_t family;
+ uint8_t prefix_len;
+ uint8_t prefix[16];
+};
+
+struct ldp_fec {
+ uint8_t type;
+ union {
+ struct fec_prefix prefix;
+ } u;
+ TAILQ_ENTRY(ldp_fec) entry;
+};
+
+struct ldp_label_mapping {
+ uint32_t label;
+ uint32_t fec_type;
+ struct fec_prefix fec;
+ uint32_t remote_label;
+ int flags;
+#define LMF_INSTALLED 0x01
+#define LMF_EXPLICIT_NULL 0x02
+ TAILQ_ENTRY(ldp_label_mapping) entry;
+};
+
+struct ldp_peer {
+ struct sockaddr_storage addr;
+ socklen_t addrlen;
+ int fd;
+ int role;
+ uint32_t lsr_id;
+ uint16_t lspace_id;
+ enum ldp_session_state state;
+ time_t hold_time;
+ time_t keepalive_time;
+ time_t last_rcvd;
+ uint32_t next_msg_id;
+ int hello_fd;
+
+ TAILQ_HEAD(, ldp_fec) fec_list;
+ TAILQ_HEAD(, ldp_label_mapping) label_out;
+ TAILQ_HEAD(, ldp_label_mapping) label_in;
+
+ TAILQ_ENTRY(ldp_peer) entry;
+};
+
+TAILQ_HEAD(peer_head, ldp_peer);
+
+extern struct peer_head peers;
+extern int verbose;
+extern int no_daemon;
+
+extern int hello_sock;
+
+/* hello.c */
+int hello_init(void);
+void hello_recv_peer(int);
+int hello_send(struct ldp_peer *);
+void hello_recv_peers(void);
+
+/* session.c */
+int session_open(struct ldp_peer *);
+int session_init(struct ldp_peer *);
+int session_keepalive(struct ldp_peer *);
+int session_recv(int, void *);
+int session_send_init(struct ldp_peer *);
+int session_send_keepalive(struct ldp_peer *);
+void session_close(struct ldp_peer *);
+struct ldp_peer *session_find(struct sockaddr *);
+struct ldp_peer *session_new(struct sockaddr *, socklen_t, int);
+
+/* label.c */
+struct ldp_label_mapping *label_find(uint32_t);
+int label_assign(struct ldp_peer *, struct ldp_fec *);
+int label_withdraw(struct ldp_peer *, struct ldp_fec *);
+uint32_t label_alloc_local(void);
+
+/* ng.c */
+int ng_init(void);
+int ng_add_label(uint32_t, uint32_t, const char *);
+int ng_del_label(uint32_t);
+int ng_get_label(uint32_t, uint32_t *, uint32_t *, char *);
+void ng_shutdown(void);
+
+#endif /* _LDPD_H_ */
diff --git a/usr.sbin/ldpd/main.c b/usr.sbin/ldpd/main.c
new file mode 100644
index 00000000000..e61a09ac05c
--- /dev/null
+++ b/usr.sbin/ldpd/main.c
@@ -0,0 +1,185 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/poll.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <net/if.h>
+#include <netinet/in.h>
+
+#include <err.h>
+#include <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#include "ldpd.h"
+
+struct peer_head peers;
+int verbose;
+int no_daemon;
+
+static void shutdown_daemon(void);
+
+static void
+sigint_handler(int sig __unused)
+{
+ shutdown_daemon();
+}
+
+static void
+shutdown_daemon(void)
+{
+ struct ldp_peer *peer;
+
+ while ((peer = TAILQ_FIRST(&peers)) != NULL) {
+ session_close(peer);
+ TAILQ_REMOVE(&peers, peer, entry);
+ free(peer);
+ }
+ ng_shutdown();
+ exit(0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct ldp_peer *peer, *tpeer;
+ int ch, nfds, ret;
+ struct pollfd *pfds;
+ int pfds_size;
+
+ while ((ch = getopt(argc, argv, "dv")) != -1) {
+ switch (ch) {
+ case 'd':
+ no_daemon = 1;
+ break;
+ case 'v':
+ verbose++;
+ break;
+ default:
+ fprintf(stderr, "usage: ldpd [-dv]\n");
+ return (EX_USAGE);
+ }
+ }
+
+ TAILQ_INIT(&peers);
+
+ if (!no_daemon) {
+ if (daemon(0, 0) == -1)
+ err(EX_OSERR, "daemon");
+ }
+
+ if (ng_init() == -1)
+ err(EX_OSERR, "ng_init");
+
+ if (hello_init() == -1)
+ err(EX_OSERR, "hello_init");
+
+ signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+
+ pfds_size = 16;
+ pfds = calloc(pfds_size, sizeof(*pfds));
+ if (pfds == NULL)
+ err(EX_OSERR, "calloc");
+
+ for (;;) {
+ int i = 0;
+
+ nfds = 0;
+
+ pfds[i].fd = hello_sock;
+ pfds[i].events = POLLIN;
+ nfds = MAX(nfds, hello_sock + 1);
+ i++;
+
+ TAILQ_FOREACH_SAFE(peer, &peers, entry, tpeer) {
+ if (i + 2 >= pfds_size) {
+ pfds_size *= 2;
+ pfds = realloc(pfds,
+ pfds_size * sizeof(*pfds));
+ if (pfds == NULL)
+ err(EX_OSERR, "realloc");
+ }
+ if (peer->fd != -1) {
+ pfds[i].fd = peer->fd;
+ pfds[i].events = POLLIN;
+ nfds = MAX(nfds, peer->fd + 1);
+ i++;
+ }
+ }
+
+ ret = poll(pfds, i, 10000);
+ if (ret == -1) {
+ if (errno == EINTR)
+ continue;
+ warn("poll");
+ break;
+ }
+
+ if (pfds[0].revents & POLLIN)
+ hello_recv_peer(hello_sock);
+
+ {
+ struct timeval now;
+ gettimeofday(&now, NULL);
+
+ TAILQ_FOREACH_SAFE(peer, &peers, entry, tpeer) {
+ if (peer->state == LDP_S_OPERATIONAL) {
+ if (peer->keepalive_time > 0 &&
+ now.tv_sec - peer->last_rcvd >
+ peer->keepalive_time) {
+ warnx("keepalive timeout");
+ session_close(peer);
+ continue;
+ }
+ }
+ }
+ }
+
+ i = 1;
+ TAILQ_FOREACH(peer, &peers, entry) {
+ if (peer->fd == -1)
+ continue;
+ if (pfds[i].revents & POLLIN)
+ session_recv(peer->fd, peer);
+ i++;
+ }
+ }
+
+ free(pfds);
+ shutdown_daemon();
+ return (0);
+}
diff --git a/usr.sbin/ldpd/ng.c b/usr.sbin/ldpd/ng.c
new file mode 100644
index 00000000000..42bbb5c6f62
--- /dev/null
+++ b/usr.sbin/ldpd/ng.c
@@ -0,0 +1,131 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/socket.h>
+
+#include <netgraph.h>
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ldpd.h"
+#include <netgraph/ng_mpls.h>
+
+static int csock = -1;
+static int dsock = -1;
+static char node_path[NG_PATHSIZ];
+
+int
+ng_init(void)
+{
+ if (NgMkSockNode(NULL, &csock, &dsock) == -1) {
+ warn("NgMkSockNode");
+ return (-1);
+ }
+
+ snprintf(node_path, sizeof(node_path), "%s:", NG_MPLS_NODE_TYPE);
+
+ return (0);
+}
+
+int
+ng_add_label(uint32_t label, uint32_t action, const char *next_hop)
+{
+ struct {
+ struct ng_mesg header;
+ struct ng_mpls_label data;
+ } msg;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.header.header.version = NG_VERSION;
+ msg.header.header.typecookie = NG_MPLS_COOKIE;
+ msg.header.header.cmd = NGM_MPLS_ADD_LABEL;
+ msg.header.header.arglen = sizeof(msg.data);
+ snprintf(msg.header.header.cmdstr, sizeof(msg.header.header.cmdstr),
+ "addlabel");
+
+ msg.data.label = label;
+ msg.data.action = action;
+ snprintf(msg.data.hook_name, sizeof(msg.data.hook_name), "%s", next_hop);
+
+ if (NgSendMsg(csock, node_path, &msg.header,
+ msg.header.header.arglen) == -1) {
+ warn("NgSendMsg addlabel");
+ return (-1);
+ }
+ return (0);
+}
+
+int
+ng_del_label(uint32_t label)
+{
+ struct {
+ struct ng_mesg header;
+ uint32_t data;
+ } msg;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.header.header.version = NG_VERSION;
+ msg.header.header.typecookie = NG_MPLS_COOKIE;
+ msg.header.header.cmd = NGM_MPLS_DEL_LABEL;
+ msg.header.header.arglen = sizeof(msg.data);
+ snprintf(msg.header.header.cmdstr, sizeof(msg.header.header.cmdstr),
+ "dellabel");
+
+ msg.data = label;
+
+ if (NgSendMsg(csock, node_path, &msg.header,
+ msg.header.header.arglen) == -1) {
+ warn("NgSendMsg dellabel");
+ return (-1);
+ }
+ return (0);
+}
+
+int
+ng_get_label(uint32_t label, uint32_t *action, uint32_t *next_label,
+ char *hook)
+{
+ /* Not yet implemented */
+ return (-1);
+}
+
+void
+ng_shutdown(void)
+{
+ if (csock != -1)
+ close(csock);
+ if (dsock != -1)
+ close(dsock);
+ csock = -1;
+ dsock = -1;
+}
diff --git a/usr.sbin/ldpd/session.c b/usr.sbin/ldpd/session.c
new file mode 100644
index 00000000000..1ed5248bd66
--- /dev/null
+++ b/usr.sbin/ldpd/session.c
@@ -0,0 +1,261 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/socket.h>
+
+#include <netinet/in.h>
+
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ldpd.h"
+
+struct ldp_peer *
+session_find(struct sockaddr *sa)
+{
+ struct ldp_peer *peer;
+ struct sockaddr_in *sin = (struct sockaddr_in *)sa;
+
+ TAILQ_FOREACH(peer, &peers, entry) {
+ struct sockaddr_in *psin = (struct sockaddr_in *)&peer->addr;
+ if (psin->sin_addr.s_addr == sin->sin_addr.s_addr &&
+ peer->state != LDP_S_NONEXISTENT)
+ return (peer);
+ }
+ return (NULL);
+}
+
+struct ldp_peer *
+session_new(struct sockaddr *sa, socklen_t salen, int role)
+{
+ struct ldp_peer *peer;
+
+ peer = calloc(1, sizeof(*peer));
+ if (peer == NULL)
+ return (NULL);
+
+ memcpy(&peer->addr, sa, salen);
+ peer->addrlen = salen;
+ peer->fd = -1;
+ peer->role = role;
+ peer->state = LDP_S_NONEXISTENT;
+ peer->hello_fd = -1;
+
+ TAILQ_INIT(&peer->fec_list);
+ TAILQ_INIT(&peer->label_out);
+ TAILQ_INIT(&peer->label_in);
+
+ TAILQ_INSERT_TAIL(&peers, peer, entry);
+
+ return (peer);
+}
+
+int
+session_open(struct ldp_peer *peer)
+{
+ struct sockaddr_in sin;
+ int fd;
+ int ret;
+
+ fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd == -1) {
+ warn("session socket");
+ return (-1);
+ }
+
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_port = htons(LDP_PORT);
+
+ ret = connect(fd, (struct sockaddr *)&peer->addr, peer->addrlen);
+ if (ret == -1 && errno != EINPROGRESS) {
+ warn("connect to %s",
+ inet_ntoa(((struct sockaddr_in *)&peer->addr)->sin_addr));
+ close(fd);
+ return (-1);
+ }
+
+ peer->fd = fd;
+ peer->role = LDP_ROLE_ACTIVE;
+ peer->state = LDP_S_INITIALIZED;
+
+ session_send_init(peer);
+
+ return (0);
+}
+
+int
+session_send_init(struct ldp_peer *peer)
+{
+ uint8_t buf[256];
+ struct ldp_pdu_header *pdu;
+ struct ldp_msg_header *msg;
+ struct ldp_tlv_header *tlv;
+ uint16_t pdu_len;
+ ssize_t n;
+
+ memset(buf, 0, sizeof(buf));
+ pdu = (struct ldp_pdu_header *)buf;
+ pdu->version = htons(LDP_VERSION);
+ pdu->lsr_id = htonl(0);
+ pdu->lspace_id = 0;
+
+ msg = (struct ldp_msg_header *)(pdu + 1);
+ msg->type = htons(LDP_MSG_INIT);
+ msg->msg_id = htonl(peer->next_msg_id++);
+
+ tlv = (struct ldp_tlv_header *)(msg + 1);
+ tlv->type = htons(0x0500);
+ tlv->length = htons(6);
+
+ pdu_len = sizeof(*pdu) + sizeof(*msg) + sizeof(*tlv) + 6;
+ pdu->pdu_length = htons(pdu_len - sizeof(pdu->version) -
+ sizeof(pdu->pdu_length));
+ msg->length = htons(sizeof(*tlv) + 6);
+
+ n = write(peer->fd, buf, pdu_len);
+ if (n == -1)
+ warn("send init");
+ return (n);
+}
+
+int
+session_send_keepalive(struct ldp_peer *peer)
+{
+ uint8_t buf[128];
+ struct ldp_pdu_header *pdu;
+ struct ldp_msg_header *msg;
+ uint16_t pdu_len;
+ ssize_t n;
+
+ memset(buf, 0, sizeof(buf));
+ pdu = (struct ldp_pdu_header *)buf;
+ pdu->version = htons(LDP_VERSION);
+ pdu->lsr_id = htonl(0);
+ pdu->lspace_id = 0;
+
+ msg = (struct ldp_msg_header *)(pdu + 1);
+ msg->type = htons(LDP_MSG_KEEPALIVE);
+ msg->msg_id = htonl(peer->next_msg_id++);
+ msg->length = 0;
+
+ pdu_len = sizeof(*pdu) + sizeof(*msg);
+ pdu->pdu_length = htons(pdu_len - sizeof(pdu->version) -
+ sizeof(pdu->pdu_length));
+
+ n = write(peer->fd, buf, pdu_len);
+ if (n == -1)
+ warn("send keepalive");
+ return (n);
+}
+
+int
+session_keepalive(struct ldp_peer *peer)
+{
+ return (session_send_keepalive(peer));
+}
+
+void
+session_close(struct ldp_peer *peer)
+{
+ if (peer->fd != -1) {
+ close(peer->fd);
+ peer->fd = -1;
+ }
+ peer->state = LDP_S_NONEXISTENT;
+}
+
+int
+session_recv(int fd, void *arg)
+{
+ struct ldp_peer *peer = arg;
+ uint8_t buf[4096];
+ struct ldp_pdu_header *pdu;
+ struct ldp_msg_header *msg;
+ struct ldp_tlv_header *tlv;
+ ssize_t n;
+ int len;
+ uint8_t *ptr;
+
+ n = read(fd, buf, sizeof(buf));
+ if (n <= 0) {
+ warnx("session recv: %s",
+ inet_ntoa(((struct sockaddr_in *)&peer->addr)->sin_addr));
+ session_close(peer);
+ return (-1);
+ }
+
+ pdu = (struct ldp_pdu_header *)buf;
+ if (ntohs(pdu->version) != LDP_VERSION)
+ return (-1);
+
+ len = ntohs(pdu->pdu_length) + sizeof(pdu->version) +
+ sizeof(pdu->pdu_length);
+ if (n < len)
+ return (-1);
+
+ ptr = (uint8_t *)(pdu + 1);
+ len -= sizeof(*pdu);
+
+ while (len >= (int)sizeof(*msg)) {
+ msg = (struct ldp_msg_header *)ptr;
+ uint16_t msglen = ntohs(msg->length);
+
+ switch (ntohs(msg->type)) {
+ case LDP_MSG_INIT:
+ if (peer->state == LDP_S_INITIALIZED) {
+ peer->state = LDP_S_OPENSENT;
+ session_send_init(peer);
+ peer->state = LDP_S_OPERATIONAL;
+ } else if (peer->state == LDP_S_OPENSENT) {
+ peer->state = LDP_S_OPERATIONAL;
+ }
+ break;
+ case LDP_MSG_KEEPALIVE:
+ peer->last_rcvd = time(NULL);
+ break;
+ case LDP_MSG_LABEL_MAPPING:
+ break;
+ case LDP_MSG_NOTIFICATION:
+ break;
+ default:
+ break;
+ }
+ ptr += sizeof(*msg) + msglen;
+ len -= sizeof(*msg) + msglen;
+ }
+
+ peer->last_rcvd = time(NULL);
+ return (0);
+}
diff --git a/usr.sbin/ngbpgd/Makefile b/usr.sbin/ngbpgd/Makefile
new file mode 100644
index 00000000000..8296a3373b9
--- /dev/null
+++ b/usr.sbin/ngbpgd/Makefile
@@ -0,0 +1,10 @@
+PACKAGE=ngbpgd
+PROG_CXX= ngbpgd
+MAN=
+
+SRCS= main.cpp
+
+WARNS?= 3
+LIBADD= netgraph
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/ngbpgd/main.cpp b/usr.sbin/ngbpgd/main.cpp
new file mode 100644
index 00000000000..d043cfd63b0
--- /dev/null
+++ b/usr.sbin/ngbpgd/main.cpp
@@ -0,0 +1,774 @@
+/*
+ * ngbpgd — BGP-4 / MP-BGP daemon for FreeBSD MPLS VPN.
+ * RFC 4271 (BGP-4), RFC 4760 (MP-BGP), RFC 4364 (BGP MPLS VPN).
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/event.h>
+#include <sys/errno.h>
+#include <sys/uio.h>
+#include <sys/param.h>
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <cstdarg>
+#include <cstdint>
+#include <ctime>
+#include <cerrno>
+#include <csignal>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <netgraph.h>
+
+/* ------------------------------------------------------------------ */
+/* MPLS control constants (user-space) */
+/* ------------------------------------------------------------------ */
+#define NGM_MPLS_COOKIE 1176518096
+#define NGM_MPLS_ADD_NHLFE 0
+#define NGM_MPLS_DEL_NHLFE 1
+
+#define NGM_MPLS_FEC_COOKIE 1145655619
+#define NGM_MPLS_FEC_ADD 0
+#define NGM_MPLS_FEC_DEL 1
+
+#define NG_MPLS_NHLFE_SWAP 0
+#define NG_HOOKSIZ 32
+
+/* FEC entry (user-space copy) */
+struct ng_mpls_fec_entry_v0 {
+ uint32_t addr;
+ uint32_t mask;
+ uint32_t out_label;
+ char out_hook[NG_HOOKSIZ];
+ uint32_t push_count;
+ uint32_t push_labels[16];
+};
+
+struct ng_mpls_nhlfe {
+ uint32_t in_label;
+ uint32_t out_label;
+ uint32_t op;
+ char out_hook[NG_HOOKSIZ];
+ uint32_t push_count;
+ uint32_t push_labels[16];
+};
+
+/* ------------------------------------------------------------------ */
+/* Constants */
+/* ------------------------------------------------------------------ */
+static const int BGP_PORT = 179;
+
+/* BGP message types */
+enum BgpMsgType : uint8_t {
+ BGP_OPEN = 1,
+ BGP_UPDATE = 2,
+ BGP_NOTIFICATION = 3,
+ BGP_KEEPALIVE = 4,
+};
+
+/* BGP path attributes */
+enum BgpAttrType : uint8_t {
+ BGP_ATTR_ORIGIN = 1,
+ BGP_ATTR_AS_PATH = 2,
+ BGP_ATTR_NEXT_HOP = 3,
+ BGP_ATTR_MED = 4,
+ BGP_ATTR_LOCAL_PREF = 5,
+ BGP_ATTR_ATOMIC_AGG = 6,
+ BGP_ATTR_AGGREGATOR = 7,
+ BGP_ATTR_COMMUNITY = 8,
+ BGP_ATTR_MP_REACH_NLRI = 14,
+ BGP_ATTR_MP_UNREACH_NLRI = 15,
+ BGP_ATTR_EXT_COMMUNITY = 16,
+};
+
+/* AFI/SAFI */
+#define AFI_IPV4 1
+#define AFI_IPV6 2
+#define SAFI_UNICAST 1
+#define SAFI_MPLS_VPN 128 /* RFC 4364 */
+
+/* ------------------------------------------------------------------ */
+/* Logging */
+/* ------------------------------------------------------------------ */
+static void logmsg(const char *fmt, ...) {
+ va_list ap;
+ time_t t = time(nullptr);
+ struct tm tm;
+ char ts[64];
+ localtime_r(&t, &tm);
+ strftime(ts, sizeof(ts), "%b %d %H:%M:%S", &tm);
+ fprintf(stderr, "%s ngbpgd: ", ts);
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fprintf(stderr, "\n");
+}
+
+/* ------------------------------------------------------------------ */
+/* Netgraph control */
+/* ------------------------------------------------------------------ */
+static int csock = -1;
+
+static bool ng_connect(const char *path) {
+ if (csock >= 0) return true;
+ int dsock;
+ if (NgMkSockNode(path, &csock, &dsock) < 0)
+ return false;
+ close(dsock);
+ return true;
+}
+
+static bool ng_send(uint32_t typecookie, uint32_t cmd,
+ const void *data, size_t datalen)
+{
+ if (csock < 0) return false;
+ return (NgSendMsg(csock, NULL, typecookie, cmd, data, datalen) == 0);
+}
+
+static void program_mpls_route(uint32_t prefix, uint8_t plen,
+ uint32_t vpn_label, uint32_t transport_label, const char *vrf_hook)
+{
+ /* Program FEC: prefix -> push(vpn_label, transport_label) via VRF hook */
+ struct ng_mpls_fec_entry_v0 fec;
+ memset(&fec, 0, sizeof(fec));
+ fec.addr = htonl(prefix);
+ fec.mask = htonl(plen ? 0xffffffff << (32 - plen) : 0);
+ fec.out_label = vpn_label;
+ fec.push_count = 2;
+ fec.push_labels[0] = vpn_label; /* innermost (S=1) */
+ fec.push_labels[1] = transport_label; /* outermost (S=0) */
+ strlcpy(fec.out_hook, vrf_hook, NG_HOOKSIZ);
+
+ ng_send(NGM_MPLS_FEC_COOKIE, NGM_MPLS_FEC_ADD,
+ &fec, sizeof(fec));
+ logmsg("FEC %d.%d.%d.%d/%d -> labels %u/%u via %s",
+ (prefix >> 24) & 0xff, (prefix >> 16) & 0xff,
+ (prefix >> 8) & 0xff, prefix & 0xff,
+ plen, vpn_label, transport_label, vrf_hook);
+
+ /* Also program NHLFE for transport label (swap) */
+ struct ng_mpls_nhlfe nhlfe;
+ memset(&nhlfe, 0, sizeof(nhlfe));
+ nhlfe.in_label = transport_label;
+ nhlfe.op = NG_MPLS_NHLFE_SWAP;
+ nhlfe.out_label = transport_label;
+ strlcpy(nhlfe.out_hook, "bgp_core", NG_HOOKSIZ);
+ ng_send(NGM_MPLS_COOKIE, NGM_MPLS_ADD_NHLFE,
+ &nhlfe, sizeof(nhlfe));
+}
+
+/* ------------------------------------------------------------------ */
+/* BGP session */
+/* ------------------------------------------------------------------ */
+struct BgpSession {
+ int fd;
+ struct sockaddr_in peer;
+ uint32_t peer_id;
+ uint16_t hold_time;
+ uint16_t keepalive_time;
+ time_t last_rx;
+ time_t last_tx;
+ uint8_t recv_buf[65536];
+ size_t recv_len;
+ uint32_t local_asn;
+ uint32_t remote_asn;
+
+ BgpSession() : fd(-1), peer_id(0), hold_time(60),
+ keepalive_time(20), last_rx(0), last_tx(0),
+ recv_len(0), local_asn(0), remote_asn(0) {}
+};
+
+#define MAX_SESSIONS 128
+static BgpSession sessions[MAX_SESSIONS];
+static int n_sessions = 0;
+
+/* RIB entry */
+struct RibEntry {
+ uint32_t prefix;
+ uint8_t plen;
+ uint32_t vpn_label;
+ uint32_t transport_label;
+ uint32_t next_hop;
+ uint32_t peer_id;
+ time_t age;
+ RibEntry *next;
+};
+
+#define RIB_HASHSZ 1024
+static RibEntry *rib[RIB_HASHSZ];
+
+static uint32_t rib_hash(uint32_t prefix, uint8_t plen) {
+ return (prefix ^ (plen << 24)) % RIB_HASHSZ;
+}
+
+static void rib_add(uint32_t prefix, uint8_t plen,
+ uint32_t vpn_label, uint32_t transport_label,
+ uint32_t next_hop, uint32_t peer_id)
+{
+ uint32_t h = rib_hash(prefix, plen);
+ RibEntry *e = new RibEntry;
+ e->prefix = prefix;
+ e->plen = plen;
+ e->vpn_label = vpn_label;
+ e->transport_label = transport_label;
+ e->next_hop = next_hop;
+ e->peer_id = peer_id;
+ e->age = time(nullptr);
+ e->next = rib[h];
+ rib[h] = e;
+}
+
+static BgpSession *session_by_fd(int fd) {
+ for (int i = 0; i < n_sessions; i++)
+ if (sessions[i].fd == fd) return &sessions[i];
+ return nullptr;
+}
+
+static BgpSession *session_by_peer(uint32_t peer_id) {
+ for (int i = 0; i < n_sessions; i++)
+ if (sessions[i].peer_id == peer_id) return &sessions[i];
+ return nullptr;
+}
+
+static void session_close(BgpSession *s) {
+ if (s->fd >= 0) {
+ close(s->fd);
+ s->fd = -1;
+ }
+ logmsg("session with %s closed",
+ inet_ntoa(s->peer.sin_addr));
+}
+
+/* ------------------------------------------------------------------ */
+/* BGP wire helpers */
+/* ------------------------------------------------------------------ */
+
+static inline uint16_t r16(const uint8_t *p) {
+ return (uint16_t)p[0] << 8 | p[1];
+}
+static inline uint32_t r32(const uint8_t *p) {
+ return (uint32_t)p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
+}
+static inline void w16(uint8_t *p, uint16_t v) {
+ p[0] = v >> 8; p[1] = v & 0xff;
+}
+static inline void w24(uint8_t *p, uint32_t v) {
+ p[0] = (v >> 16) & 0xff;
+ p[1] = (v >> 8) & 0xff;
+ p[2] = v & 0xff;
+}
+static inline void w32(uint8_t *p, uint32_t v) {
+ p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v & 0xff;
+}
+
+/* BGP message header */
+#pragma pack(push, 1)
+struct BgpHeader {
+ uint8_t marker[16]; /* all 0xff */
+ uint16_t length; /* including header */
+ uint8_t type;
+};
+#pragma pack(pop)
+
+static void build_open(uint8_t *buf, size_t *len,
+ uint32_t asn, uint32_t bgp_id, uint16_t hold)
+{
+ memset(buf, 0xff, 16); /* marker */
+ uint8_t *p = buf + 19;
+
+ w16(p, asn); p += 2; /* my AS (16-bit) */
+ w16(p, hold); p += 2; /* hold time */
+ w32(p, bgp_id); p += 4; /* BGP identifier */
+ *p++ = 0; /* optional param length */
+
+ /* Fill in header */
+ BgpHeader *h = (BgpHeader *)buf;
+ w16((uint8_t *)&h->length, p - buf);
+ h->type = BGP_OPEN;
+ *len = p - buf;
+}
+
+static void build_keepalive(uint8_t *buf, size_t *len) {
+ memset(buf, 0xff, 16);
+ BgpHeader *h = (BgpHeader *)buf;
+ w16((uint8_t *)&h->length, 19);
+ h->type = BGP_KEEPALIVE;
+ *len = 19;
+}
+
+static void build_notification(uint8_t *buf, size_t *len,
+ uint8_t code, uint8_t subcode)
+{
+ memset(buf, 0xff, 16);
+ uint8_t *p = buf + 19;
+ *p++ = code;
+ *p++ = subcode;
+ BgpHeader *h = (BgpHeader *)buf;
+ w16((uint8_t *)&h->length, p - buf);
+ h->type = BGP_NOTIFICATION;
+ *len = p - buf;
+}
+
+/* ------------------------------------------------------------------ */
+/* Send BGP message */
+/* ------------------------------------------------------------------ */
+static bool send_bgp(BgpSession *s, const uint8_t *buf, size_t len) {
+ if (s->fd < 0) return false;
+ ssize_t n = write(s->fd, buf, len);
+ if (n > 0) {
+ s->last_tx = time(nullptr);
+ return (size_t)n == len;
+ }
+ return false;
+}
+
+/* ------------------------------------------------------------------ */
+/* Parse BGP UPDATE */
+/* ------------------------------------------------------------------ */
+static void parse_update(BgpSession *s, const uint8_t *msg, size_t msglen) {
+ /* BGP UPDATE format:
+ Withdrawn Routes Length (2) + Withdrawn Routes (variable)
+ Total Path Attribute Length (2) + Path Attributes (variable)
+ NLRI (variable)
+ */
+ if (msglen < 2) return;
+ uint16_t wlen = r16(msg);
+ if (2 + wlen + 2 > msglen) return;
+
+ const uint8_t *attr_start = msg + 2 + wlen;
+ size_t attr_remain = msglen - (2 + wlen);
+ if (attr_remain < 2) return;
+ uint16_t attrs_len = r16(attr_start);
+ if (2 + attrs_len > attr_remain) return;
+
+ /* Parse path attributes */
+ const uint8_t *ap = attr_start + 2;
+ size_t ar = attrs_len;
+
+ uint32_t next_hop = 0;
+ uint32_t vpn_label = 0;
+ int afi = 0, safi = 0;
+ const uint8_t *mp_nlri = nullptr;
+ size_t mp_nlri_len = 0;
+
+ while (ar >= 2) {
+ uint8_t flags = ap[0];
+ uint8_t type = ap[1];
+ uint16_t alen;
+
+ if (flags & 0x10) {
+ /* Extended length */
+ if (ar < 4) break;
+ alen = r16(ap + 2);
+ ap += 4; ar -= 4;
+ } else {
+ if (ar < 3) break;
+ alen = ap[2];
+ ap += 3; ar -= 3;
+ }
+ if (alen > ar) break;
+
+ const uint8_t *val = ap;
+ ap += alen; ar -= alen;
+
+ switch (type) {
+ case BGP_ATTR_NEXT_HOP:
+ if (alen >= 4) next_hop = r32(val);
+ break;
+ case BGP_ATTR_MP_REACH_NLRI: {
+ /* AFI (2) + SAFI (1) + Next Hop (variable) + SNPA + NLRI */
+ if (alen < 3) break;
+ afi = r16(val);
+ safi = val[2];
+ size_t off = 3;
+ /* Skip next hop */
+ if (off >= alen) break;
+ uint8_t nh_len = val[off++];
+ off += nh_len;
+ /* Skip SNPA */
+ if (off >= alen) break;
+ uint8_t snpa_len = val[off++];
+ off += snpa_len;
+ mp_nlri = val + off;
+ mp_nlri_len = alen - off;
+ break;
+ }
+ }
+ }
+
+ /* Process VPN-IPv4 NLRI */
+ if (mp_nlri && mp_nlri_len > 0 && afi == AFI_IPV4 && safi == SAFI_MPLS_VPN) {
+ const uint8_t *np = mp_nlri;
+ size_t nr = mp_nlri_len;
+
+ while (nr > 0) {
+ uint8_t plen = np[0];
+ if (plen == 0) break;
+
+ /* Label stack (3 bytes per label, 20-bit label + 4-bit unused + S bit) */
+ int lbl_bytes = 0;
+ uint32_t labels[8];
+ int nlabels = 0;
+ while (lbl_bytes + 3 <= ((plen - 24 + 7) / 8)) {
+ break; /* labels embedded in plen; simplified parsing below */
+ }
+
+ /* Simplified: labels precede prefix in NLRI
+ For VPN-IPv4, prefix is 12 bytes: RD(8) + IP(4)
+ Labels are packed in (plen - 24)/8 bytes preceding RD */
+ int prefix_bytes = 12; /* RD(8) + IPv4(4) */
+ int nlri_bytes = (plen + 7) / 8;
+ int label_bytes = nlri_bytes - prefix_bytes;
+
+ if (label_bytes < 0) { np++; nr--; continue; }
+
+ uint32_t rd_high = 0, rd_low = 0;
+ uint32_t pfx = 0;
+
+ if (label_bytes > 0) {
+ /* Parse label(s) from front */
+ const uint8_t *lp = np + 1; /* skip plen byte */
+ int consumed = 0;
+ while (consumed + 3 <= label_bytes) {
+ uint32_t lb = ((uint32_t)lp[0] << 16) |
+ ((uint32_t)lp[1] << 8) | lp[2];
+ labels[nlabels++] = lb >> 4; /* top 20 bits */
+ if (lb & 1) break; /* S bit set */
+ lp += 3;
+ consumed += 3;
+ }
+ if (nlabels > 0) vpn_label = labels[nlabels - 1];
+
+ /* RD + prefix follows labels */
+ const uint8_t *rp = np + 1 + label_bytes;
+ rd_high = r32(rp);
+ rd_low = r32(rp + 4);
+ pfx = r32(rp + 8);
+ }
+
+ char pfx_str[64];
+ struct in_addr a = { .s_addr = htonl(pfx) };
+ inet_ntop(AF_INET, &a, pfx_str, sizeof(pfx_str));
+
+ logmsg("VPN-IPv4 %s/%d RD=%08x%08x label=%u via %s",
+ pfx_str, plen > 24 ? plen - 24 : 0,
+ rd_high, rd_low, vpn_label,
+ inet_ntoa(s->peer.sin_addr));
+
+ /* Program MPLS dataplane */
+ if (vpn_label != 0) {
+ char vrf_hook[NG_HOOKSIZ];
+ snprintf(vrf_hook, sizeof(vrf_hook), "vrf_%08x", rd_low);
+
+ /* Need transport label from LDP for the next-hop.
+ Simplified: use a static label or 0 for now. */
+ program_mpls_route(pfx, plen > 24 ? plen - 24 : 32,
+ vpn_label, 3 /* well-known: default route */,
+ vrf_hook);
+
+ rib_add(pfx, plen > 24 ? plen - 24 : 32,
+ vpn_label, 0, next_hop, s->peer_id);
+ }
+
+ /* Advance to next NLRI */
+ int advance = 1 + nlri_bytes;
+ if ((size_t)advance > nr) break;
+ np += advance;
+ nr -= advance;
+ }
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Process received BGP data */
+/* ------------------------------------------------------------------ */
+static void process_data(BgpSession *s, uint32_t bgp_id, uint32_t asn) {
+ const uint8_t *p = s->recv_buf;
+ size_t remain = s->recv_len;
+
+ while (remain >= 19) {
+ /* Validate marker */
+ int valid = 1;
+ for (int i = 0; i < 16; i++)
+ if (p[i] != 0xff) { valid = 0; break; }
+ if (!valid) break;
+
+ const BgpHeader *h = (const BgpHeader *)p;
+ uint16_t len = r16((const uint8_t *)&h->length);
+ if (len < 19 || len > remain) break;
+
+ uint8_t type = h->type;
+ const uint8_t *msg = p + 19;
+ size_t msglen = len - 19;
+
+ switch (type) {
+ case BGP_OPEN: {
+ if (msglen < 10) break;
+ uint32_t remote_as = r16(msg);
+ s->remote_asn = remote_as;
+ uint16_t hold = r16(msg + 2);
+ /* uint32_t remote_id = r32(msg + 4); */
+ s->peer_id = r32(msg + 4);
+
+ if (hold > 0 && hold < s->hold_time)
+ s->hold_time = hold;
+ s->keepalive_time = s->hold_time / 3;
+ if (s->keepalive_time < 1) s->keepalive_time = 1;
+
+ logmsg("OPEN from %s AS %u hold %u",
+ inet_ntoa(s->peer.sin_addr), remote_as, hold);
+
+ /* Send OPEN + KEEPALIVE */
+ uint8_t resp[256];
+ size_t rlen;
+ build_open(resp, &rlen, asn, bgp_id, s->hold_time);
+ send_bgp(s, resp, rlen);
+ build_keepalive(resp, &rlen);
+ send_bgp(s, resp, rlen);
+
+ s->last_rx = time(nullptr);
+ break;
+ }
+ case BGP_KEEPALIVE:
+ s->last_rx = time(nullptr);
+ break;
+
+ case BGP_UPDATE:
+ parse_update(s, msg, msglen);
+ s->last_rx = time(nullptr);
+ break;
+
+ case BGP_NOTIFICATION: {
+ if (msglen >= 2)
+ logmsg("NOTIFICATION from %s code=%u sub=%u",
+ inet_ntoa(s->peer.sin_addr), msg[0], msg[1]);
+ session_close(s);
+ break;
+ }
+ }
+
+ p += len;
+ remain -= len;
+ }
+
+ if (remain > 0 && p > s->recv_buf)
+ memmove(s->recv_buf, p, remain);
+ s->recv_len = remain;
+}
+
+/* ------------------------------------------------------------------ */
+/* Socket setup */
+/* ------------------------------------------------------------------ */
+static int setup_tcp_listener(void) {
+ int fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0) return -1;
+
+ int opt = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+ setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
+
+ struct sockaddr_in addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(BGP_PORT);
+ addr.sin_addr.s_addr = INADDR_ANY;
+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ close(fd);
+ return -1;
+ }
+ listen(fd, 32);
+ return fd;
+}
+
+/* ------------------------------------------------------------------ */
+/* Accept session */
+/* ------------------------------------------------------------------ */
+static void accept_session(int listener, int kq, uint32_t bgp_id, uint32_t asn) {
+ struct sockaddr_in peer;
+ socklen_t plen = sizeof(peer);
+ int fd = accept(listener, (struct sockaddr *)&peer, &plen);
+ if (fd < 0) return;
+
+ if (n_sessions >= MAX_SESSIONS) { close(fd); return; }
+
+ int flags = fcntl(fd, F_GETFL, 0);
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+
+ BgpSession *s = &sessions[n_sessions++];
+ s->fd = fd;
+ memcpy(&s->peer, &peer, sizeof(peer));
+ s->peer_id = ntohl(peer.sin_addr.s_addr);
+ s->local_asn = asn;
+ s->last_rx = time(nullptr);
+
+ logmsg("new session from %s", inet_ntoa(peer.sin_addr));
+
+ /* Send OPEN */
+ uint8_t buf[256];
+ size_t len;
+ build_open(buf, &len, asn, bgp_id, s->hold_time);
+ send_bgp(s, buf, len);
+
+ struct kevent ke;
+ EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, nullptr);
+ kevent(kq, &ke, 1, nullptr, 0, nullptr);
+}
+
+/* ------------------------------------------------------------------ */
+/* Connect to peer */
+/* ------------------------------------------------------------------ */
+static void connect_peer(const char *peer_str, int kq, uint32_t bgp_id, uint32_t asn) {
+ struct sockaddr_in peer;
+ memset(&peer, 0, sizeof(peer));
+ peer.sin_family = AF_INET;
+ peer.sin_port = htons(BGP_PORT);
+
+ if (inet_pton(AF_INET, peer_str, &peer.sin_addr) <= 0) {
+ logmsg("invalid peer address: %s", peer_str);
+ return;
+ }
+
+ uint32_t peer_id = ntohl(peer.sin_addr.s_addr);
+ if (session_by_peer(peer_id)) return;
+ if (n_sessions >= MAX_SESSIONS) return;
+
+ int fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0) return;
+
+ int flags = fcntl(fd, F_GETFL, 0);
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+
+ connect(fd, (struct sockaddr *)&peer, sizeof(peer));
+
+ BgpSession *s = &sessions[n_sessions++];
+ s->fd = fd;
+ memcpy(&s->peer, &peer, sizeof(peer));
+ s->peer_id = peer_id;
+ s->local_asn = asn;
+ s->last_rx = time(nullptr);
+
+ logmsg("connecting to %s", peer_str);
+
+ struct kevent ke;
+ EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, nullptr);
+ kevent(kq, &ke, 1, nullptr, 0, nullptr);
+
+ /* Send OPEN */
+ uint8_t buf[256];
+ size_t len;
+ build_open(buf, &len, asn, bgp_id, s->hold_time);
+ send_bgp(s, buf, len);
+}
+
+/* ------------------------------------------------------------------ */
+/* Main loop */
+/* ------------------------------------------------------------------ */
+static volatile int g_running = 1;
+static void sigint(int) { g_running = 0; }
+
+int main(int argc, char **argv) {
+ uint32_t bgp_id = 0;
+ uint32_t asn = 65001;
+
+ if (argc >= 2) bgp_id = inet_addr(argv[1]);
+ if (bgp_id == 0 || bgp_id == INADDR_NONE)
+ bgp_id = inet_addr("10.0.0.1");
+ if (argc >= 3) asn = atoi(argv[2]);
+
+ logmsg("starting, BGP ID %s AS %u",
+ inet_ntoa(*(struct in_addr *)&bgp_id), asn);
+
+ /* Connect to netgraph nodes */
+ ng_connect("mpls_fec:");
+
+ /* TCP listener */
+ int tcp_fd = setup_tcp_listener();
+ if (tcp_fd < 0) { logmsg("failed TCP listener"); return 1; }
+
+ /* kqueue */
+ int kq = kqueue();
+ if (kq < 0) { perror("kqueue"); return 1; }
+
+ struct kevent ev;
+ EV_SET(&ev, tcp_fd, EVFILT_READ, EV_ADD, 0, 0, nullptr);
+ kevent(kq, &ev, 1, nullptr, 0, nullptr);
+
+ signal(SIGINT, sigint);
+ signal(SIGTERM, sigint);
+
+ /* Connect to configured peers (from args 4+) */
+ for (int i = 3; i < argc; i++)
+ connect_peer(argv[i], kq, bgp_id, asn);
+
+ time_t last_ka = 0;
+
+ while (g_running) {
+ struct timespec ts = { 1, 0 };
+ struct kevent events[64];
+ int n = kevent(kq, nullptr, 0, events, 64, &ts);
+ time_t now = time(nullptr);
+
+ /* Keepalive maintenance */
+ if (now - last_ka >= 1) {
+ for (int i = 0; i < n_sessions; i++) {
+ BgpSession *s = &sessions[i];
+ if (s->fd < 0) continue;
+
+ if (now - s->last_tx >= s->keepalive_time) {
+ uint8_t buf[32];
+ size_t len;
+ build_keepalive(buf, &len);
+ send_bgp(s, buf, len);
+ }
+ if (s->hold_time > 0 &&
+ now - s->last_rx > s->hold_time * 3) {
+ logmsg("session %s timeout",
+ inet_ntoa(s->peer.sin_addr));
+ session_close(s);
+ }
+ }
+ last_ka = now;
+ }
+
+ if (n < 0) {
+ if (errno == EINTR) continue;
+ break;
+ }
+
+ for (int i = 0; i < n; i++) {
+ int fd = events[i].ident;
+
+ if (fd == tcp_fd) {
+ accept_session(tcp_fd, kq, bgp_id, asn);
+ } else {
+ BgpSession *s = session_by_fd(fd);
+ if (!s) continue;
+
+ ssize_t r = read(fd, s->recv_buf + s->recv_len,
+ sizeof(s->recv_buf) - s->recv_len);
+ if (r <= 0) {
+ session_close(s);
+ } else {
+ s->recv_len += r;
+ s->last_rx = now;
+ process_data(s, bgp_id, asn);
+ }
+ }
+ }
+ }
+
+ logmsg("shutdown");
+ for (int i = 0; i < n_sessions; i++)
+ session_close(&sessions[i]);
+ close(tcp_fd);
+ if (csock >= 0) close(csock);
+ return 0;
+}
diff --git a/usr.sbin/ngldpd/Makefile b/usr.sbin/ngldpd/Makefile
new file mode 100644
index 00000000000..3c451a538cc
--- /dev/null
+++ b/usr.sbin/ngldpd/Makefile
@@ -0,0 +1,10 @@
+PACKAGE=ngldpd
+PROG_CXX= ngldpd
+MAN=
+
+SRCS= main.cpp
+
+WARNS?= 3
+LIBADD= netgraph
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/ngldpd/main.cpp b/usr.sbin/ngldpd/main.cpp
new file mode 100644
index 00000000000..91b1a64ebea
--- /dev/null
+++ b/usr.sbin/ngldpd/main.cpp
@@ -0,0 +1,686 @@
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/event.h>
+#include <sys/errno.h>
+#include <sys/uio.h>
+#include <sys/param.h>
+
+#include <net/if.h>
+#include <net/ethernet.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+#include <arpa/inet.h>
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <cstdarg>
+#include <cstdint>
+#include <ctime>
+#include <cerrno>
+#include <csignal>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <netgraph.h>
+
+/* ------------------------------------------------------------------ */
+/* MPLS constants and structs (user-space copy of kernel defs) */
+/* ------------------------------------------------------------------ */
+#define NGM_MPLS_COOKIE 1176518096
+#define NGM_MPLS_ADD_NHLFE 0
+#define NGM_MPLS_DEL_NHLFE 1
+#define NGM_MPLS_GET_NHLFE 2
+#define NGM_MPLS_SET_TTL_MODE 3
+#define NGM_MPLS_GET_TTL_MODE 4
+
+#define NG_MPLS_NHLFE_SWAP 0
+#define NG_MPLS_NHLFE_PHP 1
+#define NG_MPLS_NHLFE_SWAP_AND_PUSH 2
+#define NG_MPLS_NHLFE_POP 3
+
+#define NG_HOOKSIZ 32
+
+#pragma pack(push, 1)
+struct ng_mpls_nhlfe_v0 {
+ uint32_t in_label;
+ uint32_t out_label;
+ uint32_t op;
+ char out_hook[NG_HOOKSIZ];
+};
+#pragma pack(pop)
+
+struct ng_mpls_nhlfe {
+ uint32_t in_label;
+ uint32_t out_label;
+ uint32_t op;
+ char out_hook[NG_HOOKSIZ];
+ uint32_t push_count;
+ uint32_t push_labels[16];
+};
+
+/* ------------------------------------------------------------------ */
+/* Constants */
+/* ------------------------------------------------------------------ */
+static const int LDP_PORT = 646;
+static const int LDP_UDP_HELLO = 646;
+
+enum LdpMsgType : uint16_t {
+ LDP_NOTIFICATION = 0x0001,
+ LDP_HELLO = 0x0100,
+ LDP_INIT = 0x0200,
+ LDP_KEEPALIVE = 0x0201,
+ LDP_LABEL_MAP = 0x0400,
+ LDP_LABEL_REQ = 0x0401,
+ LDP_LABEL_WITHDRAW = 0x0402,
+ LDP_LABEL_RELEASE = 0x0403,
+};
+
+enum LdpTlvType : uint16_t {
+ TLV_FEC = 0x0100,
+ TLV_LABEL = 0x0200,
+ TLV_GENERIC_LBL = 0x0202,
+ TLV_STATUS = 0x0300,
+ TLV_COMMON_HELLO = 0x0400,
+ TLV_IPV4_TRNS = 0x0401,
+ TLV_IPV4_IF = 0x0404,
+};
+
+enum FecType : uint8_t {
+ FEC_WILDCARD = 0x01,
+ FEC_PREFIX = 0x02,
+ FEC_HOST = 0x03,
+};
+
+enum LdpState {
+ LDP_INITIALIZED,
+ LDP_OPENSENT,
+ LDP_OPENRECV,
+ LDP_OPERATIONAL,
+};
+
+/* ------------------------------------------------------------------ */
+/* Logging */
+/* ------------------------------------------------------------------ */
+static void logmsg(const char *fmt, ...) {
+ va_list ap;
+ time_t t = time(nullptr);
+ struct tm tm;
+ char ts[64];
+ localtime_r(&t, &tm);
+ strftime(ts, sizeof(ts), "%b %d %H:%M:%S", &tm);
+ fprintf(stderr, "%s ngldpd: ", ts);
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fprintf(stderr, "\n");
+}
+
+/* ------------------------------------------------------------------ */
+/* Netgraph control */
+/* ------------------------------------------------------------------ */
+static int csock = -1;
+
+static bool ng_connect(const char *path) {
+ if (csock >= 0) return true;
+ int dsock;
+ if (NgMkSockNode(path, &csock, &dsock) < 0)
+ return false;
+ close(dsock);
+ return true;
+}
+
+static bool ng_send(uint32_t typecookie, uint32_t cmd,
+ const void *data, size_t datalen)
+{
+ if (csock < 0) return false;
+ return (NgSendMsg(csock, NULL, typecookie, cmd, data, datalen) == 0);
+}
+
+/* ------------------------------------------------------------------ */
+/* LDP session */
+/* ------------------------------------------------------------------ */
+struct LdpSession {
+ int fd;
+ struct sockaddr_in peer;
+ uint32_t peer_id;
+ LdpState state;
+ uint16_t keepalive_time;
+ uint16_t keepalive_interval;
+ time_t last_rx;
+ time_t last_tx;
+ uint8_t recv_buf[65536];
+ size_t recv_len;
+
+ LdpSession() : fd(-1), peer_id(0), state(LDP_INITIALIZED),
+ keepalive_time(15), keepalive_interval(5),
+ last_rx(0), last_tx(0), recv_len(0) {}
+};
+
+#define MAX_SESSIONS 256
+static LdpSession sessions[MAX_SESSIONS];
+static int n_sessions = 0;
+
+static LdpSession *session_by_fd(int fd) {
+ for (int i = 0; i < n_sessions; i++)
+ if (sessions[i].fd == fd) return &sessions[i];
+ return nullptr;
+}
+
+static LdpSession *session_by_peer(uint32_t peer_id) {
+ for (int i = 0; i < n_sessions; i++)
+ if (sessions[i].peer_id == peer_id) return &sessions[i];
+ return nullptr;
+}
+
+static void session_close(LdpSession *s) {
+ if (s->fd >= 0) {
+ close(s->fd);
+ s->fd = -1;
+ }
+ s->state = LDP_INITIALIZED;
+ char buf[64];
+ inet_ntop(AF_INET, &s->peer.sin_addr, buf, sizeof(buf));
+ logmsg("session with %s closed", buf);
+}
+
+/* ------------------------------------------------------------------ */
+/* LDP wire format helpers */
+/* ------------------------------------------------------------------ */
+#pragma pack(push, 1)
+struct LdpHeader {
+ uint16_t version;
+ uint16_t pdu_len;
+ uint32_t lsr_id;
+ uint16_t lbl_space;
+};
+struct LdpMsgHdr {
+ uint16_t type;
+ uint16_t length;
+ uint32_t msg_id;
+};
+struct LdpTlvHdr {
+ uint16_t type;
+ uint16_t length;
+};
+#pragma pack(pop)
+
+static inline uint16_t r16(const uint8_t *p) {
+ return (uint16_t)p[0] << 8 | p[1];
+}
+static inline uint32_t r32(const uint8_t *p) {
+ return (uint32_t)p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
+}
+static inline void w16(uint8_t *p, uint16_t v) {
+ p[0] = v >> 8; p[1] = v & 0xff;
+}
+static inline void w32(uint8_t *p, uint32_t v) {
+ p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v & 0xff;
+}
+
+/* Pad to 4 bytes, return final position */
+static uint8_t *tlv_write(uint8_t *p, uint16_t type, const void *val, uint16_t len) {
+ w16(p, type); p += 2;
+ w16(p, len); p += 2;
+ if (val && len > 0) { memcpy(p, val, len); p += len; }
+ while (reinterpret_cast<uintptr_t>(p) & 3)
+ *p++ = 0;
+ return p;
+}
+
+static uint8_t *msg_write(uint8_t *p, uint16_t type, const void *payload, uint16_t len) {
+ w16(p, type); p += 2;
+ w16(p, len); p += 2;
+ w32(p, 0); /* msg_id = 0 */ p += 4;
+ if (payload && len > 0) { memcpy(p, payload, len); p += len; }
+ return p;
+}
+
+static void build_hello(uint8_t *buf, size_t *len, uint32_t lsr_id) {
+ uint8_t *p = buf + sizeof(LdpHeader);
+ uint8_t tlv[8];
+
+ /* Common Hello Parameters */
+ memset(tlv, 0, 2); /* hold time = 0 (default, 15s suggested) */
+ tlv[0] = 0; tlv[1] = 15;
+ p = tlv_write(p, 0x0400, tlv, 2);
+
+ /* IPv4 Transport Address */
+ w32(tlv, lsr_id);
+ p = tlv_write(p, 0x0401, tlv, 4);
+
+ *len = p - buf;
+ LdpHeader *h = (LdpHeader *)buf;
+ h->version = htons(1);
+ h->pdu_len = htons(*len);
+ h->lsr_id = htonl(lsr_id);
+ h->lbl_space = 0;
+}
+
+static void build_init(uint8_t *buf, size_t *len, uint32_t lsr_id) {
+ uint8_t *p = buf + sizeof(LdpHeader);
+ uint8_t tlv[12];
+
+ /* Common Session Parameters */
+ memset(tlv, 0, 8);
+ /* flags = 0, protocol version = 1 */
+ tlv[2] = 0; tlv[3] = 1;
+ /* keepalive time = 15s */
+ tlv[4] = 0; tlv[5] = 15;
+ p = tlv_write(p, 0x0500, tlv, 8);
+
+ *len = p - buf;
+ LdpHeader *h = (LdpHeader *)buf;
+ h->version = htons(1);
+ h->pdu_len = htons(*len);
+ h->lsr_id = htonl(lsr_id);
+ h->lbl_space = 0;
+}
+
+static void build_keepalive(uint8_t *buf, size_t *len, uint32_t lsr_id) {
+ uint8_t *p = buf + sizeof(LdpHeader);
+ *len = p - buf;
+ LdpHeader *h = (LdpHeader *)buf;
+ h->version = htons(1);
+ h->pdu_len = htons(*len);
+ h->lsr_id = htonl(lsr_id);
+ h->lbl_space = 0;
+}
+
+static void build_label_map(uint8_t *buf, size_t *len,
+ uint32_t lsr_id, uint32_t prefix, uint8_t plen, uint32_t label)
+{
+ uint8_t *p = buf + sizeof(LdpHeader);
+
+ /* FEC TLV: prefix element */
+ uint8_t fec[12];
+ fec[0] = FEC_PREFIX;
+ fec[1] = plen;
+ fec[2] = 0; fec[3] = 1; /* AFI = IPv4 */
+ w32(fec + 4, prefix);
+ /* zero pad to 8 bytes */
+ w32(fec + 8, 0);
+ p = tlv_write(p, 0x0100, fec, 8);
+
+ /* Generic Label TLV */
+ uint8_t lbl[4];
+ w32(lbl, label << 12);
+ p = tlv_write(p, 0x0202, lbl, 4);
+
+ *len = p - buf;
+ LdpHeader *h = (LdpHeader *)buf;
+ h->version = htons(1);
+ h->pdu_len = htons(*len);
+ h->lsr_id = htonl(lsr_id);
+ h->lbl_space = 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Send LDP message on TCP session */
+/* ------------------------------------------------------------------ */
+static bool send_ldp(LdpSession *s, const uint8_t *buf, size_t len) {
+ if (s->fd < 0) return false;
+ ssize_t n = write(s->fd, buf, len);
+ if (n > 0) {
+ s->last_tx = time(nullptr);
+ return (size_t)n == len;
+ }
+ return false;
+}
+
+/* ------------------------------------------------------------------ */
+/* Parse and handle LDP messages */
+/* ------------------------------------------------------------------ */
+static void handle_message(LdpSession *s, const uint8_t *msg,
+ size_t msglen, uint32_t lsr_id)
+{
+ if (msglen < sizeof(LdpMsgHdr)) return;
+ uint16_t type = r16(msg);
+
+ switch (type) {
+ case LDP_INIT: {
+ logmsg("INIT from %s",
+ inet_ntoa(s->peer.sin_addr));
+ uint8_t resp[256];
+ size_t rlen;
+ build_init(resp, &rlen, lsr_id);
+ send_ldp(s, resp, rlen);
+
+ /* Also send keepalive */
+ build_keepalive(resp, &rlen, lsr_id);
+ send_ldp(s, resp, rlen);
+
+ s->state = LDP_OPERATIONAL;
+ logmsg("session %s operational",
+ inet_ntoa(s->peer.sin_addr));
+ break;
+ }
+ case LDP_KEEPALIVE:
+ s->last_rx = time(nullptr);
+ break;
+
+ case LDP_LABEL_MAP: {
+ /* Parse FEC + Label TLVs */
+ const uint8_t *t = msg + sizeof(LdpMsgHdr);
+ size_t remain = r16(msg + 2);
+ uint32_t prefix = 0;
+ uint8_t plen = 0;
+ uint32_t label = 0;
+
+ while (remain >= 4) {
+ uint16_t tt = r16(t);
+ uint16_t tl = r16(t + 2);
+ if (4 + tl > remain) break;
+ const uint8_t *v = t + 4;
+
+ if (tt == 0x0100 && tl >= 8 && v[0] == FEC_PREFIX) {
+ plen = v[1];
+ prefix = r32(v + 4);
+ } else if (tt == 0x0202 && tl >= 4) {
+ label = r32(v) >> 12;
+ }
+ size_t consumed = 4 + tl;
+ while (consumed & 3) consumed++;
+ t += consumed;
+ remain -= consumed;
+ }
+
+ if (label != 0) {
+ char pfx[64];
+ struct in_addr a = { .s_addr = htonl(prefix) };
+ inet_ntop(AF_INET, &a, pfx, sizeof(pfx));
+ logmsg("label map: %s/%d -> %u", pfx, plen, label);
+
+ /* Push NHLFE to kernel */
+ struct ng_mpls_nhlfe nhlfe;
+ memset(&nhlfe, 0, sizeof(nhlfe));
+ nhlfe.in_label = label;
+ nhlfe.op = NG_MPLS_NHLFE_SWAP;
+ nhlfe.out_label = label;
+ strlcpy(nhlfe.out_hook, "downstream", NG_HOOKSIZ);
+ ng_send(NGM_MPLS_COOKIE, NGM_MPLS_ADD_NHLFE,
+ &nhlfe, sizeof(nhlfe));
+ }
+ break;
+ }
+ case LDP_LABEL_WITHDRAW:
+ case LDP_LABEL_RELEASE:
+ case LDP_NOTIFICATION:
+ break;
+ }
+}
+
+static void process_data(LdpSession *s, uint32_t lsr_id) {
+ const uint8_t *p = s->recv_buf;
+ size_t remain = s->recv_len;
+
+ while (remain >= sizeof(LdpHeader)) {
+ const LdpHeader *h = (const LdpHeader *)p;
+ uint16_t pdu_len = ntohs(h->pdu_len);
+ if (pdu_len < sizeof(LdpHeader) || pdu_len > remain) break;
+
+ const uint8_t *msg = p + sizeof(LdpHeader);
+ size_t msg_remain = pdu_len - sizeof(LdpHeader);
+ p += pdu_len;
+ remain -= pdu_len;
+
+ while (msg_remain >= sizeof(LdpMsgHdr)) {
+ uint16_t mlen = r16(msg + 2);
+ size_t msize = sizeof(LdpMsgHdr) + mlen;
+ if (msize > msg_remain) break;
+ handle_message(s, msg, msize, lsr_id);
+ msg += msize;
+ msg_remain -= msize;
+ }
+ }
+
+ if (remain > 0 && p > s->recv_buf)
+ memmove(s->recv_buf, p, remain);
+ s->recv_len = remain;
+}
+
+/* ------------------------------------------------------------------ */
+/* Socket setup */
+/* ------------------------------------------------------------------ */
+static int setup_udp_hello(uint32_t lsr_id) {
+ int fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0) return -1;
+
+ int opt = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+
+ struct ip_mreq mreq;
+ memset(&mreq, 0, sizeof(mreq));
+ mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.2");
+ mreq.imr_interface.s_addr = htonl(lsr_id);
+ setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
+
+ struct sockaddr_in bind_addr;
+ memset(&bind_addr, 0, sizeof(bind_addr));
+ bind_addr.sin_family = AF_INET;
+ bind_addr.sin_port = htons(LDP_UDP_HELLO);
+ bind_addr.sin_addr.s_addr = INADDR_ANY;
+ if (bind(fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+static int setup_tcp_listener(void) {
+ int fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0) return -1;
+
+ int opt = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+
+ struct sockaddr_in addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(LDP_PORT);
+ addr.sin_addr.s_addr = INADDR_ANY;
+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ close(fd);
+ return -1;
+ }
+ listen(fd, 32);
+ return fd;
+}
+
+/* ------------------------------------------------------------------ */
+/* Accept / initiate sessions */
+/* ------------------------------------------------------------------ */
+static void accept_session(int listener, uint32_t lsr_id) {
+ struct sockaddr_in peer;
+ socklen_t plen = sizeof(peer);
+ int fd = accept(listener, (struct sockaddr *)&peer, &plen);
+ if (fd < 0) return;
+
+ if (n_sessions >= MAX_SESSIONS) { close(fd); return; }
+
+ int flags = fcntl(fd, F_GETFL, 0);
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+
+ LdpSession *s = &sessions[n_sessions++];
+ s->fd = fd;
+ memcpy(&s->peer, &peer, sizeof(peer));
+ s->peer_id = ntohl(peer.sin_addr.s_addr);
+ s->state = LDP_INITIALIZED;
+ s->last_rx = time(nullptr);
+
+ logmsg("new session from %s",
+ inet_ntoa(peer.sin_addr));
+
+ uint8_t buf[256];
+ size_t len;
+ build_init(buf, &len, lsr_id);
+ send_ldp(s, buf, len);
+ s->state = LDP_OPENSENT;
+}
+
+static void initiate_session(struct sockaddr_in *target, int kq, uint32_t lsr_id) {
+ uint32_t peer_id = ntohl(target->sin_addr.s_addr);
+ if (session_by_peer(peer_id)) return;
+ if (n_sessions >= MAX_SESSIONS) return;
+
+ int fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0) return;
+
+ int flags = fcntl(fd, F_GETFL, 0);
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+
+ connect(fd, (struct sockaddr *)target, sizeof(*target));
+
+ LdpSession *s = &sessions[n_sessions++];
+ s->fd = fd;
+ memcpy(&s->peer, target, sizeof(*target));
+ s->peer_id = peer_id;
+ s->state = LDP_INITIALIZED;
+ s->last_rx = time(nullptr);
+
+ logmsg("connecting to %s",
+ inet_ntoa(target->sin_addr));
+
+ struct kevent ke;
+ EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, nullptr);
+ kevent(kq, &ke, 1, nullptr, 0, nullptr);
+
+ uint8_t buf[256];
+ size_t len;
+ build_init(buf, &len, lsr_id);
+ send_ldp(s, buf, len);
+ s->state = LDP_OPENSENT;
+}
+
+/* ------------------------------------------------------------------ */
+/* Main loop */
+/* ------------------------------------------------------------------ */
+static volatile int g_running = 1;
+static void sigint(int) { g_running = 0; }
+
+int main(int argc, char **argv) {
+ uint32_t lsr_id = 0;
+ int hello_int = 5;
+
+ if (argc >= 2) lsr_id = inet_addr(argv[1]);
+ if (lsr_id == 0 || lsr_id == INADDR_NONE)
+ lsr_id = inet_addr("10.0.0.1");
+
+ logmsg("starting, LSR ID %s",
+ inet_ntoa(*(struct in_addr *)&lsr_id));
+
+ /* Connect to ng_mpls node */
+ ng_connect("mpls:");
+
+ /* Sockets */
+ int udp_fd = setup_udp_hello(lsr_id);
+ if (udp_fd < 0) { logmsg("failed UDP hello socket"); return 1; }
+
+ int tcp_fd = setup_tcp_listener();
+ if (tcp_fd < 0) { logmsg("failed TCP listener"); return 1; }
+
+ /* kqueue */
+ int kq = kqueue();
+ if (kq < 0) { perror("kqueue"); return 1; }
+
+ struct kevent ev[4];
+ EV_SET(&ev[0], udp_fd, EVFILT_READ, EV_ADD, 0, 0, nullptr);
+ EV_SET(&ev[1], tcp_fd, EVFILT_READ, EV_ADD, 0, 0, nullptr);
+ kevent(kq, ev, 2, nullptr, 0, nullptr);
+
+ signal(SIGINT, sigint);
+ signal(SIGTERM, sigint);
+
+ time_t last_hello = 0;
+ time_t last_ka = 0;
+
+ while (g_running) {
+ struct timespec ts = { 1, 0 };
+ struct kevent events[64];
+ int n = kevent(kq, nullptr, 0, events, 64, &ts);
+ time_t now = time(nullptr);
+
+ /* Periodic hellos */
+ if (now - last_hello >= hello_int) {
+ uint8_t buf[128];
+ size_t len;
+ build_hello(buf, &len, lsr_id);
+ struct sockaddr_in dst;
+ memset(&dst, 0, sizeof(dst));
+ dst.sin_family = AF_INET;
+ dst.sin_port = htons(LDP_UDP_HELLO);
+ dst.sin_addr.s_addr = inet_addr("224.0.0.2");
+ sendto(udp_fd, buf, len, 0,
+ (struct sockaddr *)&dst, sizeof(dst));
+ last_hello = now;
+ }
+
+ /* Keepalive maintenance */
+ if (now - last_ka >= 1) {
+ for (int i = 0; i < n_sessions; i++) {
+ LdpSession *s = &sessions[i];
+ if (s->fd < 0) continue;
+
+ if (now - s->last_tx >= s->keepalive_interval) {
+ uint8_t buf[128];
+ size_t len;
+ build_keepalive(buf, &len, lsr_id);
+ send_ldp(s, buf, len);
+ }
+ if (now - s->last_rx > s->keepalive_time * 3) {
+ logmsg("session %s timeout",
+ inet_ntoa(s->peer.sin_addr));
+ session_close(s);
+ }
+ }
+ last_ka = now;
+ }
+
+ if (n < 0) {
+ if (errno == EINTR) continue;
+ break;
+ }
+
+ for (int i = 0; i < n; i++) {
+ int fd = events[i].ident;
+
+ if (fd == udp_fd) {
+ uint8_t buf[512];
+ struct sockaddr_in from;
+ socklen_t flen = sizeof(from);
+ ssize_t r = recvfrom(fd, buf, sizeof(buf), 0,
+ (struct sockaddr *)&from, &flen);
+ if (r > 0 && !session_by_peer(ntohl(from.sin_addr.s_addr))) {
+ from.sin_port = htons(LDP_PORT);
+ initiate_session(&from, kq, lsr_id);
+ }
+ } else if (fd == tcp_fd) {
+ accept_session(tcp_fd, lsr_id);
+ } else {
+ LdpSession *s = session_by_fd(fd);
+ if (!s) continue;
+
+ ssize_t r = read(fd, s->recv_buf + s->recv_len,
+ sizeof(s->recv_buf) - s->recv_len);
+ if (r <= 0) {
+ session_close(s);
+ } else {
+ s->recv_len += r;
+ s->last_rx = now;
+ process_data(s, lsr_id);
+ }
+ }
+ }
+ }
+
+ logmsg("shutdown");
+ for (int i = 0; i < n_sessions; i++)
+ session_close(&sessions[i]);
+ close(udp_fd);
+ close(tcp_fd);
+ if (csock >= 0) close(csock);
+ return 0;
+}
@paigeadelethompson

paigeadelethompson commented Jul 28, 2026 •

Copy link
Copy Markdown
Author

Two VRFs on the same router with MPLS-plane inter-VRF routing would wire up like this:

             +----------------------------------------------------+
             |                    msi (PE router)                  |
             |                                                    |
   CE1--[VRF100]--downstream--[ng_mpls LSR]--downstream--[VRF200]--CE2
             |             |                          |            |
             |        NHLFE: label_100 -> to_200  NHLFE: label_200 -> to_100
             +----------------------------------------------------+

Setup concept:

VRF 100 (FIB 1)

ngctl mkpeer vrf100: mpls_vrf downstream to_100
ngctl msg vrf100: add_vrf { vrf_id=100 fib_num=1 flags=3 }

VRF 200 (FIB 2)

ngctl mkpeer vrf200: mpls_vrf downstream to_200
ngctl msg vrf200: add_vrf { vrf_id=200 fib_num=2 flags=3 }

Core LSR node

ngctl mkpeer vrf100: mpls lwr downstream
ngctl name vrf100:lwr lsr
ngctl connect vrf200: lsr downstream to_200

LSR NHLFE: incoming VPN label swaps to the other VRF's downstream

ngctl msg lsr: add_nhlfe { hook="to_200" label=100 action=1 }
ngctl msg lsr: add_nhlfe { hook="to_100" label=200 action=1 }

VPN labels in each VRF pointing to the other's prefixes

ngctl msg vrf100: add_vpn_label { vpn_label=100 vrf_id=100 \ prefix=0x0a000000 prefix_len=8 push_count=1 push_labels[0]=200 }
ngctl msg vrf200: add_vpn_label { vpn_label=200 vrf_id=200 \ prefix=0x14000000 prefix_len=8 push_count=1 push_labels[0]=100 }

Routes in each VRF's FIB via the VRF interface

setfib 1 route add 10.0.0.0/8 -interface vrf100
setfib 2 route add 20.0.0.0/8 -interface vrf200

The "MPLS plane" benefit here is that inter-VRF traffic only hits the kernel IP stack twice (ingress CE → VRF, VRF → egress CE), while the actual VRF-to-VRF transit happens at label-switching speed in the LSR — no kernel routing lookup for the inter-VRF hop.
For cross-PE (remote VRF), the LSR node's NHLFE just forwards out a physical interface instead of back to a local VRF's downstream, and the remote PE handles the VPN label termination. The same VRF node structure works unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment