Skip to content

Commit

Permalink
✨ feat: Host a relay on Hetzner (#114)
Browse files Browse the repository at this point in the history
We are hosting a [MoQ](https://quic.video) relay on a remote (bare
metal) server on Hetzner

With a lot of help from @victorpahuus
  • Loading branch information
wanjohiryan authored Sep 26, 2024
1 parent c4a6895 commit bae089e
Show file tree
Hide file tree
Showing 74 changed files with 7,107 additions and 96 deletions.
3 changes: 3 additions & 0 deletions .certs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.terraform
relay_*
terraform.tfstate
61 changes: 61 additions & 0 deletions .certs/.terraform.lock.hcl

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

24 changes: 24 additions & 0 deletions .certs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Usage
1. Update the terraform.tfvars file with your domain and email.

2. Run `terraform init` to initialize the Terraform working directory.

3. Run `terraform plan` to see the planned changes.

4. Run `terraform apply` to create the resources and obtain the certificate.
Outputs

The configuration provides two sensitive outputs:
```bash
certificate_pem: The full certificate chain
private_key_pem: The private key for the certificate
```

These can be then be used in your `moq-relay` as it requires SSL/TLS certificates.

## Note
The generated certificate and key files are saved locally and ignored by git:
```git
.terraform
relay_*
```
7 changes: 7 additions & 0 deletions .certs/input.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
variable "email" {
description = "Your email address, used for LetsEncrypt"
}

variable "domain" {
description = "domain name"
}
65 changes: 65 additions & 0 deletions .certs/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
terraform {
required_providers {
acme = {
source = "vancluever/acme"
version = "~> 2.0"
}
}
}

provider "acme" {
server_url = "https://acme-v02.api.letsencrypt.org/directory"
}

resource "acme_registration" "reg" {
email_address = "[email protected]"
}

resource "tls_private_key" "relay" {
algorithm = "ECDSA"
ecdsa_curve = "P256"
}

resource "acme_registration" "relay" {
account_key_pem = tls_private_key.relay.private_key_pem
email_address = var.email
}

resource "acme_certificate" "relay" {
account_key_pem = acme_registration.relay.account_key_pem
common_name = "relay.${var.domain}"
subject_alternative_names = ["*.relay.${var.domain}"]
key_type = tls_private_key.relay.ecdsa_curve

recursive_nameservers = ["8.8.8.8:53"]

dns_challenge {
provider = "route53"
}
}

# New resources to save certificate and private key
resource "local_file" "cert_file" {
content = "${acme_certificate.relay.certificate_pem}${acme_certificate.relay.issuer_pem}"
filename = "${path.module}/relay_cert.crt"
file_permission = "0644"
directory_permission = "0755"
}

resource "local_file" "key_file" {
content = acme_certificate.relay.private_key_pem
filename = "${path.module}/relay_key.key"
file_permission = "0600"
directory_permission = "0755"
}

# Outputs for certificate and private key
output "certificate_pem" {
value = "${acme_certificate.relay.certificate_pem}${acme_certificate.relay.issuer_pem}"
sensitive = true
}

output "private_key_pem" {
value = acme_certificate.relay.private_key_pem
sensitive = true
}
2 changes: 2 additions & 0 deletions .certs/terraform.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
domain = "fst.so"
email = "[email protected]"
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ yarn-error.log*
.sst

#Bun merging errors, EVERY time :(
bun.lockb
bun.lockb

#tests
id_*
3 changes: 3 additions & 0 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"@builder.io/qwik": "^1.8.0",
"@builder.io/qwik-city": "^1.8.0",
"@builder.io/qwik-react": "0.5.0",
"@modular-forms/qwik": "^0.27.0",
"@nestri/eslint-config": "*",
"@nestri/moq": "*",
"@nestri/typescript-config": "*",
"@nestri/ui": "*",
"@types/eslint": "8.56.10",
Expand All @@ -48,6 +50,7 @@
"react-dom": "18.2.0",
"typescript": "5.4.5",
"undici": "*",
"valibot": "^0.42.1",
"vite": "5.3.5",
"vite-tsconfig-paths": "^4.2.1",
"wrangler": "^3.0.0"
Expand Down
118 changes: 118 additions & 0 deletions apps/www/src/routes/(moq)/moq/checker/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as v from "valibot"
import { Broadcast } from "./tester";
import { cn } from "@nestri/ui/design";
import { routeLoader$ } from "@builder.io/qwik-city";
import { component$, $, useSignal } from "@builder.io/qwik";
import { MotionComponent, transition, TitleSection, Button } from "@nestri/ui/react";
import { type InitialValues, type SubmitHandler, useForm, valiForm$ } from "@modular-forms/qwik"

const Schema = v.object({
url: v.pipe(
v.string(),
v.minLength(10, "Please input a valid url"),
v.url("Please input a valid url"),
)
}, "Please fill in all the fields correctly.")

type Form = v.InferInput<typeof Schema>;

export const useFormLoader = routeLoader$<InitialValues<Form>>(async () => {
return {
url: ""
}
})

const generateRandomWord = (length: number) => {
const characters = 'abcdefghijklmnopqrstuvwxyz';
return Array.from({ length }, () => characters[Math.floor(Math.random() * characters.length)]).join('');
};

export default component$(() => {
const broadcasterOk = useSignal<boolean | undefined>();
const [state, { Form, Field }] = useForm<Form>({
loader: useFormLoader(),
validate: valiForm$(Schema)
});

const handleSubmit = $<SubmitHandler<Form>>(async (values) => {
const randomNamespace = generateRandomWord(6);
const sub = await Broadcast.init({ url: values.url, fingerprint: undefined, namespace: randomNamespace })

setTimeout(() => {
broadcasterOk.value = sub.isSubscribed()
}, 1000);
});

return (
<>
<TitleSection client:load title="MoQ Checker" description="Test the connection to your Media-Over-Quic relay!" />
<MotionComponent
initial={{ opacity: 0, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={transition}
client:load
class="flex items-center justify-center w-full"
as="div"
>
<section class="w-full flex flex-col gap-4 justify-center items-center">
<Form onSubmit$={handleSubmit} class="w-full max-w-xl flex px-3 gap-2">
<Field name="url">
{(field, props) => {
return (
<div class="w-full flex flex-col gap-2">
<div class="bg-gray-200 dark:bg-gray-800 flex rounded-lg w-full relative h-10 flex-none border focus-within:bg-gray-300/70 dark:focus-within:bg-gray-700/70 border-gray-300 dark:border-gray-700 ">
<input type="url" class={cn("w-full relative h-full bg-transparent rounded-lg p-3 focus-within:outline-none focus-within:ring-2 focus-within:ring-gray-400 dark:focus-within:ring-gray-600 focus-within:ring-offset-2 focus-visible:outline-none focus-within:ring-offset-gray-100 dark:focus-within:ring-offset-gray-900 placeholder:text-gray-500/70", typeof broadcasterOk.value !== "undefined" && broadcasterOk.value == true && "ring-2 ring-offset-2 ring-offset-gray-100 dark:ring-offset-gray-900 ring-green-500", typeof broadcasterOk.value !== "undefined" && (broadcasterOk.value == false) && "ring-2 ring-offset-2 ring-offset-gray-100 dark:ring-offset-gray-900 ring-red-500")} placeholder="https://relay.domain.com" {...props} />
</div>
{field.error && (<p class='text-[0.8rem] font-medium text-danger-600 dark:text-danger-500' >{field.error}</p>)}
</div>
)
}}
</Field>

{/* <button class={cn(buttonVariants.solid({ size: "md", intent: "neutral" }), "w-max space-y-0 relative")} style={{ height: 40, marginTop: 0 }} type="submit" >
Check
</button> */}
<Button.Root
disabled={state.submitting}
isLoading={state.submitting}
// setIsLoading={setIsLoading}
client:load
//@ts-ignore
type="submit"
style={{ height: 40, marginTop: 0 }}
intent="neutral"
size="md"
class="w-max space-y-0 relative">
{/* <Button.Icon
isLoading={isLoading.value}
client:load>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16">
<g fill="currentColor">
<path d="M.329 10.333A8.01 8.01 0 0 0 7.99 16C12.414 16 16 12.418 16 8s-3.586-8-8.009-8A8.006 8.006 0 0 0 0 7.468l.003.006l4.304 1.769A2.2 2.2 0 0 1 5.62 8.88l1.96-2.844l-.001-.04a3.046 3.046 0 0 1 3.042-3.043a3.046 3.046 0 0 1 3.042 3.043a3.047 3.047 0 0 1-3.111 3.044l-2.804 2a2.223 2.223 0 0 1-3.075 2.11a2.22 2.22 0 0 1-1.312-1.568L.33 10.333Z" /><path d="M4.868 12.683a1.715 1.715 0 0 0 1.318-3.165a1.7 1.7 0 0 0-1.263-.02l1.023.424a1.261 1.261 0 1 1-.97 2.33l-.99-.41a1.7 1.7 0 0 0 .882.84Zm3.726-6.687a2.03 2.03 0 0 0 2.027 2.029a2.03 2.03 0 0 0 2.027-2.029a2.03 2.03 0 0 0-2.027-2.027a2.03 2.03 0 0 0-2.027 2.027m2.03-1.527a1.524 1.524 0 1 1-.002 3.048a1.524 1.524 0 0 1 .002-3.048" />
</g>
</svg>
</Button.Icon> */}
<Button.Label
loadingText="Checking..."
class="text-ellipsis whitespace-nowrap"
isLoading={state.submitting}>
Check
</Button.Label>
<div class="w-[8%]" />
</Button.Root>
</Form>
{typeof broadcasterOk.value !== "undefined" && broadcasterOk.value == true ? (
<span class="w-full text-green-500 max-w-xl flex space-y-6 px-3 gap-2">
Your relay is doing okay
</span>
) : typeof broadcasterOk.value !== "undefined" && (
<span class="w-full text-red-500 max-w-xl flex space-y-6 px-3 gap-2">
Your relay has an issue
</span>
)}
</section>
</MotionComponent>
</>
)
})
Loading

0 comments on commit bae089e

Please sign in to comment.