summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
11 daysMerge pull request #367 from svlobanov/deb11-ub2604-2HEADmasterDenys Fedoryshchenko
Remove debian11 from CI (as EoL), add ubuntu 26:04 (build, test), remove qemu src-build for big-endian test
13 daysci: remove qemu build from BE (s390x) workflow using qemu from ubuntu 26.04Sergey V. Lobanov
13 daysci: remove debian11 as EoL, add ubuntu 26:04Sergey V. Lobanov
13 daysMerge pull request #364 from nuclearcat/tests/veth-fixed-macDenys Fedoryshchenko
tests: give the veth pair fixed MAC addresses
2026-09-09Merge pull request #357 from nuclearcat/fix-centralize-maxDenys Fedoryshchenko
utils: centralize max macro
2026-09-09utils: centralize max macroDenys Fedoryshchenko
Closes #354
2026-09-09tests: give the veth pair fixed MAC addressesDenys Fedoryshchenko
The tests create the veth pair with 'ip link add ... type veth peer name ...' and let the kernel pick random hardware addresses, then start accel-pppd right away. On distributions where systemd-udevd rewrites the address of a freshly created veth (MACAddressPolicy in its .link rules, seen on Ubuntu 22.04) this is a race: accel-pppd reads the address of the interface it serves once, at startup, and when udev wins it advertises an address the interface no longer has. Discovery still completes - the client copies the advertised address into its kernel PPPoE session as the peer address - but from then on the two ends disagree about it, no PPP frame is exchanged in either direction, and the session dies on LCP timeouts. Every PPPoE session test can fail this way; test_pppoe_ccp_ipcp_race failed 8 times out of 12 runs in one qemu Ubuntu-22.04 job. Create the pair with fixed, locally administered addresses derived from the name the pair already gets, so udev has nothing to change. The same job passes 12 out of 12 with this. This only removes the trigger in the tests. accel-pppd not following the address of the interface it serves is tracked in #363 and needs a fix of its own.
2026-09-07Merge pull request #359 from nuclearcat/ipcp-ccp-delay-ackDenys Fedoryshchenko
Ipcp ccp delay ack
2026-09-07Merge pull request #361 from nuclearcat/fix-protocol-buffer-accessDenys Fedoryshchenko
Fix several unsafe or unaligned integer accesses found in protocol parsing paths, including option-gated MPPE, DHCP, PPPoE, RADIUS, IPCP and IPv6CP code.
2026-09-01ppp: say why a malformed ConfReq drops the sessionDenys Fedoryshchenko
The option walkers of lcp_recv_conf_req() and its IPCP, IPV6CP and CCP counterparts bail out with *_OPT_FAIL as soon as an option header does not fit in the remaining bytes or carries an impossible length, and the caller turns that into ap_session_terminate(TERM_USER_ERROR). The bail out happens before the "recv [LCP ConfReq id=..." line is emitted, so the session simply disappeared with nothing in the log to point at the peer. The ConfAck, ConfNak and ConfRej walkers break out of their loop instead and at least close the line they had already started. Log the offending length, option id and the number of bytes left before returning, so a peer sending malformed options can be told apart from the other reasons a session ends with TERM_USER_ERROR.
2026-09-01ppp: bound IPCP and IPV6CP packets by the received sizeDenys Fedoryshchenko
ipcp_recv() and ipv6cp_recv() only rejected packets whose header length field was below PPP_HEADERLEN. The upper bound was missing, so the option walker was handed a size derived purely from the peer chosen hdr->len. ppp->buf is a fixed 8192 byte mempool block and ppp->buf_size holds the number of bytes actually read, so a short frame declaring hdr->len 0xffff made the option loop run off the end of the block: it kept fetching option headers from whatever followed in the heap and stored pointers to them in the ropt list. The read is harmless in itself but easily reaches unmapped memory and kills the daemon. Reject any packet whose declared length does not fit in what was received, the way lcp_recv() and ccp_recv() already do. The two byte slack accounts for the protocol field that precedes the header inside the buffer, and the existing buf_size >= PPP_HEADERLEN + 2 test just above keeps the subtraction from underflowing.
2026-09-01utils: centralize unaligned integer accessorsDenys Fedoryshchenko
2026-09-01memdebug: access tail canaries alignment-safelyDenys Fedoryshchenko
Allocation sizes are arbitrary, so store and load the trailing uint64_t canary with memcpy instead of dereferencing a potentially unaligned pointer.
2026-09-01ipv6: avoid integer access through address bytesDenys Fedoryshchenko
Copy interface IDs and prefix words between aligned temporaries and byte arrays instead of casting IPv6 address storage to uint64_t pointers.
2026-09-01backup: restore scalar fields alignment-safelyDenys Fedoryshchenko
Use memcpy for scalar backup headers and restored session, pool, and RADIUS values because variable-length tags do not guarantee native integer alignment.
2026-09-01radius: harden packet integer decodingDenys Fedoryshchenko
Serialize integer fields via memcpy, restrict attribute parsing to the RADIUS header's declared packet length, reject undersized packet lengths, and reject truncated attribute headers.
2026-09-01pppoe: decode tag integers alignment-safelyDenys Fedoryshchenko
Read peer-controlled PPPoE tag and TR-101 integer fields through aligned temporaries, validate PPP-Max-Payload before formatting, and avoid unaligned cookie timestamp accesses.
2026-09-01ipoe: harden DHCPv4 option decodingDenys Fedoryshchenko
Validate known options before extracting fields, bound Relay-Agent and classless-route subformats, and replace unaligned DHCP option accesses in notification, relay, and session paths.
2026-09-01dhcpv6: validate option and relay boundariesDenys Fedoryshchenko
Enforce fixed option prefixes centrally, bound nested relay messages to their declared payload, require one valid Relay-Message, and decode ORO and diagnostic fields without unaligned or oversized reads.
2026-09-01ppp: cover alignment-safe MPPE prefer setupDenys Fedoryshchenko
Serialize the MPPE option through an aligned temporary and exercise mppe=prefer in the existing unauthenticated PPPoE session test.
2026-09-01ppp: harden control packet decodingDenys Fedoryshchenko
Bound LCP and CCP declared lengths by the received frame, reject truncated option headers across all control protocols, and decode LCP payload integers without alignment assumptions. IPCP and IPv6CP outer frame bounds remain owned by the delayed-ack change in PR #359.
2026-09-01Merge pull request #351 from nuclearcat/various-fixes-on-compiler-warningsDenys Fedoryshchenko
Various fixes on compiler warnings
2026-09-01tests: check that IPCP ConfReq is not answered with TermAck during CCPDenys Fedoryshchenko
Covers the previous commit: with 'mppe=prefer' CCP is non-passive, so it is still negotiating (and needs one more round trip because pppd rejects the MPPE option) when pppd, which does not delay IPCP on CCP, sends its IPCP ConfReq. The test asserts that the session becomes active and that no IPCP TermAck was sent, reading the negotiation from the accel-pppd debug log. The check is skipped if the peer happened to send its IPCP ConfReq only after CCP was done, in that case the race is not exercised.
2026-09-01ppp: do not answer IPCP/IPV6CP ConfReq with TermAck while CCP negotiatesDenys Fedoryshchenko
When the peer's IPCP (or IPV6CP) ConfReq arrives before CCP negotiation has finished, delay_ack is set and send_conf_ack() answers with a TermAck instead of the ConfAck. A TermAck is only a valid response to a TermReq, and the trick relies on the peer retransmitting its ConfReq: conformant peers recover only after their restart timer (3 seconds added to session setup), while some clients (MikroTik RouterOS over L2TP, see issue #353) treat it as a failure and drop the session. Withhold the ConfAck instead and send it as soon as CCP settles. CCP now notifies IPCP/IPV6CP when it comes up or gives up (passive); that clears delay_ack, brings the layer up if the FSM is already Opened, and flushes the withheld ConfAck. Nothing is sent to the peer while CCP is still in progress, so IP data cannot flow before MPPE is set up. Verified with pppoe + mschap-v2: with mppe=prefer and a client that does not require MPPE, the peer's ConfReq used to be answered with a TermAck and the session came up 3 seconds later; now the ConfAck is emitted right after ccp_layer_started and the session comes up immediately. mppe=require and mppe=deny sessions are unaffected.
2026-08-31Merge pull request #346 from nuclearcat/ipoe-stale-session-flushDenys Fedoryshchenko
Ipoe stale session flush fixes
2026-08-16Merge pull request #350 from nuclearcat/pptp-fixesDenys Fedoryshchenko
PPTP fixes
2026-08-16Merge pull request #349 from nuclearcat/pptp-drop-out-of-tree-driverDenys Fedoryshchenko
pptp: drop the out-of-tree kernel driver
2026-08-14Merge pull request #347 from nuclearcat/fixup-challenge-nameDenys Fedoryshchenko
auth: fix challenge-name lifetime on reload
2026-08-14Merge pull request #352 from nuclearcat/stability-fixesDenys Fedoryshchenko
Stability fixes and function unification
2026-08-12Merge pull request #344 from nuclearcat/accel-ng-portedDenys Fedoryshchenko
l2tp hidden-AVP length fix, per-session IPv6 DNS, crypto.h cleanup
2026-08-12ipv6: assign DNS servers per session from RADIUSDenys Fedoryshchenko
The ipv6_nd and ipv6_dhcp modules could only advertise the DNS servers configured in [ipv6-dns], the same set for every subscriber. RFC 6911 defines DNS-Server-IPv6-Address (attribute 169) for exactly this, and the attribute was already in the shipped dictionary and in attr_defs.h; nothing read it. Give struct ap_session an ipv6_dns list, filled by the radius module from that attribute, and have both modules advertise it when the session has one: in the RDNSS option of the router advertisements, and in the DNS_SERVERS option of DHCPv6 replies. Sessions without a list of their own keep getting the configured servers, so nothing changes for anyone not sending the attribute. The selection is a single ipv6_dns_get() shared by both modules rather than a copy in each: they already duplicate the whole [ipv6-dns] parser, and two copies of a precedence rule are two chances to drift. It caps what it returns, so neither the RDNSS option length (one byte, in units of 8) nor the router advertisement buffer can be pushed around by what a RADIUS server sends. The radius module caps at the same 3 servers as [ipv6-dns] accepts and warns once when a reply carries more. An Access-Accept which carries the attribute replaces the whole previously assigned list rather than appending to it, so a re-authorized session ends up with the servers of the latest reply and not with a concatenation. One which does not carry it leaves the current list alone, which is how the IPv4 MS-Primary-DNS-Server attribute already behaves. ipv6_dns_test.c covers the selection: assigned wins over configured, empty list means "nothing assigned" rather than "no DNS", the cap holds for both sources, and a caller with no room gets nothing rather than a stomped buffer. Wired into the ASAN/UBSAN workflow. Inspired by the per-session IPv6 DNS support in accel-ppp-ng (commit 2df6eb99), reimplemented against mainline's ap_session and ipdb types.
2026-08-12crypto: drop the dangling crypto.h symlink and its last referencesDenys Fedoryshchenko
c912d090 ("crypto: Removed internal tomcat crypto.") deleted the crypto/ tree but left accel-pppd/include/crypto.h behind, a tracked symlink to ../../crypto/crypto.h which has pointed at nothing since. backup_file.c still includes it, so it fails to compile with fatal error: crypto.h: No such file or directory That goes unnoticed because accel-pppd/CMakeLists.txt has ADD_SUBDIRECTORY(backup) commented out, i.e. backup_file.c is not part of any build; the breakage only shows up for whoever re-enables it. Include <openssl/md5.h> instead, which is what the file actually needs (MD5_CTX and friends) and what c912d090 did for every file it touched. sstp.c and radius/packet.c defer to crypto.h for the rationale behind their OPENSSL_API_COMPAT define. Spell it out locally instead, and point at the project wide ADD_DEFINITIONS() in the top level CMakeLists.txt added by c1689506 ("openssl: suppress deprecated API warnings"), noting why the local define is kept despite being redundant with it: it has to be visible before the first OpenSSL header. Then remove the symlink, which nothing references anymore.
2026-08-12l2tp: validate the deciphered length of hidden AVPsDenys Fedoryshchenko
decode_avp() only checked the 2 bytes length prefix of the Hidden AVP Subformat when the ciphered attribute spanned more than one MD5 block. For an attribute of at most 16 bytes it deciphered the single block and returned straight away, leaving l2tp_recv() to take the prefix at face value: orig_avp_len = ntohs(*(uint16_t *)avp->val) + sizeof(*avp); ... attr->length = orig_avp_len - sizeof(*avp); That prefix is an output of the cipher, so a peer which does not know the secret (i.e. anyone able to reach the L2TP socket, no handshake needed beyond a preceding Random-Vector AVP) turns it into 16 random bits. Worse than an over-read: orig_avp_len is a uint16_t, so a prefix of 0xffff wraps to 5, attr->length becomes -1, and the ATTR_TYPE_STRING and ATTR_TYPE_OCTETS cases then run attr->val.string = _malloc(attr->length + 1); /* _malloc(0) */ memcpy(attr->val.string, orig_avp_val, attr->length); /* SIZE_MAX */ that is an unbounded memcpy() into a zero sized allocation. Remotely triggerable heap corruption on any tunnel with a secret configured. The accel-ppp encoder always pads hidden AVPs by at least 16 bytes, so it never produces an attribute short enough to reach this path; only a crafted packet does. Fix it at the source rather than at the call site: decode_avp() now returns the deciphered length through an output parameter, and the length is bounded against the room actually available in the received AVP on every path out of the function, single block included. Callers can no longer re-derive it from the AVP body and get it wrong, and the existing multi-block check keeps guarding the deciphering loop itself. Add packet_test.c, a standalone test which drives the real parser through a real UDP socket: hand-crafted packets cover the length prefix checks (including the boundaries of what fits and the single block path the encoder cannot produce), and l2tp_packet_send()/l2tp_recv() round trips cover the multi-block cipher and the unaligned AVP accessors. It reproduces the corruption above under ASan on unpatched code. Wire it, and the so far unused bitpool_test.c, into the ASAN/UBSAN workflow. Inspired by the equivalent hardening in accel-ppp-ng (commit a8ca0f3f), which bounds the length at the call site; the fix here is placed inside decode_avp() and covered by a regression test.
2026-08-12l2tp: read and write AVP values through unaligned-safe accessorsDenys Fedoryshchenko
AVPs are packed back to back in the receive buffer, so the offset of any given AVP is the sum of the lengths of all the AVPs before it, i.e. a value the peer picks. Casting avp->val to uint16_t/uint32_t/uint64_t therefore dereferences a pointer with an arbitrary alignment, which is undefined behaviour, is caught by -fsanitize=alignment, and is only harmless on x86 by accident. The same applies to the send path, where the AVPs being built are laid out the same way. Introduce unaligned_{ntohs,ntohl,be64toh}() and their store counterparts, all memcpy() based, and use them for every multi-byte field read out of or written into an AVP body. Accesses to the members of struct l2tp_avp_t itself are fine as it is declared packed. memxor() had the same problem, in a worse form: it cast both of its uint8_t pointers to uintmax_t and walked them word by word. Since it is only ever called on MD5 sized chunks, replace it with a plain byte loop which compilers vectorize just as well, and which no longer breaks strict aliasing either. No functional change intended, this is a portability and UB fix; it also removes a source of noise for the s390x (big endian) CI job.
2026-08-12Merge pull request #342 from nuclearcat/feature/metrics-json-sessionsDenys Fedoryshchenko
metrics: expose session details in JSON output
2026-08-12log_pgsql: deprecate the module and rename its build flagDenys Fedoryshchenko
The PostgreSQL logging target sees little use and has been a source of memory safety bugs. Mark it as deprecated and scheduled for removal, but give anyone depending on it a chance to object first. To make sure this is not missed, the LOG_PGSQL build flag now fails the build with an explanation, building the module requires the renamed LOG_PGSQL_DEPRECATED flag, cmake prints a warning when it is used, and the module logs a deprecation warning on startup.
2026-08-11utils: centralize min macroDenys Fedoryshchenko
Several userspace translation units carry identical local min() definitions. Move the guarded definition to utils.h and include it from each user so there is one implementation to maintain. The Linux min() macro lives in kernel-internal headers and is not part of the userspace UAPI. Clang/LLVM does not provide a compatible min macro either: C++ code uses std::min and Clang's similarly named operations use explicit builtin names. The userspace <sys/param.h> interface, where available, exposes uppercase MIN instead. Keep the #ifndef guard to preserve the behavior of the existing local definitions and avoid redefining a lowercase min macro supplied by an unrelated third-party header.
2026-08-11pppoe: check for a truncated tag header in PADIDenys Fedoryshchenko
The PADI tag loop read tag_len before checking that the tag header itself fits into the declared payload length, so a PADI ending with a partial tag made it read up to 2 bytes past the receive buffer. print_packet() and the PADR loop already have this check, add the missing one.
2026-08-11pppoe: account for the tag header in the add_tag2 boundDenys Fedoryshchenko
add_tag2() checked that the tag payload fits into the packet buffer, but the memcpy() copied the 4 byte tag header as well, so the check was short by sizeof(struct pppoe_tag) - 1 bytes. All callers build the packet in a ETHER_MAX_LEN stack buffer and pass tags taken from the received discovery packet. A PADI carrying a Host-Uniq tag of 1454..1456 bytes therefore made pppoe_send_PADO() write up to 3 bytes of peer supplied data past the end of the buffer. The PADS, PADT and error paths are affected in the same way. Include the tag header in the bound, and drop the tag_len < 0 test which can never be true since ntohs() returns an unsigned value.
2026-08-11log_pgsql: bound username and sessionid in log headerDenys Fedoryshchenko
set_hdr() copied the session username and sessionid into the message header chunk with strcpy(). That chunk comes from chunk_pool and holds only LOG_CHUNK_SIZE + 1 bytes, while the username is peer supplied and may be up to 255 bytes long (PAP), so a long username overflowed the chunk and corrupted the heap when log-pgsql was enabled. Use snprintf() to truncate both fields to the space left in the chunk.
2026-08-10triton: use offsetof() in list_entry()Denys Fedoryshchenko
list_entry() computed the member offset as &((type *)0)->member, which is a member access on a null pointer and undefined. UBSan reports it on every list traversal in the tree: runtime error: member access within null pointer of type 'struct dhcpv6_option' The generated code is the same either way, and the offset happens to be zero for many of the structures involved, so nothing misbehaves today. offsetof() is what the standard provides for this.
2026-08-10dhcpv6: read the elapsed time and preference options at their real widthDenys Fedoryshchenko
print_time() read a 32 bit word out of the Elapsed Time option, which RFC 8415 21.9 defines as exactly two octets, and clients include it in almost every message they send. With verbose enabled that is a two byte read past the option on every DHCPv6 transaction: ERROR: AddressSanitizer: heap-buffer-overflow READ of size 4 in print_time dhcpv6_packet.c:499 The value was not byte swapped either, so what got logged was not the elapsed time. Read two octets, convert them, and copy them out rather than dereferencing a pointer into the packet that need not be aligned. print_uint8() has the same shape, so check the length there too before reading the Preference octet.
2026-08-10dhcpv6: fix end pointer of an encapsulated relay messageDenys Fedoryshchenko
opth->data already points past the option header, so adding sizeof(*opth) again counted it twice and left endptr four bytes beyond the end of the Relay-Message option, and possibly beyond the received packet. The bounds check at the top of the option loop is written against that endptr, so on the next pass it accepted an option header that lies outside the buffer and read opth->len from it. ASan on a Relay-Forward packet: ERROR: AddressSanitizer: heap-buffer-overflow READ of size 2 in dhcpv6_packet_parse dhcpv6_packet.c:158 The end of the relayed message is the option payload, nothing more.
2026-08-10dhcpv6: bail out of the relay loop when it makes no progressDenys Fedoryshchenko
The relay decapsulation loop only advances pkt->hdr when it finds a Relay-Message option inside the current relay header. A Relay-Forward packet that carries no Relay-Message option leaves pkt->hdr pointing at the same header, so the outer loop runs again on the same input and allocates another struct dhcpv6_relay on every pass. A 34 byte packet, a Relay-Forward header with no options at all, is enough to allocate without limit. Instrumented with ASan it reaches 11.8 million allocations and over a gigabyte in a few seconds: ERROR: libFuzzer: out-of-memory (used: 1249Mb; limit: 512Mb) Live Heap Allocations: 781874575 bytes in 11834644 chunks #1 in dhcpv6_packet_parse dhcpv6_packet.c:142 The socket is per session, so this needs an established session, but the packet is parsed before any DHCPv6 level validation and one client can exhaust memory for the whole daemon. Remember the header at the top of each pass and treat a pass that did not move it as a malformed packet.
2026-08-10ipoe: harden local-net prefix length parsingDenys Fedoryshchenko
The netmask was built by shifting ~0, which is a signed int holding -1, and left shifting a negative value is undefined in C. Every compiler we build with produces the expected mask, so this is not a behaviour fix, but it trips -fsanitize=shift and relies on latitude the standard does not grant. Shift an unsigned operand instead. The prefix length itself was not validated properly either. strtoul() accepts a leading minus and negates, so 'local-net=10.0.0.0/-1' yields ULONG_MAX, which truncates to -1 in the int and passes the 'mask > 32' test. The shift count then becomes 33, which is out of range whether the operand is signed or unsigned. endptr was set but never looked at, so trailing garbage was silently ignored and '/abc' quietly became a /0. Keep the parsed value unsigned, reject anything that is not a complete number in 0..32, and only then narrow it.
2026-08-10ipoe: bounds check classless routes and avoid unaligned readsDenys Fedoryshchenko
The destination was always read as a 32 bit word regardless of how many significant octets the prefix length implies, and the gateway was read without checking that four bytes remain in the option. Neither read was bounded by the end of the option, so a client could make the decoder run past it. dhcpv4_check_options() only enforces a minimum length of 5 for option 121, which is short of the 9 bytes a /32 route needs. Reading the destination as a word was also wrong for any prefix shorter than /32, as it pulled in the first octets of the gateway: 10.0.1.0/24 via 1.1.1.1 printed as 10.0.0.1/24. Read only the significant octets, check the remaining length before both reads, and copy the gateway rather than dereferencing a possibly unaligned pointer.
2026-08-10ipoe: fix netmask computation for classless routesDenys Fedoryshchenko
The netmask was built with a loop shifting 1 into place, which was wrong in three ways. At i == 0 it evaluated 1 << 32, undefined for a 32 bit int, and at i == 1 it shifted into the sign bit of a signed value. The shift amount was off by one, so a /24 produced 0xfffffe00 rather than 0xffffff00. And mask1 was initialized once before the loop over the routes and only ever OR'ed into, so the mask of every route accumulated into the routes that followed it. A well formed option carrying 10.0.1.0/24 via 1.1.1.1 and 172.16.0.0/12 via 2.2.2.2 printed as 10.0.0.1/24 and 172.16.2.0/12. Compute the mask directly instead, and reject a prefix length above 32 rather than shifting by a negative amount.
2026-08-10dhcpv6: check option length before reading the status codeDenys Fedoryshchenko
print_status() reads the 2-byte status code at offset 4 of the option, but parse_option() only guarantees that the option header plus its declared payload length are within the packet. A Status Code option with a payload length below 2 as the last option in a packet therefore made print_status() read past the end of the received buffer. Skip the option if its payload is too short to hold the code.
2026-08-10dhcpv6: fix status code name table and its bounds checkDenys Fedoryshchenko
A missing comma made "UseMulticast" and "NoPrefixAvail" concatenate into a single string literal, so the table held 6 entries instead of 7: UseMulticast printed as "UseMulticastNoPrefixAvail" and NoPrefixAvail had no entry at all. The bounds check compared the status code against sizeof(status_name), which is the size of the table in bytes (48), not its number of entries. Codes 6..48 passed the check and indexed past the end of the table, so print() was handed whatever pointer-sized garbage followed it. A client can trigger this by sending a Status Code option with an out-of-range code, which is printed verbatim when verbose is enabled. Compare against the entry count instead, and drop the ntohs() < 0 test, which can never be true for an unsigned value.
2026-08-10pptp: use the kernel PPPoX UAPI headerDenys Fedoryshchenko
Linux 2.6.37 and later provide the PPTP socket address and protocol definitions in linux/if_pppox.h. Use that header directly instead of carrying an old private copy containing obsolete kernel-internal declarations.