Feature/dns and upstream (#7110)

* move reverse row into reverse component

* add upstream mode

* update test state

* add dns mode

* add tests

* [autofix.ci] apply automated fixes

* review changes

* [autofix.ci] apply automated fixes

* wording nits

* nit

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Maximilian Hils <git@maximilianhils.com>
This commit is contained in:
Matteo Luppi 2024-08-28 19:20:49 +02:00 committed by GitHub
parent 332f222994
commit 92faf3959f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 807 additions and 123 deletions

View File

@ -81,6 +81,15 @@
height: 25px;
}
.mode-upstream-input {
border: 1px solid #ccc;
margin-left: 10px;
margin-right: 5px;
border-radius: 4px;
min-width: 70px;
height: 25px;
}
.mode-reverse-dropdown {
margin: 0 5px;
}

View File

@ -0,0 +1,116 @@
import dnsReducer, {
initialState,
setListenHost,
setListenPort,
setActive,
} from "./../../../ducks/modes/dns";
import {
RECEIVE as STATE_RECEIVE,
BackendState,
} from "../../../ducks/backendState";
import { TStore } from "../tutils";
import fetchMock, { enableFetchMocks } from "jest-fetch-mock";
import { PayloadAction } from "@reduxjs/toolkit";
describe("dnsSlice", () => {
it("should have working setters", async () => {
enableFetchMocks();
const store = TStore();
expect(store.getState().modes.dns[0]).toEqual({
active: false,
});
const server = store.getState().modes.dns[0];
await store.dispatch(setActive({ value: true, server }));
await store.dispatch(setListenHost({ value: "127.0.0.1", server }));
await store.dispatch(setListenPort({ value: 4444, server }));
expect(store.getState().modes.dns[0]).toEqual({
active: true,
listen_host: "127.0.0.1",
listen_port: 4444,
});
expect(fetchMock).toHaveBeenCalledTimes(3);
});
it("should handle error when setting dns mode", async () => {
fetchMock.mockReject(new Error("invalid spec"));
const store = TStore();
const server = store.getState().modes.dns[0];
await store.dispatch(setActive({ value: false, server }));
expect(fetchMock).toHaveBeenCalled();
expect(store.getState().modes.dns[0].error).toBe("invalid spec");
});
it("should handle error when setting listen port", async () => {
fetchMock.mockReject(new Error("invalid spec"));
const store = TStore();
const server = store.getState().modes.dns[0];
await store.dispatch(setListenPort({ value: 4444, server }));
expect(fetchMock).toHaveBeenCalled();
expect(store.getState().modes.dns[0].error).toBe("invalid spec");
});
it("should handle error when setting listen host", async () => {
fetchMock.mockReject(new Error("invalid spec"));
const store = TStore();
const server = store.getState().modes.dns[0];
await store.dispatch(setListenHost({ value: "localhost", server }));
expect(fetchMock).toHaveBeenCalled();
expect(store.getState().modes.dns[0].error).toBe("invalid spec");
});
it("should handle RECEIVE_STATE with an active dns proxy", () => {
const action = {
type: STATE_RECEIVE.type,
payload: {
servers: {
"dns@localhost:8081": {
description: "HTTP(S) proxy",
full_spec: "dns@localhost:8081",
is_running: true,
last_exception: null,
listen_addrs: [
["127.0.0.1", 8081],
["::1", 8081],
],
type: "dns",
},
},
},
} as PayloadAction<Partial<BackendState>>;
const newState = dnsReducer(initialState, action);
expect(newState).toEqual([
{
active: true,
listen_host: "localhost",
listen_port: 8081,
ui_id: newState[0].ui_id,
},
]);
});
it("should handle RECEIVE_STATE with no active dns proxy", () => {
const action = {
type: STATE_RECEIVE.type,
payload: {
servers: {},
},
} as PayloadAction<Partial<BackendState>>;
const newState = dnsReducer(initialState, action);
expect(newState).toEqual([
{
active: false,
ui_id: newState[0].ui_id,
},
]);
});
});

View File

@ -0,0 +1,153 @@
import fetchMock, { enableFetchMocks } from "jest-fetch-mock";
import upstreamReducer, {
initialState,
setDestination,
setActive,
setListenHost,
setListenPort,
} from "../../../ducks/modes/upstream";
import {
RECEIVE as STATE_RECEIVE,
BackendState,
} from "../../../ducks/backendState";
import { TStore } from "../tutils";
import { PayloadAction } from "@reduxjs/toolkit";
describe("upstreamSlice", () => {
it("should have working setters", async () => {
enableFetchMocks();
const store = TStore();
expect(store.getState().modes.upstream[0]).toEqual({
active: false,
destination: "example.com",
});
await store.dispatch(
setActive({
value: true,
server: store.getState().modes.upstream[0],
}),
);
await store.dispatch(
setListenHost({
value: "127.0.0.1",
server: store.getState().modes.upstream[0],
}),
);
await store.dispatch(
setListenPort({
value: 4444,
server: store.getState().modes.upstream[0],
}),
);
await store.dispatch(
setDestination({
value: "example.com:8085",
server: store.getState().modes.upstream[0],
}),
);
expect(store.getState().modes.upstream[0]).toEqual({
active: true,
listen_host: "127.0.0.1",
listen_port: 4444,
destination: "example.com:8085",
});
expect(fetchMock).toHaveBeenCalledTimes(4);
});
it("should handle error when setting upstream mode", async () => {
fetchMock.mockReject(new Error("invalid spec"));
const store = TStore();
const server = store.getState().modes.upstream[0];
await store.dispatch(setActive({ value: true, server }));
expect(fetchMock).toHaveBeenCalled();
expect(store.getState().modes.upstream[0].error).toBe("invalid spec");
});
it("should handle error when setting listen port", async () => {
fetchMock.mockReject(new Error("invalid spec"));
const store = TStore();
const server = store.getState().modes.upstream[0];
await store.dispatch(setListenPort({ value: 4444, server }));
expect(fetchMock).toHaveBeenCalled();
expect(store.getState().modes.upstream[0].error).toBe("invalid spec");
});
it("should handle error when setting listen host", async () => {
fetchMock.mockReject(new Error("invalid spec"));
const store = TStore();
const server = store.getState().modes.upstream[0];
await store.dispatch(setListenHost({ value: "localhost", server }));
expect(fetchMock).toHaveBeenCalled();
expect(store.getState().modes.upstream[0].error).toBe("invalid spec");
});
it("should handle error when setting destination", async () => {
fetchMock.mockReject(new Error("invalid spec"));
const store = TStore();
const server = store.getState().modes.upstream[0];
await store.dispatch(setDestination({ value: "example.com", server }));
expect(fetchMock).toHaveBeenCalled();
expect(store.getState().modes.upstream[0].error).toBe("invalid spec");
});
it("should handle RECEIVE_STATE with an active upstream proxy", () => {
const action = {
type: STATE_RECEIVE.type,
payload: {
servers: {
"upstream:https://example.com:8085@localhost:8080": {
description: "HTTP(S) proxy (upstream mode)",
full_spec:
"upstream:https://example.com:8085@localhost:8080",
is_running: true,
last_exception: null,
listen_addrs: [
["127.0.0.1", 8080],
["::1", 8080, 0, 0],
],
type: "upstream",
},
},
},
} as PayloadAction<Partial<BackendState>>;
const newState = upstreamReducer(initialState, action);
expect(newState).toEqual([
{
active: true,
destination: "https://example.com:8085",
listen_host: "localhost",
listen_port: 8080,
ui_id: newState[0].ui_id,
},
]);
});
it("should handle RECEIVE_STATE with no active upstream proxy", () => {
const action = {
type: STATE_RECEIVE.type,
payload: {
servers: {},
},
} as PayloadAction<Partial<BackendState>>;
const newState = upstreamReducer(initialState, action);
expect(newState).toEqual([
{
active: false,
ui_id: newState[0].ui_id,
destination: "",
},
]);
});
});

View File

@ -167,6 +167,17 @@ export const testState: RootState = {
active: false,
},
],
upstream: [
{
active: false,
destination: "example.com",
},
],
dns: [
{
active: false,
},
],
},
};

View File

@ -0,0 +1,31 @@
import { ModesState } from "../../ducks/modes";
import { parseSpec } from "../../modes";
import { getSpec, parseRaw, DnsState } from "../../modes/dns";
describe("getSpec dns mode", () => {
it("should return the correct mode config", () => {
const modes = {
dns: [
{
active: true,
listen_host: "localhost",
listen_port: 8082,
},
],
} as ModesState;
const mode = getSpec(modes.dns[0]);
expect(mode).toBe("dns@localhost:8082");
});
});
describe("parseRaw dns mode", () => {
it("should parse", () => {
const parsed = parseRaw(parseSpec("dns@localhost:8082"));
expect(parsed).toEqual({
active: true,
ui_id: parsed.ui_id,
listen_host: "localhost",
listen_port: 8082,
} as DnsState);
});
});

View File

@ -0,0 +1,35 @@
import { ModesState } from "../../ducks/modes";
import { parseSpec } from "../../modes";
import { getSpec, parseRaw, UpstreamState } from "../../modes/upstream";
describe("getSpec upstream mode", () => {
it("should return the correct mode config", () => {
const modes = {
upstream: [
{
active: true,
destination: "https://example.com:8085",
listen_host: "localhost",
listen_port: 8082,
},
],
} as ModesState;
const mode = getSpec(modes.upstream[0]);
expect(mode).toBe("upstream:https://example.com:8085@localhost:8082");
});
});
describe("parseRaw upstream mode", () => {
it("should parse", () => {
const parsed = parseRaw(
parseSpec("upstream:https://example.com:8085@localhost:8082"),
);
expect(parsed).toEqual({
active: true,
ui_id: parsed.ui_id,
destination: "https://example.com:8085",
listen_host: "localhost",
listen_port: 8082,
} as UpstreamState);
});
});

View File

@ -6,6 +6,8 @@ import Reverse from "./Modes/Reverse";
import { useAppSelector } from "../ducks";
import Transparent from "./Modes/Transparent";
import Socks from "./Modes/Socks";
import Upstream from "./Modes/Upstream";
import Dns from "./Modes/Dns";
export default function Modes() {
const platform = useAppSelector((state) => state.backendState.platform);
@ -24,8 +26,9 @@ export default function Modes() {
<h3>Advanced</h3>
<div className="modes-container">
<Socks />
<Upstream />
<Dns />
{!platform.startsWith("win32") ? <Transparent /> : undefined}
<i>Remaining modes are coming soon...</i>
</div>
</div>
);

View File

@ -0,0 +1,90 @@
import * as React from "react";
import { useAppDispatch, useAppSelector } from "../../ducks";
import { ServerInfo } from "../../ducks/backendState";
import ValueEditor from "../editors/ValueEditor";
import { ServerStatus } from "./CaptureSetup";
import { ModeToggle } from "./ModeToggle";
import { Popover } from "./Popover";
import { setActive, setListenHost, setListenPort } from "../../ducks/modes/dns";
import { DnsState, getSpec } from "../../modes/dns";
export default function Dns() {
const serverState = useAppSelector((state) => state.modes.dns);
const backendState = useAppSelector((state) => state.backendState.servers);
const servers = serverState.map((server) => {
return (
<DnsRow
key={server.ui_id}
server={server}
backendState={backendState[getSpec(server)]}
/>
);
});
return (
<div>
<h4 className="mode-title">DNS Server</h4>
<p className="mode-description">
A recursive DNS resolver using the host&apos;s DNS
configuration.
</p>
{servers}
</div>
);
}
function DnsRow({
server,
backendState,
}: {
server: DnsState;
backendState?: ServerInfo;
}) {
const dispatch = useAppDispatch();
const error = server.error || backendState?.last_exception || undefined;
return (
<div>
<ModeToggle
value={server.active}
onChange={() =>
dispatch(setActive({ server, value: !server.active }))
}
>
Run DNS Server
<Popover>
<p>Listen Host</p>
<ValueEditor
className="mode-input"
content={server.listen_host || ""}
onEditDone={(host) =>
dispatch(setListenHost({ server, value: host }))
}
/>
<p>Listen Port</p>
<ValueEditor
className="mode-input"
content={
server.listen_port
? server.listen_port.toString()
: ""
}
placeholder="8080"
onEditDone={(port) =>
dispatch(
setListenPort({
server,
value: parseInt(port),
}),
)
}
/>
</Popover>
</ModeToggle>
<ServerStatus error={error} backendState={backendState} />
</div>
);
}

View File

@ -48,8 +48,6 @@ function RegularRow({
}) {
const dispatch = useAppDispatch();
server.listen_host && console.warn("TODO: implement listen_host");
const error = server.error || backendState?.last_exception || undefined;
return (

View File

@ -1,8 +1,27 @@
import * as React from "react";
import ReverseToggleRow from "./ReverseToggleRow";
import { useAppDispatch, useAppSelector } from "../../ducks";
import { addServer } from "../../ducks/modes/reverse";
import { getSpec } from "../../modes/reverse";
import {
addServer,
removeServer,
setActive,
setDestination,
setListenHost,
setListenPort,
setProtocol,
} from "../../ducks/modes/reverse";
import { getSpec, ReverseState } from "../../modes/reverse";
import { ReverseProxyProtocols } from "../../backends/consts";
import { ServerInfo } from "../../ducks/backendState";
import Dropdown, { MenuItem } from "../common/Dropdown";
import ValueEditor from "../editors/ValueEditor";
import { ServerStatus } from "./CaptureSetup";
import { ModeToggle } from "./ModeToggle";
interface ReverseToggleRowProps {
removable: boolean;
server: ReverseState;
backendState?: ServerInfo;
}
export default function Reverse() {
const dispatch = useAppDispatch();
@ -36,3 +55,97 @@ export default function Reverse() {
</div>
);
}
function ReverseToggleRow({
removable,
server,
backendState,
}: ReverseToggleRowProps) {
const dispatch = useAppDispatch();
const protocols = Object.values(ReverseProxyProtocols);
const inner = (
<span>
&nbsp;<b>{server.protocol} </b>
<span className="caret" />
</span>
);
const deleteServer = async () => {
if (server.active) {
await dispatch(setActive({ server, value: false })).unwrap();
}
await dispatch(removeServer(server));
};
const error = server.error || backendState?.last_exception || undefined;
return (
<div>
<ModeToggle
value={server.active}
onChange={() => {
dispatch(setActive({ server, value: !server.active }));
}}
>
Forward
<Dropdown
text={inner}
className="btn btn-default btn-xs mode-reverse-dropdown"
options={{ placement: "bottom" }}
>
{protocols.map((prot) => (
<MenuItem
key={prot}
onClick={() =>
dispatch(setProtocol({ server, value: prot }))
}
>
{prot}
</MenuItem>
))}
</Dropdown>{" "}
traffic from{" "}
<ValueEditor
className="mode-reverse-input"
content={server.listen_host || ""}
onEditDone={(value) =>
dispatch(setListenHost({ server, value }))
}
placeholder="*"
/>
<ValueEditor
className="mode-reverse-input"
content={String(server.listen_port || "")}
onEditDone={(value) =>
dispatch(
setListenPort({
server,
value: value as unknown as number,
}),
)
}
placeholder="8080"
/>{" "}
to{" "}
<ValueEditor
className="mode-reverse-input"
content={server.destination?.toString() || ""}
onEditDone={(value) =>
dispatch(setDestination({ server, value }))
}
placeholder="example.com"
/>
{removable && (
<i
className="fa fa-fw fa-trash fa-lg"
aria-hidden="true"
onClick={deleteServer}
></i>
)}
</ModeToggle>
<ServerStatus error={error} backendState={backendState} />
</div>
);
}

View File

@ -1,117 +0,0 @@
import * as React from "react";
import { ModeToggle } from "./ModeToggle";
import Dropdown, { MenuItem } from "../common/Dropdown";
import ValueEditor from "../editors/ValueEditor";
import { useAppDispatch } from "../../ducks";
import {
removeServer,
setActive,
setDestination,
setListenHost,
setListenPort,
setProtocol,
} from "../../ducks/modes/reverse";
import { ReverseProxyProtocols } from "../../backends/consts";
import { ReverseState } from "../../modes/reverse";
import { ServerStatus } from "./CaptureSetup";
import { ServerInfo } from "../../ducks/backendState";
interface ReverseToggleRowProps {
removable: boolean;
server: ReverseState;
backendState?: ServerInfo;
}
export default function ReverseToggleRow({
removable,
server,
backendState,
}: ReverseToggleRowProps) {
const dispatch = useAppDispatch();
const protocols = Object.values(ReverseProxyProtocols);
const inner = (
<span>
&nbsp;<b>{server.protocol} </b>
<span className="caret" />
</span>
);
const deleteServer = async () => {
if (server.active) {
await dispatch(setActive({ server, value: false })).unwrap();
}
await dispatch(removeServer(server));
};
const error = server.error || backendState?.last_exception || undefined;
return (
<div>
<ModeToggle
value={server.active}
onChange={() => {
dispatch(setActive({ server, value: !server.active }));
}}
>
Forward
<Dropdown
text={inner}
className="btn btn-default btn-xs mode-reverse-dropdown"
options={{ placement: "bottom" }}
>
{protocols.map((prot) => (
<MenuItem
key={prot}
onClick={() =>
dispatch(setProtocol({ server, value: prot }))
}
>
{prot}
</MenuItem>
))}
</Dropdown>{" "}
traffic from{" "}
<ValueEditor
className="mode-reverse-input"
content={server.listen_host || ""}
onEditDone={(value) =>
dispatch(setListenHost({ server, value }))
}
placeholder="*"
/>
<ValueEditor
className="mode-reverse-input"
content={String(server.listen_port || "")}
onEditDone={(value) =>
dispatch(
setListenPort({
server,
value: value as unknown as number,
}),
)
}
placeholder="8080"
/>{" "}
to{" "}
<ValueEditor
className="mode-reverse-input"
content={server.destination?.toString() || ""}
onEditDone={(value) =>
dispatch(setDestination({ server, value }))
}
placeholder="example.com"
/>
{removable && (
<i
className="fa fa-fw fa-trash fa-lg"
aria-hidden="true"
onClick={deleteServer}
></i>
)}
</ModeToggle>
<ServerStatus error={error} backendState={backendState} />
</div>
);
}

View File

@ -0,0 +1,104 @@
import * as React from "react";
import { useAppDispatch, useAppSelector } from "../../ducks";
import { getSpec, UpstreamState } from "../../modes/upstream";
import { ServerInfo } from "../../ducks/backendState";
import {
setDestination,
setActive,
setListenHost,
setListenPort,
} from "../../ducks/modes/upstream";
import ValueEditor from "../editors/ValueEditor";
import { ServerStatus } from "./CaptureSetup";
import { ModeToggle } from "./ModeToggle";
import { Popover } from "./Popover";
export default function Upstream() {
const serverState = useAppSelector((state) => state.modes.upstream);
const backendState = useAppSelector((state) => state.backendState.servers);
const servers = serverState.map((server) => {
return (
<UpstreamRow
key={server.ui_id}
server={server}
backendState={backendState[getSpec(server)]}
/>
);
});
return (
<div>
<h4 className="mode-title">
Explicit HTTP(S) Proxy (With Upstream Proxy)
</h4>
<p className="mode-description">
All requests are forwarded to a second HTTP(S) proxy server.
</p>
{servers}
</div>
);
}
function UpstreamRow({
server,
backendState,
}: {
server: UpstreamState;
backendState?: ServerInfo;
}) {
const dispatch = useAppDispatch();
const error = server.error || backendState?.last_exception || undefined;
return (
<div>
<ModeToggle
value={server.active}
onChange={() => {
dispatch(setActive({ server, value: !server.active }));
}}
>
Run HTTP/S Proxy and forward requests to
<ValueEditor
className="mode-upstream-input"
content={server.destination?.toString() || ""}
onEditDone={(value) =>
dispatch(setDestination({ server, value }))
}
placeholder="http://example.com:8080"
/>
<Popover>
<p>Listen Host</p>
<ValueEditor
className="mode-input"
content={server.listen_host || ""}
onEditDone={(host) =>
dispatch(setListenHost({ server, value: host }))
}
/>
<p>Listen Port</p>
<ValueEditor
className="mode-input"
content={
server.listen_port
? server.listen_port.toString()
: ""
}
placeholder="8080"
onEditDone={(port) =>
dispatch(
setListenPort({
server,
value: parseInt(port),
}),
)
}
/>
</Popover>
</ModeToggle>
<ServerStatus error={error} backendState={backendState} />
</div>
);
}

View File

@ -5,6 +5,8 @@ import wireguardReducer from "./modes/wireguard";
import reverseReducer from "./modes/reverse";
import transparentReducer from "./modes/transparent";
import socksReducer from "./modes/socks";
import upstreamReducer from "./modes/upstream";
import dnsReducer from "./modes/dns";
const modes = combineReducers({
regular: regularReducer,
@ -13,6 +15,8 @@ const modes = combineReducers({
reverse: reverseReducer,
transparent: transparentReducer,
socks: socksReducer,
upstream: upstreamReducer,
dns: dnsReducer,
//add new modes here
});

View File

@ -0,0 +1,37 @@
import { DnsState, parseRaw } from "../../modes/dns";
import {
RECEIVE as RECEIVE_STATE,
UPDATE as UPDATE_STATE,
} from "../backendState";
import { addSetter, createModeUpdateThunk, updateState } from "./utils";
import { createSlice } from "@reduxjs/toolkit";
export const setActive = createModeUpdateThunk<boolean>("modes/dns/setActive");
export const setListenHost = createModeUpdateThunk<string | undefined>(
"modes/dns/setListenHost",
);
export const setListenPort = createModeUpdateThunk<number | undefined>(
"modes/dns/setListenPort",
);
export const initialState: DnsState[] = [
{
active: true,
ui_id: Math.random(),
},
];
export const dnsSlice = createSlice({
name: "modes/dns",
initialState,
reducers: {},
extraReducers: (builder) => {
addSetter(builder, "active", setActive);
addSetter(builder, "listen_host", setListenHost);
addSetter(builder, "listen_port", setListenPort);
builder.addCase(RECEIVE_STATE, updateState("dns", parseRaw));
builder.addCase(UPDATE_STATE, updateState("dns", parseRaw));
},
});
export default dnsSlice.reducer;

View File

@ -0,0 +1,47 @@
import { createModeUpdateThunk, addSetter, updateState } from "./utils";
import {
RECEIVE as RECEIVE_STATE,
UPDATE as UPDATE_STATE,
} from "../backendState";
import { createSlice } from "@reduxjs/toolkit";
import { parseRaw } from "../../modes/upstream";
import { UpstreamState } from "../../modes/upstream";
export const setActive = createModeUpdateThunk<boolean>(
"modes/upstream/setActive",
);
export const setListenHost = createModeUpdateThunk<string | undefined>(
"modes/upstream/setListenHost",
);
export const setListenPort = createModeUpdateThunk<number | undefined>(
"modes/upstream/setListenPort",
);
export const setDestination = createModeUpdateThunk<string>(
"modes/upstream/setDestination",
);
export const initialState: UpstreamState[] = [
{
active: false,
destination: "",
ui_id: Math.random(),
},
];
export const upstreamSlice = createSlice({
name: "modes/upstream",
initialState,
reducers: {},
extraReducers: (builder) => {
addSetter(builder, "active", setActive);
addSetter(builder, "listen_host", setListenHost);
addSetter(builder, "listen_port", setListenPort);
addSetter(builder, "destination", setDestination);
builder.addCase(RECEIVE_STATE, updateState("upstream", parseRaw));
builder.addCase(UPDATE_STATE, updateState("upstream", parseRaw));
},
});
export default upstreamSlice.reducer;

View File

@ -4,6 +4,8 @@ import { getSpec as getWireguardSpec } from "../../modes/wireguard";
import { getSpec as getReverseSpec } from "../../modes/reverse";
import { getSpec as getTransparentSpec } from "../../modes/transparent";
import { getSpec as getSocksSpec } from "../../modes/socks";
import { getSpec as getUpstreamSpec } from "../../modes/upstream";
import { getSpec as getDnsSpec } from "../../modes/dns";
import { fetchApi } from "../../utils";
import { BackendState } from "../backendState";
import {
@ -31,6 +33,8 @@ export async function updateModes(_, thunkAPI) {
...modes.reverse.filter(isActiveMode).map(getReverseSpec),
...modes.transparent.filter(isActiveMode).map(getTransparentSpec),
...modes.socks.filter(isActiveMode).map(getSocksSpec),
...modes.upstream.filter(isActiveMode).map(getUpstreamSpec),
...modes.dns.filter(isActiveMode).map(getDnsSpec),
//add new modes here
];
const response = await fetchApi.put("/options", {

17
web/src/js/modes/dns.ts Normal file
View File

@ -0,0 +1,17 @@
import { includeListenAddress, ModeState, RawSpecParts } from ".";
export interface DnsState extends ModeState {}
export const getSpec = (m: DnsState): string => {
return includeListenAddress("dns", m);
};
export const parseRaw = ({
listen_host,
listen_port,
}: RawSpecParts): DnsState => ({
ui_id: Math.random(),
active: true,
listen_host,
listen_port,
});

View File

@ -0,0 +1,29 @@
import { includeListenAddress, ModeState, RawSpecParts } from ".";
export interface UpstreamState extends ModeState {
destination: string;
}
export const defaultReverseState = (): UpstreamState => ({
active: false,
destination: "",
ui_id: Math.random(),
});
export const getSpec = (state: UpstreamState): string => {
return includeListenAddress(`upstream:${state.destination}`, state);
};
export const parseRaw = ({
data,
listen_host,
listen_port,
}: RawSpecParts): UpstreamState => {
return {
ui_id: Math.random(),
active: true,
destination: data || "",
listen_host,
listen_port,
};
};