Skip to content

Commit

Permalink
flow rules
Browse files Browse the repository at this point in the history
  • Loading branch information
sinamics committed Jul 14, 2023
1 parent ef2b2f1 commit f996974
Show file tree
Hide file tree
Showing 12 changed files with 2,541 additions and 7 deletions.
1,083 changes: 1,083 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
"studio": "prisma studio"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.1.9",
"@codemirror/lang-markdown": "^6.2.0",
"@codemirror/lang-python": "^6.1.3",
"@codemirror/language-data": "^6.3.1",
"@heroicons/react": "^2.0.16",
"@next-auth/prisma-adapter": "^1.0.5",
"@prisma/client": "^4.9.0",
Expand All @@ -23,6 +27,9 @@
"@trpc/next": "^10.33.1",
"@trpc/react-query": "^10.33.1",
"@trpc/server": "^10.33.1",
"@uiw/codemirror-extensions-classname": "^4.21.7",
"@uiw/codemirror-theme-okaidia": "^4.21.7",
"@uiw/react-codemirror": "^4.21.7",
"axios": "^1.3.4",
"bcryptjs": "^2.4.3",
"classnames": "^2.3.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
*/
-- AlterTable
ALTER TABLE "network" ADD COLUMN "autoAssignIp" BOOLEAN DEFAULT true,
ADD COLUMN "flowRule" TEXT,
ADD COLUMN "ipAssignments" TEXT NOT NULL;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ model network_members {
model network {
nwid String @id
nwname String
flowRule String?
autoAssignIp Boolean? @default(true)
ipAssignments String
nw_userid User @relation(fields: [authorId], references: [id])
Expand Down
2 changes: 1 addition & 1 deletion src/components/modules/addMemberById.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const AddMemberById = () => {
</span>
</label>
<label className="input-group">
<span>Member ID</span>
<span className="bg-base-300">Member ID</span>
<input
onChange={inputHandler}
name="memberid"
Expand Down
123 changes: 123 additions & 0 deletions src/components/modules/networkFlowRules.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { toast } from "react-hot-toast";
import { api } from "~/utils/api";
import CodeMirror from "@uiw/react-codemirror";
import { okaidia } from "@uiw/codemirror-theme-okaidia";
import { classname } from "@uiw/codemirror-extensions-classname";
import { python } from "@codemirror/lang-python";
import { useEffect, useState } from "react";
import { EditorView } from "@codemirror/view";
// import { useDebounce } from "usehooks-ts";
import { type CustomBackendError } from "~/types/errorHandling";
import { useRouter } from "next/router";

const initialErrorState = { error: null, line: null };
export const NetworkFlowRules = () => {
const { query } = useRouter();
// Local state to store changes to the flow route
const {
data: defaultFlowRoute,
// isLoading,
mutate: fetchFlowRoute,
} = api.network.getFlowRule.useMutation();

const [flowRoute, setFlowRoute] = useState(defaultFlowRoute);
const [ruleError, setRuleError] = useState(initialErrorState);
// const debouncedFlowRoute = useDebounce(flowRoute, 500);

const { mutate: updateFlowRoute } = api.network.setFlowRule.useMutation({
onSuccess: () => {
void fetchFlowRoute({ nwid: query.id as string, reset: false });
},
onError: ({ message }) => {
try {
const err = JSON.parse(message) as CustomBackendError;
setRuleError({ error: err.error, line: err.line });
void toast.error(err.error);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
});

// const debouncedUpdateFlowRoute = useCallback(() => {
// void updateFlowRoute({ flowRoute });
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [debouncedFlowRoute]);
useEffect(() => {
setFlowRoute(defaultFlowRoute);
}, [defaultFlowRoute]);
useEffect(() => {
fetchFlowRoute({
nwid: query.id as string,
reset: false,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Handle changes in CodeMirror
const handleFlowRouteChange = (value: string) => {
setFlowRoute(value);
setRuleError(initialErrorState);
// debouncedUpdateFlowRoute();
};
// Reset the flow route to the default value
const handleReset = () => {
setFlowRoute(defaultFlowRoute);
void fetchFlowRoute({ nwid: query.id as string, reset: true });
setRuleError(initialErrorState);
};
const classnameExt = classname({
add: (lineNumber) => {
if (lineNumber === ruleError.line) {
return "first-line";
}
},
});
const errorColorTheme = EditorView.baseTheme({
"&dark .first-line": { backgroundColor: "#AB2204" },
"&light .first-line": { backgroundColor: "#AB2204" },
});

return (
<div
tabIndex={0}
className="collapse-arrow collapse w-full border border-base-300 bg-base-200"
>
<input type="checkbox" />
<div className="collapse-title">Flow Rules</div>
<div className="collapse-content" style={{ width: "100%" }}>
<CodeMirror
tabIndex={0}
value={flowRoute}
onChange={handleFlowRouteChange}
maxHeight="1500px"
// height="1000px"
width="100%"
theme={okaidia}
extensions={[python(), errorColorTheme, classnameExt]}
basicSetup={{
lineNumbers: false,
highlightActiveLineGutter: false,
highlightActiveLine: false,
}}
/>
<div className="space-x-4">
<button
onClick={() =>
void updateFlowRoute({
flowRoute: flowRoute || "#",
nwid: query.id as string,
})
}
className="btn my-3 bg-base-300"
>
Save Changes
</button>
<button onClick={handleReset} className="btn-outline btn my-3 ">
Reset
</button>
</div>
</div>
</div>
);
};
5 changes: 3 additions & 2 deletions src/components/modules/networkIpAssignments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { toast } from "react-hot-toast";
import { type CustomError } from "~/types/errorHandling";
import { api } from "~/utils/api";
import cn from "classnames";

export const NetworkIpAssignment = () => {
const { query } = useRouter();
const {
Expand Down Expand Up @@ -72,7 +73,7 @@ export const NetworkIpAssignment = () => {
<div
key={cidr}
className={cn(
"badge badge-ghost badge-outline badge-lg rounded-md text-xs opacity-30 md:text-base",
"badge-ghost badge-outline badge badge-lg rounded-md text-xs opacity-30 md:text-base",
{
"badge badge-lg rounded-md bg-primary text-xs text-white opacity-70 md:text-base":
network.autoAssignIp,
Expand All @@ -86,7 +87,7 @@ export const NetworkIpAssignment = () => {
key={cidr}
onClick={() => submitUpdate({ ipPool: cidr })}
className={cn(
"badge badge-ghost badge-outline badge-lg rounded-md text-xs opacity-30 md:text-base",
"badge-ghost badge-outline badge badge-lg rounded-md text-xs opacity-30 md:text-base",
{ "hover:bg-primary": network.autoAssignIp }
)}
>
Expand Down
4 changes: 4 additions & 0 deletions src/pages/network/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Input from "~/components/elements/input";
import toast from "react-hot-toast";
import { DeletedNetworkMembersTable } from "~/components/modules/deletedNetworkMembersTable";
import { useModalStore } from "~/utils/store";
import { NetworkFlowRules } from "~/components/modules/networkFlowRules";

const NetworkById = () => {
const [state, setState] = useState({
Expand Down Expand Up @@ -255,6 +256,9 @@ const NetworkById = () => {
) : null}
</div>
</div>
<div className="w-5/5 mx-auto flex px-4 py-4 text-sm sm:w-4/5 sm:px-10 md:text-base">
<NetworkFlowRules />
</div>
<div className="w-5/5 divider mx-auto flex px-4 py-4 text-sm sm:w-4/5 sm:px-10 md:text-base">
Network Actions
</div>
Expand Down
141 changes: 141 additions & 0 deletions src/server/api/routers/networkRouter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { IPv4gen } from "~/utils/IPv4gen";
Expand All @@ -21,6 +23,7 @@ import {
} from "~/types/network";
import { type APIError } from "~/server/helpers/errorHandler";
import { type network_members } from "@prisma/client";
import RuleCompiler from "~/utils/rule-compiler";

const customConfig: Config = {
dictionaries: [adjectives, animals],
Expand Down Expand Up @@ -383,4 +386,142 @@ export const networkRouter = createTRPCRouter({
}
}
}),
setFlowRule: protectedProcedure
.input(
z.object({
nwid: z.string().nonempty(),
flowRoute: z.string().nonempty(),
})
)
.mutation(async ({ ctx, input }) => {
const { flowRoute } = input;

const rules = [];
const caps = {};
const tags = {};
// try {
const err: string[] = RuleCompiler(flowRoute, rules, caps, tags);
if (err) {
console.log("ERRRRRRRRRRRROR", err);
throw new TRPCError({
message: JSON.stringify({
error: `ERROR parsing ${process.argv[2]} line ${err[0]} column ${err[1]}: ${err[2]}`,
line: err[0],
}),
code: "BAD_REQUEST",
cause: err[0],
});
}
const capsArray = [];
const capabilitiesByName = {};
for (const n in caps) {
capsArray.push(caps[n]);
capabilitiesByName[n] = caps[n].id;
}
const tagsArray = [];
for (const n in tags) {
const t = tags[n];
const dfl = t["default"];
tagsArray.push({
id: t.id,
default: dfl || dfl === 0 ? dfl : null,
});
}

// const res = JSON.stringify({
// config: {
// rules: rules,
// capabilities: capsArray,
// tags: tagsArray,
// },
// capabilitiesByName: capabilitiesByName,
// tagsByName: tags,
// });
// console.log(res, null, 2);

// update zerotier network with the new flow route
await ztController.network_update(input.nwid, {
rules,
capabilities: capsArray,
tags: tagsArray,
});

// update network in prisma
await ctx.prisma.network.update({
where: { nwid: input.nwid },
data: {
flowRule: flowRoute,
},
});
// resolve(res);
// } catch (error) {
// throw new TRPCError({
// message: `FAILED! ERROR parsing: ${error}`,
// code: "BAD_REQUEST",
// });

// // console.log("error", error);
// }
}),
getFlowRule: protectedProcedure
.input(
z.object({
nwid: z.string().nonempty(),
reset: z.boolean().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const DEFAULT_NETWORK_ROUTE_CONFIG = `#
# This is a default rule set that allows IPv4 and IPv6 traffic but otherwise
# behaves like a standard Ethernet switch.
#
# Please keep in mind that ZeroTier versions prior to 1.2.0 do NOT support advanced
# network rules.
#
# Since both senders and receivers enforce rules, you will get the following
# behavior in a network with both old and new versions:
#
# (old: 1.1.14 and older, new: 1.2.0 and newer)
#
# old <--> old: No rules are honored.
# old <--> new: Rules work but are only enforced by new side. Tags will NOT work, and
# capabilities will only work if assigned to the new side.
# new <--> new: Full rules engine support including tags and capabilities.
#
# We recommend upgrading all your devices to 1.2.0 as soon as convenient. Version
# 1.2.0 also includes a significantly improved software update mechanism that is
# turned on by default on Mac and Windows. (Linux and mobile are typically kept up
# to date using package/app management.)
#
#
# Allow only IPv4, IPv4 ARP, and IPv6 Ethernet frames.
#
drop
not ethertype ipv4
and not ethertype arp
and not ethertype ipv6
;
#
# Uncomment to drop non-ZeroTier issued and managed IP addresses.
#
# This prevents IP spoofing but also blocks manual IP management at the OS level and
# bridging unless special rules to exempt certain hosts or traffic are added before
# this rule.
#
#drop
# not chr ipauth
#;
# Accept anything else. This is required since default is 'drop'.
accept;`;

const flow = await ctx.prisma.network.findFirst({
where: { nwid: input.nwid },
});

if (!flow.flowRule || input.reset) {
return DEFAULT_NETWORK_ROUTE_CONFIG;
}

return flow.flowRule;
}),
});
6 changes: 6 additions & 0 deletions src/types/errorHandling.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ interface ShapeError {
data?: ErrorData;
}

interface CustomBackendError {
error?: string;
line?: number;
}

export interface CustomError {
shape?: ShapeError;
message?: CustomBackendError;
}
Loading

0 comments on commit f996974

Please sign in to comment.