summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
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-30cmake: fix kernel module installationDenys Fedoryshchenko
Install each module from its real build directory, propagate staged install roots, and fail when kbuild modules_install fails. Add a rootless CTest and CI regression for successful and failed installs. Best practices followed: Use the standard CMake DESTDIR and kbuild INSTALL_MOD_PATH staging interfaces: https://cmake.org/cmake/help/latest/envvar/DESTDIR.html https://docs.kernel.org/kbuild/modules.html#install-mod-path Check child command status with execute_process RESULT_VARIABLE: https://cmake.org/cmake/help/latest/command/execute_process.html Register repeatable tests with CTest add_test: https://cmake.org/cmake/help/latest/command/add_test.html
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.
2026-08-10sstp: reject packets shorter than headerDenys Fedoryshchenko
A peer can send an SSTP packet with a zero encoded length and an unknown packet type. The receive dispatcher accepts the unknown type, after which buf_pull() consumes no data and the handler loops forever on the same packet, monopolizing a Triton worker. Reject all packet lengths smaller than the SSTP header before dispatch so malformed packets close the connection without entering the non-progressing loop.
2026-08-09pptp: drop the out-of-tree kernel driverDenys Fedoryshchenko
drivers/pptp is version 0.8.5 of the PPTP driver, the direct ancestor of mainline drivers/net/ppp/pptp.c. Mainline merged that code in 2.6.37 (2011) from the same author and has maintained it since; this copy received none of the subsequent fixes and is no longer worth carrying: - It cannot be built. struct flowi's nl_u union, the 3-argument ip_route_output_key(), sock_no_poll, the old ip_select_ident() signature and nf_reset() all disappeared long ago, so nothing past roughly 2.6.36 compiles and -DBUILD_PPTP_DRIVER=TRUE is a build failure everywhere. - It is not needed. The PPTP bits of accel-pppd/include/if_pppox.h are identical to the mainline UAPI header, so ctrl/pptp's socket(AF_PPPOX, SOCK_STREAM, PX_PROTO_PPTP) reaches the in-kernel module (alias net-pf-24-proto-2) unchanged. The daemon uses no interface the bundled driver added. The deleted drivers/pptp/if_pppox.h was included only by drivers/pptp/pptp.c; all other if_pppox.h includes use either the kernel UAPI header or accel-pppd's userspace copy. - It is unsafe. Among others: the pskb_may_pull() in pptp_rcv() is commented out, so the GRE header is parsed with no length validation at all; the skb->len - headersize comparison in pptp_rcv_core() underflows and leads to an out-of-bounds read and a negative skb_pull(); pptp_getname() copies uninitialised stack to userspace; and pptp_bind()/pptp_connect() never check sockaddr_len (CVE-2015-8569). Mainline fixed each of these years ago. Remove the directory along with the BUILD_PPTP_DRIVER option, the accel-pptp-kmod package and the ip_gre conflict warning, which only existed because this module claimed IPPROTO_GRE. accel-pppd/ctrl/pptp and accel-pppd/include/if_pppox.h are unaffected. PPTP now requires the kernel's own pptp module; kernels older than 2.6.37 are no longer supported.
2026-08-09pptp: reject a malformed bind addressDenys Fedoryshchenko
An unparsable bind= value went through inet_addr() unchecked and became 255.255.255.255, so the only symptom was bind() failing with "Cannot assign requested address", which does not point at the configuration. Parse with inet_aton() and name the offending value instead. Also zero the address before filling it in, so the padding passed to bind() is not stack garbage.
2026-08-09pptp: pass a proper value to SO_REUSEADDRDenys Fedoryshchenko
The option value was the address of the listening descriptor rather than a boolean, so the effect depended on the descriptor number: it enabled SO_REUSEADDR only because that number happened to be non-zero, and would disable it if the daemon were ever started with the lower descriptors closed. Use a dedicated flag, as cli/telnet.c does.
2026-08-09pptp: make echo-failure=0 disable the check explicitlyDenys Fedoryshchenko
load_config() accepts echo-failure=0, but the test for it was "++echo_sent == conf_echo_failure", which can never match once the counter has been incremented, so a zero left dead peers undetected without saying so anywhere. Test the option first and compare with >=, which keeps the behaviour for every configured value and makes the disabled case readable, and document it in accel-ppp.conf.5.
2026-08-09pptp: fix byte order of peer call id in Call-Disconnect-NotifyDenys Fedoryshchenko
conn->peer_call_id is assigned msg->call_id straight from the wire, so it holds a network order value, but send_pptp_call_disconnect_notify() then applies htons() to it. On little-endian hosts the field is swapped twice and a peer call id of 0x1234 is sent as 0x3412, so the peer cannot match the notify to its call. Big-endian hosts are unaffected, as both swaps are no-ops there. Store the call id in host order, which is what the htons() at the point of use expects. Nothing else reads the field.
2026-08-09pptp: check getsockname()/getpeername() resultsDenys Fedoryshchenko
Both calls were issued on an uninitialised struct sockaddr_in and their results ignored, so a failure would build the tunnel endpoints, and the call socket's local call id, out of stack garbage. Fail the call instead. In pptp_connect() the local address was fetched into the same variable that held the peer address obtained from accept(), so a failure there would silently set called-station-id to the calling station. Read it into its own variable before the connection is set up, and re-arm the address length before each accept() rather than leaving it at whatever the previous iteration wrote.
2026-08-09pptp: close call socket when Outgoing-Call-Reply cannot be sentDenys Fedoryshchenko
The PPPoX socket created for the call is only handed to conn->ppp.fd after the reply has been posted, so returning early on a post_msg() failure leaks the descriptor: disconnect() knows nothing about it and establish_ppp() has not run yet. The establish_ppp() failure path just below already closes it.