Commit 7f0cb72f authored by Andreas Kempe's avatar Andreas Kempe
Browse files

Implement basic proxy functionality

This implements basic SOCKS5 proxy functionality. Only no authentication
is supported and there is currently no compression.
parent e69c8dbc
use "net"
interface ProxyClient
"""
This interface is used by ClientNotifier for relaying data.
The actor implementing this interface is expected to relay the
data from the client to the server.
When the local connection has been accepted, the ClientNotifier
should call accepted().
When the remote connection is established to the server, the
ClientNotifier should call connected().
"""
// Notify relay that the remote connection has been established.
be connected(conn: TCPConnection tag)
// Notify the relay that the local connection has been accepted.
be accepted(conn: TCPConnection tag)
// Kill the relay. Generally done if a connection closes.
be kill(caller: TCPConnection tag)
// Relay data to the other end-point.
be relay(from: TCPConnection tag, data: Array[U8 val] val)
class ClientAcceptor is TCPListenNotify
"""
Accepts client connections and initiates a connection to the
server for relaying.
"""
let env: Env
let auth: AmbientAuth
let _env: Env
let _auth: AmbientAuth
let remote_address: String
let remote_port: String
let _remote_address: String
let _remote_port: String
new create(env': Env, auth': AmbientAuth,
remote_address': String,
remote_port': String) =>
env = env'
auth = auth'
remote_address = remote_address'
remote_port = remote_port'
new create(env: Env, auth: AmbientAuth,
remote_address: String,
remote_port: String) =>
_env = env
_auth = auth
_remote_address = remote_address
_remote_port = remote_port
fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ =>
let client = Client(env.out, auth)
let client = Client(_env.out)
/*
* At this point, the local connection is established. Before
......@@ -51,46 +29,15 @@ class ClientAcceptor is TCPListenNotify
* remote server and create another notifier for that remote
* connection.
*/
TCPConnection(auth, recover ClientNotifier(env.out, client) end,
remote_address, remote_port)
TCPConnection(_auth, recover ProxyNotifier(_env.out, client) end,
_remote_address, _remote_port)
recover ClientNotifier(env.out, client) end
recover ProxyNotifier(_env.out, client) end
fun ref not_listening(listen: TCPListener ref) =>
env.out.print("No longer listening")
class ClientNotifier is TCPConnectionNotify
"""
This class passes data and events from the TCPConnection to the
ProxyClient actor that handles relaying data between connections.
"""
let out: OutStream
let client: ProxyClient tag
new create(out': OutStream, client': ProxyClient tag) =>
out = out'
client = client'
fun ref accepted(conn: TCPConnection ref) =>
client.accepted(conn)
fun ref connected(conn: TCPConnection ref) =>
client.connected(conn)
fun ref closed(conn: TCPConnection ref) =>
client.kill(conn)
fun ref received(conn: TCPConnection ref,
data: Array[U8] iso,
times: USize) : Bool =>
client.relay(conn, consume data)
true
fun ref connect_failed(conn: TCPConnection ref) =>
client.kill(conn)
_env.out.print("No longer listening")
actor Client is ProxyClient
actor Client is ProxyInterface
"""
This class simply shuffles data from the locally connected client
to the remote server. The connections are established by the TCP
......@@ -100,42 +47,40 @@ actor Client is ProxyClient
passed between the two connections.
"""
let auth: AmbientAuth
let out: OutStream
let _out: OutStream
var local: (TCPConnection tag | None)
var remote: (TCPConnection tag | None)
var _local: (TCPConnection tag | None)
var _remote: (TCPConnection tag | None)
var local_buffer: Array[U8]
var remote_buffer: Array[U8]
var _local_buffer: Array[U8]
var _remote_buffer: Array[U8]
new create(out': OutStream, auth': AmbientAuth) =>
out = out'
auth = auth'
local = None
remote = None
local_buffer = Array[U8]
remote_buffer = Array[U8]
new create(out: OutStream) =>
_out = out
_local = None
_remote = None
_local_buffer = Array[U8]
_remote_buffer = Array[U8]
fun get_opposite(conn: TCPConnection tag) : TCPConnection tag? =>
if local is conn then
remote as TCPConnection
fun _get_opposite(conn: TCPConnection tag) : TCPConnection tag? =>
if _local is conn then
_remote as TCPConnection
else
local as TCPConnection
_local as TCPConnection
end
be accepted(conn: TCPConnection tag) =>
local = conn
_local = conn
if remote_buffer.size() > 0 then
send_buffer(conn, remote_buffer)
if _remote_buffer.size() > 0 then
send_buffer(conn, _remote_buffer)
end
be connected(conn: TCPConnection tag) =>
remote = conn
_remote = conn
if local_buffer.size() > 0 then
send_buffer(conn, local_buffer)
if _local_buffer.size() > 0 then
send_buffer(conn, _local_buffer)
end
fun send_buffer(conn: TCPConnection tag, buffer: Array[U8]) =>
......@@ -153,24 +98,24 @@ actor Client is ProxyClient
try
// If this fails, the other connection was never opened.
// Ignore the error.
get_opposite(caller)?.dispose()
_get_opposite(caller)?.dispose()
end
be relay(from: TCPConnection tag, data: Array[U8 val] val) =>
try
get_opposite(from)?.write(data)
_get_opposite(from)?.write(data)
else
/*
* If we couldn't get the opposite connection, that means
* one side still hasn't connected. We queue the data and
* send it when the connection is made.
*/
if from is local then
local_buffer.concat(data.values())
elseif from is remote then
remote_buffer.concat(data.values())
if from is _local then
_local_buffer.concat(data.values())
elseif from is _remote then
_remote_buffer.concat(data.values())
else
out.print("ERROR: Received data from connection " +
"that was neither local nor remote")
_out.print("ERROR: Received data from connection " +
"that was neither local nor remote")
end
end
......@@ -38,7 +38,7 @@ actor Main
OptionSpec.string("address", "Listen address"
where short' = 'a', default' = "localhost")
OptionSpec.string("port", "Listen port"
where short' = 'p', default' = "8081")
where short' = 'p', default' = "8080")
], [])?
let cs =
......@@ -75,7 +75,7 @@ actor Main
env.out.print("Listening on " + address + ":" +
port + "/tcp")
env.out.print("Server at " + remote_address + ":"
+ port + "/tcp")
+ remote_port + "/tcp")
recover iso
ClientAcceptor(env,
env.root as AmbientAuth,
......@@ -84,7 +84,11 @@ actor Main
end
else
env.out.print("Running as server")
recover iso ProxyAcceptor(env) end
recover iso
let auth = env.root as AmbientAuth
ProxyAcceptor(env,
TCPAuth(auth))
end
end
try
......
use "net"
class ProxyAcceptor is TCPListenNotify
let env: Env
let _env: Env
let _auth: TCPAuth
new create(env': Env) =>
env = env'
new create(env: Env, auth: TCPAuth) =>
_env = env
_auth = auth
fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ =>
recover Proxy(env) end
let proxy = Proxy(_env.out, _auth)
recover ProxyNotifier(_env.out, proxy) end
fun ref not_listening(listen: TCPListener ref) =>
None
_env.out.print("Stopped listening")
class Proxy is TCPConnectionNotify
let env: Env
class SocksNotifier is TCPConnectionNotify
let _proxy: Proxy tag
new create(env': Env) =>
env = env'
new create(proxy: Proxy tag) =>
_proxy = proxy
fun ref connected(conn: TCPConnection ref) =>
_proxy.socks_connected(conn, conn.local_address(),
conn.local_address().port())
fun ref connect_failed(conn: TCPConnection ref) =>
_proxy.kill(conn)
fun ref closed(conn: TCPConnection ref) =>
_proxy.kill(conn)
fun ref received(conn: TCPConnection ref,
data: Array[U8] iso,
times: USize) : Bool =>
conn.write(String.from_array(consume data))
_proxy.socks_relay(consume data)
true
fun ref connect_failed(conn: TCPConnection ref) =>
None
actor Proxy is ProxyInterface
let _out: OutStream
let _auth: TCPAuth
var _socks_relay: (SocksRelay | None)
new create(out: OutStream, auth: TCPAuth) =>
_out = out
_auth = auth
_socks_relay = None
be accepted(conn: TCPConnection tag) =>
_socks_relay = SocksRelay(_out, conn)
be connected(conn: TCPConnection tag) =>
_out.print("ERROR: connected called in proxy. Should not happen!")
be socks_connected(conn: TCPConnection tag, local_address: NetAddress val,
local_port: U16) =>
try
(_socks_relay as SocksRelay).connected(conn, local_address,
local_port)
end
be kill(caller: TCPConnection tag) =>
try
(_socks_relay as SocksRelay).kill()
_socks_relay = None
end
fun ipv4_to_str(address: U32): String =>
(address >> 24).string() + "." +
((address >> 16) and 0xFF).string() + "." +
((address >> 8) and 0xFF).string() + "." +
(address and 0xFF).string()
fun ipv6_to_str(address: U128): String =>
var res: String = ""
var i: U128 = 8
while i > 1 do
res = res + ((address >> (16 * (i - 1))) and 0xFFFF).string() + ":"
i = i - 1
end
res = res + (address and 0xFFFF).string()
res
fun _new_connection(msg: SocksRequestMessage) =>
var address: String
address =
match msg.destination_address
| let a: U32 =>
ipv4_to_str(a)
| let a: U128 =>
ipv6_to_str(a)
| let a: String =>
a
end
match msg.command
| let c: SocksCmdConnect =>
_out.print("Opening connection to " + address + ":" +
msg.destination_port.string())
TCPConnection(_auth, recover SocksNotifier(this) end,
address,
msg.destination_port.string())
end
be relay(from: TCPConnection tag, data: Array[U8 val] val) =>
try
let r = _socks_relay as SocksRelay
match r.parse(data)?
| None => return
| let result: SocksRequestMessage =>
_new_connection(result)
end
else
_out.print("Failed to parse data from local client")
end
be socks_relay(data: Array[U8 val] val) =>
try
let r = _socks_relay as SocksRelay
r.forward_to_local(data)
end
use "net"
interface ProxyInterface
"""
This interface is used by Notifiers for relaying data.
The actor implementing this interface is expected to relay the
data from the client to the server.
When the local connection has been accepted, the ClientNotifier
should call accepted().
When the remote connection is established to the server, the
ClientNotifier should call connected().
"""
// Notify relay that the remote connection has been established.
be connected(conn: TCPConnection tag)
// Notify the relay that the local connection has been accepted.
be accepted(conn: TCPConnection tag)
// Kill the relay. Generally done if a connection closes.
be kill(caller: TCPConnection tag)
// Relay data to the other end-point.
be relay(from: TCPConnection tag, data: Array[U8 val] val)
class ProxyNotifier is TCPConnectionNotify
"""
This class passes data and events from the TCPConnection to the
ProxyInterface actor that handles relaying data between connections.
"""
let _out: OutStream
let _client: ProxyInterface tag
new create(out: OutStream, client: ProxyInterface tag) =>
_out = out
_client = client
fun ref accepted(conn: TCPConnection ref) =>
_client.accepted(conn)
fun ref connected(conn: TCPConnection ref) =>
_client.connected(conn)
fun ref closed(conn: TCPConnection ref) =>
_client.kill(conn)
fun ref received(conn: TCPConnection ref,
data: Array[U8] iso,
times: USize) : Bool =>
_client.relay(conn, consume data)
true
fun ref connect_failed(conn: TCPConnection ref) =>
_client.kill(conn)
use "net"
primitive AuthNone
fun apply(): U8 => 0x00
primitive AuthNotFound
fun apply(): U8 => 0xFF
type SocksAuthMethod is (AuthNone | AuthNotFound)
class SocksConnectionRequest
"""
Represents the initial SOCKS5 connection request message. It
contains three fields.
+--------+-------------+------------------------------+
| VER: 1 | NMETHODS: 1 | METHODS: NMETHODS (1 to 255) |
+--------+-------------+------------------------------+
VER is the version number and should always be 5.
NMETHODS specifies the length of
METHODS is an array of U8 values that indicates what
authentication methads the client finds acceptable.
"""
let version: U8 = 5
let method: SocksAuthMethod
new create(data: Array[U8] val)? =>
var method': SocksAuthMethod = AuthNotFound
if data(0)? != version then
error
end
// data(1) specifies the length of the method field.
let nmethods = data(1)?
if nmethods == 0 then
error
end
// TODO: Support authentication
var cnt: USize = 0
repeat
match data(cnt + 2)?
| AuthNone() =>
method' = AuthNone
break
end
cnt = cnt + 1
until cnt >= nmethods.usize() end
method = method'
class SocksMethodSelectionMessage
"""
Represents a authentication method selection message sent as a
response to the initial connection request. It contains two
fields.
+--------+-----------+
| VER: 1 | METHOD: 1 |
+--------+-----------+
VER is the version number and should always be 5.
METHOD is the selected authentication method.
"""
let version: U8 = 5
let method: SocksAuthMethod
new create(method': SocksAuthMethod) =>
method = method'
fun send(conn: TCPConnection) =>
var data: Array[U8] iso = recover Array[U8](2) end
data.push(5)
data.push(method())
conn.write(consume data)
primitive SocksCmdConnect
fun apply(): U8 => 0x01
primitive SocksCmdBind
fun apply(): U8 => 0x02
primitive SocksCmdUDPAssociate
fun apply(): U8 => 0x03
type SocksCommand is (SocksCmdConnect | SocksCmdBind |
SocksCmdUDPAssociate)
primitive SocksAddrIPv4
fun apply(): U8 => 0x01
primitive SocksDomainName
fun apply(): U8 => 0x03
primitive SocksAddrIPv6
fun apply(): U8 => 0x04
type SocksAddressType is (SocksAddrIPv4 | SocksDomainName |
SocksAddrIPv6)
type SocksAddress is (U32 val | U128 val | String val)
class SocksAddressParser
fun parse(atype: SocksAddressType,
offset: USize,
data: Array[U8] val): (SocksAddress, U16)? =>
var address: SocksAddress = ""
var port: U16 = 0
match atype
| let t: SocksAddrIPv4 =>
var i: U32 = 0
let ipv4_len: U32 = 4
var address': U32 = 0
while i < ipv4_len do
address' = address' +
(data(offset + i.usize())?.u32() <<
(8 * (ipv4_len - i - 1)))
i = i + 1
end
address = address'
port = (data(offset + 4)?.u16() << 8) + data(offset + 5)?.u16()
| let t: SocksAddrIPv6 =>
var i: U128 = 0
let ipv6_len: U128 = 16
var address': U128 = 0
while i < ipv6_len do
address' = address' +
(data(offset + i.usize())?.u128() <<
(8 * (ipv6_len - i - 1)))
i = i + 1
end
address = address'
port = (data(offset + 16)?.u16() << 8) + data(offset + 17)?.u16()
| let t: SocksDomainName =>
var i: USize = 0
let length: USize = data(offset)?.usize()
var domain_name: Array[U8] iso =
recover Array[U8](length) end
while i < length do
domain_name.push(data(offset + 1 + i)?)
i = i + 1
end
address = String.from_array(consume domain_name)
port = (data(offset + 1 + length)?.u16() << 8) +
data(offset + 1 + length + 1)?.u16()
end
(address, port)
class SocksRequestMessage
"""
Represents a SOCKS5 request. It is a request to establish a TCP
connection, relay UDP traffic or listen for incoming TCP
connections at/to the provided address and port.
+--------+--------+--------+---------+--------------------+-------------+
| VER: 1 | CMD: 1 | RSV: 1 | ATYP: 1 | DST.ADDR: Variable | DST.PORT: 2 |
+--------+--------+--------+---------+--------------------+-------------+
VER is the version number and should always be 5.
CMD is the command to perform.
RSV is reserved and should be set to 0x00.
ATYP is the address type.
DST.ADDR contains the destination address. It can be an IPv4 or
IPv6 address or a domain name.
DST.PORT contains the destination port number.
"""
let version: U8 = 5
let command: SocksCommand
let address_type: SocksAddressType
let destination_address: SocksAddress
let destination_port: U16
new create(data: Array[U8] val)? =>
// VER
if data(0)? != version then
error
end
// CMD
match data(1)?
| SocksCmdConnect() =>
command = SocksCmdConnect