1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// SPDX-License-Identifier: MIT
use std::{io, net::Ipv6Addr};
use netlink_packet_core::DecodeError;
use crate::{L2tpIfName, TunnelId};
/// Errors returned by this crate.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// A netlink message or attribute value is invalid.
///
/// This covers kernel replies that fail to decode and attribute values
/// rejected by `netlink-packet-l2tp`, such as interface names that break
/// the kernel's rules; `netlink_packet_core::DecodeError` converts into
/// this variant.
#[error("netlink error: {0}")]
Netlink(String),
/// An interface name breaks the kernel's `dev_valid_name()` rules.
///
/// Returned by
/// [`L2tpIfNameExt::from_name`](crate::L2tpIfNameExt::from_name) and by
/// the CLI's name arguments; the message names the rule that was
/// broken.
#[error("{0}")]
InvalidIfName(String),
/// The kernel sent a well-formed reply that this crate cannot use.
#[error("unexpected kernel reply: {0}")]
UnexpectedReply(String),
/// Generic-netlink family discovery for `l2tp` failed.
#[error("genetlink family resolution failed: {0}")]
FamilyResolution(String),
/// I/O failure returned by a socket, thread or netlink operation.
#[error("I/O error: {0}")]
Io(#[from] io::Error),
/// Cookie validation failed because the byte length is invalid.
#[error("invalid cookie length {0}: must be 0, 4, or 8")]
InvalidCookieLength(usize),
/// A cookie string is not an even number of hexadecimal digits.
#[error("invalid cookie: expected 0, 8 or 16 hexadecimal digits")]
InvalidCookieHex,
/// An endpoint string could not be parsed.
#[error("invalid endpoint {0}")]
InvalidEndpoint(String),
/// The kernel supports only L2TP protocol versions 2 and 3.
#[error("unsupported L2TP protocol version {0}")]
UnsupportedVersion(u8),
/// L2TPv2 tunnel and session identifiers are 16-bit.
#[error("identifier {0} does not fit the 16 bits of an L2TPv2 identifier")]
V2IdTooLarge(u32),
/// L2TPv2 cannot receive data over IP encapsulation; use UDP.
#[error("L2TPv2 does not support IP encapsulation; use UDP")]
V2IpEncapsulation,
/// The supplied PPP unit and socket belong to different network namespaces.
#[error("the PPP unit and socket belong to different network namespaces")]
NamespaceMismatch,
/// Local and remote endpoint address families do not match.
#[error(
"address family mismatch: local and remote must be the same IP version"
)]
AddressFamilyMismatch,
/// The operation does not match the socket's encapsulation (UDP or IP).
#[error("encapsulation mismatch: operation does not apply to this socket")]
EncapsulationMismatch,
/// The managed IP socket is bound with a different connection ID than
/// the tunnel configuration names.
#[error(
"tunnel ID mismatch: socket is bound to {socket}, config names \
{config}"
)]
TunnelIdMismatch {
/// Connection ID the socket is bound with.
socket: TunnelId,
/// Tunnel ID named by the configuration.
config: TunnelId,
},
/// The socket was adopted without a peer connection ID and the kernel
/// does not report it, so the socket cannot be reconnected.
#[error("the peer connection ID of this adopted L2TP/IP socket is unknown")]
UnknownPeerConnId,
/// A link-local IPv6 endpoint was given without an interface scope.
///
/// The kernel refuses to bind or connect a link-local address without an
/// interface; see [`IpEndpoint::v6_scoped`](crate::IpEndpoint::v6_scoped).
#[error("link-local IPv6 address {0} requires an interface scope")]
ScopeRequired(Ipv6Addr),
/// The kernel acknowledged a tunnel deletion, but the tunnel was still
/// listed when [`delete_tunnel`](crate::L2tpHandle::delete_tunnel)
/// stopped waiting for its deferred teardown.
///
/// The deletion is not undone: the tunnel disappears once the kernel's
/// work queue has run, and a repeated deletion then fails with `ENODEV`.
/// Treat the tunnel as deleted; only an immediate re-creation of the
/// same identifier may still fail with `EEXIST`. [`kind`](Self::kind)
/// reports [`io::ErrorKind::TimedOut`] for this variant.
#[error(
"tunnel {0} is still listed after its deletion was acknowledged; the \
kernel removes it later"
)]
DeletionPending(TunnelId),
/// The endpoints and device of one socket name different interfaces.
#[error(
"interface scope mismatch: {} and {}",
.expected.as_bytes().escape_ascii(),
.found.as_bytes().escape_ascii()
)]
ScopeMismatch {
/// The interface named first.
expected: L2tpIfName,
/// The conflicting interface.
found: L2tpIfName,
},
/// Unmanaged tunnels cannot use link-local IPv6 endpoints, because the
/// kernel creates their socket without an interface scope.
#[error(
"unmanaged tunnels cannot use link-local IPv6 endpoints; use a \
managed TunnelSocket with an interface scope"
)]
UnmanagedLinkLocal,
/// Operation requires an owned tunnel socket fd in this process.
#[error(
"operation requires a managed socket (tunnel was created without \
L2TP_ATTR_FD)"
)]
UnmanagedSocket,
/// The kernel cannot disable IPv4 UDP checksums on an unmanaged IPv6
/// socket connected to an IPv4-mapped peer.
#[error(
"disabling UDP checksums for an IPv4-mapped peer requires a managed \
socket; use IPv4 endpoints or the managed tunnel API"
)]
UnmanagedUdpChecksum,
/// Kernel returned an error code and message.
#[error("kernel error {code}: {message}")]
KernelError {
/// Positive errno value.
code: i32,
/// Human-readable kernel error text.
message: String,
},
}
impl Error {
/// Returns the OS error code, for kernel and OS-level I/O errors.
pub fn raw_os_error(&self) -> Option<i32> {
match self {
Self::KernelError { code, .. } => Some(*code),
Self::Io(err) => err.raw_os_error(),
_ => None,
}
}
/// Returns the I/O error kind, so callers can match kernel errors such
/// as [`io::ErrorKind::AlreadyExists`] without depending on `libc`.
/// [`DeletionPending`](Self::DeletionPending) reports
/// [`io::ErrorKind::TimedOut`].
pub fn kind(&self) -> Option<io::ErrorKind> {
match self {
Self::KernelError { code, .. } => {
Some(io::Error::from_raw_os_error(*code).kind())
}
Self::Io(err) => Some(err.kind()),
Self::DeletionPending(_) => Some(io::ErrorKind::TimedOut),
_ => None,
}
}
pub(crate) fn other(message: impl Into<String>) -> Self {
Self::Io(io::Error::other(message.into()))
}
}
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Self {
Self::Netlink(err.to_string())
}
}
/// Crate-local result type.
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use std::io;
use super::{Error, Ipv6Addr, L2tpIfName};
#[test]
fn kernel_error_exposes_os_code_and_kind() {
let err = Error::KernelError {
code: libc::EEXIST,
message: "exists".to_string(),
};
assert_eq!(err.raw_os_error(), Some(libc::EEXIST));
assert_eq!(err.kind(), Some(io::ErrorKind::AlreadyExists));
assert_eq!(Error::AddressFamilyMismatch.kind(), None);
assert_eq!(Error::UnmanagedLinkLocal.raw_os_error(), None);
}
#[test]
fn scope_errors_name_the_interfaces() {
let err = Error::ScopeMismatch {
expected: L2tpIfName::new("eth0").unwrap(),
found: L2tpIfName::new("eth1").unwrap(),
};
assert_eq!(err.to_string(), "interface scope mismatch: eth0 and eth1");
// Names may hold terminal controls or invalid UTF-8; show them
// escaped rather than lossily.
let err = Error::ScopeMismatch {
expected: L2tpIfName::new(b"\x1b[2Jeth0").unwrap(),
found: L2tpIfName::new(b"eth\xff1").unwrap(),
};
assert_eq!(
err.to_string(),
"interface scope mismatch: \\x1b[2Jeth0 and eth\\xff1"
);
let err =
Error::ScopeRequired(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1));
assert!(err.to_string().contains("fe80::1"));
}
#[test]
fn deletion_pending_is_a_timeout_without_an_os_code() {
let err = Error::DeletionPending(crate::TunnelId(7));
assert_eq!(err.kind(), Some(io::ErrorKind::TimedOut));
assert_eq!(err.raw_os_error(), None);
assert!(err.to_string().contains("tunnel 7"));
}
#[test]
fn netlink_decode_error_keeps_message() {
let err =
Error::from(netlink_packet_core::DecodeError::from("truncated"));
assert_eq!(err.to_string(), "netlink error: truncated");
}
}