Permalink
Browse files

Implement IP blacklist for user-initiated requests.

  • Loading branch information...
kentonv committed Mar 2, 2017
1 parent d616a50 commit 164997fb958effbc90c5328c166706280a84aaa1
@@ -150,6 +150,10 @@ Router.map(function () {
path: "/admin/preinstalled-apps",
controller: newAdminRoute,
});
this.route("newAdminNetworking", {
path: "/admin/networking",
controller: newAdminRoute,
});
this.route("newAdminMaintenance", {
path: "/admin/maintenance",
controller: newAdminRoute,
@@ -99,6 +99,10 @@ <h2>Configuration</h2>
<div class="item-name">App sources</div>
<div class="item-subtext">Where to look for apps and app updates.</div>
{{/adminNavItem}}
{{#adminNavItem routeName="newAdminNetworking"}}
<div class="item-name">Networking</div>
<div class="item-subtext">Control how the network is accessed.</div>
{{/adminNavItem}}
</ul>
</li>
<li>
@@ -0,0 +1,105 @@
// Sandstorm - Personal Cloud Sandbox
// Copyright (c) 2017 Sandstorm Development Group, Inc. and contributors
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { PRIVATE_IPV4_ADDRESSES, PRIVATE_IPV6_ADDRESSES } from "/imports/constants.js";
const DEFAULT_IP_BLACKLIST = PRIVATE_IPV4_ADDRESSES.concat(PRIVATE_IPV6_ADDRESSES).join("\n");
Template.newAdminNetworking.onCreated(function () {
this.originalIpBlacklist = globalDb.getSettingWithFallback("ipBlacklist", "");
this.ipBlacklist = new ReactiveVar(this.originalIpBlacklist);
this.formState = new ReactiveVar({
state: "edit", // Other allowed states: "submitting", "success", and "error"
message: undefined,
});
});
Template.newAdminNetworking.helpers({
ipBlacklist() {
const instance = Template.instance();
return instance.ipBlacklist.get();
},
saveDisabled() {
const instance = Template.instance();
return instance.formState.get().state === "submitting" ||
instance.ipBlacklist.get() === instance.originalIpBlacklist;
},
restoreDisabled() {
const instance = Template.instance();
return instance.ipBlacklist.get() === DEFAULT_IP_BLACKLIST;
},
hasError() {
const instance = Template.instance();
return instance.formState.get().state === "error";
},
hasSuccess() {
const instance = Template.instance();
return instance.formState.get().state === "success";
},
message() {
const instance = Template.instance();
return instance.formState.get().message;
},
});
Template.newAdminNetworking.events({
"submit .admin-networking"(evt) {
evt.preventDefault();
evt.stopPropagation();
},
"input textarea.ip-blacklist"(evt) {
evt.preventDefault();
evt.stopPropagation();
const instance = Template.instance();
instance.ipBlacklist.set(evt.currentTarget.value);
},
"click .save"(evt) {
const instance = Template.instance();
const newIpBlacklist = instance.ipBlacklist.get();
instance.formState.set({
state: "submitting",
message: "",
});
Meteor.call("setSetting", undefined, "ipBlacklist", newIpBlacklist, (err) => {
if (err) {
instance.formState.set({
state: "error",
message: err.message,
});
} else {
instance.originalIpBlacklist = newIpBlacklist;
instance.formState.set({
state: "success",
message: "Saved changes.",
});
}
});
},
"click .restore"(evt) {
const instance = Template.instance();
instance.ipBlacklist.set(DEFAULT_IP_BLACKLIST);
},
});
@@ -0,0 +1,37 @@
<template name="newAdminNetworking">
<h1>
<ul class="admin-breadcrumbs">
<li>{{#linkTo route="newAdminRoot"}}Admin{{/linkTo}}</li>
<li>Security</li>
</ul>
</h1>
{{#if hasSuccess}}
{{#focusingSuccessBox}}
{{message}}
{{/focusingSuccessBox}}
{{/if}}
{{#if hasError}}
{{#focusingErrorBox}}
{{message}}
{{/focusingErrorBox}}
{{/if}}
<form class="admin-networking">
<div class="form-group">
<label>
Server-side request IP blacklist:
<textarea class="ip-blacklist" value="{{ ipBlacklist }}"></textarea>
</label>
<span class="form-subtext">Users will be prohibited from making requests to these IP addresses. This includes making a request from an app, downloading an SPK file from a user-provided URL, etc. You may specify one IP address or network (in CIDR notation, e.g. "127.0.0.0/8") per line. The default value includes standard local and private network addresses. Note that when an HTTP proxy is in use, this setting may be ignored; the proxy must implement its own blacklist.</span>
</div>
{{!-- TODO(someday): Allow whitelisting certain IPs or hosts? --}}
{{!-- TODO(someday): Configure HTTP proxy here. --}}
<div class="button-row">
<button type="submit" class="save" disabled="{{saveDisabled}}">Save</button>
<button type="button" class="restore" disabled="{{restoreDisabled}}">Restore defaults</button>
</div>
</form>
</template>
@@ -0,0 +1,20 @@
.admin-networking {
@extend %standard-form;
textarea {
min-height: 200px;
font-family: monospace;
}
.save {
@extend %button-base;
@extend %button-primary;
margin-left: 10px;
}
.restore {
@extend %button-base;
@extend %button-secondary;
margin-left: 10px;
}
}
@@ -104,3 +104,4 @@
@import "_admin-users.scss";
@import "_admin-stats.scss";
@import "_admin-hosting-management.scss";
@import "_admin-networking.scss";
View
@@ -14,6 +14,56 @@
// See the License for the specific language governing permissions and
// limitations under the License.
ACCOUNT_DELETION_SUSPENSION_TIME = 7 * 60 * 60 * 24 * 1000; // 7 days in ms
const ACCOUNT_DELETION_SUSPENSION_TIME = 7 * 60 * 60 * 24 * 1000; // 7 days in ms
export { ACCOUNT_DELETION_SUSPENSION_TIME };
// Lists below developed from RFC6890, which is an overview of all special addresses.
const PRIVATE_IPV4_ADDRESSES = [
"10.0.0.0/8", // RFC1918 reserved for internal network
"127.0.0.0/8", // RFC1122 loopback / localhost
"169.254.0.0/16", // RFC3927 "link local" (auto-configured LAN in absence of DHCP)
"172.16.0.0/12", // RFC1918 reserved for internal network
"192.168.0.0/16", // RFC1918 reserved for internal network
];
const PRIVATE_IPV6_ADDRESSES = [
"::1/128", // RFC4291 loopback / localhost
"fc00::/7", // RFC4193 unique private network
"fe80::/10", // RFC4291 "link local" (auto-configured LAN in absence of DHCP)
];
const SPECIAL_IPV4_ADDRESSES = [
"0.0.0.0/8", // RFC1122 "this host" / wildcard
"100.64.0.0/10", // RFC6598 "shared address space" for carrier-grade NAT
"192.0.0.0/24", // RFC6890 reserved for special protocols
"192.0.2.0/24", // RFC5737 "example address" block 1 -- like example.com for IPs
"192.88.99.0/24", // RFC3068 6to4 relay
"198.18.0.0/15", // RFC2544 standard benchmarks
"198.51.100.0/24", // RFC5737 "example address" block 2 -- like example.com for IPs
"203.0.113.0/24", // RFC5737 "example address" block 3 -- like example.com for IPs
"224.0.0.0/4", // RFC1112 multicast
"240.0.0.0/4", // RFC1112 multicast / reserved for future use
"255.255.255.255/32" // RFC0919 broadcast address
];
const SPECIAL_IPV6_ADDRESSES = [
"::/128", // RFC4291 unspecified address / wildcard
"64:ff9b::/96", // RFC6052 IPv4-IPv6 translation
"::ffff:0:0/96", // RFC4291 IPv4-mapped address
// TODO(someday): I don't understand the difference between the above
// two. Both are described as mapping ip4 addresses into the ip6
// space. Perhaps this should be allowed, however, we'd need to
// filter the ip4 address against the ip4 blacklist, so special
// handling would be needed.
"100::/64", // RFC6666 discard-only address block
"2001::/23", // RFC2928 reserved for special protocols
"2001:2::/48", // RFC5180 standard benchmarks
"2001:db8::/32", // RFC3849 "example address" block -- like example.com for IPs
"2001:10::/28", // RFC4843 ORCHID
"2002::/16", // RFC3056 6to4 relay
"ff00::/8", // RFC4291 multicast
];
export {
ACCOUNT_DELETION_SUSPENSION_TIME, PRIVATE_IPV4_ADDRESSES, PRIVATE_IPV6_ADDRESSES,
SPECIAL_IPV4_ADDRESSES, SPECIAL_IPV6_ADDRESSES
};
@@ -9,6 +9,7 @@ import { _ } from "meteor/underscore";
import { Match } from "meteor/check";
import { userPictureUrl, fetchPicture } from "/imports/server/accounts/picture.js";
import { waitPromise } from "/imports/server/async-helpers.js";
import { PRIVATE_IPV4_ADDRESSES, PRIVATE_IPV6_ADDRESSES } from "/imports/constants.js";
const Future = Npm.require("fibers/future");
const Url = Npm.require("url");
@@ -777,6 +778,15 @@ function removeFeatureKeys(db, backend) {
db.notifications.remove({ "admin.type": "trialFeatureKeyExpired" });
}
function setIpBlacklist(db, backend) {
if (Meteor.settings.public.isTesting) {
db.collections.settings.insert({ _id: "ipBlacklist", value: "192.168.0.0/16" });
} else {
const defaultIpBlacklist = PRIVATE_IPV4_ADDRESSES.concat(PRIVATE_IPV6_ADDRESSES).join("\n");
db.collections.settings.insert({ _id: "ipBlacklist", value: defaultIpBlacklist });
}
}
// This must come after all the functions named within are defined.
// Only append to this list! Do not modify or remove list entries;
// doing so is likely change the meaning and semantics of user databases.
@@ -813,6 +823,7 @@ const MIGRATIONS = [
setNewServer,
addMembraneRequirementsToIdentities,
addEncryptionToFrontendRefIpNetwork,
setIpBlacklist,
];
const NEW_SERVER_STARTUP = [
Oops, something went wrong.

0 comments on commit 164997f

Please sign in to comment.