Prog blog

Setup tor hidden service on node.js

Setup tor hidden service on node.js

The text was created as an example of communication with Tor, because node.js does not have an official library for this, and after reading the protocol it turns out that it is unnecessary and enough net.Socket to communicate.

Tor is free software that allows you to remain anonymous while using the Internet. Anonymity is achieved by connecting to other relays (computers) on the Tor network. A very interesting element of this network is hidden services. We can provide local services on the network completely anonymously and we can access them through an address in the .onion domain. For more information about this, please visit the Tor project.

The Tor network as a daemon in the system has a special port to support it 9051. Thanks to it we can control it from other applications in the system. Ask for a new network connection, read and update configurations, create hidden services and much more. If you are interested, please read the documentation Tor control protocol.

To access the port control, you need to update your basic torrc configuration. For your own purposes, i use Tor as a service in the docker torproxy and set it to '0.0.0.0:9051'.

torrc

ControlPort 0.0.0.0:9051

Access to the Tor control port can be tested using telnet.

shell

$ telnet 0.0.0.0 9051
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.

In addition, for greater security, access to the port can be secured by password. For this purpose we need to generate a password hash:

shell

tor --hash-password password
16:94D87DAEACD5274060844DAD7AAC00239BBA59C61455407034007C435F

and then add it to the torrc

torrc

ControlPort 0.0.0.0:9051
CookieAuthentication 1
HashedControlPassword 16:94D87DAEACD5274060844DAD7AAC00239BBA59C61455407034007C435F

Now, when connecting as the first message, we must send the set password with the command AUTHENTICATE.

shell

$ telnet 0.0.0.0 9051
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
AUTHENTICATE "password"
250 OK

Only after we receive the answer 250 OK can we use the remaining commands.

To create a hidden service there is a command ADD_ONION.

telnet

ADD_ONION NEW:BEST Port=3000,127.0.0.1:8000
250-ServiceID=ygwtgk26eauhj4xj
250-PrivateKey=RSA1024:MIICXQIBAAKBgQDd/Nery040dYLVmizkzx14egmm2n/rHADDnjWs70SH2LTs7OZp47iRz27N2LXgeBZKJ/BOxJteakZSsNi+7q0GypFxLtn2A5Wt7OkXHEyX1kJVVPb1L2H1NtrmhcIp67PScU+ACur8RVmMFM8gk0kZhUYEzXxtkGEE9SgrZMhBowIDAQABAoGBAK6Sz7z/llH0894tcA7EvoVnW73G6YAotoUgH1APnlJC3w3Pw+gUOKsaeSYSvf14Evdgb0BSp2jZpNIU129X3eMZXuZNXapLnMJtG2isD9MPEleNky6g3+xH1gWU279i1hQ3PrnO0zsAQbpgg4l7tOafTvp7T0Jr71YhU/0W6A7RAkEA8hOIV6/8FKGYDph2jUOzM9n45Jn0FADoCheFAKTfFFhnU46HBubfP7+8t6AI57y8as5LkFx52poexJLECoia2wJBAOrBg3hoEmUivIF5uKCEOdSxVbV6HZVHfCa6BppAuLdAmckdT6JpgL/Hi0YMLbuZn85CXi9pclxLrgDjiMlkWtkCQGZOLPPqEx3hATNB6fBfqS+DsjVifw2pKGeDcxlHlGOzzM86UARxs+3sKWRtRPYzBGobD0JOYs0pD1HCOQm+m7sCQQCBn4aThouZsnt51pcecsRn799OjHYnRkiELbWVmOUlspL33KprGGxR+MGrVPAvpRz7S+Qrs8LEubkDnp+Z7ELJAkAxYE4fpxb1xk+l9fh3g3OqUH3Z+ZU3xLTLg477LiuXIXcW7b/X2CO8s9ZxvvAxSpy+9gF0uV4M4jzEKYMNbYp8
250 OK

As the first parameter we can set NEW:BEST which means that it will be a new hidden service with the best algorithm available in the tor (currently BEST is 'RSA1024'). As a result we get the service id and its private key, which we can use to re-add the service as in the example below.

telnet

ADD_ONION RSA1024:MIICXQIBAAKBgQDd/Nery040dYLVmizkzx14egmm2n/rHADDnjWs70SH2LTs7OZp47iRz27N2LXgeBZKJ/BOxJteakZSsNi+7q0GypFxLtn2A5Wt7OkXHEyX1kJVVPb1L2H1NtrmhcIp67PScU+ACur8RVmMFM8gk0kZhUYEzXxtkGEE9SgrZMhBowIDAQABAoGBAK6Sz7z/llH0894tcA7EvoVnW73G6YAotoUgH1APnlJC3w3Pw+gUOKsaeSYSvf14Evdgb0BSp2jZpNIU129X3eMZXuZNXapLnMJtG2isD9MPEleNky6g3+xH1gWU279i1hQ3PrnO0zsAQbpgg4l7tOafTvp7T0Jr71YhU/0W6A7RAkEA8hOIV6/8FKGYDph2jUOzM9n45Jn0FADoCheFAKTfFFhnU46HBubfP7+8t6AI57y8as5LkFx52poexJLECoia2wJBAOrBg3hoEmUivIF5uKCEOdSxVbV6HZVHfCa6BppAuLdAmckdT6JpgL/Hi0YMLbuZn85CXi9pclxLrgDjiMlkWtkCQGZOLPPqEx3hATNB6fBfqS+DsjVifw2pKGeDcxlHlGOzzM86UARxs+3sKWRtRPYzBGobD0JOYs0pD1HCOQm+m7sCQQCBn4aThouZsnt51pcecsRn799OjHYnRkiELbWVmOUlspL33KprGGxR+MGrVPAvpRz7S+Qrs8LEubkDnp+Z7ELJAkAxYE4fpxb1xk+l9fh3g3OqUH3Z+ZU3xLTLg477LiuXIXcW7b/X2CO8s9ZxvvAxSpy+9gF0uV4M4jzEKYMNbYp8 Port=3000,127.0.0.1:8000
250-ServiceID=ygwtgk26eauhj4xj
250 OK

Then we only get the service id in response.

The second parameter of the command is the mapping of services on a given port. The first value is the port for hidden service. For 3000 it will be 'ygwtgk26eauhj4xj.onion:3000' and the second value after the comma is the address of the local service which will be visible under port 3000.

Here's an important point. Hidden service is available until the telnet connection is closed. Use the 'Detach' flag to keep the service available even after the call is ended.

telnet

ADD_ONION NEW:BEST Flags=Detach Port=3000,127.0.0.1:8000
250-ServiceID=ygwtgk26eauhj4xj
250-PrivateKey=RSA1024:MIICXQIBAAKBgQDd/Nery040dYLVmizkzx14egmm2n/rHADDnjWs70SH2LTs7OZp47iRz27N2LXgeBZKJ/BOxJteakZSsNi+7q0GypFxLtn2A5Wt7OkXHEyX1kJVVPb1L2H1NtrmhcIp67PScU+ACur8RVmMFM8gk0kZhUYEzXxtkGEE9SgrZMhBowIDAQABAoGBAK6Sz7z/llH0894tcA7EvoVnW73G6YAotoUgH1APnlJC3w3Pw+gUOKsaeSYSvf14Evdgb0BSp2jZpNIU129X3eMZXuZNXapLnMJtG2isD9MPEleNky6g3+xH1gWU279i1hQ3PrnO0zsAQbpgg4l7tOafTvp7T0Jr71YhU/0W6A7RAkEA8hOIV6/8FKGYDph2jUOzM9n45Jn0FADoCheFAKTfFFhnU46HBubfP7+8t6AI57y8as5LkFx52poexJLECoia2wJBAOrBg3hoEmUivIF5uKCEOdSxVbV6HZVHfCa6BppAuLdAmckdT6JpgL/Hi0YMLbuZn85CXi9pclxLrgDjiMlkWtkCQGZOLPPqEx3hATNB6fBfqS+DsjVifw2pKGeDcxlHlGOzzM86UARxs+3sKWRtRPYzBGobD0JOYs0pD1HCOQm+m7sCQQCBn4aThouZsnt51pcecsRn799OjHYnRkiELbWVmOUlspL33KprGGxR+MGrVPAvpRz7S+Qrs8LEubkDnp+Z7ELJAkAxYE4fpxb1xk+l9fh3g3OqUH3Z+ZU3xLTLg477LiuXIXcW7b/X2CO8s9ZxvvAxSpy+9gF0uV4M4jzEKYMNbYp8
250 OK

The available services in detached mode can be seen with the command GETINFO onions/detached.

telnet

GETINFO onions/detached
250+onions/detached=ygwtgk26eauhj4xj
250 OK

Telnet communication is not too complicated, to get the same effect in node.js, we have to use 'net.Socket' and connect to the control port 9051.

import { Socket } from "net";

export const connect = (host: string, port: number) => {
  return new Promise<Socket>((resolve) => {
    const socket = new Socket();
    socket.connect(port, host, () => resolve(socket));
  });
}

All operations will be converted to 'Promise' because it will be easier to combine instructions asynchronously.

We need to create a 'sendCommand' function which will return the result of the 'Socket' answer.

export const sendCommand = (socket: Socket, command: string) => {
  return new Promise<string>((resolve, reject) => {
    socket.once("data", (data: Buffer) => {
      const result = data.toString();
      if (result.includes("250 OK")) {
        resolve(result);
      } else {
        reject(new Error(result));
      }
    });
    socket.write(`${command}\n`);
  });
}

Each correctly executed command returns '250 OK', so we can easily identify the answers and return them as resolve or reject. Also important here, socket returns data only once for each command, so the 'once' method is used, not 'on'.

Once we have 'sendCommand' we can finally program our first command 'AUTHENTICATE'.

export const authenticate = (socket: Socket, password: string) => {
  return sendCommand(socket, `AUTHENTICATE "${password}"`);
};

After authorization we can create our first hidden service with the command ADD_ONION.

interface AddOnionResponse {
  serviceId: string;
  keyType?: string;
  privateKey?: string;
}

export const addOnion = async (socket: Socket, options: {
  keyType?: string;
  privateKey?: string;
  flags?: string;
  port: string;
}): Promise<AddOnionResponse> => {
  if (options.privateKey && !options.keyType) {
    throw new Error('Set keyType');
  }
  const keyArgument = options.keyType ? options.privateKey ? `${options.keyType}:${options.privateKey}` : `NEW:${options.keyType}` : 'NEW:BEST';
  const flagsArgument = options.flags ? `Flags=${options.flags} ` : '';
  const portArgument = `Port=${options.port}`;
  const result = await sendCommand(socket, `ADD_ONION ${keyArgument} ${flagsArgument} ${portArgument}`);
  const lines = result.split('\r\n');
  const serviceId = lines[0].split('250-ServiceID=')[1]; // 250-ServiceID=pismseqxalmmrdgwy6orjwt5xs36bepp4gekudhpgtmclyvc2a6i6sid
  if (lines[1].startsWith('250-PrivateKey=')) {
    const privateKeyResult = lines[1].split('250-PrivateKey=')[1].split(':'); // 250-PrivateKey=ED25519-V3:wDnhv3eEu5CfBUNZhzOMlEZI9pSZBH9vvvYhJsLMjUcMpzj9HCFmjef52KyfmelhGfN72CaY2RsSqHE60huxtQ==
    const keyType = privateKeyResult[0];
    const privateKey = privateKeyResult[1]; 
    return {
      serviceId,
      keyType,
      privateKey
    }
  } else {
    return {
      serviceId,
      keyType: options.keyType,
      privateKey: options.privateKey
    };
  }
}

The function is very powerful. Apart from giving different arguments for the command, we also have to make a basic 'parse' so that we can later use the received answer in the program.

The last command is GETINFO.

export const getInfo = (socket: Socket, keyword: string) => {
  return sendCommand(socket, `GETINFO ${keyword}`);
}

Using GETINFO we can get a lot of information about the Tor daemon. Which can be parse in separate functions.

In addition, I leave below an example how to create a detached hidden service from these functions.

const addOnion = async (serviceId: OnionInfo | null) => {
  console.log('connect');
  const socket = await connect('localhost', 9051);
  console.log('authenticate');
  await authenticate(socket, 'password');
  const onionPort = `80,127.0.0.1:3000`;
  console.log(`add_onion: new`);
  const result = await addOnion(socket, {
    keyType: 'ED25519-V3',
    flags: 'Detach',
    port: onionPort
  });
  console.log(`new onion: ${result.serviceId}`);
  console.log('getinfo onions/detached');
  const onionsDetached = await getInfo(socket, 'onions/detached');
  console.log(onionsDetached);
  socket.destroy();
}