Add direct proxying
parent
1ee93c5715
commit
dbb4c0cfe1
|
@ -0,0 +1,117 @@
|
|||
'use strict';
|
||||
|
||||
import assert from 'assert';
|
||||
import { sanitize } from './util.js';
|
||||
import { DIRECT_PROXY } from './settings.js';
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
const CONNECTION_ESTABLISHED_REPLY = textEncoder.encode('HTTP/1.0 200 Connection Established\r\nProxy-agent: Apache/2.4.41 (Ubuntu)\r\n\r\n');
|
||||
|
||||
// Not sure what this does, but it seems irrelevant.
|
||||
const GEOIP_RESPONSE = `HTTP/1.1 200 OK
|
||||
Server: nginx/1.24.0
|
||||
Date: %NOW%
|
||||
Content-Type: application/json
|
||||
Content-Length: 19
|
||||
Connection: keep-alive
|
||||
Cache-Control: max-age=604800, private
|
||||
Access-Control-Allow-Origin: *
|
||||
|
||||
{"continent":"NA"}
|
||||
`;
|
||||
|
||||
const LIST_RESPONSE = `HTTP/1.1 200 OK
|
||||
Server: nginx/1.24.0
|
||||
Date: %NOW%
|
||||
Content-Type: application/json
|
||||
Content-Length: %LENGTH%
|
||||
Last-Modified: %NOW%
|
||||
Connection: keep-alive
|
||||
Access-Control-Allow-Origin: *
|
||||
|
||||
%PAYLOAD%
|
||||
`;
|
||||
|
||||
const PAYLOAD = {
|
||||
'total': { 'servers': DIRECT_PROXY.length, 'clients': 0 },
|
||||
'total_max': { 'server': DIRECT_PROXY.length, 'clients': 0 },
|
||||
'list': DIRECT_PROXY.map(([vip, ip, port]) => {
|
||||
return {
|
||||
'address': vip,
|
||||
'ip': vip,
|
||||
'port': port,
|
||||
'proto_min': 37,
|
||||
'proto_max': 42,
|
||||
}}),
|
||||
};
|
||||
|
||||
// Fake a CONNECT proxy to simulate servers.minetest.net response
|
||||
export class ConnectProxy {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
this.firstLine = true;
|
||||
this.conn = null;
|
||||
}
|
||||
|
||||
forward(data) {
|
||||
if (this.firstLine) {
|
||||
this.firstLine = false;
|
||||
this.handle_handshake(data);
|
||||
return;
|
||||
}
|
||||
if (!(data instanceof ArrayBuffer)) {
|
||||
throw new Error("ConnectProxy received non-binary messages");
|
||||
}
|
||||
data = textDecoder.decode(data);
|
||||
assert(data.endsWith('\r\n\r\n'));
|
||||
let lines = data.split('\r\n');
|
||||
assert(lines.length >= 1);
|
||||
let tokens = lines[0].split(' ');
|
||||
assert(tokens[0] == 'GET');
|
||||
let url = sanitize(tokens[1]);
|
||||
const now = (new Date()).toUTCString();
|
||||
let response;
|
||||
if (url.startsWith('/geoip')) {
|
||||
response = GEOIP_RESPONSE.replace(/%NOW%/g, now);
|
||||
} else if (url.startsWith('/list')) {
|
||||
const payload = JSON.stringify(PAYLOAD);
|
||||
response = LIST_RESPONSE.replace(/%NOW%/g, now).replace('%LENGTH%', payload.length + 1).replace('%PAYLOAD%', payload);
|
||||
this.client.log("Sending virtual server list")
|
||||
} else {
|
||||
this.client.log(`Invalid GET request for ${url}`);
|
||||
this.client.close();
|
||||
return;
|
||||
}
|
||||
this.client.send(textEncoder.encode(response));
|
||||
}
|
||||
|
||||
handle_handshake(data) {
|
||||
// The CONNECT line and it's headers could be split among several packets.
|
||||
// In a real server, this would aggregate data until it sees \r\n\r\n
|
||||
// But minetest-wasm always sends it as one packet, so just assume that.
|
||||
data = textDecoder.decode(data);
|
||||
assert(data.endsWith('\r\n\r\n'));
|
||||
let lines = data.split('\r\n');
|
||||
assert(lines.length >= 1);
|
||||
let tokens = lines[0].split(' ');
|
||||
assert.strictEqual(tokens.length, 3);
|
||||
assert.strictEqual(tokens[0], 'CONNECT');
|
||||
assert.strictEqual(tokens[2], 'HTTP/1.1');
|
||||
let host_port = tokens[1].split(':');
|
||||
assert.strictEqual(host_port.length, 2);
|
||||
let host = host_port[0];
|
||||
let port = parseInt(host_port[1]);
|
||||
if (host != 'servers.minetest.net' || port != 80) {
|
||||
this.client.log(`Ignoring request to proxy to ${host}:${port}`);
|
||||
this.client.close();
|
||||
return;
|
||||
}
|
||||
this.client.log('Connected for server list');
|
||||
this.client.send(CONNECTION_ESTABLISHED_REPLY);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.client.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
'use strict';
|
||||
|
||||
import dgram from 'dgram';
|
||||
|
||||
export class UDPProxy {
|
||||
constructor(client, ip, port) {
|
||||
const socket = dgram.createSocket('udp4');
|
||||
this.client = client;
|
||||
this.socket = socket;
|
||||
this.ip = ip;
|
||||
this.port = port;
|
||||
this.sendok = false;
|
||||
this.sendqueue = [];
|
||||
socket.on('listening', this.handle_listening.bind(this));
|
||||
socket.on('error', this.handle_error.bind(this));
|
||||
socket.on('message', this.handle_message.bind(this));
|
||||
socket.bind();
|
||||
}
|
||||
|
||||
forward(data) {
|
||||
// This creates a view of the ArrayBuffer
|
||||
data = new Uint8Array(data);
|
||||
if (data.byteLength < 4 ||
|
||||
data[0] != 0x4f ||
|
||||
data[1] != 0x45 ||
|
||||
data[2] != 0x74 ||
|
||||
data[3] != 0x03) {
|
||||
throw new Error('Client sent packet with invalid protocol.');
|
||||
}
|
||||
|
||||
if (this.sendok) {
|
||||
// data must be a typed array here
|
||||
this.socket.send(data, this.port, this.ip);
|
||||
} else {
|
||||
this.sendqueue.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
handle_listening() {
|
||||
const sourcePort = this.socket.address().port;
|
||||
this.log(`Bound ${sourcePort} -> ${this.ip}:${this.port}`);
|
||||
this.sendok = true;
|
||||
if (this.sendqueue.length > 0) {
|
||||
for (const data of this.sendqueue) {
|
||||
this.socket.send(data, this.port, this.ip);
|
||||
}
|
||||
this.sendqueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
handle_error(err) {
|
||||
this.log("Socket error: " + err);
|
||||
this.close();
|
||||
}
|
||||
|
||||
handle_message(msg, rinfo) {
|
||||
if (rinfo.address != this.ip || rinfo.port != this.port) {
|
||||
this.log("Ignoring unsolicited packet from " + rinfo.address + " port " + rinfo.port);
|
||||
return;
|
||||
}
|
||||
this.client.send(msg);
|
||||
}
|
||||
|
||||
log(msg) {
|
||||
this.client.log(msg);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
this.client.close();
|
||||
}
|
||||
}
|
36
client.js
36
client.js
|
@ -1,8 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
import assert from 'assert';
|
||||
import { isIPv4 } from 'net';
|
||||
import { format } from 'util'; // node.js built-in
|
||||
|
||||
import { DIRECT_PROXY } from './settings.js';
|
||||
import { ConnectProxy } from './ConnectProxy.js';
|
||||
import { UDPProxy } from './UDPProxy.js';
|
||||
import { extract_ip_chain, sanitize } from './util.js';
|
||||
import { vpn_make, vpn_connect} from './vpn.js';
|
||||
import { format } from 'util'; // node.js built-in
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
let lastlog = null;
|
||||
|
@ -99,6 +105,20 @@ export class Client {
|
|||
return;
|
||||
}
|
||||
response = 'BIND OK';
|
||||
} else if (command == 'PROXY') {
|
||||
assert(tokens[2] == 'TCP' || tokens[2] == 'UDP');
|
||||
const isUDP = (tokens[2] == 'UDP');
|
||||
const ip = sanitize(tokens[3]);
|
||||
const port = parseInt(sanitize(tokens[4]));
|
||||
assert(isIPv4(ip));
|
||||
assert(port >= 1 && port < 65536);
|
||||
this.target = route(this, isUDP, ip, port);
|
||||
if (!this.target) {
|
||||
this.log(`Proxy to udp=${isUDP}, ip=${ip}, port=${port} rejected`);
|
||||
response = 'PROXY FAIL';
|
||||
} else {
|
||||
response = 'PROXY OK';
|
||||
}
|
||||
} else {
|
||||
this.log('Unhandled command: ', data);
|
||||
this.close();
|
||||
|
@ -108,3 +128,17 @@ export class Client {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
const PROXY_MAP = new Map(DIRECT_PROXY.map(([vip,ip,port]) => [vip, [ip, port]]));
|
||||
|
||||
function route(client, isUDP, ip, port) {
|
||||
if (!isUDP && ip == '10.0.0.1' && port == 8080) {
|
||||
return new ConnectProxy(client);
|
||||
}
|
||||
if (isUDP && PROXY_MAP.has(ip)) {
|
||||
let [real_ip, real_port] = PROXY_MAP.get(ip);
|
||||
return new UDPProxy(client, real_ip, real_port);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
3
main.js
3
main.js
|
@ -1,7 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const PROXY_PORT = 8888;
|
||||
|
||||
import { PROXY_PORT } from './settings.js';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { Client } from './client.js';
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
export const PROXY_PORT = 8888;
|
||||
|
||||
// [virtual_ip, real_ip, real_port]
|
||||
//
|
||||
// The virtual IP is the one that minetest-wasm sees.
|
||||
// The virtual port is the same as the real port.
|
||||
//
|
||||
export const DIRECT_PROXY = [
|
||||
// This allows clients to connect to a server running on the proxy itself.
|
||||
['192.168.0.1', '127.0.0.1', 30000],
|
||||
|
||||
// This would allow clients to connect to 1.2.3.4, port 40000
|
||||
//['192.168.0.2', '1.2.3.4', 40000],
|
||||
];
|
Loading…
Reference in New Issue