diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 674fbe5..1380285 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -55,7 +55,14 @@ case "${DEBUG:-0}" in esac if [ "$role" = exit-node ]; then - echo "[entrypoint] dropping outbound TCP RSTs inside the container netns" + echo "[entrypoint] dropping outbound TCP RSTs inside the container netns (except our own, fwmark 100)" + # The mark-100 RETURN rule must come first: it exempts RSTs the exit node's + # own raw socket sends on purpose (rawSocketMark in tunnel/rawsocket_linux.go) + # so only the kernel's spurious auto-RSTs (no fwmark, since they're not + # sent through that socket) get dropped below. + if ! iptables -A OUTPUT -p tcp --tcp-flags RST RST -m mark --mark 100 -j RETURN; then + echo "[entrypoint] WARNING: iptables failed (missing NET_ADMIN?); kernel RSTs will kill tunnel connections" >&2 + fi if ! iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP; then echo "[entrypoint] WARNING: iptables failed (missing NET_ADMIN?); kernel RSTs will kill tunnel connections" >&2 fi diff --git a/transport/yandex/yandex.go b/transport/yandex/yandex.go index 4df6f49..a6f2bef 100644 --- a/transport/yandex/yandex.go +++ b/transport/yandex/yandex.go @@ -173,6 +173,26 @@ func (t *YandexDocsTransport) connectToDoc(attempt int) { UserID: userID, } + connectedAt := time.Now() + + // Wait for the server's engine.io OPEN packet ("0{...sid...}") before + // sending anything. Sending our socket.io "40"/"42" packets right + // after the WS upgrade (the old behavior) races the server's own + // handshake packet - observed empirically as the server closing with + // 1005 within ~50-100ms of accepting the connection, right after it + // emits its "0{...}" packet, because the client wrote to the + // namespace before the handshake it announces was actually open. + _, first, err := conn.ReadMessage() + if err != nil { + utils.Debugf("[YDOCS] Read error waiting for engine.io open: %v", err) + conn.Close() + t.scheduleReconnect(attempt) + return + } + if len(first) == 0 || first[0] != '0' { + utils.Debugf("[YDOCS] unexpected first message (wanted engine.io open \"0...\"): %s", string(first)) + } + t.Mu.Lock() t.session = session t.SetConnected(true) @@ -195,7 +215,6 @@ func (t *YandexDocsTransport) connectToDoc(attempt int) { messagePart, _ := json.Marshal([]interface{}{"message", authData}) session.safeWrite(websocket.TextMessage, []byte(fmt.Sprintf("42%s", string(messagePart)))) - connectedAt := time.Now() for t.IsRunning() { _, message, err := conn.ReadMessage() if err != nil { diff --git a/tunnel/rawsocket_linux.go b/tunnel/rawsocket_linux.go index cadff71..5130b0b 100644 --- a/tunnel/rawsocket_linux.go +++ b/tunnel/rawsocket_linux.go @@ -10,6 +10,7 @@ import ( "syscall" "time" + "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/stack" @@ -18,6 +19,15 @@ import ( "universal-bypass-tool/utils" ) +// rawSocketMark tags every packet WritePackets sends through sendFd (SYNs, +// data, and our own legitimate RSTs) so entrypoint.sh's OUTPUT DROP rule for +// RST packets can exempt them by fwmark. Without this, that rule can't tell +// our raw socket's own RSTs apart from the kernel's spurious auto-RSTs +// (fired because this netstack's sockets have no kernel-side counterpart) +// and drops both — which surfaces here as Sendto returning EPERM. Must match +// the mark used in docker/entrypoint.sh. +const rawSocketMark = 100 + type RawSocketEndpoint struct { dispatcher stack.NetworkDispatcher sendFd int @@ -41,6 +51,13 @@ func NewRawSocketEndpoint(nicID tcpip.NICID) (*RawSocketEndpoint, error) { return nil, fmt.Errorf("IP_HDRINCL: %v", err) } + if err := unix.SetsockoptInt(sendFd, unix.SOL_SOCKET, unix.SO_MARK, rawSocketMark); err != nil { + // Non-fatal: without the mark, entrypoint.sh's RST-drop rule can't + // exempt our own RSTs and we're back to the original EPERM bug, but + // the tunnel itself still works. + utils.Debugf("[RAW-NIC%d] SO_MARK failed (need NET_ADMIN?): %v", nicID, err) + } + recvFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_TCP) if err != nil { syscall.Close(sendFd)