Documentation
¶
Overview ¶
Package compress implements OpenVPN's data-channel compression framing and LZO decompression.
Nothing is compressed on send: every payload goes out behind the uncompressed marker. A payload the peer LZO-compressed is decompressed; one it LZ4-compressed is refused with ErrCompressed.
Why the framing is load-bearing ¶
A peer with a compression directive frames *every* data packet, whether or not it compressed that packet, and drops what it cannot parse. A client that sends an unframed packet to such a peer is silently dropped, and a client that strips the wrong number of bytes hands the tunnel an IP packet shifted by one — one that decrypts cleanly, passes every authentication check, and means nothing.
The four framings ¶
There are four distinct shapes on the wire, and which one applies is decided by the directive *and* its COMP_F_SWAP flag rather than by the algorithm's name. All four were measured from real 2.4.12, 2.5.11 and 2.6.22 peers; see docker/COMPRESSION-VECTORS.md §6 and testdata/vectors.json.
directive payload wire comp-lzo not compressed 0xFA ‖ plain comp-lzo compressed 0x66 ‖ LZO(plain) compress (bare) not compressed 0xFB ‖ plain[1:] ‖ plain[0] compress lz4 not compressed 0xFB ‖ plain[1:] ‖ plain[0] compress lz4 compressed 0x69 ‖ LZ4(plain), swapped the same way compress lz4-v2 not compressed plain, unchanged compress lz4-v2 compressed 0x50 0x01 ‖ LZ4(plain) compress stub-v2 not compressed plain, unchanged comp-lzo no not compressed 0xFA ‖ plain
The swap is the one a reading of the header cannot predict: the framing byte *replaces* the payload's first byte, which moves to the tail, so that the rest of the payload keeps its alignment. Prepending where OpenVPN swaps shifts every subsequent byte by one, which is the shape of corruption that looks like a decryption failure.
References ¶
Read from the OpenVPN 2.4.12, 2.5.11 and 2.6.22 release tarballs the test rig pins (SHA-256 in docker/openvpn-server/versions.env):
- src/openvpn/comp.h — the framing bytes and COMP_F_SWAP
- src/openvpn/options.c, add_option() — which directive sets which flags
- src/openvpn/lzo.c — lzo_compress(), lzo_decompress()
- src/openvpn/compstub.c — stub_compress(), stubv2_decompress()
- src/openvpn/comp-lz4.c — lz4_compress(), lz4v2_compress()
- src/openvpn/comp.c — check_compression_settings_valid()
The LZO1X codec is github.com/buengese/go-lzo.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrCompressed = errors.New("compress: peer sent a compressed payload and no codec is linked")
ErrCompressed reports a peer that compressed a payload with an algorithm this package cannot decompress. Codec.Unwrap returns a *CompressedPayloadError, which names the algorithm and satisfies errors.Is against this sentinel.
Functions ¶
func PeerDeclaresFraming ¶
PeerDeclaresFraming reports whether a peer's options string says it has a compression framework enabled.
The options string is OpenVPN's OCC block, exchanged inside the key-method-2 packet, and it is the only place a peer states its compression setting: 2.4, 2.5 and 2.6 all omit the directive from PUSH_REPLY entirely.
What it can answer is narrower than it looks, and was measured rather than assumed. OpenVPN writes the literal token "comp-lzo" whenever any framework is on, the v2 stub included: a server configured "compress stub-v2", with no comp-lzo directive anywhere, still advertises "comp-lzo" here, while a server with no compression advertises nothing. So this answers whether the peer expects a framing at all, and never which one — the profile answers that. A client that framed on its own profile against a peer that declared nothing would put a leading byte on every packet the peer discards: a handshake that completes over a tunnel that carries nothing.
Types ¶
type AllowCompression ¶
type AllowCompression int
AllowCompression is the --allow-compression policy, which 2.5 introduced and which decides whether a compressing algorithm may be enabled at all.
const ( // AllowUnset is the directive being absent, which is OpenVPN's default // and places no restriction of its own. AllowUnset AllowCompression = iota // AllowNo is "allow-compression no": COMP_F_ALLOW_STUB_ONLY. A framing // stub stays legal; an algorithm that can compress is refused, which is // what check_compression_settings_valid() enforces. AllowNo // AllowAsym is "allow-compression asym": accept a compressed payload from // the peer, never send one, which this client does whatever the policy. AllowAsym // AllowYes is "allow-compression yes": compression permitted in both // directions. It is the only value under which a 2.5 or 2.6 peer will // compress on send at all. AllowYes )
The --allow-compression values, from src/openvpn/options.c, add_option().
func ParseAllowCompression ¶
func ParseAllowCompression(arg string) (AllowCompression, bool)
ParseAllowCompression maps the argument of an --allow-compression directive to a policy, reporting false for a value OpenVPN would refuse.
func (AllowCompression) String ¶
func (a AllowCompression) String() string
String names the policy as the directive spells its argument.
type Codec ¶
type Codec struct {
// contains filtered or unexported fields
}
Codec is one session's compression: Wrap frames outgoing payloads for its mode, Unwrap strips the framing from incoming ones and decompresses those the peer LZO-compressed. It is safe for concurrent use.
func NewCodec ¶
NewCodec returns the codec for mode m. A decompressed payload may be at most maxLen bytes, the tunnel MTU.
func (*Codec) Decompressed ¶
Decompressed returns how many payloads Unwrap has decompressed.
func (*Codec) Unwrap ¶
Unwrap strips the codec's incoming framing from payload and returns the IP packet. An LZO payload decompresses to at most the codec's maxLen; a longer or malformed stream is an error.
A payload the peer compressed with LZ4 returns a *CompressedPayloadError, which errors.Is matches against ErrCompressed. A framing byte that belongs to no known state is an error too: OpenVPN drops such a packet.
func (*Codec) Wrap ¶
Wrap applies the codec's outgoing framing to payload.
The payload is never compressed: the byte written is always the uncompressed marker for the framing in force. A peer accepts that marker in every mode, and OpenVPN's own compressors emit exactly this whenever compression would not shrink the packet.
For ModeNone, and for a v2 framing over a payload that needs no escaping, the payload is returned unchanged and unaliased-from; every other case returns a fresh slice.
type CompressedPayloadError ¶
type CompressedPayloadError struct {
// Mode is the session's compression mode.
Mode Mode
// Algorithm is OpenVPN's name for the codec the peer used: "lz4" or
// "lz4v2".
Algorithm string
// Marker is the framing byte that announced the compression.
Marker byte
// Length is the size of the compressed payload that was refused.
Length int
}
CompressedPayloadError is the error Codec.Unwrap returns for a payload compressed with an algorithm it cannot decompress. It carries the algorithm so that the caller can name it in a ClassUnsupported failure.
func (*CompressedPayloadError) Error ¶
func (e *CompressedPayloadError) Error() string
Error describes the refused payload, naming the algorithm and the marker.
func (*CompressedPayloadError) Unwrap ¶
func (e *CompressedPayloadError) Unwrap() error
Unwrap returns ErrCompressed so that errors.Is identifies the class of failure without the caller having to know the algorithm.
type Framing ¶
type Framing int
Framing is the wire framing a mode uses, which is not the same question as which algorithm it names: COMP_F_SWAP is an option flag rather than a property of the codec, so two modes over one algorithm can differ on it.
const ( // FramingNone adds nothing on send and strips nothing on receive. FramingNone Framing = iota // FramingV1 prepends one byte: the algorithm's compress marker, or // NO_COMPRESS_BYTE. Used by comp-lzo, and by the non-swapping stub that // "comp-lzo no" selects. FramingV1 // FramingV1Swap writes one byte in place of the payload's first byte and // moves that byte to the tail. Used by bare "compress" and by // "compress lz4" — every mode carrying COMP_F_SWAP. FramingV1Swap // FramingV2 writes nothing at all for an uncompressed payload, and a // 0x50 indicator plus an algorithm byte otherwise. Used by // "compress lz4-v2" and "compress stub-v2". FramingV2 )
The four framings OpenVPN puts on a data-channel payload. The prepending and swapping forms are distinct: COMP_F_SWAP is set independently of the algorithm, so the two cannot be one framing.
type Mode ¶
type Mode int
Mode is the compression setting in force on the data channel: an algorithm together with the option flags that decide its framing.
It is deliberately finer-grained than the algorithm name. Bare "compress" and "comp-lzo no" both select COMP_ALG_STUB and neither ever compresses, yet they put different bytes on the wire, because only the first carries COMP_F_SWAP — a flag the directive does not spell out.
const ( // ModeNone is no compression framing at all: nothing is added on send // and nothing is stripped on receive. It is what a profile with no // compression directive gets, and what most servers push. ModeNone Mode = iota // ModeStub is bare "compress" (and "compress stub"): COMP_ALG_STUB with // COMP_F_SWAP. ModeStub // ModeStubNoSwap is "comp-lzo no": COMP_ALG_STUB with no flags, which is // the only spelling that reaches the non-swapping stub framing. ModeStubNoSwap // ModeLZO is "comp-lzo", "comp-lzo yes", "comp-lzo adaptive" and // "compress lzo": COMP_ALG_LZO, which never swaps — // lzo_compress_init() asserts !(flags & COMP_F_SWAP). ModeLZO // ModeLZ4 is "compress lz4": COMP_ALG_LZ4 with COMP_F_SWAP, which // lz4_compress_init() asserts is set. ModeLZ4 // ModeLZ4v2 is "compress lz4-v2": COMP_ALGV2_LZ4, the v2 framing that // adds nothing unless it compressed. ModeLZ4v2 // ModeStubV2 is "compress stub-v2": COMP_ALGV2_UNCOMPRESSED, the v2 // framing with no codec behind it, which is a way of asking for no // framing at all. ModeStubV2 )
The compression modes this client can frame for. None of them compresses on send; ModeLZO, ModeLZ4 and ModeLZ4v2 differ from the stubs only in that a peer using them may compress.
func EffectiveMode ¶
func EffectiveMode(profileMode, pushedMode Mode, allow AllowCompression) (Mode, error)
EffectiveMode returns the compression the data channel must use.
A pushed directive outranks the profile's, because the server names the framing it will actually apply and a client that keeps its own frames every packet wrong. A profile directive applies when the server pushes nothing, the usual case: OpenVPN 2.4's server does not push its compression setting at all, so a config saying comp-lzo and a server saying comp-lzo agree without a word about it on the wire.
"allow-compression no" refuses a compressing algorithm from *either* source: COMP_F_ALLOW_STUB_ONLY, which check_compression_settings_valid() fails the connection over rather than quietly downgrading. A framing stub survives. pushedMode is ModeNone when the server pushed nothing; "comp-lzo no" pushes ModeStubNoSwap, which is distinguishable from it.
func ModeForDirective ¶
ModeForDirective maps a compression directive — keyword and optional argument — to the Mode whose framing it selects, reporting false for a keyword that is not a compression directive or an argument OpenVPN refuses.
This is the single place the directive-to-flags table lives: the profile parser, the PUSH_REPLY parser and ParseMode all go through it, because a mode the profile spells one way and the push spells another must still frame the same.
Reference: src/openvpn/options.c, add_option(), the "comp-lzo" and "compress" cases, verified against the 2.4.12 tarball the rig pins.
func ParseMode ¶
ParseMode inspects a comma-separated option list — a PUSH_REPLY, or a single directive on its own — and returns the compression Mode it asks for.
The last compression directive wins, as it does in OpenVPN, where each option is applied in turn over the same options struct; a list with no compression directive yields ModeNone. Every mode that reaches the data channel reaches it through here, so a framing that is right for a Mode this parser never produces is not right for anything.
func (Mode) Algorithm ¶
Algorithm returns OpenVPN's own name for the algorithm behind the mode: "lzo", "stub", "stubv2", "lz4" or "lz4v2", matching the compress_alg.name field the capture recorded. ModeNone has none and returns "".
func (Mode) Compresses ¶
Compresses reports whether a peer in this mode may put a genuinely compressed payload on the wire. Only these modes are refused by "allow-compression no".
func (Mode) Decompresses ¶
Decompresses reports whether a Codec decompresses what a peer in this mode compresses. Only ModeLZO does; ModeLZ4 and ModeLZ4v2 yield ErrCompressed.