/24 IPv4와 /44 IPv6 대역을 갖고 있지만 대부분의 주소는 쓰지 않고 있다. 그래서 연결마다 주소 풀에서 출발지 주소를 무작위로 고르는 HTTP 프록시 fluxy를 만들었다.
Loading...
Loading...
Loading...
Loading...
Loading...
내 ASN을 얻은 뒤로 줄곧 /44 IPv6 대역을 갖고 있었고, 나중에는 /24 IPv4 대역도 생겼다. 이 주소들은 BIRD로 광고하고 있어서 전 세계에서 내 쪽으로 라우팅할 수 있다. 하지만 평소에 쓰는 건 서버에 설정한 몇 개뿐이고, 나머지 대부분은 놀고 있었다.
/24에는 IPv4 주소가 256개 있고, /44에는 IPv6 주소가 2^{84}개 있다. 후자는 사실상 다 쓸 수 없을 만큼 큰 수다. 한편으로는 요청마다 서로 다른 주소로 나가게 할 주소 풀이 필요할 때도 있었다. 그래서 fluxy를 만들었다. 연결할 때마다 내 주소 풀에서 출발지 주소를 무작위로 고르는 HTTP 프록시다.
준비
fluxy는 연결을 시작하기 전에 소켓을 주소 풀의 주소 하나에 바인딩하는 방식으로 동작한다.
그러려면 먼저 이 주소들로 돌아오는 경로가 있어야 한다. 즉, BIRD로 프리픽스를 광고해 두어야 한다. 그래야 해당 주소로 보낸 패킷에 대한 응답이 내 쪽으로 돌아올 수 있다. 광고 설정은 앞서 소개한 대로 protocol static에 프리픽스를 적고 BGP 세션에서 내보내면 된다.
또한 로컬 시스템이 이 주소들로 돌아오는 응답을 받고, 소켓도 해당 주소에 바인딩할 수 있게 해야 한다. 그렇다고 수백 개, 심지어 2^{84}개의 주소를 네트워크 인터페이스에 하나씩 설정할 수는 없다. 먼저 lo에 local 타입의 경로 두 개를 추가해 프리픽스 전체로 향하는 패킷을 로컬 시스템이 처리하게 한다.
BASH
ip -4 route add local 44.31.223.0/24 dev lo
ip -6 route add local 2a0e:aa07:e0a0::/44 dev lo
이 두 경로는 응답을 받는 역할을 하지만, IPv4와 IPv6는 바인딩 검사 방식에 조금 차이가 있다. IPv4는 이 local 경로에 포함된 주소에 바로 바인딩할 수 있다. 반면 IPv6는 인터페이스에 개별적으로 설정하지 않은 주소에 대한 바인딩도 허용해야 한다.
BASH
sysctl -w net.ipv6.ip_nonlocal_bind=1
이 설정은 현재 실행 중인 시스템에만 적용된다. 재부팅 후에도 유지하려면 /etc/sysctl.d/ 아래의 설정 파일에 net.ipv6.ip_nonlocal_bind=1을 적어 두면 된다.
IPv4는 ip addr add 44.31.223.0/24 dev lo만으로도 프리픽스 전체가 로컬 경로가 된다. 이는 루프백 인터페이스에만 적용되는 특수 처리다. 이 주소를 dummy 인터페이스에 두고 싶다면 프리픽스 전체에 대한 local 경로를 명시적으로 설정해야 하며, ip addr add만으로는 충분하지 않다.
이렇게 하면 전체 경로가 이어진다.
보낼 때는 출발지 주소만 풀 안의 주소 하나로 바뀌고, 패킷은 여전히 기본 경로를 따라 업스트림으로 나간다. 돌아올 때는 내가 광고하는 프리픽스로 향하는 응답이므로 BGP 경로를 따라 이 서버로 돌아와 local 경로를 통해 해당 소켓에 전달된다.
무작위로 주소 고르기
무작위 주소를 만드는 방식은 간단하다. 프리픽스의 네트워크 비트는 유지하고, 호스트 비트는 난수로 만든 다음 둘을 비트 OR 연산으로 합친다. IPv6 주소는 u128로 표현할 수 있으니 정수로 계산하면 된다.
Rust
fn get_rand_ipv6(&self) -> IpAddr {
if let Some((ipv6, prefix_len)) = self.config.ipv6 {
let mut rng = rand::thread_rng();
let rand: u128 = rng.gen();
let net_part = (ipv6 >> (128 - prefix_len)) << (128 - prefix_len);
let host_part = (rand << prefix_len) >> prefix_len;
let ipv6 = net_part | host_part;
IpAddr::V6(Ipv6Addr::from(ipv6))
} else {
panic!("IPv6 subnet not configured")
}
}
먼저 오른쪽으로 시프트한 뒤 왼쪽으로 시프트해서 프리픽스의 호스트 비트를 0으로 만들면 네트워크 부분을 얻는다. 난수에는 반대로 왼쪽 시프트 후 오른쪽 시프트를 적용해 상위 비트를 0으로 만들고 호스트 부분만 남긴다. 마지막으로 두 부분을 합치면 프리픽스 안의 무작위 주소가 된다. /44의 호스트 부분은 84비트라 주소가 중복될 가능성은 거의 없다. 사실상 연결마다 고유한 출발지 주소를 갖는 셈이다. IPv4도 같은 방식이며 u128 대신 u32를 쓰면 된다.
HTTP 프록시
프록시 부분은 tokio 위에서 hyper로 작성했다. HTTP 프록시 요청은 두 종류라 처리 방식도 둘로 나뉜다.
하나는 CONNECT다. HTTPS 사이트에 접속할 때 클라이언트는 먼저 CONNECT 요청을 보내 프록시가 목적지의 443 포트에 연결하게 한다. 연결이 이루어지면 프록시는 양쪽의 데이터를 옮기기만 하면 된다. 그래서 fluxy는 CONNECT를 받으면 새 소켓을 만들고 무작위 주소에 바인딩한 뒤 목적지에 연결하고, 클라이언트와 목적지 사이의 트래픽을 이어 준다.
Rust
let socket = match addr {
SocketAddr::V4(_) => TcpSocket::new_v4()?,
SocketAddr::V6(_) => TcpSocket::new_v6()?,
};
let bind_addr = self.get_rand_socket_addr();
if socket.bind(bind_addr).is_ok() {
println!("{addr_str} via {bind_addr}");
if let Ok(mut server) = socket.connect(addr).await {
tokio::io::copy_bidirectional(upgraded, &mut server).await?;
return Ok(());
}
}
다른 하나는 일반 HTTP 요청이다. 프록시가 클라이언트 대신 요청을 보내고 응답을 그대로 돌려줘야 한다. 여기서는 hyper의 클라이언트를 그대로 사용했다. HttpConnector에 로컬 주소를 지정할 수 있으므로 요청마다 새 connector를 만들고 무작위 주소를 넣으면 된다.
Rust
let bind_addr = self.get_rand_ip();
let mut http = HttpConnector::new();
http.set_local_address(Some(bind_addr));
let client = Client::builder()
.http1_title_case_headers(true)
.http1_preserve_header_case(true)
.build(http);
let res = client.request(req).await?;
각 연결에 사용한 주소는 example.com:443 via [2a0e:aa07:e0a0:...]:...처럼 한 줄씩 출력된다. 이 출력으로 주소가 바뀌는지, 어떤 주소를 쓰고 있는지 확인할 수 있다.
-b는 수신 주소로, 생략하면 0.0.0.0:51080을 사용한다. -4와 -6은 각각 IPv4와 IPv6 주소 풀이며 둘 다 선택 사항이라 하나만 설정해도 동작한다. 주소 풀에 프리픽스 전체를 줄 필요도 없다. 이 예시처럼 /44 중 /48 하나만 지정하면 fluxy는 그 /48 안에서만 주소를 무작위로 고른다.
이제 curl로 프록시를 거쳐 IP 확인 사이트에 접속하면 된다. 몇 번 연속 요청해 보면 반환되는 주소가 매번 바뀌는 걸 볼 수 있다.
내 ASN을 얻은 뒤로 줄곧 /44 IPv6 대역을 갖고 있었고, 나중에는 /24 IPv4 대역도 생겼다. 이 주소들은 BIRD로 광고하고 있어서 전 세계에서 내 쪽으로 라우팅할 수 있다. 하지만 평소에 쓰는 건 서버에 설정한 몇 개뿐이고, 나머지 대부분은 놀고 있었다.
/24에는 IPv4 주소가 256개 있고, /44에는 IPv6 주소가
2^{84}개 있다. 후자는 사실상 다 쓸 수 없을 만큼 큰 수다. 한편으로는 요청마다 서로 다른 주소로 나가게 할 주소 풀이 필요할 때도 있었다. 그래서 fluxy를 만들었다. 연결할 때마다 내 주소 풀에서 출발지 주소를 무작위로 고르는 HTTP 프록시다.준비
fluxy는 연결을 시작하기 전에 소켓을 주소 풀의 주소 하나에 바인딩하는 방식으로 동작한다.
그러려면 먼저 이 주소들로 돌아오는 경로가 있어야 한다. 즉, BIRD로 프리픽스를 광고해 두어야 한다. 그래야 해당 주소로 보낸 패킷에 대한 응답이 내 쪽으로 돌아올 수 있다. 광고 설정은 앞서 소개한 대로 protocol static에 프리픽스를 적고 BGP 세션에서 내보내면 된다.
또한 로컬 시스템이 이 주소들로 돌아오는 응답을 받고, 소켓도 해당 주소에 바인딩할 수 있게 해야 한다. 그렇다고 수백 개, 심지어
2^{84}개의 주소를 네트워크 인터페이스에 하나씩 설정할 수는 없다. 먼저 lo에 local 타입의 경로 두 개를 추가해 프리픽스 전체로 향하는 패킷을 로컬 시스템이 처리하게 한다.이 두 경로는 응답을 받는 역할을 하지만, IPv4와 IPv6는 바인딩 검사 방식에 조금 차이가 있다. IPv4는 이 local 경로에 포함된 주소에 바로 바인딩할 수 있다. 반면 IPv6는 인터페이스에 개별적으로 설정하지 않은 주소에 대한 바인딩도 허용해야 한다.
이 설정은 현재 실행 중인 시스템에만 적용된다. 재부팅 후에도 유지하려면 /etc/sysctl.d/ 아래의 설정 파일에 net.ipv6.ip_nonlocal_bind=1을 적어 두면 된다.
IPv4는 ip addr add 44.31.223.0/24 dev lo만으로도 프리픽스 전체가 로컬 경로가 된다. 이는 루프백 인터페이스에만 적용되는 특수 처리다. 이 주소를 dummy 인터페이스에 두고 싶다면 프리픽스 전체에 대한 local 경로를 명시적으로 설정해야 하며, ip addr add만으로는 충분하지 않다.
이렇게 하면 전체 경로가 이어진다.
보낼 때는 출발지 주소만 풀 안의 주소 하나로 바뀌고, 패킷은 여전히 기본 경로를 따라 업스트림으로 나간다. 돌아올 때는 내가 광고하는 프리픽스로 향하는 응답이므로 BGP 경로를 따라 이 서버로 돌아와 local 경로를 통해 해당 소켓에 전달된다.
무작위로 주소 고르기
무작위 주소를 만드는 방식은 간단하다. 프리픽스의 네트워크 비트는 유지하고, 호스트 비트는 난수로 만든 다음 둘을 비트 OR 연산으로 합친다. IPv6 주소는 u128로 표현할 수 있으니 정수로 계산하면 된다.
먼저 오른쪽으로 시프트한 뒤 왼쪽으로 시프트해서 프리픽스의 호스트 비트를 0으로 만들면 네트워크 부분을 얻는다. 난수에는 반대로 왼쪽 시프트 후 오른쪽 시프트를 적용해 상위 비트를 0으로 만들고 호스트 부분만 남긴다. 마지막으로 두 부분을 합치면 프리픽스 안의 무작위 주소가 된다. /44의 호스트 부분은 84비트라 주소가 중복될 가능성은 거의 없다. 사실상 연결마다 고유한 출발지 주소를 갖는 셈이다. IPv4도 같은 방식이며 u128 대신 u32를 쓰면 된다.
HTTP 프록시
프록시 부분은 tokio 위에서 hyper로 작성했다. HTTP 프록시 요청은 두 종류라 처리 방식도 둘로 나뉜다.
하나는 CONNECT다. HTTPS 사이트에 접속할 때 클라이언트는 먼저 CONNECT 요청을 보내 프록시가 목적지의 443 포트에 연결하게 한다. 연결이 이루어지면 프록시는 양쪽의 데이터를 옮기기만 하면 된다. 그래서 fluxy는 CONNECT를 받으면 새 소켓을 만들고 무작위 주소에 바인딩한 뒤 목적지에 연결하고, 클라이언트와 목적지 사이의 트래픽을 이어 준다.
다른 하나는 일반 HTTP 요청이다. 프록시가 클라이언트 대신 요청을 보내고 응답을 그대로 돌려줘야 한다. 여기서는 hyper의 클라이언트를 그대로 사용했다. HttpConnector에 로컬 주소를 지정할 수 있으므로 요청마다 새 connector를 만들고 무작위 주소를 넣으면 된다.
각 연결에 사용한 주소는 example.com:443 via [2a0e:aa07:e0a0:...]:...처럼 한 줄씩 출력된다. 이 출력으로 주소가 바뀌는지, 어떤 주소를 쓰고 있는지 확인할 수 있다.
배포와 사용
fluxy는 crates.io에 올려 두었으므로 cargo로 바로 설치할 수 있다.
실행할 때 수신 주소와 두 주소 풀을 지정한다.
-b는 수신 주소로, 생략하면 0.0.0.0:51080을 사용한다. -4와 -6은 각각 IPv4와 IPv6 주소 풀이며 둘 다 선택 사항이라 하나만 설정해도 동작한다. 주소 풀에 프리픽스 전체를 줄 필요도 없다. 이 예시처럼 /44 중 /48 하나만 지정하면 fluxy는 그 /48 안에서만 주소를 무작위로 고른다.
이제 curl로 프록시를 거쳐 IP 확인 사이트에 접속하면 된다. 몇 번 연속 요청해 보면 반환되는 주소가 매번 바뀌는 걸 볼 수 있다.