use crate::noise::{CAPABILITY_VERSION, Error};
pub const UPGRADE_TOKEN: &str = "tailscale-control-protocol";
pub const HANDSHAKE_HEADER_LEN: usize = 136;
pub const MAX_REQUEST: usize = 512;
pub fn write_key_request<'o>(host: &str, out: &'o mut [u8]) -> Result<&'o [u8], Error> {
let mut w = Writer::new(out);
w.str("GET /key?v=")?;
w.u16(CAPABILITY_VERSION)?;
w.str(" HTTP/1.1\r\nHost: ")?;
w.str(host)?;
w.str("\r\nConnection: close\r\n\r\n")?;
Ok(w.finish())
}
pub fn write_upgrade_request<'o>(
host: &str,
initiation: &[u8],
out: &'o mut [u8],
) -> Result<&'o [u8], Error> {
let mut w = Writer::new(out);
w.str("POST /ts2021 HTTP/1.1\r\nHost: ")?;
w.str(host)?;
w.str("\r\nUser-Agent: tailfeather\r\nContent-Length: 0\r\nConnection: upgrade\r\nUpgrade: ")?;
w.str(UPGRADE_TOKEN)?;
w.str("\r\nX-Tailscale-Handshake: ")?;
w.base64(initiation)?;
w.str("\r\n\r\n")?;
Ok(w.finish())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Response {
pub status: u16,
pub header_len: usize,
}
pub fn parse_response(bytes: &[u8]) -> Result<Response, Error> {
if bytes.len() < 12 {
return Err(Error::Incomplete);
}
if !bytes.starts_with(b"HTTP/1.1 ") && !bytes.starts_with(b"HTTP/1.0 ") {
return Err(Error::Malformed);
}
let status = ascii_u16(&bytes[9..12]).ok_or(Error::Malformed)?;
let header_len = find_blank_line(bytes).ok_or(Error::Incomplete)?;
Ok(Response { status, header_len })
}
pub fn is_upgrade(bytes: &[u8], response: Response) -> bool {
response.status == 101 && contains_ignoring_case(&bytes[..response.header_len], UPGRADE_TOKEN)
}
fn find_blank_line(bytes: &[u8]) -> Option<usize> {
bytes
.windows(4)
.position(|w| w == b"\r\n\r\n")
.map(|i| i + 4)
}
fn ascii_u16(digits: &[u8]) -> Option<u16> {
let mut value = 0u16;
for &c in digits {
if !c.is_ascii_digit() {
return None;
}
value = value.checked_mul(10)?.checked_add((c - b'0') as u16)?;
}
Some(value)
}
fn contains_ignoring_case(haystack: &[u8], needle: &str) -> bool {
let needle = needle.as_bytes();
haystack
.windows(needle.len())
.any(|w| w.iter().zip(needle).all(|(a, b)| a.eq_ignore_ascii_case(b)))
}
struct Writer<'o> {
out: &'o mut [u8],
len: usize,
}
impl<'o> Writer<'o> {
fn new(out: &'o mut [u8]) -> Self {
Self { out, len: 0 }
}
fn bytes(&mut self, bytes: &[u8]) -> Result<(), Error> {
let end = self.len + bytes.len();
if end > self.out.len() {
return Err(Error::BufferTooSmall);
}
self.out[self.len..end].copy_from_slice(bytes);
self.len = end;
Ok(())
}
fn str(&mut self, text: &str) -> Result<(), Error> {
self.bytes(text.as_bytes())
}
fn u16(&mut self, mut value: u16) -> Result<(), Error> {
let mut digits = [0u8; 5];
let mut count = 0;
loop {
digits[count] = b'0' + (value % 10) as u8;
count += 1;
value /= 10;
if value == 0 {
break;
}
}
for i in 0..count {
self.bytes(&[digits[count - 1 - i]])?;
}
Ok(())
}
fn base64(&mut self, input: &[u8]) -> Result<(), Error> {
const ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for chunk in input.chunks(3) {
let b = [
chunk[0],
*chunk.get(1).unwrap_or(&0),
*chunk.get(2).unwrap_or(&0),
];
let n = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
let mut group = [
ALPHABET[(n >> 18) as usize & 63],
ALPHABET[(n >> 12) as usize & 63],
ALPHABET[(n >> 6) as usize & 63],
ALPHABET[n as usize & 63],
];
if chunk.len() < 3 {
group[3] = b'=';
}
if chunk.len() < 2 {
group[2] = b'=';
}
self.bytes(&group)?;
}
Ok(())
}
fn finish(self) -> &'o [u8] {
&self.out[..self.len]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_key_request_matches_the_captured_one() {
let mut out = [0u8; MAX_REQUEST];
let request = write_key_request("127.0.0.1:8080", &mut out).unwrap();
let text = core::str::from_utf8(request).unwrap();
assert!(text.starts_with("GET /key?v=131 HTTP/1.1\r\n"));
assert!(text.contains("Host: 127.0.0.1:8080\r\n"));
assert!(text.ends_with("\r\n\r\n"));
}
#[test]
fn the_upgrade_request_carries_every_header_the_capture_shows() {
let mut out = [0u8; MAX_REQUEST];
let request = write_upgrade_request("127.0.0.1:8080", &[0x41; 101], &mut out).unwrap();
let text = core::str::from_utf8(request).unwrap();
assert!(text.starts_with("POST /ts2021 HTTP/1.1\r\n"));
assert!(text.contains("Content-Length: 0\r\n"));
assert!(text.contains("Connection: upgrade\r\n"));
assert!(text.contains("Upgrade: tailscale-control-protocol\r\n"));
assert!(text.contains("X-Tailscale-Handshake: "));
assert!(text.ends_with("\r\n\r\n"));
}
#[test]
fn base64_matches_rfc_4648s_vectors_including_padding() {
const VECTORS: &[(&str, &str)] = &[
("", ""),
("f", "Zg=="),
("fo", "Zm8="),
("foo", "Zm9v"),
("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy"),
];
for (input, expected) in VECTORS {
let mut out = [0u8; MAX_REQUEST];
let mut w = Writer::new(&mut out);
w.base64(input.as_bytes()).unwrap();
assert_eq!(
core::str::from_utf8(w.finish()).unwrap(),
*expected,
"base64({input:?})"
);
}
let mut out = [0u8; MAX_REQUEST];
let mut w = Writer::new(&mut out);
w.base64(&[0xfb, 0xff, 0xbf]).unwrap();
assert_eq!(core::str::from_utf8(w.finish()).unwrap(), "+/+/");
}
#[test]
fn an_initiation_sized_input_encodes_to_the_declared_header_length() {
let mut out = [0u8; MAX_REQUEST];
let mut w = Writer::new(&mut out);
w.base64(&[0x41; 101]).unwrap();
let encoded = w.finish();
assert_eq!(encoded.len(), HANDSHAKE_HEADER_LEN);
assert!(encoded.ends_with(b"="));
assert!(!encoded.ends_with(b"=="));
}
#[test]
fn a_101_naming_our_protocol_is_recognised_and_the_body_offset_is_exact() {
let response = b"HTTP/1.1 101 Switching Protocols\r\nConnection: upgrade\r\nUpgrade: tailscale-control-protocol\r\nX-Content-Type-Options: nosniff\r\n\r\n\x02\x000W";
let parsed = parse_response(response).unwrap();
assert_eq!(parsed.status, 101);
assert!(is_upgrade(response, parsed));
assert_eq!(&response[parsed.header_len..], b"\x02\x000W");
}
#[test]
fn an_incomplete_response_asks_for_more_rather_than_guessing() {
assert_eq!(parse_response(b"HTTP/1.1"), Err(Error::Incomplete));
assert_eq!(
parse_response(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: x\r\n"),
Err(Error::Incomplete)
);
assert_eq!(parse_response(b"not http at all"), Err(Error::Malformed));
}
#[test]
fn anything_but_a_101_upgrade_is_not_an_upgrade() {
let refused = b"HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n\r\n";
let parsed = parse_response(refused).unwrap();
assert_eq!(parsed.status, 400);
assert!(!is_upgrade(refused, parsed));
let other = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n";
let parsed = parse_response(other).unwrap();
assert!(!is_upgrade(other, parsed));
}
#[test]
fn a_buffer_too_small_for_the_request_is_an_error_not_a_truncation() {
let mut tiny = [0u8; 32];
assert_eq!(
write_upgrade_request("127.0.0.1:8080", &[0x41; 101], &mut tiny),
Err(Error::BufferTooSmall)
);
}
}