Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolving with zone files from Sia #2

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 69 additions & 5 deletions lib/handover.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
const {wire, util} = require('bns');
const {BufferReader} = require('bufio');
const Ethereum = require('./ethereum');
const Sia = require('./sia');

const plugin = exports;

Expand All @@ -23,6 +24,11 @@ class Plugin {
projectSecret: node.config.str('handover-infura-projectsecret')
});

this.sia = new Sia({
logger: node.logger.context('handover:sia'),
portal: node.config.str('handover-sia-portal', 'siasky.net') // or skyportal.xyz, etc.
});

// Plugin can not operate if node doesn't have DNS resolvers
if (!this.ns || !this.node.rs)
return;
Expand Down Expand Up @@ -66,6 +72,23 @@ class Plugin {
return this.sendSOA(name);
}
break;

case '_sia.':
if (type === wire.types.DS) {
// TODO: plugin should have ephemeral DNSSEC keys.
// We should return that here, signed by HNS root ZSK.
} else {
return this.sendSOA(name);
}
break;
case '_siaregistry.':
if (type === wire.types.DS) {
// TODO: plugin should have ephemeral DNSSEC keys.
// We should return that here, signed by HNS root ZSK.
} else {
return this.sendSOA(name);
}
break;
}

// Next, try actually resolving the name with the HNS root zone.
Expand All @@ -85,7 +108,7 @@ class Plugin {
// and query it for the user's original request
if (rr.data.ns.slice(-5) === '.eth.') {
// If the recursive is being minimal, don't look up the name.
// Send the SOA back and get the full query from the recursive .
// Send the SOA back and get the full query from the recursive.
if (labels.length < 2) {
return this.sendSOA(name);
}
Expand All @@ -107,7 +130,7 @@ class Plugin {
// the user's original request
if (rr.data.ns.slice(-6) === '._eth.') {
// If the recursive is being minimal, don't look up the name.
// Send the SOA back and get the full query from the recursive .
// Send the SOA back and get the full query from the recursive.
if (labels.length < 2) {
return this.sendSOA(name);
}
Expand All @@ -124,14 +147,55 @@ class Plugin {
rr.data.ns
);
}

// Resolve DNS by looking up the zone file at the specified
// skylink and query it for the user's original request
if (rr.data.ns.slice(-6) === '._sia.') {
if (labels.length < 1) {
return this.sendSOA(name);
}
this.logger.debug(
'Intercepted referral to ._sia: %s %s -> %s NS: %s',
name,
wire.typesByVal[type],
rr.name,
rr.data.ns
);
data = await this.sia.resolveDnsFromSkylink(
name,
type,
rr.data.ns
);
}

// Resolve DNS by looking up the specified sia registry entry
// for the zone file skylink address, then query the zone file
// for the user's original request
if (rr.data.ns.slice(-14) === '._siaregistry.') {
if (labels.length === 0) {
return this.sendSOA(name);
}
this.logger.debug(
'Intercepted referral to ._siaregistry: %s %s -> %s NS: %s',
name,
wire.typesByVal[type],
rr.name,
rr.data.ns
);
data = await this.sia.resolveDnsFromRegistry(
name,
type,
rr.data.ns
);
}
}

// If the Ethereum stuff came up empty, return the
// If the above scheme resolvers came up empty, return the
// HNS root server response unmodified.
if (!data || data.length === 0)
return res;

// If we did get an answer from Ethereum, mark the response
// If we did get an answer from the resolvers, mark the response
// as authoritative and insert the new answer.
this.logger.debug('Returning answers from alternate naming system');
const replacementRes = new wire.Message();
Expand All @@ -141,7 +205,7 @@ class Plugin {
replacementRes.answer.push(wire.Record.read(br));
}

// Answers resolved from Ethereum appear to come directly
// Answers resolved from the resolvers appear to come directly
// from the HNS root zone.
this.ns.signRRSet(replacementRes.answer, type);

Expand Down
147 changes: 147 additions & 0 deletions lib/sia.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
'use strict';

const axios = require('axios').default;
const { Zone } = require('bns');
const LRU = require('blru');

class Sia {
constructor({ logger, portal }) {
this.logger = logger;
this.cache = new SiaCache();

const match = portal.match(/(?:https?\:\/\/)?([\w\.\-_]+)\/?/i);
if (match) this.portal = match[1];
else this.logger.error('Invalid portal.');
}

async resolveDnsFromSkylink(name, type, ns, node) {
this.logger.debug('Resolving', name, type, ns);
if (!node) node = name;

const labels = ns.split('.');

if (labels.length !== 3) return null;
if (labels[1] !== '_sia') return null;

const skylink = labels[0];
if (skylink.length !== 46) return null;

const skylinkContent = await this.getSkylinkContent(skylink);
return this.resolveDnsFromZone(skylinkContent, name, type);
}

async resolveDnsFromRegistry(name, type, ns, node) {
this.logger.debug('Resolving from Registry: ', name, type, ns);
if (!node) node = name;

const labels = ns.split('.');

if (labels.length !== 7) return null;
if (labels[5] !== '_siaregistry') return null;

// Pubkey and datakey are split into 2 labels of 32 bytes each because of max label length
const algo = labels[0];
const pubKey = labels[1] + labels[2];
const dataKey = labels[3] + labels[4];
if (algo.length <= 0 || pubKey.length !== 64 || dataKey.length !== 64)
return null;

const registryData = await this.getRegistryEntry(algo, pubKey, dataKey);
const skylinkContent = await this.getSkylinkContent(registryData);
return this.resolveDnsFromZone(skylinkContent, name, type);
}

async getSkylinkContent(skylink) {
const item = this.cache.get(skylink);
if (item) return item;

const skylinkContent = await this._getSkylinkContent(skylink);
this.cache.set(skylink, skylinkContent);
return skylinkContent;
}

async _getSkylinkContent(skylink) {
this.logger.debug('Fetching skylink...', skylink);
const content = await axios({
url: `https://${this.portal}/${skylink}`,
responseType: 'text',
transformResponse: [
(data) => {
return data;
},
], // force text response: https://github.com/axios/axios/issues/907
maxBodyLength: 20000, // in bytes
});
this.logger.spam(content.data);
return content.data;
}

async getRegistryEntry(algo, pubKey, dataKey) {
const key = `${pubKey};${dataKey}`;
const item = this.cache.get(key);
if (item) return item;

const registryEntry = await this._getRegistryEntry(algo, pubKey, dataKey);
this.cache.set(key, registryEntry);
return registryEntry;
}

async _getRegistryEntry(algo, pubKey, dataKey) {
this.logger.debug('Fetching sia registry...', algo, pubKey, dataKey);
const content = await axios({
url: `https://${this.portal}/skynet/registry?publickey=${algo}:${pubKey}&datakey=${dataKey}`,
maxBodyLength: 20000, // in bytes
});
this.logger.spam(content.data);
return Buffer.from(content.data.data, 'hex').toString();
}

async resolveDnsFromZone(zoneContent, name, type) {
const zone = new Zone();

try {
zone.fromString(zoneContent);
} catch (error) {
if (error.type !== 'ParseError') this.logger.error(error);
return null;
}

this.logger.spam(zone);
const records = zone.get(name, type).map((rec) => rec.toRaw());
if (!records.length) return null;

return Buffer.concat(records);
}
}

class SiaCache {
constructor(size = 3000) {
this.cache = new LRU(size);
this.CACHE_TTL = 30 * 60 * 1000;
}

set(key, data) {
this.cache.set(key, {
time: Date.now(),
data,
});

return this;
}

get(key) {
const item = this.cache.get(key);

if (!item) return null;

if (Date.now() > item.time + this.CACHE_TTL) return null;

return item.data;
}

reset() {
this.cache.reset();
}
}

module.exports = Sia;
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
},
"homepage": "https://github.com/imperviousinc/handover#readme",
"dependencies": {
"axios": "^0.21.1",
"blru": "^0.1.6",
"bufio": "^1.0.7",
"ethers": "^5.0.31"
},
Expand Down