mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-10-19 15:33:18 +00:00
worktree-wt-issue-430-431 (#436)
This commit is contained in:
parent
9c415c6571
commit
6e8f65e6b6
17 changed files with 1037 additions and 39 deletions
|
|
@ -15,6 +15,10 @@
|
|||
- FIXED: **System Info listed tools such as jq, sha256sum and ipset without a version when they were on the USB drive b4 is installed on** - b4 asked a tool for its version only when it sat in a standard system directory such as `/usr/bin` or `/opt/bin`.
|
||||
- FIXED: **A `regexp:` entry listed in two sets, such as a set and its copy made with Duplicate, worked only in the set higher in the list, so when one of them was limited by Source devices, some devices got the wrong set or none for the names that entry matches** - b4 kept each pattern only for the first set that listed it.
|
||||
- FIXED: **A device added by hand by its IPv6 address, typed in another form such as in capitals or with leading zeros, was not recognised when b4 matched its traffic to a set's domains, so a set that selects it under Source devices skipped it and one with All devices except selected applied to it** - b4 looked packets up by the standard form of their address, while it kept the device's address as typed.
|
||||
- FIXED: **Apps that use TCP Fast Open could not open sites through a set with a strategy** - such an app puts the start of its secure connection into the packet that opens the connection. b4 split that packet like ordinary data, and every piece opened the connection again with different numbering. The site kept one of them and could not read the start. [#431](https://github.com/DanielLavrushin/b4/issues/431)
|
||||
- FIXED: **Window Mode sent malformed fake packets on connections that use TCP timestamps, which Linux turns on by default** - each fake kept the header length of the original packet but carried only the first part of the header.
|
||||
- FIXED: **SYN MD5 Signature could send its fake SYN without a usable MD5 option, such as for an app that uses TCP Fast Open** - when the real SYN's options left no room, the option was left out. When they ended with an end-of-options marker, it went after that marker, where servers stop reading. A server it reached could take the fake for a real SYN.
|
||||
- FIXED: **b4 could stop on a malformed TCP packet sent to an address a set covers** - the packet's header claimed to be shorter than any TCP header can be. b4 read past the end of the packet while applying the set's strategy.
|
||||
|
||||
## [1.86.0] - 2026-10-09
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@
|
|||
- ИСПРАВЛЕНО: **Информация о системе показывала утилиты, например jq, sha256sum и ipset, без версии, если они лежали на USB-накопителе, на который установлен b4** - b4 спрашивал версию только у утилиты из стандартного системного каталога, например `/usr/bin` или `/opt/bin`.
|
||||
- ИСПРАВЛЕНО: **Запись `regexp:`, указанная в двух сетах, например в сете и его копии, созданной кнопкой «Дублировать», работала только в том из них, что выше в списке, поэтому, если один из них был ограничен устройствами-источниками, часть устройств получала для имён, совпадающих с этой записью, не тот сет или никакой** - b4 закреплял каждый шаблон только за первым сетом, в котором он встретился.
|
||||
- ИСПРАВЛЕНО: **Устройство, добавленное вручную по IPv6-адресу, введённому в другой записи, например заглавными буквами или с ведущими нулями, не распознавалось, когда b4 сопоставлял его трафик с доменами сетов, поэтому сет, в устройствах-источниках которого оно выбрано, его пропускал, а сет с режимом «Все устройства, кроме выбранных» к нему применялся** - b4 искал пакеты по стандартной записи их адреса, а адрес устройства хранил в том виде, в каком его ввели.
|
||||
- ИСПРАВЛЕНО: **Приложения с TCP Fast Open не могли открыть сайты через сет со стратегией** - такое приложение кладёт начало защищённого соединения в пакет, который открывает соединение. b4 разрезал этот пакет как обычные данные, и каждая часть заново открывала соединение со своей нумерацией. Сайт оставлял одну из них и не мог прочитать начало. [#431](https://github.com/DanielLavrushin/b4/issues/431)
|
||||
- ИСПРАВЛЕНО: **«Режим окна» отправлял повреждённые фейковые пакеты на соединениях с метками времени TCP, которые Linux включает по умолчанию** - каждый фейк сохранял длину заголовка исходного пакета, но нёс только первую часть заголовка.
|
||||
- ИСПРАВЛЕНО: **С включённым SYN MD5 Signature фейковый SYN мог уйти без рабочей опции MD5, например у приложения с TCP Fast Open** - если опции настоящего SYN не оставляли места, опция не добавлялась. Если они заканчивались маркером конца опций, она вставала после него, а там серверы уже ничего не читают. Сервер, до которого доходил такой фейк, мог принять его за настоящий SYN.
|
||||
- ИСПРАВЛЕНО: **b4 мог остановиться из-за повреждённого TCP-пакета, отправленного на адрес из сета** - заголовок пакета объявлял длину меньше, чем бывает у любого TCP-заголовка. Применяя стратегию сета, b4 читал данные за концом пакета.
|
||||
|
||||
## [1.86.0] - 2026-10-09
|
||||
|
||||
|
|
|
|||
|
|
@ -494,7 +494,14 @@ func TestARetransmittedFastOpenSynKeepsTheName(t *testing.T) {
|
|||
const key = "10.0.0.1:12345->1.2.3.4:443"
|
||||
|
||||
hello := buildClientHello("i.ytimg.com", 16, 0xAB)
|
||||
w.ProcessPacket(makeV4TCPPacketFlags(hello, isn, 0x02))
|
||||
sent := captureInjected(t, w)
|
||||
syn := makeV4TCPPacketFlags(hello, isn, 0x02)
|
||||
w.ProcessPacket(syn)
|
||||
if got := sent(); len(got) != 1 {
|
||||
t.Fatalf("a fast-open SYN must go out as one SYN without the ClientHello, not split like data, got %d packets", len(got))
|
||||
} else {
|
||||
assertSynWithoutPayload(t, got[0], syn)
|
||||
}
|
||||
w.ProcessPacket(makeV4TCPPacketFlags(nil, isn, 0x02))
|
||||
if host, _, found := w.tlsCache.Lookup(key); !found || host != "i.ytimg.com" {
|
||||
t.Fatalf("a retransmitted SYN of the same connection must keep the name its first SYN carried, got %q found=%v", host, found)
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ func (w *Worker) handleTCPPacket(vc *verdictCtx, pkt *pktInfo, cfg *config.Confi
|
|||
return vc.accept()
|
||||
}
|
||||
datOff := int((tcp[12]>>4)&0x0f) * 4
|
||||
if len(tcp) < datOff {
|
||||
if datOff < TCPHeaderMinLen || len(tcp) < datOff {
|
||||
return vc.accept()
|
||||
}
|
||||
payload := tcp[datOff:]
|
||||
|
|
@ -463,22 +463,29 @@ func (w *Worker) handleTCPPacket(vc *verdictCtx, pkt *pktInfo, cfg *config.Confi
|
|||
|
||||
observeFlow(cfg, pkt, sport, dport, set, "")
|
||||
|
||||
syn := pkt.raw
|
||||
if len(payload) > 0 && needsTCPInjection(set) {
|
||||
if stripped, ok := synWithoutPayload(pkt.raw, pkt.ver, pkt.ihl, datOff); ok {
|
||||
syn = stripped
|
||||
}
|
||||
}
|
||||
|
||||
if pkt.ver == IPv4 {
|
||||
if set.TCP.SynFake {
|
||||
w.sendFakeSyn(set, pkt.raw, pkt.ihl, datOff)
|
||||
w.sendFakeSyn(set, syn, pkt.ihl, datOff)
|
||||
}
|
||||
if set.Faking.TCPMD5 {
|
||||
w.sendFakeSynWithMD5(set, pkt.raw, pkt.ihl, pkt.dst)
|
||||
w.sendFakeSynWithMD5(set, syn, pkt.ihl, pkt.dst)
|
||||
}
|
||||
_ = w.sock.SendIPv4(pkt.raw, pkt.dst)
|
||||
_ = w.sock.SendIPv4(syn, pkt.dst)
|
||||
} else {
|
||||
if set.TCP.SynFake {
|
||||
w.sendFakeSynV6(set, pkt.raw, pkt.ihl, datOff)
|
||||
w.sendFakeSynV6(set, syn, pkt.ihl, datOff)
|
||||
}
|
||||
if set.Faking.TCPMD5 {
|
||||
w.sendFakeSynWithMD5V6(set, pkt.raw, pkt.dst)
|
||||
w.sendFakeSynWithMD5V6(set, syn, pkt.dst)
|
||||
}
|
||||
_ = w.sock.SendIPv6(pkt.raw, pkt.dst)
|
||||
_ = w.sock.SendIPv6(syn, pkt.dst)
|
||||
}
|
||||
|
||||
if set.TCP.Incoming.Mode != config.ConfigOff || set.TCP.RSTProtection.Enabled || set.Escalate.Active() {
|
||||
|
|
@ -678,10 +685,15 @@ func (w *Worker) handleTCPPacket(vc *verdictCtx, pkt *pktInfo, cfg *config.Confi
|
|||
observeFlow(cfg, pkt, sport, dport, flowSet, host)
|
||||
}
|
||||
|
||||
var strippedSyn []byte
|
||||
if isSyn && len(payload) > 0 && matched && set != nil && !routeHandsOff && needsTCPInjection(set) {
|
||||
strippedSyn, _ = synWithoutPayload(pkt.raw, pkt.ver, pkt.ihl, datOff)
|
||||
}
|
||||
|
||||
stallCount := 0
|
||||
stallFirstSeen := time.Time{}
|
||||
stallTracked := false
|
||||
if matched && isClientHello && host != "" && cfg.IsTCPPort(dport) &&
|
||||
if matched && isClientHello && strippedSyn == nil && host != "" && cfg.IsTCPPort(dport) &&
|
||||
(set.TCP.IPBlockDetect.Enabled || set.Escalate.Active()) {
|
||||
stallCount, stallFirstSeen = w.destState.RecordClientHello(connKey, host)
|
||||
stallTracked = true
|
||||
|
|
@ -774,6 +786,28 @@ func (w *Worker) handleTCPPacket(vc *verdictCtx, pkt *pktInfo, cfg *config.Confi
|
|||
return vc.accept()
|
||||
}
|
||||
|
||||
if isSyn && len(payload) > 0 {
|
||||
if strippedSyn == nil {
|
||||
return vc.accept()
|
||||
}
|
||||
if set.TCP.DropSACK {
|
||||
if pkt.ver == 4 {
|
||||
strippedSyn = sock.StripSACKFromTCP(strippedSyn)
|
||||
} else {
|
||||
strippedSyn = sock.StripSACKFromTCPv6(strippedSyn)
|
||||
}
|
||||
}
|
||||
if !vc.drop() {
|
||||
return 0
|
||||
}
|
||||
if pkt.ver == 4 {
|
||||
_ = w.sock.SendIPv4(strippedSyn, pkt.dst)
|
||||
} else {
|
||||
_ = w.sock.SendIPv6(strippedSyn, pkt.dst)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if !injectAcquire() {
|
||||
return vc.accept()
|
||||
}
|
||||
|
|
@ -884,7 +918,7 @@ func (w *Worker) handleUDPPacket(vc *verdictCtx, pkt *pktInfo, cfg *config.Confi
|
|||
|
||||
namedByHello := false
|
||||
helloDatagram := false
|
||||
if isQUIC && (host == "" || coalescesInitials(set)) {
|
||||
if isQUIC && (host == "" || coalescesInitials(set) || coalescesInitials(w.escalatedSetFor(cfg, host, pkt.srcMac))) {
|
||||
if h, ok := sni.ParseQUICClientHelloSNI(payload); ok {
|
||||
helloDatagram = true
|
||||
host = h
|
||||
|
|
|
|||
|
|
@ -444,7 +444,11 @@ func (w *Worker) sendFakeSNISequence(cfg *config.SetConfig, original []byte, dst
|
|||
}
|
||||
badsum := !sock.TCPChecksumValid(fake)
|
||||
if fk.MD5OnFake {
|
||||
fake = sock.AddTCPMD5Option(fake, false)
|
||||
withMD5, ok := sock.AddTCPMD5Option(fake, false)
|
||||
if !ok {
|
||||
log.Tracef("Fake SNI to %s: no room for the TCP MD5 option, sending it without", dst.String())
|
||||
}
|
||||
fake = withMD5
|
||||
if badsum {
|
||||
sock.FixTCPChecksum(fake)
|
||||
sock.CorruptTCPChecksum(fake)
|
||||
|
|
|
|||
|
|
@ -359,7 +359,11 @@ func (w *Worker) sendFakeSNISequencev6(cfg *config.SetConfig, original []byte, d
|
|||
}
|
||||
badsum := !sock.TCPChecksumValidV6(fake)
|
||||
if faking.MD5OnFake {
|
||||
fake = sock.AddTCPMD5Option(fake, true)
|
||||
withMD5, ok := sock.AddTCPMD5Option(fake, true)
|
||||
if !ok {
|
||||
log.Tracef("Fake SNI v6 to %s: no room for the TCP MD5 option, sending it without", dst.String())
|
||||
}
|
||||
fake = withMD5
|
||||
if badsum {
|
||||
sock.FixTCPChecksumV6(fake)
|
||||
sock.CorruptTCPChecksumV6(fake)
|
||||
|
|
|
|||
|
|
@ -288,6 +288,44 @@ func TestAnAddressLearnedByACoalescingSetDoesNotHideABlockedName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAnEscalationToACoalescingSetCoalescesOnALearnedAddress(t *testing.T) {
|
||||
fallback := newDomainOnlyQUICSet(config.UDPModeCoalesce)
|
||||
fallback.Id = "coalesce-fallback"
|
||||
fallback.Name = "CoalesceFallback"
|
||||
fallback.Targets.SNIDomains = []string{"fallback.example"}
|
||||
|
||||
learning := newDomainOnlyQUICSet(config.ConfigOff)
|
||||
learning.Id = "learning"
|
||||
learning.Name = "Learning"
|
||||
learning.Targets.SNIDomains = []string{"escalate.example"}
|
||||
learning.Targets.DomainOnly = false
|
||||
learning.Escalate.To = fallback.Id
|
||||
|
||||
for _, escalated := range []bool{false, true} {
|
||||
w := newQUICFlowWorker(t, &learning, &fallback)
|
||||
sent := captureInjected(t, w)
|
||||
if v := sendQUIC(w, buildQUICInitialWithSNI(t, []byte{0xa0, 1, 2, 3, 4, 5, 6, 7}, "www.escalate.example"), 50401); v != engine.VerdictAccept {
|
||||
t.Fatalf("the learning set leaves QUIC alone, got %v", v)
|
||||
}
|
||||
if escalated {
|
||||
w.destState.SetEscalation("www.escalate.example", fallback.Id, escalateReasonStall, time.Minute)
|
||||
}
|
||||
|
||||
hello := buildQUICInitialWithSNI(t, []byte{0xa1, 1, 2, 3, 4, 5, 6, 7}, "www.escalate.example")
|
||||
v := sendQUIC(w, hello, 50402)
|
||||
got := sent()
|
||||
if !escalated {
|
||||
if v != engine.VerdictAccept || len(got) != 0 {
|
||||
t.Errorf("without escalation the learning set leaves the ClientHello datagram alone, got %v and %d packets", v, len(got))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if v != engine.VerdictDrop || len(got) != 1 || len(got[0]) <= 28+len(hello) {
|
||||
t.Errorf("after escalation to a coalescing set, a ClientHello datagram to the learned address must go out coalesced, got %v and %d packets", v, len(got))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAStaticAddressSetKeepsTheLaterDatagramsOfAQUICFlow(t *testing.T) {
|
||||
named := newDomainOnlyQUICSet(config.UDPModeDrop)
|
||||
static := config.NewSetConfig()
|
||||
|
|
|
|||
42
src/nfq/sendcapture_test.go
Normal file
42
src/nfq/sendcapture_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package nfq
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/daniellavrushin/b4/sock"
|
||||
)
|
||||
|
||||
func captureInjected(t *testing.T, w *Worker) func() [][]byte {
|
||||
t.Helper()
|
||||
pair := func() [2]int {
|
||||
fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_SEQPACKET|syscall.SOCK_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("socketpair: %v", err)
|
||||
}
|
||||
return [2]int{fds[0], fds[1]}
|
||||
}
|
||||
v4, v6 := pair(), pair()
|
||||
w.sock = sock.NewSenderWithFDs(v4[0], v6[0])
|
||||
t.Cleanup(func() {
|
||||
w.wg.Wait()
|
||||
for _, fd := range []int{v4[0], v4[1], v6[0], v6[1]} {
|
||||
_ = syscall.Close(fd)
|
||||
}
|
||||
})
|
||||
return func() [][]byte {
|
||||
w.wg.Wait()
|
||||
var sent [][]byte
|
||||
buf := make([]byte, 65536)
|
||||
for _, fd := range []int{v4[1], v6[1]} {
|
||||
for {
|
||||
n, _, err := syscall.Recvfrom(fd, buf, syscall.MSG_DONTWAIT)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
sent = append(sent, append([]byte(nil), buf[:n]...))
|
||||
}
|
||||
}
|
||||
return sent
|
||||
}
|
||||
}
|
||||
|
|
@ -149,10 +149,15 @@ func (w *Worker) sendFakeSynWithMD5(set *config.SetConfig, raw []byte, ihl int,
|
|||
sock.FixIPv4Checksum(fakeSyn[:ihl])
|
||||
sock.FixTCPChecksum(fakeSyn)
|
||||
|
||||
// Add MD5 option
|
||||
fakeSyn = sock.AddTCPMD5Option(fakeSyn, false)
|
||||
signed, ok := sock.AddTCPMD5Option(fakeSyn, false)
|
||||
if !ok {
|
||||
signed, ok = sock.AddTCPMD5Option(withoutTCPOptions(fakeSyn, ihl), false)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
_ = w.sock.SendIPv4(fakeSyn, dst)
|
||||
_ = w.sock.SendIPv4(signed, dst)
|
||||
}
|
||||
|
||||
func (w *Worker) sendFakeSynWithMD5V6(set *config.SetConfig, raw []byte, dst net.IP) {
|
||||
|
|
@ -172,8 +177,54 @@ func (w *Worker) sendFakeSynWithMD5V6(set *config.SetConfig, raw []byte, dst net
|
|||
seq := binary.BigEndian.Uint32(fakeSyn[ipv6HdrLen+4 : ipv6HdrLen+8])
|
||||
binary.BigEndian.PutUint32(fakeSyn[ipv6HdrLen+4:ipv6HdrLen+8], seq-10000)
|
||||
|
||||
// Add MD5 option (also fixes checksums)
|
||||
fakeSyn = sock.AddTCPMD5Option(fakeSyn, true)
|
||||
signed, ok := sock.AddTCPMD5Option(fakeSyn, true)
|
||||
if !ok {
|
||||
signed, ok = sock.AddTCPMD5Option(withoutTCPOptions(fakeSyn, ipv6HdrLen), true)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
_ = w.sock.SendIPv6(fakeSyn, dst)
|
||||
_ = w.sock.SendIPv6(signed, dst)
|
||||
}
|
||||
|
||||
func withoutTCPOptions(pkt []byte, l3Len int) []byte {
|
||||
if len(pkt) < l3Len+20 {
|
||||
return pkt
|
||||
}
|
||||
tcpHL := int(pkt[l3Len+12]>>4) * 4
|
||||
if tcpHL < 20 || len(pkt) < l3Len+tcpHL {
|
||||
return pkt
|
||||
}
|
||||
out := make([]byte, len(pkt)-tcpHL+20)
|
||||
copy(out, pkt[:l3Len+20])
|
||||
copy(out[l3Len+20:], pkt[l3Len+tcpHL:])
|
||||
out[l3Len+12] = 5<<4 | out[l3Len+12]&0x0f
|
||||
if out[0]>>4 == IPv6 {
|
||||
binary.BigEndian.PutUint16(out[4:6], uint16(len(out)-IPv6HeaderLen))
|
||||
sock.FixTCPChecksumV6(out)
|
||||
} else {
|
||||
binary.BigEndian.PutUint16(out[2:4], uint16(len(out)))
|
||||
sock.FixIPv4Checksum(out[:l3Len])
|
||||
sock.FixTCPChecksum(out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func synWithoutPayload(raw []byte, ver byte, l3Len, tcpHdrLen int) ([]byte, bool) {
|
||||
end := l3Len + tcpHdrLen
|
||||
if tcpHdrLen < TCPHeaderMinLen || len(raw) <= end || (ver == IPv6 && !hasPlainIPv6Header(raw, ipProtoTCP)) {
|
||||
return nil, false
|
||||
}
|
||||
syn := make([]byte, end)
|
||||
copy(syn, raw[:end])
|
||||
if ver == IPv4 {
|
||||
binary.BigEndian.PutUint16(syn[2:4], uint16(end))
|
||||
sock.FixIPv4Checksum(syn[:l3Len])
|
||||
sock.FixTCPChecksum(syn)
|
||||
} else {
|
||||
binary.BigEndian.PutUint16(syn[4:6], uint16(end-IPv6HeaderLen))
|
||||
sock.FixTCPChecksumV6(syn)
|
||||
}
|
||||
return syn, true
|
||||
}
|
||||
|
|
|
|||
116
src/nfq/tcpheader_test.go
Normal file
116
src/nfq/tcpheader_test.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package nfq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/engine"
|
||||
"github.com/daniellavrushin/b4/sock"
|
||||
)
|
||||
|
||||
func mustNotPanic(t *testing.T, what string, f func()) {
|
||||
t.Helper()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("%s panicked: %v", what, r)
|
||||
}
|
||||
}()
|
||||
f()
|
||||
}
|
||||
|
||||
func packetWithDataOffset(ver byte, words, flags byte) []byte {
|
||||
if ver == IPv4 {
|
||||
pkt := makeV4TCPPacket(make([]byte, 64), 1000)
|
||||
pkt[20+12] = words << 4
|
||||
pkt[20+13] = flags
|
||||
return pkt
|
||||
}
|
||||
pkt := makeV6TCPPacket(make([]byte, 64), 1000)
|
||||
pkt[IPv6HeaderLen+12] = words << 4
|
||||
pkt[IPv6HeaderLen+13] = flags
|
||||
return pkt
|
||||
}
|
||||
|
||||
func TestAPacketWithATCPHeaderShorterThan20BytesPassesUntouched(t *testing.T) {
|
||||
type scenario struct {
|
||||
name string
|
||||
flags byte
|
||||
apply func(*config.SetConfig)
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{"dropsack syn", 0x02, func(s *config.SetConfig) { s.TCP.DropSACK = true }},
|
||||
{"syn fake", 0x02, func(s *config.SetConfig) { s.TCP.SynFake = true }},
|
||||
{"md5 syn", 0x02, func(s *config.SetConfig) { s.Faking.TCPMD5 = true }},
|
||||
{"window", 0x18, func(s *config.SetConfig) { s.TCP.Win.Mode = "oscillate" }},
|
||||
}
|
||||
for _, strategy := range []string{"tcp", "ip", "oob", "tls", "disorder", "extsplit", "firstbyte", "combo", "hybrid"} {
|
||||
scenarios = append(scenarios, scenario{"strategy " + strategy, 0x18, func(s *config.SetConfig) { s.Fragmentation.Strategy = strategy }})
|
||||
}
|
||||
for words := byte(0); words < 5; words++ {
|
||||
for _, ver := range []byte{IPv4, IPv6} {
|
||||
for _, sc := range scenarios {
|
||||
t.Run(fmt.Sprintf("v%d/offset%d/%s", ver, words, sc.name), func(t *testing.T) {
|
||||
set := newStaticAddressSet(true)
|
||||
set.Targets.IpsToMatch = []string{"1.2.3.4", "2001:db8::2"}
|
||||
sc.apply(&set)
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
mustNotPanic(t, "ProcessPacket", func() {
|
||||
if v := w.ProcessPacket(packetWithDataOffset(ver, words, sc.flags)); v != engine.VerdictAccept {
|
||||
t.Errorf("a packet with a %d-byte TCP header must pass untouched, got %v", int(words)*4, v)
|
||||
}
|
||||
})
|
||||
if got := sent(); len(got) != 0 {
|
||||
t.Errorf("nothing may be injected for a malformed header, sent %d packets", len(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewritersRefuseATCPHeaderShorterThan20Bytes(t *testing.T) {
|
||||
for words := byte(0); words < 5; words++ {
|
||||
for _, ver := range []byte{IPv4, IPv6} {
|
||||
t.Run(fmt.Sprintf("v%d/offset%d", ver, words), func(t *testing.T) {
|
||||
l3 := 20
|
||||
if ver == IPv6 {
|
||||
l3 = IPv6HeaderLen
|
||||
}
|
||||
|
||||
syn := packetWithDataOffset(ver, words, 0x02)
|
||||
mustNotPanic(t, "synWithoutPayload", func() {
|
||||
if _, ok := synWithoutPayload(syn, ver, l3, int(words)*4); ok {
|
||||
t.Error("synWithoutPayload must refuse a TCP header shorter than 20 bytes")
|
||||
}
|
||||
})
|
||||
|
||||
mustNotPanic(t, "AddTCPMD5Option", func() {
|
||||
if _, ok := sock.AddTCPMD5Option(syn, ver == IPv6); ok {
|
||||
t.Error("AddTCPMD5Option must refuse a TCP header shorter than 20 bytes")
|
||||
}
|
||||
})
|
||||
|
||||
set := newFragmentingYTImagesSet()
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
data := packetWithDataOffset(ver, words, 0x18)
|
||||
for _, mode := range []string{"oscillate", "random", "escalate", "zero"} {
|
||||
set.TCP.Win.Mode = mode
|
||||
mustNotPanic(t, "window manipulation "+mode, func() {
|
||||
if ver == IPv4 {
|
||||
w.ManipulateWindowIPv4(&set, data, net.IP{1, 2, 3, 4})
|
||||
} else {
|
||||
w.ManipulateWindowIPv6(&set, data, net.ParseIP("2001:db8::2"))
|
||||
}
|
||||
})
|
||||
}
|
||||
if got := sent(); len(got) != 0 {
|
||||
t.Errorf("window manipulation must send nothing for a malformed header, sent %d packets", len(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
439
src/nfq/tfo_test.go
Normal file
439
src/nfq/tfo_test.go
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
package nfq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/engine"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
"github.com/daniellavrushin/b4/sock"
|
||||
)
|
||||
|
||||
var fastOpenSynOptions = []byte{
|
||||
0x02, 0x04, 0x05, 0xb4,
|
||||
0x04, 0x02,
|
||||
0x08, 0x0a, 0x11, 0x22, 0x33, 0x44, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01,
|
||||
0x03, 0x03, 0x07,
|
||||
0x22, 0x0a, 0xc0, 0x0c, 0x1e, 0xc0, 0x0c, 0x1e, 0xc0, 0x0c,
|
||||
0x01, 0x01,
|
||||
}
|
||||
|
||||
func fastOpenSyn(ver byte, payload []byte, isn uint32) []byte {
|
||||
return synWithOptions(ver, fastOpenSynOptions, payload, isn)
|
||||
}
|
||||
|
||||
func synWithOptions(ver byte, opts, payload []byte, isn uint32) []byte {
|
||||
segment := append(append([]byte(nil), opts...), payload...)
|
||||
doff := byte((20+len(opts))/4) << 4
|
||||
if ver == IPv4 {
|
||||
pkt := makeV4TCPPacket(segment, isn)
|
||||
pkt[20+12] = doff
|
||||
pkt[20+13] = 0x02
|
||||
sock.FixIPv4Checksum(pkt[:20])
|
||||
sock.FixTCPChecksum(pkt)
|
||||
return pkt
|
||||
}
|
||||
pkt := makeV6TCPPacket(segment, isn)
|
||||
pkt[IPv6HeaderLen+12] = doff
|
||||
pkt[IPv6HeaderLen+13] = 0x02
|
||||
sock.FixTCPChecksumV6(pkt)
|
||||
return pkt
|
||||
}
|
||||
|
||||
func newDualStackInjectingWorker(t *testing.T, sets ...*config.SetConfig) *Worker {
|
||||
t.Helper()
|
||||
cfg := config.NewConfig()
|
||||
cfg.Sets = sets
|
||||
cfg.Queue.IPv6Enabled = true
|
||||
w := newTestWorker(t, &cfg)
|
||||
w.sock = &sock.Sender{}
|
||||
t.Cleanup(w.wg.Wait)
|
||||
return w
|
||||
}
|
||||
|
||||
func ipv4HeaderChecksumValid(h []byte) bool {
|
||||
var sum uint32
|
||||
for i := 0; i+1 < len(h); i += 2 {
|
||||
sum += uint32(binary.BigEndian.Uint16(h[i : i+2]))
|
||||
}
|
||||
for sum>>16 != 0 {
|
||||
sum = sum&0xffff + sum>>16
|
||||
}
|
||||
return sum == 0xffff
|
||||
}
|
||||
|
||||
func tcpOptionKinds(pkt []byte) []byte {
|
||||
l3 := int(pkt[0]&0x0f) * 4
|
||||
if pkt[0]>>4 == IPv6 {
|
||||
l3 = IPv6HeaderLen
|
||||
}
|
||||
opts := pkt[l3+20 : l3+int(pkt[l3+12]>>4)*4]
|
||||
var kinds []byte
|
||||
for i := 0; i < len(opts) && opts[i] != 0; {
|
||||
if opts[i] == 1 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
kinds = append(kinds, opts[i])
|
||||
if i+1 >= len(opts) || opts[i+1] < 2 {
|
||||
break
|
||||
}
|
||||
i += int(opts[i+1])
|
||||
}
|
||||
return kinds
|
||||
}
|
||||
|
||||
func assertSynWithoutPayload(t *testing.T, got, orig []byte) {
|
||||
t.Helper()
|
||||
v4 := orig[0]>>4 == IPv4
|
||||
l3 := IPv6HeaderLen
|
||||
if v4 {
|
||||
l3 = int(orig[0]&0x0f) * 4
|
||||
}
|
||||
tcpHL := int(orig[l3+12]>>4) * 4
|
||||
if len(got) != l3+tcpHL {
|
||||
t.Fatalf("the SYN must keep its headers and lose its payload: %d bytes, want %d", len(got), l3+tcpHL)
|
||||
}
|
||||
for i := range got {
|
||||
lengthField := (v4 && (i == 2 || i == 3)) || (!v4 && (i == 4 || i == 5))
|
||||
checksum := (v4 && (i == 10 || i == 11)) || i == l3+16 || i == l3+17
|
||||
if !lengthField && !checksum && got[i] != orig[i] {
|
||||
t.Fatalf("header byte %d changed: %#x, want %#x", i, got[i], orig[i])
|
||||
}
|
||||
}
|
||||
if v4 {
|
||||
if n := int(binary.BigEndian.Uint16(got[2:4])); n != len(got) {
|
||||
t.Errorf("IPv4 total length %d, want %d", n, len(got))
|
||||
}
|
||||
if !ipv4HeaderChecksumValid(got[:l3]) || !sock.TCPChecksumValid(got) {
|
||||
t.Error("the stripped SYN must carry valid checksums")
|
||||
}
|
||||
return
|
||||
}
|
||||
if n := int(binary.BigEndian.Uint16(got[4:6])); n != tcpHL {
|
||||
t.Errorf("IPv6 payload length %d, want %d", n, tcpHL)
|
||||
}
|
||||
if !sock.TCPChecksumValidV6(got) {
|
||||
t.Error("the stripped SYN must carry a valid checksum")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryStrategySendsAFastOpenSynWithoutItsPayload(t *testing.T) {
|
||||
hello := buildClientHello("i.ytimg.com", 16, 0xAB)
|
||||
for _, strategy := range []string{"tcp", "ip", "oob", "tls", "disorder", "extsplit", "firstbyte", "combo", "hybrid"} {
|
||||
for _, ver := range []byte{IPv4, IPv6} {
|
||||
name, l3 := strategy+"/v4", 20
|
||||
if ver == IPv6 {
|
||||
name, l3 = strategy+"/v6", IPv6HeaderLen
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
set := newFragmentingYTImagesSet()
|
||||
set.Fragmentation.Strategy = strategy
|
||||
|
||||
data := fastOpenSyn(ver, hello, 1000)
|
||||
data[l3+13] = 0x18
|
||||
control := newDualStackInjectingWorker(t, &set)
|
||||
controlSent := captureInjected(t, control)
|
||||
if v := control.ProcessPacket(data); v != engine.VerdictDrop || len(controlSent()) == 0 {
|
||||
t.Fatalf("the ClientHello as a data segment must get the set's strategy, got %v", v)
|
||||
}
|
||||
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
syn := fastOpenSyn(ver, hello, 1000)
|
||||
if v := w.ProcessPacket(syn); v != engine.VerdictDrop {
|
||||
t.Fatalf("a fast-open SYN of a set that processes the hello must be taken over, got %v", v)
|
||||
}
|
||||
got := sent()
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("a fast-open SYN must go out as one SYN, got %d packets", len(got))
|
||||
}
|
||||
assertSynWithoutPayload(t, got[0], syn)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheHelloResentAfterAStrippedSynGetsTheSetsStrategy(t *testing.T) {
|
||||
set := newFragmentingYTImagesSet()
|
||||
set.TCP.IPBlockDetect.Enabled = true
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
ch, _ := log.GetConnectionHub().Subscribe()
|
||||
defer log.GetConnectionHub().Unsubscribe(ch)
|
||||
|
||||
hello := buildClientHello("i.ytimg.com", 16, 0xAB)
|
||||
const isn = uint32(1000)
|
||||
syn := fastOpenSyn(IPv4, hello, isn)
|
||||
w.ProcessPacket(syn)
|
||||
if got := loggedSet(t, ch); got != set.Name {
|
||||
t.Fatalf("the fast-open SYN row: want set %q, got %q", set.Name, got)
|
||||
}
|
||||
if got := sent(); len(got) != 1 {
|
||||
t.Fatalf("the fast-open SYN must go out as one SYN, got %d packets", len(got))
|
||||
} else {
|
||||
assertSynWithoutPayload(t, got[0], syn)
|
||||
}
|
||||
if n := len(w.destState.conns); n != 0 {
|
||||
t.Errorf("a ClientHello cut off the SYN never reaches the server and must not be counted, got %d tracked connections", n)
|
||||
}
|
||||
|
||||
if v := w.ProcessPacket(makeV4TCPPacketFlags(hello, isn+1, 0x18)); v != engine.VerdictDrop {
|
||||
t.Fatalf("the ClientHello the client resends after the handshake must get the set's strategy, got %v", v)
|
||||
}
|
||||
if got := loggedSet(t, ch); got != set.Name {
|
||||
t.Errorf("the resent ClientHello row: want set %q, got %q", set.Name, got)
|
||||
}
|
||||
pieces := sent()
|
||||
if len(pieces) != 2 {
|
||||
t.Fatalf("the tcp strategy must split the resent ClientHello in two, got %d packets", len(pieces))
|
||||
}
|
||||
for _, p := range pieces {
|
||||
if p[20+13]&0x02 != 0 {
|
||||
t.Error("a piece of the resent ClientHello must not be a SYN")
|
||||
}
|
||||
}
|
||||
if e := w.destState.conns["10.0.0.1:12345->1.2.3.4:443"]; e == nil || e.retransmits != 1 {
|
||||
t.Errorf("the ClientHello the SYN carried never reached the server and must not count as a retransmission, got %+v", e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFastOpenSynThatKeepsItsHelloCountsForStallDetection(t *testing.T) {
|
||||
hello := buildClientHello("i.ytimg.com", 16, 0xAB)
|
||||
ext := makeV6ExtHeader(6, 8)
|
||||
extSyn := makeV6TCPPacketWithExt(hello, 1000, 60, ext)
|
||||
extSyn[IPv6HeaderLen+len(ext)+13] = 0x02
|
||||
|
||||
detecting := newPassiveSet()
|
||||
detecting.TCP.IPBlockDetect.Enabled = true
|
||||
escalating := newPassiveSet()
|
||||
escalating.Escalate.To = "next-set"
|
||||
behindExt := newFragmentingYTImagesSet()
|
||||
behindExt.TCP.IPBlockDetect.Enabled = true
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
set config.SetConfig
|
||||
syn []byte
|
||||
}{
|
||||
{"a set that leaves the hello alone, IP block detection", detecting, fastOpenSyn(IPv4, hello, 1000)},
|
||||
{"a set that leaves the hello alone, escalation", escalating, fastOpenSyn(IPv4, hello, 1000)},
|
||||
{"IPv6 extension headers, IP block detection", behindExt, extSyn},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
set := tc.set
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
if v := w.ProcessPacket(tc.syn); v != engine.VerdictAccept {
|
||||
t.Fatalf("this fast-open SYN must go out with its ClientHello, got %v", v)
|
||||
}
|
||||
if len(w.destState.conns) != 1 {
|
||||
t.Fatalf("a ClientHello that reaches the server in the SYN must be counted once, got %d tracked connections", len(w.destState.conns))
|
||||
}
|
||||
for key, e := range w.destState.conns {
|
||||
if e.retransmits != 1 {
|
||||
t.Errorf("%s: counted %d times, want 1", key, e.retransmits)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fillInjectionBacklog(t *testing.T) {
|
||||
t.Helper()
|
||||
added := 0
|
||||
for len(injectInflight) < cap(injectInflight) {
|
||||
injectInflight <- struct{}{}
|
||||
added++
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
for i := 0; i < added; i++ {
|
||||
<-injectInflight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAFullInjectionBacklogStillStripsAFastOpenSynAndCountsWhatGoesOutUnmodified(t *testing.T) {
|
||||
set := newFragmentingYTImagesSet()
|
||||
set.TCP.IPBlockDetect.Enabled = true
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
fillInjectionBacklog(t)
|
||||
|
||||
hello := buildClientHello("i.ytimg.com", 16, 0xAB)
|
||||
syn := fastOpenSyn(IPv4, hello, 1000)
|
||||
if v := w.ProcessPacket(syn); v != engine.VerdictDrop {
|
||||
t.Fatalf("a full injection backlog must not let the fast-open SYN out with its ClientHello, got %v", v)
|
||||
}
|
||||
if got := sent(); len(got) != 1 {
|
||||
t.Fatalf("the fast-open SYN must go out as one SYN, got %d packets", len(got))
|
||||
} else {
|
||||
assertSynWithoutPayload(t, got[0], syn)
|
||||
}
|
||||
if n := len(w.destState.conns); n != 0 {
|
||||
t.Errorf("the ClientHello cut off the SYN must not be counted, got %d tracked connections", n)
|
||||
}
|
||||
|
||||
if v := w.ProcessPacket(makeV4TCPPacketFlags(hello, 1001, 0x18)); v != engine.VerdictAccept {
|
||||
t.Fatalf("with the backlog full the resent ClientHello goes out unmodified, got %v", v)
|
||||
}
|
||||
if e := w.destState.conns["10.0.0.1:12345->1.2.3.4:443"]; e == nil || e.retransmits != 1 {
|
||||
t.Errorf("a ClientHello that goes out unmodified must be counted once, got %+v", e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFastOpenSynKeepsItsPayloadWhenTheSetLeavesTheHelloAlone(t *testing.T) {
|
||||
set := newPassiveSet()
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
if v := w.ProcessPacket(fastOpenSyn(IPv4, buildClientHello("i.ytimg.com", 16, 0xAB), 1000)); v != engine.VerdictAccept {
|
||||
t.Errorf("a set that leaves the ClientHello alone must leave the fast-open SYN alone, got %v", v)
|
||||
}
|
||||
if got := sent(); len(got) != 0 {
|
||||
t.Errorf("nothing may be injected for it, got %d packets", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFastOpenSynBehindIPv6ExtensionHeadersGoesOutUnchanged(t *testing.T) {
|
||||
hello := buildClientHello("i.ytimg.com", 16, 0xAB)
|
||||
ext := makeV6ExtHeader(6, 8)
|
||||
set := newFragmentingYTImagesSet()
|
||||
|
||||
control := newDualStackInjectingWorker(t, &set)
|
||||
controlSent := captureInjected(t, control)
|
||||
if v := control.ProcessPacket(makeV6TCPPacketWithExt(hello, 1000, 60, ext)); v != engine.VerdictDrop || len(controlSent()) != 1 {
|
||||
t.Fatalf("the ClientHello as data behind an extension header must reach the injector, got %v", v)
|
||||
}
|
||||
|
||||
syn := makeV6TCPPacketWithExt(hello, 1000, 60, ext)
|
||||
syn[IPv6HeaderLen+len(ext)+13] = 0x02
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
if v := w.ProcessPacket(syn); v != engine.VerdictAccept {
|
||||
t.Errorf("a fast-open SYN whose payload cannot be cut off must go out as the client sent it, got %v", v)
|
||||
}
|
||||
if got := sent(); len(got) != 0 {
|
||||
t.Errorf("nothing may be injected for it, got %d packets", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropSACKTreatsAFastOpenSynLikeABareSyn(t *testing.T) {
|
||||
for _, ver := range []byte{IPv4, IPv6} {
|
||||
set := newStaticAddressSet(true)
|
||||
set.Targets.IpsToMatch = []string{"1.2.3.4", "2001:db8::2"}
|
||||
set.TCP.DropSACK = true
|
||||
|
||||
var out [][]byte
|
||||
for _, payload := range [][]byte{nil, buildClientHello("i.ytimg.com", 16, 0xAB)} {
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
if v := w.ProcessPacket(fastOpenSyn(ver, payload, 1000)); v != engine.VerdictDrop {
|
||||
t.Fatalf("v%d payload %d: a DropSACK set rewrites the SYN, got %v", ver, len(payload), v)
|
||||
}
|
||||
got := sent()
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("v%d payload %d: want one SYN, got %d packets", ver, len(payload), len(got))
|
||||
}
|
||||
out = append(out, got[0])
|
||||
}
|
||||
if bytes.IndexByte(tcpOptionKinds(out[0]), 4) >= 0 {
|
||||
t.Errorf("v%d: DropSACK must take SACK-permitted off a bare SYN", ver)
|
||||
}
|
||||
if !bytes.Equal(out[0], out[1]) {
|
||||
t.Errorf("v%d: a fast-open SYN must leave as the same SYN a bare one does:\n bare %x\n tfo %x", ver, out[0], out[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheSynFakePathSendsAFastOpenSynWithoutItsPayload(t *testing.T) {
|
||||
set := newStaticAddressSet(true)
|
||||
set.TCP.SynFake = true
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
|
||||
syn := fastOpenSyn(IPv4, buildClientHello("i.ytimg.com", 16, 0xAB), 1000)
|
||||
if v := w.ProcessPacket(syn); v != engine.VerdictDrop {
|
||||
t.Fatalf("the SYN fake path takes the SYN over, got %v", v)
|
||||
}
|
||||
got := sent()
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("want the fake SYN and the real one, got %d packets", len(got))
|
||||
}
|
||||
if sock.TCPChecksumValid(got[0]) || len(got[0]) != 20+20+len(fastOpenSynOptions) {
|
||||
t.Error("the fake SYN keeps its broken checksum and carries no ClientHello")
|
||||
}
|
||||
assertSynWithoutPayload(t, got[1], syn)
|
||||
}
|
||||
|
||||
func TestTheMD5FakeSynAlwaysCarriesTheMD5Option(t *testing.T) {
|
||||
roomy := []byte{0x02, 0x04, 0x05, 0xb4, 0x04, 0x02, 0x08, 0x0a, 0, 0, 0, 1, 0, 0, 0, 0, 0x01, 0x03, 0x03, 0x07}
|
||||
tight := []byte{0x02, 0x04, 0x05, 0xb4, 0x01, 0x03, 0x03, 0x06, 0x01, 0x01, 0x08, 0x0a, 0, 0, 0, 1, 0, 0, 0, 0, 0x04, 0x02, 0x00, 0x00}
|
||||
hello := buildClientHello("i.ytimg.com", 16, 0xAB)
|
||||
cases := []struct {
|
||||
name string
|
||||
opts, payload []byte
|
||||
keepsOptions bool
|
||||
}{
|
||||
{"options that leave room", roomy, nil, true},
|
||||
{"options ending in EOL", []byte{0x02, 0x04, 0x05, 0xb4, 0x00, 0x00, 0x00, 0x00}, nil, true},
|
||||
{"24 bytes of options", tight, nil, false},
|
||||
{"fast open", fastOpenSynOptions, hello, false},
|
||||
}
|
||||
for _, ver := range []byte{IPv4, IPv6} {
|
||||
for _, tc := range cases {
|
||||
t.Run(fmt.Sprintf("v%d/%s", ver, tc.name), func(t *testing.T) {
|
||||
set := newStaticAddressSet(true)
|
||||
set.Targets.IpsToMatch = []string{"1.2.3.4", "2001:db8::2"}
|
||||
set.Faking.TCPMD5 = true
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
|
||||
const isn = uint32(50000)
|
||||
syn := synWithOptions(ver, tc.opts, tc.payload, isn)
|
||||
if v := w.ProcessPacket(syn); v != engine.VerdictDrop {
|
||||
t.Fatalf("the MD5 SYN path takes the SYN over, got %v", v)
|
||||
}
|
||||
got := sent()
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("want the MD5 fake and the real SYN, got %d packets", len(got))
|
||||
}
|
||||
fake, real := got[0], got[1]
|
||||
l3, ttl := 20, fake[8]
|
||||
if ver == IPv6 {
|
||||
l3, ttl = IPv6HeaderLen, fake[7]
|
||||
}
|
||||
|
||||
if seq := binary.BigEndian.Uint32(fake[l3+4 : l3+8]); seq != isn-10000 || ttl >= 64 {
|
||||
t.Errorf("the fake SYN: seq %d ttl %d, want seq %d and a low TTL", seq, ttl, isn-10000)
|
||||
}
|
||||
want := []byte{19}
|
||||
if tc.keepsOptions {
|
||||
want = append(tcpOptionKinds(syn), 19)
|
||||
}
|
||||
if kinds := tcpOptionKinds(fake); !bytes.Equal(kinds, want) {
|
||||
t.Errorf("the fake SYN's options %v, want %v", kinds, want)
|
||||
}
|
||||
if len(fake) != l3+int(fake[l3+12]>>4)*4 {
|
||||
t.Error("the fake SYN must not carry the ClientHello")
|
||||
}
|
||||
if ver == IPv4 {
|
||||
if int(binary.BigEndian.Uint16(fake[2:4])) != len(fake) || !ipv4HeaderChecksumValid(fake[:l3]) || !sock.TCPChecksumValid(fake) {
|
||||
t.Error("the fake SYN's length or checksums are wrong")
|
||||
}
|
||||
} else if int(binary.BigEndian.Uint16(fake[4:6])) != len(fake)-IPv6HeaderLen || !sock.TCPChecksumValidV6(fake) {
|
||||
t.Error("the fake SYN's length or checksum is wrong")
|
||||
}
|
||||
|
||||
if len(tc.payload) > 0 {
|
||||
assertSynWithoutPayload(t, real, syn)
|
||||
} else if !bytes.Equal(real, syn) {
|
||||
t.Error("a SYN without payload must go out unchanged")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ func (w *Worker) ManipulateWindowIPv4(cfg *config.SetConfig, packet []byte, dst
|
|||
}
|
||||
|
||||
tcpHdrLen := int((packet[ipHdrLen+12] >> 4) * 4)
|
||||
if len(packet) < ipHdrLen+tcpHdrLen {
|
||||
if tcpHdrLen < TCPHeaderMinLen || len(packet) < ipHdrLen+tcpHdrLen {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -64,14 +64,20 @@ func (w *Worker) ManipulateWindowIPv4(cfg *config.SetConfig, packet []byte, dst
|
|||
}
|
||||
}
|
||||
|
||||
func windowFake(packet []byte, l3Len int) []byte {
|
||||
end := l3Len + int(packet[l3Len+12]>>4)*4
|
||||
fake := make([]byte, end)
|
||||
copy(fake, packet[:end])
|
||||
return fake
|
||||
}
|
||||
|
||||
// sendOscillatingWindows sends fake packets with oscillating window sizes
|
||||
func (w *Worker) sendOscillatingWindows(packet []byte, dst net.IP, ipHdrLen int, wm *WindowManipulator) {
|
||||
log.Tracef("Window manipulation: oscillating mode")
|
||||
|
||||
// Send fake packets with different windows BEFORE real packet
|
||||
for i, winSize := range wm.values {
|
||||
fake := make([]byte, ipHdrLen+20) // Just headers, no payload for fakes
|
||||
copy(fake, packet[:ipHdrLen+20])
|
||||
fake := windowFake(packet, ipHdrLen)
|
||||
|
||||
// Set window size
|
||||
binary.BigEndian.PutUint16(fake[ipHdrLen+14:ipHdrLen+16], uint16(winSize))
|
||||
|
|
@ -143,8 +149,7 @@ func (w *Worker) sendRandomWindows(packet []byte, dst net.IP, ipHdrLen int, wm *
|
|||
numFakes := 3 + r.Intn(3)
|
||||
|
||||
for i := 0; i < numFakes; i++ {
|
||||
fake := make([]byte, ipHdrLen+20)
|
||||
copy(fake, packet[:ipHdrLen+20])
|
||||
fake := windowFake(packet, ipHdrLen)
|
||||
|
||||
// Random window from configured values or fully random
|
||||
var winSize uint16
|
||||
|
|
@ -181,8 +186,7 @@ func (w *Worker) sendEscalatingWindows(packet []byte, dst net.IP, ipHdrLen int)
|
|||
windows := []uint16{0, 100, 500, 1460, 8192, 32768, 65535}
|
||||
|
||||
for i, win := range windows {
|
||||
fake := make([]byte, ipHdrLen+20)
|
||||
copy(fake, packet[:ipHdrLen+20])
|
||||
fake := windowFake(packet, ipHdrLen)
|
||||
|
||||
binary.BigEndian.PutUint16(fake[ipHdrLen+14:ipHdrLen+16], win)
|
||||
|
||||
|
|
@ -220,6 +224,11 @@ func (w *Worker) ManipulateWindowIPv6(cfg *config.SetConfig, packet []byte, dst
|
|||
return
|
||||
}
|
||||
|
||||
tcpHdrLen := int(packet[ipv6HdrLen+12]>>4) * 4
|
||||
if tcpHdrLen < TCPHeaderMinLen || len(packet) < ipv6HdrLen+tcpHdrLen {
|
||||
return
|
||||
}
|
||||
|
||||
wm := NewWindowManipulator(&cfg.TCP)
|
||||
|
||||
switch wm.mode {
|
||||
|
|
@ -240,8 +249,7 @@ func (w *Worker) sendOscillatingWindowsV6(packet []byte, dst net.IP, wm *WindowM
|
|||
ipv6HdrLen := 40
|
||||
|
||||
for i, winSize := range wm.values {
|
||||
fake := make([]byte, ipv6HdrLen+20)
|
||||
copy(fake, packet[:ipv6HdrLen+20])
|
||||
fake := windowFake(packet, ipv6HdrLen)
|
||||
|
||||
// Set window
|
||||
binary.BigEndian.PutUint16(fake[ipv6HdrLen+14:ipv6HdrLen+16], uint16(winSize))
|
||||
|
|
@ -253,7 +261,7 @@ func (w *Worker) sendOscillatingWindowsV6(packet []byte, dst net.IP, wm *WindowM
|
|||
}
|
||||
|
||||
// Update payload length
|
||||
binary.BigEndian.PutUint16(fake[4:6], 20)
|
||||
binary.BigEndian.PutUint16(fake[4:6], uint16(len(fake)-ipv6HdrLen))
|
||||
|
||||
// ACK only
|
||||
fake[ipv6HdrLen+13] = 0x10
|
||||
|
|
@ -295,8 +303,7 @@ func (w *Worker) sendRandomWindowsV6(packet []byte, dst net.IP, wm *WindowManipu
|
|||
numFakes := 3 + r.Intn(3)
|
||||
|
||||
for i := 0; i < numFakes; i++ {
|
||||
fake := make([]byte, ipv6HdrLen+20)
|
||||
copy(fake, packet[:ipv6HdrLen+20])
|
||||
fake := windowFake(packet, ipv6HdrLen)
|
||||
|
||||
var winSize uint16
|
||||
if len(wm.values) > 0 {
|
||||
|
|
@ -311,7 +318,7 @@ func (w *Worker) sendRandomWindowsV6(packet []byte, dst net.IP, wm *WindowManipu
|
|||
fake[7] = 1
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(fake[4:6], 20)
|
||||
binary.BigEndian.PutUint16(fake[4:6], uint16(len(fake)-ipv6HdrLen))
|
||||
|
||||
sock.FixTCPChecksumV6(fake)
|
||||
_ = w.sock.SendIPv6(fake, dst)
|
||||
|
|
@ -325,8 +332,7 @@ func (w *Worker) sendEscalatingWindowsV6(packet []byte, dst net.IP) {
|
|||
windows := []uint16{0, 100, 500, 1460, 8192, 32768, 65535}
|
||||
|
||||
for i, win := range windows {
|
||||
fake := make([]byte, ipv6HdrLen+20)
|
||||
copy(fake, packet[:ipv6HdrLen+20])
|
||||
fake := windowFake(packet, ipv6HdrLen)
|
||||
|
||||
binary.BigEndian.PutUint16(fake[ipv6HdrLen+14:ipv6HdrLen+16], win)
|
||||
fake[7] = uint8(10 - i)
|
||||
|
|
@ -334,7 +340,7 @@ func (w *Worker) sendEscalatingWindowsV6(packet []byte, dst net.IP) {
|
|||
fake[7] = 1
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(fake[4:6], 20)
|
||||
binary.BigEndian.PutUint16(fake[4:6], uint16(len(fake)-ipv6HdrLen))
|
||||
|
||||
sock.FixTCPChecksumV6(fake)
|
||||
_ = w.sock.SendIPv6(fake, dst)
|
||||
|
|
|
|||
80
src/nfq/win_test.go
Normal file
80
src/nfq/win_test.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package nfq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/daniellavrushin/b4/sock"
|
||||
)
|
||||
|
||||
func dataPacketWithOptions(ver byte, opts []byte) []byte {
|
||||
segment := append(append([]byte(nil), opts...), tlsAppData(64)...)
|
||||
doff := byte((20+len(opts))/4) << 4
|
||||
if ver == IPv4 {
|
||||
pkt := makeV4TCPPacket(segment, 5000)
|
||||
pkt[20+12] = doff
|
||||
sock.FixIPv4Checksum(pkt[:20])
|
||||
sock.FixTCPChecksum(pkt)
|
||||
return pkt
|
||||
}
|
||||
pkt := makeV6TCPPacket(segment, 5000)
|
||||
pkt[IPv6HeaderLen+12] = doff
|
||||
sock.FixTCPChecksumV6(pkt)
|
||||
return pkt
|
||||
}
|
||||
|
||||
func TestWindowFakesCarryTheWholeTCPHeader(t *testing.T) {
|
||||
timestamps := []byte{0x01, 0x01, 0x08, 0x0a, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x20, 0x02}
|
||||
for _, mode := range []string{"oscillate", "random", "escalate", "zero"} {
|
||||
for _, ver := range []byte{IPv4, IPv6} {
|
||||
for _, opts := range [][]byte{nil, timestamps} {
|
||||
t.Run(fmt.Sprintf("%s/v%d/options=%d", mode, ver, len(opts)), func(t *testing.T) {
|
||||
set := newFragmentingYTImagesSet()
|
||||
set.TCP.Win.Mode = mode
|
||||
w := newDualStackInjectingWorker(t, &set)
|
||||
sent := captureInjected(t, w)
|
||||
|
||||
pkt := dataPacketWithOptions(ver, opts)
|
||||
l3 := 20
|
||||
if ver == IPv4 {
|
||||
w.ManipulateWindowIPv4(&set, pkt, net.IP{1, 2, 3, 4})
|
||||
} else {
|
||||
l3 = IPv6HeaderLen
|
||||
w.ManipulateWindowIPv6(&set, pkt, net.ParseIP("2001:db8::2"))
|
||||
}
|
||||
tcpHL := 20 + len(opts)
|
||||
want := l3 + tcpHL
|
||||
if mode == "zero" {
|
||||
want = len(pkt)
|
||||
}
|
||||
|
||||
fakes := sent()
|
||||
if len(fakes) == 0 {
|
||||
t.Fatal("window manipulation sent nothing")
|
||||
}
|
||||
for i, f := range fakes {
|
||||
if len(f) != want {
|
||||
t.Fatalf("fake %d: %d bytes, want %d", i, len(f), want)
|
||||
}
|
||||
if hl := int(f[l3+12]>>4) * 4; hl != tcpHL {
|
||||
t.Fatalf("fake %d: TCP header length %d, want %d", i, hl, tcpHL)
|
||||
}
|
||||
if !bytes.Equal(f[l3+20:l3+tcpHL], pkt[l3+20:l3+tcpHL]) {
|
||||
t.Errorf("fake %d: the TCP options must be the original's", i)
|
||||
}
|
||||
if ver == IPv4 {
|
||||
if n := int(binary.BigEndian.Uint16(f[2:4])); n != len(f) || !ipv4HeaderChecksumValid(f[:l3]) || !sock.TCPChecksumValid(f) {
|
||||
t.Errorf("fake %d: total length %d of %d bytes, or a bad checksum", i, n, len(f))
|
||||
}
|
||||
} else if n := int(binary.BigEndian.Uint16(f[4:6])); n != len(f)-IPv6HeaderLen || !sock.TCPChecksumValidV6(f) {
|
||||
t.Errorf("fake %d: payload length %d of %d bytes, or a bad checksum", i, n, len(f))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -255,7 +255,10 @@ func TestBadChecksumSurvivesTheMD5Option(t *testing.T) {
|
|||
if !badsum {
|
||||
t.Fatal("tcp_check must produce a bad checksum")
|
||||
}
|
||||
signed := AddTCPMD5Option(fake, false)
|
||||
signed, ok := AddTCPMD5Option(fake, false)
|
||||
if !ok {
|
||||
t.Fatal("a 20-byte TCP header has room for the MD5 option")
|
||||
}
|
||||
if !TCPChecksumValid(signed) {
|
||||
t.Fatal("AddTCPMD5Option is expected to recompute the checksum; the caller must corrupt it again")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ func NewSender(mark int) (*Sender, error) {
|
|||
return NewSenderWithMark(mark)
|
||||
}
|
||||
|
||||
func NewSenderWithFDs(fd4, fd6 int) *Sender {
|
||||
return &Sender{fd4: fd4, fd6: fd6}
|
||||
}
|
||||
|
||||
func (s *Sender) SendIPv4(packet []byte, destIP net.IP) error {
|
||||
if log.Level(log.CurLevel.Load()) >= log.LevelTrace {
|
||||
log.Tracef("Sending IPv4 packet to %s, len=%d", destIP.String(), len(packet))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"encoding/binary"
|
||||
)
|
||||
|
||||
func AddTCPMD5Option(packet []byte, isIPv6 bool) []byte {
|
||||
func AddTCPMD5Option(packet []byte, isIPv6 bool) ([]byte, bool) {
|
||||
var ipHdrLen int
|
||||
if isIPv6 {
|
||||
ipHdrLen = 40
|
||||
|
|
@ -14,11 +14,14 @@ func AddTCPMD5Option(packet []byte, isIPv6 bool) []byte {
|
|||
}
|
||||
|
||||
if len(packet) < ipHdrLen+20 {
|
||||
return packet
|
||||
return packet, false
|
||||
}
|
||||
|
||||
tcpHdrLen := int((packet[ipHdrLen+12] >> 4) * 4)
|
||||
payloadStart := ipHdrLen + tcpHdrLen
|
||||
if tcpHdrLen < 20 || len(packet) < payloadStart {
|
||||
return packet, false
|
||||
}
|
||||
|
||||
md5Opt := make([]byte, 20)
|
||||
md5Opt[0] = 1
|
||||
|
|
@ -29,7 +32,7 @@ func AddTCPMD5Option(packet []byte, isIPv6 bool) []byte {
|
|||
|
||||
newTCPHdrLen := tcpHdrLen + 20
|
||||
if newTCPHdrLen > 60 {
|
||||
return packet
|
||||
return packet, false
|
||||
}
|
||||
|
||||
newPacket := make([]byte, len(packet)+20)
|
||||
|
|
@ -39,6 +42,9 @@ func AddTCPMD5Option(packet []byte, isIPv6 bool) []byte {
|
|||
|
||||
if tcpHdrLen > 20 {
|
||||
copy(newPacket[ipHdrLen+20:ipHdrLen+tcpHdrLen], packet[ipHdrLen+20:ipHdrLen+tcpHdrLen])
|
||||
if !prepareOptionsForAppend(newPacket[ipHdrLen+20 : ipHdrLen+tcpHdrLen]) {
|
||||
return packet, false
|
||||
}
|
||||
}
|
||||
|
||||
copy(newPacket[ipHdrLen+tcpHdrLen:ipHdrLen+tcpHdrLen+20], md5Opt)
|
||||
|
|
@ -58,5 +64,25 @@ func AddTCPMD5Option(packet []byte, isIPv6 bool) []byte {
|
|||
FixTCPChecksum(newPacket)
|
||||
}
|
||||
|
||||
return newPacket
|
||||
return newPacket, true
|
||||
}
|
||||
|
||||
func prepareOptionsForAppend(opts []byte) bool {
|
||||
for i := 0; i < len(opts); {
|
||||
switch opts[i] {
|
||||
case 0:
|
||||
for j := i; j < len(opts); j++ {
|
||||
opts[j] = 1
|
||||
}
|
||||
return true
|
||||
case 1:
|
||||
i++
|
||||
default:
|
||||
if i+1 >= len(opts) || opts[i+1] < 2 || i+int(opts[i+1]) > len(opts) {
|
||||
return false
|
||||
}
|
||||
i += int(opts[i+1])
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
136
src/sock/tcp_md5_test.go
Normal file
136
src/sock/tcp_md5_test.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package sock
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func withTCPOptions(pkt []byte, l3 int, opts []byte) []byte {
|
||||
out := append(append(append([]byte(nil), pkt[:l3+20]...), opts...), pkt[l3+20:]...)
|
||||
out[l3+12] = byte((20+len(opts))/4) << 4
|
||||
if l3 == 40 {
|
||||
binary.BigEndian.PutUint16(out[4:6], uint16(len(out)-40))
|
||||
FixTCPChecksumV6(out)
|
||||
} else {
|
||||
binary.BigEndian.PutUint16(out[2:4], uint16(len(out)))
|
||||
FixIPv4Checksum(out[:20])
|
||||
FixTCPChecksum(out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestAddTCPMD5OptionAddsTheOptionOrSaysItCannot(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
v6 bool
|
||||
opts int
|
||||
fits bool
|
||||
}{
|
||||
{"v4 without options", false, 0, true},
|
||||
{"v4 with 20 bytes of options", false, 20, true},
|
||||
{"v4 with 24 bytes of options", false, 24, false},
|
||||
{"v6 without options", true, 0, true},
|
||||
{"v6 with 24 bytes of options", true, 24, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
base, l3 := buildMinimalIPv4TCPPacket(30), 20
|
||||
if tc.v6 {
|
||||
base, l3 = buildMinimalIPv6TCPPacket(30), 40
|
||||
}
|
||||
pkt := withTCPOptions(base, l3, bytes.Repeat([]byte{0x01}, tc.opts))
|
||||
got, ok := AddTCPMD5Option(pkt, tc.v6)
|
||||
if ok != tc.fits {
|
||||
t.Fatalf("ok=%v, want %v", ok, tc.fits)
|
||||
}
|
||||
if !tc.fits {
|
||||
if !bytes.Equal(got, pkt) {
|
||||
t.Error("a packet without room for the option must come back unchanged")
|
||||
}
|
||||
return
|
||||
}
|
||||
hdr := l3 + 20 + tc.opts
|
||||
if len(got) != len(pkt)+20 || int(got[l3+12]>>4)*4 != 20+tc.opts+20 {
|
||||
t.Fatalf("want 20 more option bytes, got %d bytes with a %d-byte TCP header", len(got), int(got[l3+12]>>4)*4)
|
||||
}
|
||||
if !bytes.Equal(got[hdr:hdr+4], []byte{1, 1, 19, 18}) {
|
||||
t.Errorf("the MD5 option is missing: %x", got[hdr:hdr+4])
|
||||
}
|
||||
if !bytes.Equal(got[hdr+20:], pkt[hdr:]) {
|
||||
t.Error("the payload must follow the option unchanged")
|
||||
}
|
||||
if tc.v6 {
|
||||
if int(binary.BigEndian.Uint16(got[4:6])) != len(got)-40 || !TCPChecksumValidV6(got) {
|
||||
t.Error("payload length or checksum is wrong")
|
||||
}
|
||||
} else if int(binary.BigEndian.Uint16(got[2:4])) != len(got) || !TCPChecksumValid(got) {
|
||||
t.Error("total length or checksum is wrong")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func optionKindsAReceiverReads(pkt []byte, l3 int) []byte {
|
||||
opts := pkt[l3+20 : l3+int(pkt[l3+12]>>4)*4]
|
||||
var kinds []byte
|
||||
for i := 0; i < len(opts) && opts[i] != 0; {
|
||||
if opts[i] == 1 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
kinds = append(kinds, opts[i])
|
||||
if i+1 >= len(opts) || opts[i+1] < 2 {
|
||||
break
|
||||
}
|
||||
i += int(opts[i+1])
|
||||
}
|
||||
return kinds
|
||||
}
|
||||
|
||||
func TestAddTCPMD5OptionStaysReadableAfterAnEndOfOptionList(t *testing.T) {
|
||||
for _, v6 := range []bool{false, true} {
|
||||
base, l3 := buildMinimalIPv4TCPPacket(30), 20
|
||||
if v6 {
|
||||
base, l3 = buildMinimalIPv6TCPPacket(30), 40
|
||||
}
|
||||
pkt := withTCPOptions(base, l3, []byte{0x02, 0x04, 0x05, 0xb4, 0x00, 0x00, 0x00, 0x00})
|
||||
got, ok := AddTCPMD5Option(pkt, v6)
|
||||
if !ok {
|
||||
t.Fatalf("v6=%v: an 8-byte option list has room for the MD5 option", v6)
|
||||
}
|
||||
if kinds := optionKindsAReceiverReads(got, l3); !bytes.Equal(kinds, []byte{2, 19}) {
|
||||
t.Errorf("v6=%v: a receiver reads options %v, want MSS and MD5", v6, kinds)
|
||||
}
|
||||
if !bytes.Equal(got[l3+20:l3+24], pkt[l3+20:l3+24]) || !bytes.Equal(got[l3+48:], pkt[l3+28:]) {
|
||||
t.Errorf("v6=%v: the MSS option and the payload must be kept", v6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTCPMD5OptionRefusesMalformedOptions(t *testing.T) {
|
||||
for _, opts := range [][]byte{{0x02, 0x00, 0x01, 0x01}, {0x02, 0x08, 0x05, 0xb4}} {
|
||||
pkt := withTCPOptions(buildMinimalIPv4TCPPacket(30), 20, opts)
|
||||
if got, ok := AddTCPMD5Option(pkt, false); ok || !bytes.Equal(got, pkt) {
|
||||
t.Errorf("options %x: a malformed option length must be refused and the packet returned unchanged", opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTCPMD5OptionRefusesAHeaderLongerThanThePacket(t *testing.T) {
|
||||
pkt := buildMinimalIPv4TCPPacket(0)
|
||||
pkt[20+12] = 0xa0
|
||||
if _, ok := AddTCPMD5Option(pkt, false); ok {
|
||||
t.Error("a TCP header that claims more bytes than the packet has must be refused")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTCPMD5OptionRefusesAHeaderShorterThan20Bytes(t *testing.T) {
|
||||
for words := byte(0); words < 5; words++ {
|
||||
pkt := buildMinimalIPv4TCPPacket(64)
|
||||
pkt[20+12] = words << 4
|
||||
if got, ok := AddTCPMD5Option(pkt, false); ok || !bytes.Equal(got, pkt) {
|
||||
t.Errorf("a %d-byte TCP header must be refused and returned unchanged", int(words)*4)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue