Compare commits
No commits in common. "gh-pages" and "main" have entirely different histories.
17
.eleventy.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
||||||
|
|
||||||
|
module.exports = (config) => {
|
||||||
|
config.addPlugin(syntaxHighlight);
|
||||||
|
|
||||||
|
config.addWatchTarget("./assets/css");
|
||||||
|
|
||||||
|
config.addPassthroughCopy("./assets");
|
||||||
|
config.addPassthroughCopy("./src/**/*.{jpg,png}");
|
||||||
|
|
||||||
|
return {
|
||||||
|
dir: {
|
||||||
|
input: "src",
|
||||||
|
output: "dist",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
23
.gitea/workflows/eleventy.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: eleventy
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- run: echo "LAST_COMMIT=`git rev-parse --short HEAD`" >> $GITHUB_ENV
|
||||||
|
- name: Build
|
||||||
|
uses: TartanLlama/actions-eleventy@master
|
||||||
|
with:
|
||||||
|
install_dependencies: true
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "20.x"
|
||||||
|
cache: "npm"
|
||||||
|
- run: npx wrangler pages deploy dist --project-name=$CF_PROJECT_NAME --branch=$GITHUB_REF_NAME
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ vars.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
CF_PROJECT_NAME: ${{ vars.CF_PROJECT_NAME }}
|
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.direnv
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 faultables
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
427
LICENSE-CC-BY-SA
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
Attribution-ShareAlike 4.0 International
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
|
does not provide legal services or legal advice. Distribution of
|
||||||
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
|
other relationship. Creative Commons makes its licenses and related
|
||||||
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
|
warranties regarding its licenses, any material licensed under their
|
||||||
|
terms and conditions, or any related information. Creative Commons
|
||||||
|
disclaims all liability for damages resulting from their use to the
|
||||||
|
fullest extent possible.
|
||||||
|
|
||||||
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
|
conditions that creators and other rights holders may use to share
|
||||||
|
original works of authorship and other material subject to copyright
|
||||||
|
and certain other rights specified in the public license below. The
|
||||||
|
following considerations are for informational purposes only, are not
|
||||||
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
|
Considerations for licensors: Our public licenses are
|
||||||
|
intended for use by those authorized to give the public
|
||||||
|
permission to use material in ways otherwise restricted by
|
||||||
|
copyright and certain other rights. Our licenses are
|
||||||
|
irrevocable. Licensors should read and understand the terms
|
||||||
|
and conditions of the license they choose before applying it.
|
||||||
|
Licensors should also secure all rights necessary before
|
||||||
|
applying our licenses so that the public can reuse the
|
||||||
|
material as expected. Licensors should clearly mark any
|
||||||
|
material not subject to the license. This includes other CC-
|
||||||
|
licensed material, or material used under an exception or
|
||||||
|
limitation to copyright. More considerations for licensors:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
|
Considerations for the public: By using one of our public
|
||||||
|
licenses, a licensor grants the public permission to use the
|
||||||
|
licensed material under specified terms and conditions. If
|
||||||
|
the licensor's permission is not necessary for any reason--for
|
||||||
|
example, because of any applicable exception or limitation to
|
||||||
|
copyright--then that use is not regulated by the license. Our
|
||||||
|
licenses grant only permissions under copyright and certain
|
||||||
|
other rights that a licensor has authority to grant. Use of
|
||||||
|
the licensed material may still be restricted for other
|
||||||
|
reasons, including because others have copyright or other
|
||||||
|
rights in the material. A licensor may make special requests,
|
||||||
|
such as asking that all changes be marked or described.
|
||||||
|
Although not required by our licenses, you are encouraged to
|
||||||
|
respect those requests where reasonable. More_considerations
|
||||||
|
for the public:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||||
|
License
|
||||||
|
|
||||||
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
|
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||||
|
License"). To the extent this Public License may be interpreted as a
|
||||||
|
contract, You are granted the Licensed Rights in consideration of Your
|
||||||
|
acceptance of these terms and conditions, and the Licensor grants You
|
||||||
|
such rights in consideration of benefits the Licensor receives from
|
||||||
|
making the Licensed Material available under these terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
|
||||||
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
|
Rights that is derived from or based upon the Licensed Material
|
||||||
|
and in which the Licensed Material is translated, altered,
|
||||||
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
|
permission under the Copyright and Similar Rights held by the
|
||||||
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
|
Material is a musical work, performance, or sound recording,
|
||||||
|
Adapted Material is always produced where the Licensed Material is
|
||||||
|
synched in timed relation with a moving image.
|
||||||
|
|
||||||
|
b. Adapter's License means the license You apply to Your Copyright
|
||||||
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
|
accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
c. BY-SA Compatible License means a license listed at
|
||||||
|
creativecommons.org/compatiblelicenses, approved by Creative
|
||||||
|
Commons as essentially the equivalent of this Public License.
|
||||||
|
|
||||||
|
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
|
closely related to copyright including, without limitation,
|
||||||
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
|
Rights, without regard to how the rights are labeled or
|
||||||
|
categorized. For purposes of this Public License, the rights
|
||||||
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
|
Rights.
|
||||||
|
|
||||||
|
e. Effective Technological Measures means those measures that, in the
|
||||||
|
absence of proper authority, may not be circumvented under laws
|
||||||
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
|
agreements.
|
||||||
|
|
||||||
|
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
g. License Elements means the license attributes listed in the name
|
||||||
|
of a Creative Commons Public License. The License Elements of this
|
||||||
|
Public License are Attribution and ShareAlike.
|
||||||
|
|
||||||
|
h. Licensed Material means the artistic or literary work, database,
|
||||||
|
or other material to which the Licensor applied this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
i. Licensed Rights means the rights granted to You subject to the
|
||||||
|
terms and conditions of this Public License, which are limited to
|
||||||
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
|
under this Public License.
|
||||||
|
|
||||||
|
k. Share means to provide material to the public by any means or
|
||||||
|
process that requires permission under the Licensed Rights, such
|
||||||
|
as reproduction, public display, public performance, distribution,
|
||||||
|
dissemination, communication, or importation, and to make material
|
||||||
|
available to the public including in ways that members of the
|
||||||
|
public may access the material from a place and at a time
|
||||||
|
individually chosen by them.
|
||||||
|
|
||||||
|
l. Sui Generis Database Rights means rights other than copyright
|
||||||
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
|
as amended and/or succeeded, as well as other essentially
|
||||||
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
m. You means the individual or entity exercising the Licensed Rights
|
||||||
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
|
Section 2 -- Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License,
|
||||||
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
|
in part; and
|
||||||
|
|
||||||
|
b. produce, reproduce, and Share Adapted Material.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
|
License does not apply, and You do not need to comply with
|
||||||
|
its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section
|
||||||
|
6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The
|
||||||
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
|
all media and formats whether now known or hereafter created,
|
||||||
|
and to make technical modifications necessary to do so. The
|
||||||
|
Licensor waives and/or agrees not to assert any right or
|
||||||
|
authority to forbid You from making technical modifications
|
||||||
|
necessary to exercise the Licensed Rights, including
|
||||||
|
technical modifications necessary to circumvent Effective
|
||||||
|
Technological Measures. For purposes of this Public License,
|
||||||
|
simply making modifications authorized by this Section 2(a)
|
||||||
|
(4) never produces Adapted Material.
|
||||||
|
|
||||||
|
5. Downstream recipients.
|
||||||
|
|
||||||
|
a. Offer from the Licensor -- Licensed Material. Every
|
||||||
|
recipient of the Licensed Material automatically
|
||||||
|
receives an offer from the Licensor to exercise the
|
||||||
|
Licensed Rights under the terms and conditions of this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
b. Additional offer from the Licensor -- Adapted Material.
|
||||||
|
Every recipient of Adapted Material from You
|
||||||
|
automatically receives an offer from the Licensor to
|
||||||
|
exercise the Licensed Rights in the Adapted Material
|
||||||
|
under the conditions of the Adapter's License You apply.
|
||||||
|
|
||||||
|
c. No downstream restrictions. You may not offer or impose
|
||||||
|
any additional or different terms or conditions on, or
|
||||||
|
apply any Effective Technological Measures to, the
|
||||||
|
Licensed Material if doing so restricts exercise of the
|
||||||
|
Licensed Rights by any recipient of the Licensed
|
||||||
|
Material.
|
||||||
|
|
||||||
|
6. No endorsement. Nothing in this Public License constitutes or
|
||||||
|
may be construed as permission to assert or imply that You
|
||||||
|
are, or that Your use of the Licensed Material is, connected
|
||||||
|
with, or sponsored, endorsed, or granted official status by,
|
||||||
|
the Licensor or others designated to receive attribution as
|
||||||
|
provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
|
b. Other rights.
|
||||||
|
|
||||||
|
1. Moral rights, such as the right of integrity, are not
|
||||||
|
licensed under this Public License, nor are publicity,
|
||||||
|
privacy, and/or other similar personality rights; however, to
|
||||||
|
the extent possible, the Licensor waives and/or agrees not to
|
||||||
|
assert any such rights held by the Licensor to the limited
|
||||||
|
extent necessary to allow You to exercise the Licensed
|
||||||
|
Rights, but not otherwise.
|
||||||
|
|
||||||
|
2. Patent and trademark rights are not licensed under this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
3. To the extent possible, the Licensor waives any right to
|
||||||
|
collect royalties from You for the exercise of the Licensed
|
||||||
|
Rights, whether directly or through a collecting society
|
||||||
|
under any voluntary or waivable statutory or compulsory
|
||||||
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
|
reserves any right to collect such royalties.
|
||||||
|
|
||||||
|
|
||||||
|
Section 3 -- License Conditions.
|
||||||
|
|
||||||
|
Your exercise of the Licensed Rights is expressly made subject to the
|
||||||
|
following conditions.
|
||||||
|
|
||||||
|
a. Attribution.
|
||||||
|
|
||||||
|
1. If You Share the Licensed Material (including in modified
|
||||||
|
form), You must:
|
||||||
|
|
||||||
|
a. retain the following if it is supplied by the Licensor
|
||||||
|
with the Licensed Material:
|
||||||
|
|
||||||
|
i. identification of the creator(s) of the Licensed
|
||||||
|
Material and any others designated to receive
|
||||||
|
attribution, in any reasonable manner requested by
|
||||||
|
the Licensor (including by pseudonym if
|
||||||
|
designated);
|
||||||
|
|
||||||
|
ii. a copyright notice;
|
||||||
|
|
||||||
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
|
iv. a notice that refers to the disclaimer of
|
||||||
|
warranties;
|
||||||
|
|
||||||
|
v. a URI or hyperlink to the Licensed Material to the
|
||||||
|
extent reasonably practicable;
|
||||||
|
|
||||||
|
b. indicate if You modified the Licensed Material and
|
||||||
|
retain an indication of any previous modifications; and
|
||||||
|
|
||||||
|
c. indicate the Licensed Material is licensed under this
|
||||||
|
Public License, and include the text of, or the URI or
|
||||||
|
hyperlink to, this Public License.
|
||||||
|
|
||||||
|
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||||
|
reasonable manner based on the medium, means, and context in
|
||||||
|
which You Share the Licensed Material. For example, it may be
|
||||||
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
|
hyperlink to a resource that includes the required
|
||||||
|
information.
|
||||||
|
|
||||||
|
3. If requested by the Licensor, You must remove any of the
|
||||||
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
|
reasonably practicable.
|
||||||
|
|
||||||
|
b. ShareAlike.
|
||||||
|
|
||||||
|
In addition to the conditions in Section 3(a), if You Share
|
||||||
|
Adapted Material You produce, the following conditions also apply.
|
||||||
|
|
||||||
|
1. The Adapter's License You apply must be a Creative Commons
|
||||||
|
license with the same License Elements, this version or
|
||||||
|
later, or a BY-SA Compatible License.
|
||||||
|
|
||||||
|
2. You must include the text of, or the URI or hyperlink to, the
|
||||||
|
Adapter's License You apply. You may satisfy this condition
|
||||||
|
in any reasonable manner based on the medium, means, and
|
||||||
|
context in which You Share Adapted Material.
|
||||||
|
|
||||||
|
3. You may not offer or impose any additional or different terms
|
||||||
|
or conditions on, or apply any Effective Technological
|
||||||
|
Measures to, Adapted Material that restrict exercise of the
|
||||||
|
rights granted under the Adapter's License You apply.
|
||||||
|
|
||||||
|
|
||||||
|
Section 4 -- Sui Generis Database Rights.
|
||||||
|
|
||||||
|
Where the Licensed Rights include Sui Generis Database Rights that
|
||||||
|
apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
|
portion of the contents of the database;
|
||||||
|
|
||||||
|
b. if You include all or a substantial portion of the database
|
||||||
|
contents in a database in which You have Sui Generis Database
|
||||||
|
Rights, then the database in which You have Sui Generis Database
|
||||||
|
Rights (but not its individual contents) is Adapted Material,
|
||||||
|
|
||||||
|
including for purposes of Section 3(b); and
|
||||||
|
c. You must comply with the conditions in Section 3(a) if You Share
|
||||||
|
all or a substantial portion of the contents of the database.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 4 supplements and does not
|
||||||
|
replace Your obligations under this Public License where the Licensed
|
||||||
|
Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
|
||||||
|
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
|
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||||
|
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||||
|
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||||
|
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||||
|
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||||
|
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||||
|
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||||
|
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||||
|
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||||
|
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||||
|
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||||
|
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||||
|
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||||
|
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||||
|
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||||
|
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||||
|
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
c. The disclaimer of warranties and limitation of liability provided
|
||||||
|
above shall be interpreted in a manner that, to the extent
|
||||||
|
possible, most closely approximates an absolute disclaimer and
|
||||||
|
waiver of all liability.
|
||||||
|
|
||||||
|
|
||||||
|
Section 6 -- Term and Termination.
|
||||||
|
|
||||||
|
a. This Public License applies for the term of the Copyright and
|
||||||
|
Similar Rights licensed here. However, if You fail to comply with
|
||||||
|
this Public License, then Your rights under this Public License
|
||||||
|
terminate automatically.
|
||||||
|
|
||||||
|
b. Where Your right to use the Licensed Material has terminated under
|
||||||
|
Section 6(a), it reinstates:
|
||||||
|
|
||||||
|
1. automatically as of the date the violation is cured, provided
|
||||||
|
it is cured within 30 days of Your discovery of the
|
||||||
|
violation; or
|
||||||
|
|
||||||
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||||
|
right the Licensor may have to seek remedies for Your violations
|
||||||
|
of this Public License.
|
||||||
|
|
||||||
|
c. For the avoidance of doubt, the Licensor may also offer the
|
||||||
|
Licensed Material under separate terms or conditions or stop
|
||||||
|
distributing the Licensed Material at any time; however, doing so
|
||||||
|
will not terminate this Public License.
|
||||||
|
|
||||||
|
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 7 -- Other Terms and Conditions.
|
||||||
|
|
||||||
|
a. The Licensor shall not be bound by any additional or different
|
||||||
|
terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
|
b. Any arrangements, understandings, or agreements regarding the
|
||||||
|
Licensed Material not stated herein are separate from and
|
||||||
|
independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 8 -- Interpretation.
|
||||||
|
|
||||||
|
a. For the avoidance of doubt, this Public License does not, and
|
||||||
|
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||||
|
conditions on any use of the Licensed Material that could lawfully
|
||||||
|
be made without permission under this Public License.
|
||||||
|
|
||||||
|
b. To the extent possible, if any provision of this Public License is
|
||||||
|
deemed unenforceable, it shall be automatically reformed to the
|
||||||
|
minimum extent necessary to make it enforceable. If the provision
|
||||||
|
cannot be reformed, it shall be severed from this Public License
|
||||||
|
without affecting the enforceability of the remaining terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
c. No term or condition of this Public License will be waived and no
|
||||||
|
failure to comply consented to unless expressly agreed to by the
|
||||||
|
Licensor.
|
||||||
|
|
||||||
|
d. Nothing in this Public License constitutes or may be interpreted
|
||||||
|
as a limitation upon, or waiver of, any privileges and immunities
|
||||||
|
that apply to the Licensor or You, including from the legal
|
||||||
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons is not a party to its public
|
||||||
|
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||||
|
its public licenses to material it publishes and in those instances
|
||||||
|
will be considered the “Licensor.” The text of the Creative Commons
|
||||||
|
public licenses is dedicated to the public domain under the CC0 Public
|
||||||
|
Domain Dedication. Except for the limited purpose of indicating that
|
||||||
|
material is shared under a Creative Commons public license or as
|
||||||
|
otherwise permitted by the Creative Commons policies published at
|
||||||
|
creativecommons.org/policies, Creative Commons does not authorize the
|
||||||
|
use of the trademark "Creative Commons" or any other trademark or logo
|
||||||
|
of Creative Commons without its prior written consent including,
|
||||||
|
without limitation, in connection with any unauthorized modifications
|
||||||
|
to any of its public licenses or any other arrangements,
|
||||||
|
understandings, or agreements concerning use of licensed material. For
|
||||||
|
the avoidance of doubt, this paragraph does not form part of the
|
||||||
|
public licenses.
|
||||||
|
|
||||||
|
Creative Commons may be contacted at creativecommons.org.
|
11
README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# rizaldy.today
|
||||||
|
|
||||||
|
WIP as always
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [ ] RSS (penting)
|
||||||
|
- [ ] SEO (why not)
|
||||||
|
- [ ] Pagination
|
||||||
|
- [ ] Self hosting fonts
|
||||||
|
- [ ] SWR cache bunny
|
@ -1,148 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
Expose Web Services at Home via Tailscale for Fun
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment l-fragment--blog">
|
|
||||||
<div class="c-article">
|
|
||||||
<h1>Expose Web Services at Home via Tailscale for Fun</h1>
|
|
||||||
|
|
||||||
<p class="c-article__meta-info">
|
|
||||||
<time>Oct 16, 2021</time> •
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#tailscale">TAILSCALE</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>I have a small homelab server at home running TrueNAS Core. My home network sits behind NAT and there are probably 3 routers in front of me.
|
|
||||||
On the other hand I also have a small VM that has a static public IP address somewhere in Singapore. Some of my services need to be exposed to the internet for my friends to access—for example—this blog via DNS because remembering a domain address is more fun than a random number with 4 dots.</p>
|
|
||||||
<p>The problem is that this blog is running on my NAS and using a private IP address.</p>
|
|
||||||
<p>Also, my ISP assigns a public IP Address to my network dynamically. The simplest way to expose my services on my NAS to the Internet might be using Dynamic DNS but sometimes it's not as easy as it sounds.</p>
|
|
||||||
<p>So maybe I need to connect one of my VMs in Singapore with my NAS at home. Since it is not possible to connect ethernet cable from NAS to DigitalOcean data center in Singapore, so I have to connect it virtually.</p>
|
|
||||||
<p>And yes, by creating a VPN.</p>
|
|
||||||
<h2>Tailscale VPN</h2>
|
|
||||||
<p>Previously I used <a href="https://wireguard.com">Wireguard</a> with a hub-and-spoke network because managing the keys on each of my machines was quite a chore.</p>
|
|
||||||
<p>Then I found out <a href="https://zerotier.com">Zerotier</a> from a random page on Reddit. Zerotier uses a mesh network and it's cute how my machines can talk to each other on a peer-to-peer basis.</p>
|
|
||||||
<p>My problem with Zerotier is sometimes the network is somewhat unreliable and maybe it's my poor VM's fault. Also sometimes my machines just randomly can't talk to each other via the Zerotier assigned address and I believe it's a firewall issue.</p>
|
|
||||||
<p>So I found out Tailscale from one of my friends on Twitter (actually he is my boss at work). Tailscale is built on Wireguard and is a mesh network. Even though Wireguard has <a href="https://github.com/WireGuard/wg-dynamic">its own</a> mesh network solution, it's still WIP and using <a href="https://github.com/k4yt3x/wg-meshconf">alternative</a> is quite difficult because, again, managing keys is a pain.</p>
|
|
||||||
<p>Then I give Tailscale a try. Installing and running Tailscale is easy enough that even my gf (who is non an IT person) can use it without wondering what public/private key means.</p>
|
|
||||||
<p>Our devices can talk to each other even though we're on different networks, and that's cool. In most cases, we can communicate peer-to-peer and that's really great.</p>
|
|
||||||
<h2>Exposing web services on different private networks</h2>
|
|
||||||
<p>We have a secret journal running on my NAS and only accessible over the local network. The domain address is <a href="https://1460.rizaldy.club">1460.rizaldy.club</a> and resolves to a local IP address on the 192.168.1.0/24 subnet so maybe if you access it you will see a random web page (or none at all) rather than our secret journal.</p>
|
|
||||||
<p>My gf's private network uses the 10.26.0.0/24 while our journal lives in 192.168.1.242. The key is I use <a href="https://tailscale.com/kb/1019/subnets/">subnet routers</a> and I have Tailscale on my router (and on my device as well) at home. While Tailscale for iOS (and others) has "accept routes" enabled by default, that means our secret journal is directly accessible out of the box because my router advertises the 192.168.1.0/24 subnet and we're on the same tailnet.</p>
|
|
||||||
<p><img src="./Untitled-2021-10-16-0110.png" alt="some diagram"></p>
|
|
||||||
<p>And that's cool.</p>
|
|
||||||
<p>I never even touched Tailscale.app on her phone just to make sure everything was working fine, because it is. We can access it anywhere without having to expose the service to the internet, and that's it the point.</p>
|
|
||||||
<h2>Exposing web services on a private network to the Internet</h2>
|
|
||||||
<p>I have a minio instance on my NAS and sometimes I upload cat photos there. My minio is accessible at <a href="https://faultables-s3.lan">faultables-s3.lan</a> address and since I'm using <a href="https://tailscale.com/kb/1081/magicdns">Magic DNS</a> too, devices on my tailnet can resolve that domain (thanks to split tunnel) then anyone on my tailnet can see the cat photo I uploaded so maybe I can stop use imgur service as well.</p>
|
|
||||||
<p>But I also want my friends to know because my friends are nice and they deserve to see cats. To allow my friend to access it without doing anything, I need a static public IP address and my VM has it. Connecting my VM with my NAS via Tailscale was the answer and that's why you can see <a href="https://s3.edgy.social/0x0/bff5d074d399bdfec6071e9168398406.jpg">this cat</a> right on your screen.</p>
|
|
||||||
<p>The setup is pretty simple, I just pointed s3.faultable.dev to my VM's IP, set up <a href="https://caddyserver.com">Caddy Web Server</a> there, and told Caddy to proxy the request to 192.168.1.170:9000.</p>
|
|
||||||
<p>Here's another diagram:</p>
|
|
||||||
<p><img src="./Untitled-2021-10-15-2135-2048x985.png" alt="I did my best to visualize it"></p>
|
|
||||||
<p>Packets between my VM and my router are transmitted in an end-to-end encrypted using the Wireguard protocol so no one can see and/or modify the packets even if no one cares and that's cool.</p>
|
|
||||||
<h2>Reverse Proxy as a Service for fun</h2>
|
|
||||||
<p>So I just mentioned Tailscale on Twitter about my recent random thoughts:</p>
|
|
||||||
<p><img src="./Screen-Shot-2021-10-15-at-9.47.40-PM.png" alt="https://static-tweet.vercel.app/1448966405391405056"></p>
|
|
||||||
<p><a href="https://twitter.com/apenwarr/status/1448972965110898693">@apenwarr's</a> answer regarding my thoughts is perfectly understandable: it's not Tailscale's main business (nor focus) so why don't I create one?</p>
|
|
||||||
<p>Imagine you don't have to touch any server to just proxy web requests from the public internet to machines in your tailnet. Invite my machine to your tailnet, tell me the address of the domain you own, then tell me where to proxy requests for it.</p>
|
|
||||||
<p>In my mind is to setup an OpenResty instance with Redis as a data store, so let's make it official.</p>
|
|
||||||
<p>I'll be using a service from Fly.io (because I'm very interested in their service) to deploy OpenResty. For a high-level view of how it works, here's another cool diagram:</p>
|
|
||||||
<p><img src="./Untitled-2021-10-16-0110-2.png" alt=""></p>
|
|
||||||
<p>Actually I haven't deployed any instances to fly.io when creating this diagram, but if you can see this post it's very likely the diagram above is working.</p>
|
|
||||||
<p>Now let's try this out.</p>
|
|
||||||
<h2>Minimum Viable RPaaS</h2>
|
|
||||||
<p>We'll use openresty/openresty Docker's image as base image because we'll deploy it to fly.io plus we'll bring the Tailscale app on it later.</p>
|
|
||||||
<p>We'll be using the <a href="https://hub.docker.com/r/openresty/openresty">openresty/openresty</a> docker image as the base image as we'll be deploying it to fly.io plus we'll be bringing the Tailscale app over later.</p>
|
|
||||||
<p>I'll be using a managed Redis solution from <a href="https://upstash.com">Upstash</a> instead of Fly.io and we'll talk about it later. Now let's write the nginx.conf file:</p>
|
|
||||||
<pre class="language-nginx"><code class="language-nginx"><span class="token directive"><span class="token keyword">worker_processes</span> <span class="token number">2</span></span><span class="token punctuation">;</span><br><span class="token directive"><span class="token keyword">error_log</span> logs/error.log info</span><span class="token punctuation">;</span><br><br><span class="token directive"><span class="token keyword">events</span></span> <span class="token punctuation">{</span><br> <span class="token directive"><span class="token keyword">worker_connections</span> <span class="token number">1024</span></span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token directive"><span class="token keyword">env</span> REDIS_HOST</span><span class="token punctuation">;</span><br><span class="token directive"><span class="token keyword">env</span> REDIS_PORT</span><span class="token punctuation">;</span><br><span class="token directive"><span class="token keyword">env</span> REDIS_PASSWORD</span><span class="token punctuation">;</span><br><br><span class="token directive"><span class="token keyword">http</span></span> <span class="token punctuation">{</span><br> <span class="token directive"><span class="token keyword">server</span></span> <span class="token punctuation">{</span><br> <span class="token directive"><span class="token keyword">listen</span> <span class="token number">80</span></span><span class="token punctuation">;</span><br><br> <span class="token directive"><span class="token keyword">location</span> /</span> <span class="token punctuation">{</span><br> <span class="token directive"><span class="token keyword">set</span> <span class="token variable">$upstream</span> <span class="token string">''</span></span><span class="token punctuation">;</span><br><br> <span class="token directive"><span class="token keyword">access_by_lua</span> <span class="token string">'<br> local redis = require "resty.redis"<br> local redisc = redis:new()<br> local target = ngx.var.host<br><br> local redis_host = os.getenv("REDIS_HOST")<br> local redis_port = os.getenv("REDIS_PORT")<br> local redis_password = os.getenv("REDIS_PASSWORD")<br><br> local connect, err = redisc:connect(redis_host, redis_port, {<br> ssl = true<br> })<br><br> if not connect then<br> ngx.log(ngx.ERR, "failed to connect to redis", err)<br> return ngx.exit(500)<br> end<br><br> local auth, err = redisc:auth(redis_password)<br><br> if not auth then<br> ngx.say("failed to authenticate", err)<br> return ngx.exit(500)<br> end<br><br> local get_upstream, err = redisc:get(target)<br><br> if not get_upstream or get_upstream == ngx.null then<br> ngx.log(ngx.ERR, "no host found for key", key)<br> return ngx.exit(404)<br> end<br><br> ngx.var.upstream = get_upstream<br> '</span></span><span class="token punctuation">;</span><br><br> <span class="token directive"><span class="token keyword">proxy_pass</span> http://<span class="token variable">$upstream</span></span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
|
|
||||||
<p>The configuration above in short is to tell OpenResty to handle the request based on its Host header and forward the request based on the existing values. When the host is not listed in our Redis record, we will return a 404 Not Found page because it is.</p>
|
|
||||||
<p>The very simple data we're storing for now is just like this:</p>
|
|
||||||
<pre class="language-bash"><code class="language-bash"><span class="token operator"><</span>target_host<span class="token operator">></span>:<span class="token operator"><</span>upstream_ip<span class="token operator">></span>:<span class="token operator"><</span>upstream_port<span class="token operator">></span></code></pre>
|
|
||||||
<p>So if I want to handle every incoming request from nginx.init8.lol to 100.73.204.66:42069, the operation is as simple as <code>SET nginx.init8.lol 100.73.204.66:42069</code>.</p>
|
|
||||||
<p><a href="./Screen-Shot-2021-10-16-at-4.07.16-AM-2048x1305.png"><img src="./Screen-Shot-2021-10-16-at-4.07.16-AM-2048x1305.png" alt=""></a></p>
|
|
||||||
<p>Please keep in mind that since packets are end-to-end encrypted means that target upstream need to be directly to be the Tailscale IP. For example, port forwarding from your WAN to your LAN on your router won't work because packets are encrypted unless you set reverse proxy on your router (and let the proxy do the job).</p>
|
|
||||||
<h2>Making it more official</h2>
|
|
||||||
<p>Now let's deploy our OpenResty to the fly.io platform. In order to connect Tailscale with Fly.io we can use <a href="https://tailscale.com/kb/1132/flydotio">this guide</a> from Tailscale Docs. Our fly.io instance will only have Tailscale IPv6 so we need to point our domain to use Tailscale IPv6 so fly.io can route traffic to your machine.</p>
|
|
||||||
<p>So I'm going to create a frontend for this service so that it can interact with Redis via a REST API. It's still a WIP at the moment (I'm writing this post while creating the service lol) but I wanted to let my internet friends know I'm developing something fun (or at least for me).</p>
|
|
||||||
<p>The common setting than this is to set <a href="https://ngrok.io">ngrok.io</a>, tell ngrok.io which port you want to expose then you will get a unique URL. That's pretty cool but what if I'm dogfooding this?
|
|
||||||
So I point app.init8.lol to ts-proxy.fly.io, invite the machine to my tailnet, then tell the proxy to point that domain to [fd7a:115c:a1e0:ab12:4843:cd96:6258:9a11]:80 which is my Tailscale IPv6 address on my mac that running Next.js app on tmux.</p>
|
|
||||||
<p><img src="./Screen-Shot-2021-10-16-at-5.43.20-AM.png" alt=""></p>
|
|
||||||
<p>Go <a href="https://app.init8.lol">visit this</a> to try it out before my mac dies! (dead)</p>
|
|
||||||
<h2>What's next?</h2>
|
|
||||||
<p>My main point is to destroy my DigitalOcean droplet which only does one thing which is proxying requests. Every time I run new service, I need to SSH into my VM; update <code>Caddyfile</code>, <code>systemctl reload caddy</code>, and so on. Apart from that I need to manage & maintain the server and I'm too lazy for that.</p>
|
|
||||||
<p>In future I would like to add some nice functionality like:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Issuing an SSL certificate from Let's Encrypt. In theory this is possible as long as your domain can complete HTTP-01 challenge.</li>
|
|
||||||
<li>GUI access to manage proxies so I don't have to use curl anymore when adding new service</li>
|
|
||||||
<li>Hardening the security</li>
|
|
||||||
<li>Make it work properly & correctly</li>
|
|
||||||
</ul>
|
|
||||||
<p>This project is pretty fun and it took me 5-ish hours while I wrote this blog post to create a PoC.</p>
|
|
||||||
<p>If you have a web service at home and want to expose it to the public internet via the Fly.io network, mention me on <s>Twitter <a href="https://twitter.com/200GbE">@200GbE</a> (updated)</s> Mastodon <a href="https://edgy.social/@rizaldy">@rizaldy@edgy.social</a> and let's chat.</p>
|
|
||||||
<p>You can also check out <a href="https://git.edgy.social/rizaldy/rpaas">this project</a> on this repository to learn more, especially there's something I haven't attached here like the Dockerfile and fly.toml files.</p>
|
|
||||||
<h2>Demo (dead)</h2>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://nginx.init8.lol">Demo 1</a> (machine with a static public IP address, my devbox)</li>
|
|
||||||
<li><a href="https://app.init8.lol">Demo 2</a> (machine with a dynamic public IP address, my laptop)</li>
|
|
||||||
<li><a href="https://ts-proxy.fly.dev">Demo 3</a> (fly.io instance for this project)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
121
blog/index.html
@ -1,121 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
Blog
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment">
|
|
||||||
<h2>Blog</h2>
|
|
||||||
<p>Technically a blog.</p>
|
|
||||||
|
|
||||||
|
|
||||||
<article class="c-article"><div>
|
|
||||||
<h3>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="/blog/pac-is-actually-useful/">Proxy Auto-Configuration (PAC) is actually useful</a>
|
|
||||||
</h3><p class="c-article__meta-info">
|
|
||||||
<time>Jul 05, 2023</time> •
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#tailscale">TAILSCALE</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#proxy">PROXY</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
</p><p>So I just got my new Mac machine last week. It's just a small machine with 8 CPUs and 8 GB of memory. I've been thinking about buying one for a long time and now the time has come!</p>
|
|
||||||
</div><div>
|
|
||||||
<h3>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="/blog/syncthing-anywhere-with-tailscale/">Syncthing Anywhere With Tailscale</a>
|
|
||||||
</h3><p class="c-article__meta-info">
|
|
||||||
<time>Jan 04, 2022</time> •
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#tailscale">TAILSCALE</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#syncthing">SYNCTHING</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
</p><p>I don't archive data very often but when I do it must be for a very important one. On the other hand, I somewhat don't trust "cloud" providers and would avoid them as much as I can since my paranoid level is kinda high.</p>
|
|
||||||
</div><div>
|
|
||||||
<h3>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="/blog/expose-web-service-at-home-via-tailscale-for-fun/">Expose Web Services at Home via Tailscale for Fun</a>
|
|
||||||
</h3><p class="c-article__meta-info">
|
|
||||||
<time>Oct 16, 2021</time> •
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#tailscale">TAILSCALE</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
</p><p>I have a small homelab server at home running TrueNAS Core. My home network sits behind NAT and there are probably 3 routers in front of me.
|
|
||||||
</div></article>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,157 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
Proxy Auto-Configuration (PAC) is actually useful
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment l-fragment--blog">
|
|
||||||
<div class="c-article">
|
|
||||||
<h1>Proxy Auto-Configuration (PAC) is actually useful</h1>
|
|
||||||
|
|
||||||
<p class="c-article__meta-info">
|
|
||||||
<time>Jul 05, 2023</time> •
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#tailscale">TAILSCALE</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#proxy">PROXY</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>So I just got my new Mac machine last week. It's just a small machine with 8 CPUs and 8 GB of memory. I've been thinking about buying one for a long time and now the time has come!</p>
|
|
||||||
<p>I'll be using this tiny PC as my main workstation, but unlike my previous approach of setting up development environments, I now have plans to isolate each existing environment into VMs,
|
|
||||||
like, one for private; one for side projects, one for community stuff, and one for work.</p>
|
|
||||||
<p>I want to keep this tiny PC as clean as possible.</p>
|
|
||||||
<h2>The dumb way to use Tailscale</h2>
|
|
||||||
<p>This is the main topic of this post, but I'm doing my best to make this post not feel like a shot tweet. On my laptop I use 4 different identities (accounts) in Tailscale,
|
|
||||||
and I used to use Tailscale's <a href="https://tailscale.com/blog/fast-user-switching/">Fast user switching</a> feature which was quite useful. Often I forget to switch back
|
|
||||||
to my personal account after I realize I can't SSH into my server at home via its hostname.</p>
|
|
||||||
<p>So here's the story: I don't want to "switch" anymore so I don't forget one more time.</p>
|
|
||||||
<p>I create every VM I need and use different identities for Tailscale there. The OS is NixOS and I'm using <a href="https://orbstack.dev">OrbStack</a> to provision VMs on my tiny PC.
|
|
||||||
When I'm not working from home, I can SSH into the VM with this tiny PC as a jumphost while hoping my internet at home is working fine.</p>
|
|
||||||
<h3>First, it was DNS</h3>
|
|
||||||
<p>Every <a href="https://tailscale.com/kb/1136/tailnet/">Tailnet</a> has its own <a href="https://tailscale.com/kb/1217/tailnet-name/">unique name</a> with <code>ts.net</code> as root domains. My tiny PC's hostname
|
|
||||||
is <code>mac-mini</code> (sounds boring) so I can access it via <code>mac-mini.duck-map.ts.net</code>, and yes, duck-map.ts.net is my <em>real</em> Tailnet name.</p>
|
|
||||||
<p>The first problem is that <code>ts.net</code> can only be resolved by the "MagicDNS server" which resides on your own device accessed via <code>100.100.100.100</code>. This means that
|
|
||||||
when you try to query names for a machine that is outside your tailnet, you will get a bogus NXDOMAIN — which is good.</p>
|
|
||||||
<p>The second problem is that you can't route packets to machines outside your tailnet, of course.</p>
|
|
||||||
<p>Maybe I could use <a href="https://tailscale.com/kb/1019/subnets/">Subnet Routers</a> to advertise subnets of the bridge interface used by my VM but that only solves half of the problem (excluding DNS queries).</p>
|
|
||||||
<p>And what about the <a href="https://tailscale.com/kb/1103/exit-nodes/">Exit Node</a> option? Of course not the answer.</p>
|
|
||||||
<h3>Then, it was routing tables</h3>
|
|
||||||
<p>In certain cases I had to access an internal application that was only accessible through Tailscale to troubleshoot (#sysadminlife). I don't use proxies much but when I do my favorite
|
|
||||||
is to <code>ssh -D 6669 somewhere</code> and use 127.0.0.1:6669 as SOCKS5 proxy servers.</p>
|
|
||||||
<p><img src="./236D9096-4D0A-43A5-9827-6227230FB8FE.jpg" alt="236D9096-4D0A-43A5-9827-6227230FB8FE.jpg"></p>
|
|
||||||
<p>From the screenshot above, <code>kudxxx.tailnet-xxxxx12.ts.net</code> is the machine that resides on the tailnet where I work. I can't resolve the name, because, well, I'm using my own personal tailnet.</p>
|
|
||||||
<p>Then I can use <code>ssh -D 6669 delman@orb</code> where <code>delman</code> is the name of the VM to the tailnet at work. <code>socks5h</code> indicates that the DNS query is made on SOCKS5 proxy servers.</p>
|
|
||||||
<p>If referring to the screenshot above, I think it works.</p>
|
|
||||||
<h3>Proxy Auto-Configuration</h3>
|
|
||||||
<p>What if I need to access different machines on different tailnets like <code>heavy-rotation.duck-map.ts.net</code> and <code>kudxxx.tailnet-xxxxx12.ts.net</code> at the same time?</p>
|
|
||||||
<p>On MacOS (or maybe other OS too) you can only use 1 proxy server on your machine at a time. So <code>ssh -D 6669 delman@orb</code> and <code>ssh -D 4848 heavy-rotation@orb</code> require extra work when
|
|
||||||
you need to use either one.</p>
|
|
||||||
<p><img src="./AF9155DC-0B3D-4A4C-9F64-7435D6F77738.jpg" alt="./AF9155DC-0B3D-4A4C-9F64-7435D6F77738.jpg"></p>
|
|
||||||
<p>And no, using transparent proxies doesn't help.</p>
|
|
||||||
<p>And then I just came across an old technology called <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file">Proxy Auto-Configuration</a>
|
|
||||||
which is the title of this post. The concept is actually simple: a PAC is just a JavaScript file that calls a <code>FindProxyForURL</code> function that returns a single string. The minimal
|
|
||||||
script is something like this:</p>
|
|
||||||
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">FindProxyForURL</span> <span class="token punctuation">(</span><span class="token parameter">url<span class="token punctuation">,</span> host</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token function">alert</span><span class="token punctuation">(</span><span class="token string">'url '</span> <span class="token operator">+</span> url<span class="token punctuation">)</span><br> <span class="token function">alert</span><span class="token punctuation">(</span><span class="token string">'host '</span> <span class="token operator">+</span> host<span class="token punctuation">)</span><br><br> <span class="token keyword">return</span> <span class="token string">'DIRECT'</span><br><span class="token punctuation">}</span></code></pre>
|
|
||||||
<p>You don't need to save it using <code>.js</code> extensions by the way.</p>
|
|
||||||
<p>As far as I know that <code>alert</code> function doesn't work in Safari but it works fine in Firefox and Chrome. This is how it looks when "debugging the PAC" using Firefox:</p>
|
|
||||||
<p><img src="./1A5B132F-D2F9-4B1F-9923-434B045CFF60.jpg" alt="./1A5B132F-D2F9-4B1F-9923-434B045CFF60.jpg"></p>
|
|
||||||
<p>In many cases, you don't need to do that just to verify if your PAC is working — checking the access.log where you hosted the pac file must be enough.</p>
|
|
||||||
<h3>Making it official</h3>
|
|
||||||
<p>Now, here is the strategy:</p>
|
|
||||||
<ul>
|
|
||||||
<li>If I access <code>.duck-map.ts.net</code>, proxy the requests to <code>127.0.0.1:4848</code></li>
|
|
||||||
<li>If I access <code>.tailnet-xxxxx12.ts.net</code>, proxy the requests to <code>127.0.0.1:6669</code></li>
|
|
||||||
<li>Other than that don't proxy the requests to anywhere</li>
|
|
||||||
</ul>
|
|
||||||
<p>Actually I can use <code>mac-mini</code> instead of <code>127.0.0.1</code> as the hostname so I can use the PAC file everywhere using the same URL.</p>
|
|
||||||
<p>In every VM I use <a href="https://github.com/ginuerzh/gost"><code>gost</code></a> as SOCKS5 server. I can create a simple systemd service for <code>gost</code> like this:</p>
|
|
||||||
<pre class="language-js"><code class="language-js">systemd<span class="token punctuation">.</span>services<span class="token punctuation">.</span>gost <span class="token operator">=</span> <span class="token punctuation">{</span><br> description <span class="token operator">=</span> <span class="token string">"gost"</span><span class="token punctuation">;</span><br><br> after <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"network.target"</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br> wantedBy <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"multi-user.target"</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><br> serviceConfig <span class="token operator">=</span> <span class="token punctuation">{</span><br> ExecStart <span class="token operator">=</span> <span class="token string">"${pkgs.gost}/bin/gost -L=:6669"</span><span class="token punctuation">;</span><br> Restart <span class="token operator">=</span> <span class="token string">"always"</span><span class="token punctuation">;</span><br> RestartSec <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
|
|
||||||
<p>And then <code>nixos-rebuild switch</code> as usual, then check:</p>
|
|
||||||
<pre class="language-bash"><code class="language-bash">$ systemctl status gost<br><br>● gost.service - gost<br> Loaded: loaded <span class="token punctuation">(</span>/etc/systemd/system/gost.service<span class="token punctuation">;</span> enabled<span class="token punctuation">;</span> preset: enabled<span class="token punctuation">)</span><br> Drop-In: /nix/store/rmhm2f4izkfxkpaix0ca2pxnvyswkxfi-system-units/service.d<br> └─zzz-lxc-service.conf<br> Active: active <span class="token punctuation">(</span>running<span class="token punctuation">)</span> since Wed <span class="token number">2023</span>-07-05 <span class="token number">15</span>:57:51 WIB<span class="token punctuation">;</span> 8h ago<br> Main PID: <span class="token number">43857</span> <span class="token punctuation">(</span>gost<span class="token punctuation">)</span><br> IP: <span class="token number">16</span>.1M in, <span class="token number">15</span>.8M out<br> IO: 0B read, 0B written<br> Tasks: <span class="token number">11</span> <span class="token punctuation">(</span>limit: <span class="token number">7106</span><span class="token punctuation">)</span><br> Memory: <span class="token number">10</span>.5M<br> CPU: <span class="token number">4</span>.504s<br> CGroup: /system.slice/gost.service<br> └─43857 /nix/store/q34c64p4cnxh67yxsqxjpjsgdmg8ilpq-gost-2.11.5/bin/gost <span class="token parameter variable">-L</span><span class="token operator">=</span>:6669</code></pre>
|
|
||||||
<p>For the contents of the PAC file, it could be like this:</p>
|
|
||||||
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">FindProxyForURL</span> <span class="token punctuation">(</span><span class="token parameter">url<span class="token punctuation">,</span> host</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> work <span class="token operator">=</span> <span class="token string">"SOCKS5 mac-mini:6669"</span><br> community <span class="token operator">=</span> <span class="token string">"SOCKS5 mac-mini:4848"</span><br><br> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">shExpMatch</span><span class="token punctuation">(</span>host<span class="token punctuation">,</span> <span class="token string">"*.tailnet-xxxxx12.ts.net"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">return</span> work<br> <span class="token punctuation">}</span><br><br> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">shExpMatch</span><span class="token punctuation">(</span>host<span class="token punctuation">,</span> <span class="token string">"*.duck-map.ts.net"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">return</span> community<br> <span class="token punctuation">}</span><br><br> <span class="token keyword">return</span> <span class="token string">"DIRECT"</span><br><span class="token punctuation">}</span></code></pre>
|
|
||||||
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file#shexpmatch"><code>shExpMatch</code></a> is a function to check if the string
|
|
||||||
matches a specified shell glob expression. When one of the conditions is met, it tells the client <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file#return_value_format">how to connect</a> to the target.</p>
|
|
||||||
<p><a href="./F89B89ED-50B6-4A0C-B6C2-078BFE35CCE6.jpg"><img src="./F89B89ED-50B6-4A0C-B6C2-078BFE35CCE6.jpg" alt="./F89B89ED-50B6-4A0C-B6C2-078BFE35CCE6.jpg"></a></p>
|
|
||||||
<p><small><center><em>*click image above to enlarge*</em></center></small></p>
|
|
||||||
<p>As you may see I can access <code>*.duck-map.ts.net</code> and <code>*.tailnet-xxxxx12.ts.net</code> at the same time without switching accounts. If you check <code>(1)</code> and <code>(2)</code> in the screenshot above, the value in "remote address" is where the proxy server is running.</p>
|
|
||||||
<h3>But why not share nodes?</h3>
|
|
||||||
<p>I can <a href="https://tailscale.com/kb/1084/sharing/">share</a> my <code>mac-mini</code> devices to every tailnet I want, but why not? I don't know, maybe the answer is the same as why I installed and used a different account on Tailscale, in different VMs.</p>
|
|
||||||
<p>Also, "shared devices" are <em>quarantined</em> by default. Which means my <code>mac-mini</code> can't initiate connections to devices on the "shared network" until they talk to it first — although it's not a big deal.</p>
|
|
||||||
<h3>Why not Tailscale Funnel?</h3>
|
|
||||||
<p>It's a different story. Tailscale Funnel is all about exposing devices to the <em>wider</em> internet. This means that even anyone with no Tailscale installed can access
|
|
||||||
(usually a web service) via the boring HTTPS protocol. No MagicDNS. No CGNAT IPs. Just the internet we are used to.</p>
|
|
||||||
<h2>Conclusion</h2>
|
|
||||||
<p>There is no way to verify the integrity of the PAC file especially if you load it via a remote address using an insecure procotol. Which means MiTM attacks are by no means impossible. Maybe someone
|
|
||||||
is snooping on your network especially if you are on a public network that uses a captive portal. There's nothing to stop anyone from modifying a PAC file if <em>they</em> want and can.</p>
|
|
||||||
<p>But it's worth noting that almost all traffic in 2023 uses end-to-end encryption via HTTPS protocols. If you're installing a "CA certificate" because someone out of nowhere
|
|
||||||
asked you to do so, don't do it. If it's too late, maybe consider stopping receiving candy from random people at the bar as well, if that happens to you.</p>
|
|
||||||
<p>A simple <code>python -m http.server</code> or <code>caddy:alpine</code> web server can help serve your PAC files on the machine you control. And since you're in control, you probably already have Tailscale installed on the device and can
|
|
||||||
use a secure transport (such as Wireguard protocols) to load the PAC file.</p>
|
|
||||||
<p>The Proxy Auto-Configuration was <a href="https://developer.mozilla.org/en-US/docs/web/http/proxy_servers_and_tunneling/proxy_auto-configuration_pac_file#history_and_implementation">introduced</a> into Netscape Navigator 2.0 in the late 1990s
|
|
||||||
at the same time when JavaScript was introduced. For years I have wondered why my machine has "Automatic proxy configuration" options and why I would ever need it.</p>
|
|
||||||
<p>And now I know.</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,116 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
Syncthing Anywhere With Tailscale
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment l-fragment--blog">
|
|
||||||
<div class="c-article">
|
|
||||||
<h1>Syncthing Anywhere With Tailscale</h1>
|
|
||||||
|
|
||||||
<p class="c-article__meta-info">
|
|
||||||
<time>Jan 04, 2022</time> •
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#tailscale">TAILSCALE</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#syncthing">SYNCTHING</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>I don't archive data very often but when I do it must be for a very important one. On the other hand, I somewhat don't trust "cloud" providers and would avoid them as much as I can since my paranoid level is kinda high.</p>
|
|
||||||
<p>And just like everyone else, I run servers at home. The servers are not that powerful but sufficient for my needs. I have two servers run but the most important (and data-heavy) is the box with TrueNAS OS.
|
|
||||||
Previously I use NextCloud to store my data plus I could access it anywhere. NextCloud is a powerful platform with rich features, I even can run an ActivityPub-based social network there. But the client app is somewhat heavy and the server consumes more resources than I thought, plus the caching system on the client app is somewhat buggy. I can't depend my life on NextCloud, or maybe someday will.</p>
|
|
||||||
<p>I know I can use <code>rsync(1)</code> or even <code>rclone(1)</code> on my computer but they both do different jobs. I need a system to pull-and-push data not just push.</p>
|
|
||||||
<p>And don't ask me why I'm not using BTSync.</p>
|
|
||||||
<p>Then I found <a href="https://syncthing.net">Syncthing</a> somewhere. It only does one thing and probably does it well: <strong>sync data</strong>. Its tagline is <em>"Syncthing is a <strong>continuous file synchronization program</strong>. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet."</em> and it really took my heart.</p>
|
|
||||||
<p>And then I installed Syncthing, used it for some time (until now), and that's why we are here.</p>
|
|
||||||
<h2>Syncing concepts</h2>
|
|
||||||
<p>Syncthing will establish direct connections between clients (peer-to-peer) as much as possible, and as we know, p2p connection is never easy. And if that's not possible, traffic is bounced through the "relay" until both computers figured out how to establish a direct connection — once again, if possible.</p>
|
|
||||||
<p>Syncthing uses <a href="https://docs.syncthing.net/dev/device-ids.html">"Device ID"</a> as an identifier so that neither party needs to know each other's IP address thanks to <a href="https://docs.syncthing.net/users/stdiscosrv.html">Global Discovery</a>. I just used the Local Discovery feature and turned the 'Enable Relaying' option off so I know the only connection made to .syncthing.net should be just sending an (anonymous) usage report, and I'm fine with that.</p>
|
|
||||||
<p>While this approach only makes my life harder, at least I know what I know even though all packets are encrypted in transit (via TLS) and are every device is authenticated by a cryptographic thing.</p>
|
|
||||||
<h2>LAN anywhere</h2>
|
|
||||||
<p>All my servers at home are almost locked-down. Only traffic to port 80 is allowed in on my WAN. Not even port forwarding because I don't know much about computer and networking security.
|
|
||||||
So I designed my home network to be limited to working only on LAN. I don't know exactly how but I just made TCP listen only to the private network. Like, If I can only visit Syncthing GUI via 192.168.1.56 then I can only visit Syncthing GUI over 192.168.1.0/24 network, on my network.</p>
|
|
||||||
<p>Speaking of my Syncthing, it's running on FreeBSD jail with a dedicated IP address from my DHCP server.</p>
|
|
||||||
<p>And as always, the only way to connect a private network over public networks is by establishing a VPN, assuming I'm not interested yet in a commercial "Zero Trust Network" solution that maybe tunnels any TCP/UDP packets through a commercial reverse proxy because the packets are not end-to-end encrypted.</p>
|
|
||||||
<p>Anyways, I use <a href="https://tailscale.com">Tailscale</a> and am a huge fan of both the team and the product. Tailscale is built on top Wireguard, and I used to use direct Wireguard until I was overwhelmed with mesh networking.</p>
|
|
||||||
<p>Using Tailscale is pretty straight forward, I don't have to manage keys; know each peer's IP address and public key, define DNS and even perform key rotation. It just works.</p>
|
|
||||||
<p>The problem is that my Syncthing is running on the FreeBSD jail and can't dial <code>devd(8)</code> in the jail whereas Tailscale needs to monitor network state changes (they have a <a href="https://github.com/tailscale/tailscale/pull/3508">workaround</a> for this! but I haven't tried it yet). Since I'm also running Tailscale on my TrueNAS, I can use <a href="https://tailscale.com/kb/1019/subnets">"subnet router"</a> which in short acts as a gateway of my physical subnet.</p>
|
|
||||||
<pre class="language-bash"><code class="language-bash">tailscale up --advertise-routes<span class="token operator">=</span><span class="token number">192.168</span>.1.0/24</code></pre>
|
|
||||||
<p>With the above command, I can access my Syncthing (and other jails too) wherever I am!</p>
|
|
||||||
<h2>Throughput</h2>
|
|
||||||
<p>Although the average latency in transmitting ICMP packets is unnoticeable, in the real world we mostly deal with TCP packets.</p>
|
|
||||||
<p>Syncthing transmits packets over TLS on top of TCP (which is great!) but in my needs it adds overhead as packets are already transmitted over secure protocol (Wireguard).</p>
|
|
||||||
<p>When I'm on my home network, the throughput is about 44 Mbits/s to my Syncthing jail since my laptop is connected over a WiFi network and on a different subnet. Ideally if on the same subnet (and over wired connection) my router and server can reach up to 900MiB/s.</p>
|
|
||||||
<p>What if I'm out of the house?</p>
|
|
||||||
<p>On a 50 Mbps network, using iperf3, I get around 6.49 MiB/s to my Syncthing jail and 8.56 MiB/s to direct host (depending on your network) which is… acceptable, kinda.
|
|
||||||
How about transferring 1.2GB files to my Syncthing jail over Tailscale?</p>
|
|
||||||
<p><img src="./CleanShot-2022-01-04-at-9.18.04@2x.png" alt=""></p>
|
|
||||||
<p>2.02 MiB/s is not bad enough, I guess?</p>
|
|
||||||
<h2>Conclusion</h2>
|
|
||||||
<p>Tailscale here is optional as Syncthing does the NAT traversal for you and also uses a secure protocol. Syncthing will do its best to establish a peer to peer connection and that's great!</p>
|
|
||||||
<p>However, with Tailscale I can access my "shared directory" via SAMBA on my other devices, anywhere. And also don't need any "Relay Server" only if my device can't talk peer-to-peer as Tailscale will do it for me :))</p>
|
|
||||||
<p>As closing, using both Tailscale and Syncthing is the best combination if you don't want to depend on a (cloud) storage providers.</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,77 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
Unboxing The Cloud (UTC): Intro
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment l-fragment--blog">
|
|
||||||
<div class="c-article">
|
|
||||||
<h1>Unboxing The Cloud (UTC): Intro</h1>
|
|
||||||
|
|
||||||
<p class="c-article__meta-info">
|
|
||||||
<time></time> •
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#utc">UTC</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>WIP</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,77 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
Unboxing The Cloud (UTC): VMs
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment l-fragment--blog">
|
|
||||||
<div class="c-article">
|
|
||||||
<h1>Unboxing The Cloud (UTC): VMs</h1>
|
|
||||||
|
|
||||||
<p class="c-article__meta-info">
|
|
||||||
<time></time> •
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="c-article__meta-info-tag"
|
|
||||||
><a class="u-no-underline u-underline--hover" href="#utc">UTC</a></span
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>WIP</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,69 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
Colophon
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment">
|
|
||||||
<h2>Website colophon</h2>
|
|
||||||
<p>This page outlines technical details about this website. You can learn more about the purpose and content of the website, as well as the author on the <a href="/">About</a> page.</p>
|
|
||||||
<p>This site was built using a boring <a href="https://www.11ty.dev">static site generator</a>, written in a boring <a href="https://daringfireball.net/projects/markdown/">template</a> <a href="https://shopify.github.io/liquid/">language</a> and served by boring <a href="https://min.io">S3 compatible storage</a> behind a boring <a href="https://nginx.org/en/">reverse proxy</a>.</p>
|
|
||||||
<p>The three primary fonts I use are:</p>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://www.fontshare.com/fonts/sentient">Sentient</a> (serif)</li>
|
|
||||||
<li><a href="https://www.fontshare.com/fonts/clash-display">Clash Display</a> (sans-serif)</li>
|
|
||||||
<li><a href="https://www.jetbrains.com/lp/mono/">JetBrains Mono</a> (monospace)</li>
|
|
||||||
</ul>
|
|
||||||
<p>Syntax highlighting is done by the great <a href="https://prismjs.com/">Prism.js</a>, statically generated of course.</p>
|
|
||||||
<p>If you enjoy the site, consider to <a href="https://github.com/faultables/rizaldy.today/discussions/new?category=show-and-tell">giving me a word</a> or <a href="https://github.com/sponsors/faultables">some love</a> (my love language is words of affirmation and github sponsors btw).</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
134
index.html
@ -1,134 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
About
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment">
|
|
||||||
<div class="c-bio u-clearfix">
|
|
||||||
<div class="c-bio__avatar">
|
|
||||||
<img src="/assets/img/avatar.jpg" alt="Rizaldy">
|
|
||||||
</div>
|
|
||||||
<div class="c-bio__info">
|
|
||||||
<h2>Rizaldy</h2>
|
|
||||||
<p>Senior DevOps Engineer</p>
|
|
||||||
<p><ul><li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="mailto:me@rizaldy.club">
|
|
||||||
Email: me@rizaldy.club</a>
|
|
||||||
</li><li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://keybase.io/rizaldy">
|
|
||||||
Keybase: keybase.io/rizaldy</a>
|
|
||||||
</li><li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://read.cv/rizaldy">
|
|
||||||
CV: read.cv/rizaldy</a>
|
|
||||||
</li></ul></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>Hi, I'm Rizaldy. I am currently a Senior DevOps Engineer at a Data Infrastructure Company based in Jakarta, ID. There have been so many changes in my life, but a few things that have remained unchanged are my interests in:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Infrastructure Management</li>
|
|
||||||
<li>System Administration</li>
|
|
||||||
<li>Developer Experience</li>
|
|
||||||
</ul>
|
|
||||||
<p>I used to do software development profesionally since 2015, mostly web application development. Since the 2020s my deep interests has been in making software developers more productive & happy while keeping systems up & running, securely.</p>
|
|
||||||
<p>I will be happy to help solve problems in any organization in any industry with my expertise.</p>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<div class="l-fragment">
|
|
||||||
<h2>Writings</h2>
|
|
||||||
<p>I love writing. Here are the 3 most recent posts:</p>
|
|
||||||
<article class="c-article"><div>
|
|
||||||
<h3>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="/blog/pac-is-actually-useful/">Proxy Auto-Configuration (PAC) is actually useful</a>
|
|
||||||
</h3><p>So I just got my new Mac machine last week. It's just a small machine with 8 CPUs and 8 GB of memory. I've been thinking about buying one for a long time and now the time has come!</p>
|
|
||||||
</div><div>
|
|
||||||
<h3>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="/blog/syncthing-anywhere-with-tailscale/">Syncthing Anywhere With Tailscale</a>
|
|
||||||
</h3><p>I don't archive data very often but when I do it must be for a very important one. On the other hand, I somewhat don't trust "cloud" providers and would avoid them as much as I can since my paranoid level is kinda high.</p>
|
|
||||||
</div><div>
|
|
||||||
<h3>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="/blog/expose-web-service-at-home-via-tailscale-for-fun/">Expose Web Services at Home via Tailscale for Fun</a>
|
|
||||||
</h3><p>I have a small homelab server at home running TrueNAS Core. My home network sits behind NAT and there are probably 3 routers in front of me.
|
|
||||||
</div></article>
|
|
||||||
<br />
|
|
||||||
<p><a href="/blog">View all posts</a></p>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<div class="l-fragment">
|
|
||||||
<h2>Services</h2>
|
|
||||||
<p>I am currently unavailable for consulting work (expected until Jan 2024).</p>
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="/services/devops/">
|
|
||||||
DevOps Engineering
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p>I can help organizations move fast and break less thing by adopting best practices from system administration; release engineering, infrastructure provisioning and management, security, to DevOps advocacy.</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="/services/rnd/">
|
|
||||||
R&D Engineer
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p>Reduce time to market without compromising quality and stability by choosing the right tools for the job. I can help you to choose the right one.</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="/services/sre/">
|
|
||||||
Site Reliability Engineering
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p>"Hope is not a strategy" as traditional SRE would say. I can help scale from availability; latency, performance, efficiency, change management, monitoring, emergency response, to capacity planning.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
14
package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "rizaldy.today",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "eleventy --serve",
|
||||||
|
"build": "eleventy",
|
||||||
|
"test": "echo \"it works\""
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@11ty/eleventy": "^2.0.0",
|
||||||
|
"@11ty/eleventy-plugin-syntaxhighlight": "^4.2.0"
|
||||||
|
}
|
||||||
|
}
|
1493
pnpm-lock.yaml
Normal file
@ -1,62 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
DevOps Engineering
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment">
|
|
||||||
<h2>Still WIP</h2>
|
|
||||||
<p>Thank you for stopping by.</p>
|
|
||||||
<br>
|
|
||||||
<a href="/"> Back to home </a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,62 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
DevRel
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment">
|
|
||||||
<h2>Still WIP</h2>
|
|
||||||
<p>Thank you for stopping by.</p>
|
|
||||||
<br>
|
|
||||||
<a href="/"> Back to home </a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,62 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
R&D Engineer
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment">
|
|
||||||
<h2>Still WIP</h2>
|
|
||||||
<p>Thank you for stopping by.</p>
|
|
||||||
<br>
|
|
||||||
<a href="/"> Back to home </a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,62 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
Site Reliability Engineering
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment">
|
|
||||||
<h2>Still WIP</h2>
|
|
||||||
<p>Thank you for stopping by.</p>
|
|
||||||
<br>
|
|
||||||
<a href="/"> Back to home </a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,62 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>
|
|
||||||
Web App Development
|
|
||||||
- rizaldy.today
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="me" href="https://edgy.social/@rizaldy">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://unpkg.com">
|
|
||||||
<link rel="preconnect" href="https://api.fontshare.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
|
||||||
|
|
||||||
<link href="/assets/css/reset.css" rel="stylesheet">
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="l-container">
|
|
||||||
<div class="l-fragment">
|
|
||||||
<h2>Still WIP</h2>
|
|
||||||
<p>Thank you for stopping by.</p>
|
|
||||||
<br>
|
|
||||||
<a href="/"> Back to home </a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="c-footer u-clearfix">
|
|
||||||
<div class="c-footer__copyleft">
|
|
||||||
<p class="u-text-left">
|
|
||||||
© MMXXIII Rizaldy. Any and all opinions listed here are my own and not representative of my
|
|
||||||
employers; future, past and present.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="c-footer__links">
|
|
||||||
<ul>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
|
||||||
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
|
||||||
<li>
|
|
||||||
<a class="u-no-underline u-underline--hover" href="https://github.com/faultables/rizaldy.today/commit/419894b"
|
|
||||||
>Source Code (419894b)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
10
shell.nix
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
with import <nixpkgs> {};
|
||||||
|
|
||||||
|
pkgs.mkShell {
|
||||||
|
name = "rizaldy.today";
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
nodejs
|
||||||
|
nodePackages.pnpm
|
||||||
|
];
|
||||||
|
}
|
15
src/_data/consulting.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const CONSULTING_STATUS = {
|
||||||
|
AVAILABLE: "AVAILABLE",
|
||||||
|
UNAVAILABLE: "UNAVAILABLE",
|
||||||
|
};
|
||||||
|
|
||||||
|
const consulting = {
|
||||||
|
status: CONSULTING_STATUS.UNAVAILABLE,
|
||||||
|
clear_status_after: 1704091020,
|
||||||
|
timezone: "UTC+7",
|
||||||
|
hours_per_week: 25,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
return consulting;
|
||||||
|
};
|
21
src/_data/contacts.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const contacts = [
|
||||||
|
{
|
||||||
|
name: "Email",
|
||||||
|
label: "me@rizaldy.club",
|
||||||
|
uri: "mailto:me@rizaldy.club",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Keybase",
|
||||||
|
label: "keybase.io/rizaldy",
|
||||||
|
uri: "https://keybase.io/rizaldy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CV",
|
||||||
|
label: "read.cv/rizaldy",
|
||||||
|
uri: "https://read.cv/rizaldy",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
return contacts;
|
||||||
|
};
|
9
src/_data/employer.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const currentEmployment = {
|
||||||
|
name: "a Data Infrastructure Company",
|
||||||
|
position: "Senior DevOps Engineer",
|
||||||
|
location: "Jakarta, ID",
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
return currentEmployment;
|
||||||
|
};
|
13
src/_data/site.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const siteConfig = {
|
||||||
|
name: "rizaldy.today",
|
||||||
|
repo: "https://forge.edgy.social/rizaldy/rizaldy.today",
|
||||||
|
last_commit: process.env.LAST_COMMIT || "main",
|
||||||
|
author: {
|
||||||
|
name: "Rizaldy",
|
||||||
|
avatar: "/assets/img/avatar.jpg",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
return siteConfig;
|
||||||
|
};
|
24
src/_includes/elements/archives.liquid
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<article class="c-article">
|
||||||
|
{%- for post in posts limit: limit reversed -%}
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
<a class="u-no-underline u-underline--hover" href="{{ post.url }}">{{ post.data.title }}</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{%- if with_meta_info? -%}
|
||||||
|
<p class="c-article__meta-info">
|
||||||
|
<time>{{ post.date | date: '%b %d, %Y' }}</time> •
|
||||||
|
{% for tag in post.data.tags %}
|
||||||
|
{% unless tag == 'posts' %}
|
||||||
|
<span class="c-article__meta-info-tag"
|
||||||
|
><a class="u-no-underline u-underline--hover" href="#{{tag}}">{{ tag | upcase }}</a></span
|
||||||
|
>
|
||||||
|
{% endunless %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{{ post.content | split: '\n' | first }}
|
||||||
|
</div>
|
||||||
|
{%- endfor -%}
|
||||||
|
</article>
|
20
src/_includes/elements/bio.liquid
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<div class="c-bio u-clearfix">
|
||||||
|
<div class="c-bio__avatar">
|
||||||
|
<img src="{{ site.author.avatar }}" alt="{{ site.author.name }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="c-bio__info">
|
||||||
|
<h2>{{ site.author.name }}</h2>
|
||||||
|
<p>{{ employer.position }}</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{%- for contact in contacts -%}
|
||||||
|
<li>
|
||||||
|
<a class="u-no-underline u-underline--hover" href="{{ contact.uri }}">
|
||||||
|
{{ contact.name }}: {{ contact.label -}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{%- endfor -%}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
20
src/_includes/elements/footer.liquid
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<footer class="c-footer u-clearfix">
|
||||||
|
<div class="c-footer__copyleft">
|
||||||
|
<p class="u-text-left">
|
||||||
|
© MMXXIII {{ site.author.name }}. Any and all opinions listed here are my own and not representative of my
|
||||||
|
employers; future, past and present.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="c-footer__links">
|
||||||
|
<ul>
|
||||||
|
<li><a class="u-no-underline u-underline--hover" href="/">About</a></li>
|
||||||
|
<li><a class="u-no-underline u-underline--hover" href="/blog">Writings</a></li>
|
||||||
|
<li><a class="u-no-underline u-underline--hover" href="/colophon">Colophon</a></li>
|
||||||
|
<li>
|
||||||
|
<a class="u-no-underline u-underline--hover" href="{{ site.repo }}/commit/{{ site.last_commit }}"
|
||||||
|
>Source Code ({{ site.last_commit }})</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</footer>
|
22
src/_includes/elements/head.liquid
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>
|
||||||
|
{{ title }}
|
||||||
|
- rizaldy.today
|
||||||
|
</title>
|
||||||
|
|
||||||
|
<link rel="me" href="https://edgy.social/@rizaldy">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://unpkg.com">
|
||||||
|
<link rel="preconnect" href="https://api.fontshare.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
|
|
||||||
|
<link href="https://unpkg.com/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
||||||
|
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@1&f[]=sentient@1&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet">
|
||||||
|
|
||||||
|
<link href="/assets/css/reset.css" rel="stylesheet">
|
||||||
|
<link href="/assets/css/style.css" rel="stylesheet">
|
||||||
|
</head>
|
15
src/_includes/fragments/about.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<div class="l-fragment">
|
||||||
|
|
||||||
|
{% render "elements/bio", site: site, contacts: contacts, employer: employer %}
|
||||||
|
|
||||||
|
Hi, I'm {{ site.author.name }}. I am currently a {{ employer.position }} at {{ employer.name }} based in {{ employer.location }}. There have been so many changes in my life, but a few things that have remained unchanged are my interests in:
|
||||||
|
|
||||||
|
- Infrastructure Management
|
||||||
|
- System Administration
|
||||||
|
- Developer Experience
|
||||||
|
|
||||||
|
I used to do software development profesionally since 2015, mostly web application development. Since the 2020s my deep interests has been in making software developers more productive & happy while keeping systems up & running, securely.
|
||||||
|
|
||||||
|
I will be happy to help solve problems in any organization in any industry with my expertise.
|
||||||
|
|
||||||
|
</div>
|
27
src/_includes/fragments/services.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<div class="l-fragment">
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
{% if consulting.status == "AVAILABLE" %}
|
||||||
|
I am currently available for consulting work and if you need hand with an issue you currently have in your organization, let's talk. My
|
||||||
|
current time zone is {{ consulting.timezone }} and I'm available to work {{ consulting.hours_per_week }} hours per week.
|
||||||
|
{% else %}
|
||||||
|
I am currently unavailable for consulting work (expected until {{ consulting.clear_status_after | date: "%b %Y" }}).
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for service in services %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
<a class="u-no-underline u-underline--hover" href="{{ service.url }}">
|
||||||
|
{{ service.data.title }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{{ service.content | split: '\n' | first }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
13
src/_includes/fragments/writings.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<div class="l-fragment">
|
||||||
|
|
||||||
|
## Writings
|
||||||
|
|
||||||
|
I love writing. Here are the 3 most recent posts:
|
||||||
|
|
||||||
|
{% render "elements/archives", posts: posts, limit: 3 %}
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
[View all posts](/blog)
|
||||||
|
|
||||||
|
</div>
|
8
src/_includes/layouts/archives.liquid
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
layout: layouts/base
|
||||||
|
---
|
||||||
|
<div class="l-fragment">
|
||||||
|
{{ content }}
|
||||||
|
|
||||||
|
{% render 'elements/archives', posts: collections.posts, with_meta_info?: true %}
|
||||||
|
</div>
|
14
src/_includes/layouts/base.liquid
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
{% render 'elements/head', title: title %}
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main class="l-container">
|
||||||
|
{{ content }}
|
||||||
|
|
||||||
|
{% render 'elements/footer', site: site %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% render 'elements/scripts' %}
|
||||||
|
</body>
|
||||||
|
</html>
|
21
src/_includes/layouts/blog.liquid
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
layout: layouts/base
|
||||||
|
---
|
||||||
|
<div class="l-fragment l-fragment--blog">
|
||||||
|
<div class="c-article">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
|
<p class="c-article__meta-info">
|
||||||
|
<time>{{ date | date: '%b %d, %Y' }}</time> •
|
||||||
|
{% for tag in tags %}
|
||||||
|
{% unless tag == 'posts' %}
|
||||||
|
<span class="c-article__meta-info-tag"
|
||||||
|
><a class="u-no-underline u-underline--hover" href="#{{tag}}">{{ tag | upcase }}</a></span
|
||||||
|
>
|
||||||
|
{% endunless %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
9
src/_includes/layouts/service.liquid
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: layouts/base
|
||||||
|
---
|
||||||
|
<div class="l-fragment">
|
||||||
|
<h2>Still WIP</h2>
|
||||||
|
<p>Thank you for stopping by.</p>
|
||||||
|
<br>
|
||||||
|
<a href="/"> Back to home </a>
|
||||||
|
</div>
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 965 KiB After Width: | Height: | Size: 965 KiB |
Before Width: | Height: | Size: 989 KiB After Width: | Height: | Size: 989 KiB |
Before Width: | Height: | Size: 595 KiB After Width: | Height: | Size: 595 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
title: Expose Web Services at Home via Tailscale for Fun
|
||||||
|
layout: layouts/blog
|
||||||
|
date: "2021-10-16T13:37:56+07:00"
|
||||||
|
tags:
|
||||||
|
- posts
|
||||||
|
- tailscale
|
||||||
|
---
|
||||||
|
|
||||||
|
I have a small homelab server at home running TrueNAS Core. My home network sits behind NAT and there are probably 3 routers in front of me.
|
||||||
|
On the other hand I also have a small VM that has a static public IP address somewhere in Singapore. Some of my services need to be exposed to the internet for my friends to access—for example—this blog via DNS because remembering a domain address is more fun than a random number with 4 dots.
|
||||||
|
|
||||||
|
The problem is that this blog is running on my NAS and using a private IP address.
|
||||||
|
|
||||||
|
Also, my ISP assigns a public IP Address to my network dynamically. The simplest way to expose my services on my NAS to the Internet might be using Dynamic DNS but sometimes it's not as easy as it sounds.
|
||||||
|
|
||||||
|
So maybe I need to connect one of my VMs in Singapore with my NAS at home. Since it is not possible to connect ethernet cable from NAS to DigitalOcean data center in Singapore, so I have to connect it virtually.
|
||||||
|
|
||||||
|
And yes, by creating a VPN.
|
||||||
|
|
||||||
|
## Tailscale VPN
|
||||||
|
|
||||||
|
Previously I used [Wireguard](https://wireguard.com) with a hub-and-spoke network because managing the keys on each of my machines was quite a chore.
|
||||||
|
|
||||||
|
Then I found out [Zerotier](https://zerotier.com) from a random page on Reddit. Zerotier uses a mesh network and it's cute how my machines can talk to each other on a peer-to-peer basis.
|
||||||
|
|
||||||
|
My problem with Zerotier is sometimes the network is somewhat unreliable and maybe it's my poor VM's fault. Also sometimes my machines just randomly can't talk to each other via the Zerotier assigned address and I believe it's a firewall issue.
|
||||||
|
|
||||||
|
So I found out Tailscale from one of my friends on Twitter (actually he is my boss at work). Tailscale is built on Wireguard and is a mesh network. Even though Wireguard has [its own](https://github.com/WireGuard/wg-dynamic) mesh network solution, it's still WIP and using [alternative](https://github.com/k4yt3x/wg-meshconf) is quite difficult because, again, managing keys is a pain.
|
||||||
|
|
||||||
|
Then I give Tailscale a try. Installing and running Tailscale is easy enough that even my gf (who is non an IT person) can use it without wondering what public/private key means.
|
||||||
|
|
||||||
|
Our devices can talk to each other even though we're on different networks, and that's cool. In most cases, we can communicate peer-to-peer and that's really great.
|
||||||
|
|
||||||
|
## Exposing web services on different private networks
|
||||||
|
|
||||||
|
We have a secret journal running on my NAS and only accessible over the local network. The domain address is [1460.rizaldy.club](https://1460.rizaldy.club) and resolves to a local IP address on the 192.168.1.0/24 subnet so maybe if you access it you will see a random web page (or none at all) rather than our secret journal.
|
||||||
|
|
||||||
|
My gf's private network uses the 10.26.0.0/24 while our journal lives in 192.168.1.242. The key is I use [subnet routers](https://tailscale.com/kb/1019/subnets/) and I have Tailscale on my router (and on my device as well) at home. While Tailscale for iOS (and others) has "accept routes" enabled by default, that means our secret journal is directly accessible out of the box because my router advertises the 192.168.1.0/24 subnet and we're on the same tailnet.
|
||||||
|
|
||||||
|
![some diagram](./Untitled-2021-10-16-0110.png)
|
||||||
|
|
||||||
|
And that's cool.
|
||||||
|
|
||||||
|
I never even touched Tailscale.app on her phone just to make sure everything was working fine, because it is. We can access it anywhere without having to expose the service to the internet, and that's it the point.
|
||||||
|
|
||||||
|
## Exposing web services on a private network to the Internet
|
||||||
|
|
||||||
|
I have a minio instance on my NAS and sometimes I upload cat photos there. My minio is accessible at [faultables-s3.lan](https://faultables-s3.lan) address and since I'm using [Magic DNS](https://tailscale.com/kb/1081/magicdns) too, devices on my tailnet can resolve that domain (thanks to split tunnel) then anyone on my tailnet can see the cat photo I uploaded so maybe I can stop use imgur service as well.
|
||||||
|
|
||||||
|
But I also want my friends to know because my friends are nice and they deserve to see cats. To allow my friend to access it without doing anything, I need a static public IP address and my VM has it. Connecting my VM with my NAS via Tailscale was the answer and that's why you can see [this cat](https://s3.edgy.social/0x0/bff5d074d399bdfec6071e9168398406.jpg) right on your screen.
|
||||||
|
|
||||||
|
The setup is pretty simple, I just pointed s3.faultable.dev to my VM's IP, set up [Caddy Web Server](https://caddyserver.com) there, and told Caddy to proxy the request to 192.168.1.170:9000.
|
||||||
|
|
||||||
|
Here's another diagram:
|
||||||
|
|
||||||
|
![I did my best to visualize it](./Untitled-2021-10-15-2135-2048x985.png)
|
||||||
|
|
||||||
|
Packets between my VM and my router are transmitted in an end-to-end encrypted using the Wireguard protocol so no one can see and/or modify the packets even if no one cares and that's cool.
|
||||||
|
|
||||||
|
## Reverse Proxy as a Service for fun
|
||||||
|
|
||||||
|
So I just mentioned Tailscale on Twitter about my recent random thoughts:
|
||||||
|
|
||||||
|
![https://static-tweet.vercel.app/1448966405391405056](./Screen-Shot-2021-10-15-at-9.47.40-PM.png)
|
||||||
|
|
||||||
|
[@apenwarr's](https://twitter.com/apenwarr/status/1448972965110898693) answer regarding my thoughts is perfectly understandable: it's not Tailscale's main business (nor focus) so why don't I create one?
|
||||||
|
|
||||||
|
Imagine you don't have to touch any server to just proxy web requests from the public internet to machines in your tailnet. Invite my machine to your tailnet, tell me the address of the domain you own, then tell me where to proxy requests for it.
|
||||||
|
|
||||||
|
In my mind is to setup an OpenResty instance with Redis as a data store, so let's make it official.
|
||||||
|
|
||||||
|
I'll be using a service from Fly.io (because I'm very interested in their service) to deploy OpenResty. For a high-level view of how it works, here's another cool diagram:
|
||||||
|
|
||||||
|
![](./Untitled-2021-10-16-0110-2.png)
|
||||||
|
|
||||||
|
Actually I haven't deployed any instances to fly.io when creating this diagram, but if you can see this post it's very likely the diagram above is working.
|
||||||
|
|
||||||
|
Now let's try this out.
|
||||||
|
|
||||||
|
## Minimum Viable RPaaS
|
||||||
|
|
||||||
|
We'll use openresty/openresty Docker's image as base image because we'll deploy it to fly.io plus we'll bring the Tailscale app on it later.
|
||||||
|
|
||||||
|
We'll be using the [openresty/openresty](https://hub.docker.com/r/openresty/openresty) docker image as the base image as we'll be deploying it to fly.io plus we'll be bringing the Tailscale app over later.
|
||||||
|
|
||||||
|
I'll be using a managed Redis solution from [Upstash](https://upstash.com) instead of Fly.io and we'll talk about it later. Now let's write the nginx.conf file:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
worker_processes 2;
|
||||||
|
error_log logs/error.log info;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
env REDIS_HOST;
|
||||||
|
env REDIS_PORT;
|
||||||
|
env REDIS_PASSWORD;
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
set $upstream '';
|
||||||
|
|
||||||
|
access_by_lua '
|
||||||
|
local redis = require "resty.redis"
|
||||||
|
local redisc = redis:new()
|
||||||
|
local target = ngx.var.host
|
||||||
|
|
||||||
|
local redis_host = os.getenv("REDIS_HOST")
|
||||||
|
local redis_port = os.getenv("REDIS_PORT")
|
||||||
|
local redis_password = os.getenv("REDIS_PASSWORD")
|
||||||
|
|
||||||
|
local connect, err = redisc:connect(redis_host, redis_port, {
|
||||||
|
ssl = true
|
||||||
|
})
|
||||||
|
|
||||||
|
if not connect then
|
||||||
|
ngx.log(ngx.ERR, "failed to connect to redis", err)
|
||||||
|
return ngx.exit(500)
|
||||||
|
end
|
||||||
|
|
||||||
|
local auth, err = redisc:auth(redis_password)
|
||||||
|
|
||||||
|
if not auth then
|
||||||
|
ngx.say("failed to authenticate", err)
|
||||||
|
return ngx.exit(500)
|
||||||
|
end
|
||||||
|
|
||||||
|
local get_upstream, err = redisc:get(target)
|
||||||
|
|
||||||
|
if not get_upstream or get_upstream == ngx.null then
|
||||||
|
ngx.log(ngx.ERR, "no host found for key", key)
|
||||||
|
return ngx.exit(404)
|
||||||
|
end
|
||||||
|
|
||||||
|
ngx.var.upstream = get_upstream
|
||||||
|
';
|
||||||
|
|
||||||
|
proxy_pass http://$upstream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The configuration above in short is to tell OpenResty to handle the request based on its Host header and forward the request based on the existing values. When the host is not listed in our Redis record, we will return a 404 Not Found page because it is.
|
||||||
|
|
||||||
|
The very simple data we're storing for now is just like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<target_host>:<upstream_ip>:<upstream_port>
|
||||||
|
```
|
||||||
|
|
||||||
|
So if I want to handle every incoming request from nginx.init8.lol to 100.73.204.66:42069, the operation is as simple as `SET nginx.init8.lol 100.73.204.66:42069`.
|
||||||
|
|
||||||
|
[![](./Screen-Shot-2021-10-16-at-4.07.16-AM-2048x1305.png)](./Screen-Shot-2021-10-16-at-4.07.16-AM-2048x1305.png)
|
||||||
|
|
||||||
|
Please keep in mind that since packets are end-to-end encrypted means that target upstream need to be directly to be the Tailscale IP. For example, port forwarding from your WAN to your LAN on your router won't work because packets are encrypted unless you set reverse proxy on your router (and let the proxy do the job).
|
||||||
|
|
||||||
|
## Making it more official
|
||||||
|
|
||||||
|
Now let's deploy our OpenResty to the fly.io platform. In order to connect Tailscale with Fly.io we can use [this guide](https://tailscale.com/kb/1132/flydotio) from Tailscale Docs. Our fly.io instance will only have Tailscale IPv6 so we need to point our domain to use Tailscale IPv6 so fly.io can route traffic to your machine.
|
||||||
|
|
||||||
|
So I'm going to create a frontend for this service so that it can interact with Redis via a REST API. It's still a WIP at the moment (I'm writing this post while creating the service lol) but I wanted to let my internet friends know I'm developing something fun (or at least for me).
|
||||||
|
|
||||||
|
The common setting than this is to set [ngrok.io](https://ngrok.io), tell ngrok.io which port you want to expose then you will get a unique URL. That's pretty cool but what if I'm dogfooding this?
|
||||||
|
So I point app.init8.lol to ts-proxy.fly.io, invite the machine to my tailnet, then tell the proxy to point that domain to [fd7a:115c:a1e0:ab12:4843:cd96:6258:9a11]:80 which is my Tailscale IPv6 address on my mac that running Next.js app on tmux.
|
||||||
|
|
||||||
|
![](./Screen-Shot-2021-10-16-at-5.43.20-AM.png)
|
||||||
|
|
||||||
|
Go [visit this](https://app.init8.lol) to try it out before my mac dies! (dead)
|
||||||
|
|
||||||
|
## What's next?
|
||||||
|
|
||||||
|
My main point is to destroy my DigitalOcean droplet which only does one thing which is proxying requests. Every time I run new service, I need to SSH into my VM; update `Caddyfile`, `systemctl reload caddy`, and so on. Apart from that I need to manage & maintain the server and I'm too lazy for that.
|
||||||
|
|
||||||
|
In future I would like to add some nice functionality like:
|
||||||
|
|
||||||
|
- Issuing an SSL certificate from Let's Encrypt. In theory this is possible as long as your domain can complete HTTP-01 challenge.
|
||||||
|
- GUI access to manage proxies so I don't have to use curl anymore when adding new service
|
||||||
|
- Hardening the security
|
||||||
|
- Make it work properly & correctly
|
||||||
|
|
||||||
|
This project is pretty fun and it took me 5-ish hours while I wrote this blog post to create a PoC.
|
||||||
|
|
||||||
|
If you have a web service at home and want to expose it to the public internet via the Fly.io network, mention me on ~~Twitter [@200GbE](https://twitter.com/200GbE) (updated)~~ Mastodon [@rizaldy@edgy.social](https://edgy.social/@rizaldy) and let's chat.
|
||||||
|
|
||||||
|
You can also check out [this project](https://git.edgy.social/rizaldy/rpaas) on this repository to learn more, especially there's something I haven't attached here like the Dockerfile and fly.toml files.
|
||||||
|
|
||||||
|
## Demo (dead)
|
||||||
|
|
||||||
|
- [Demo 1](https://nginx.init8.lol) (machine with a static public IP address, my devbox)
|
||||||
|
- [Demo 2](https://app.init8.lol) (machine with a dynamic public IP address, my laptop)
|
||||||
|
- [Demo 3](https://ts-proxy.fly.dev) (fly.io instance for this project)
|
8
src/blog/index.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: Blog
|
||||||
|
layout: layouts/archives
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blog
|
||||||
|
|
||||||
|
Technically a blog.
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
Before Width: | Height: | Size: 532 KiB After Width: | Height: | Size: 532 KiB |
188
src/blog/pac-is-actually-useful/index.md
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
---
|
||||||
|
title: Proxy Auto-Configuration (PAC) is actually useful
|
||||||
|
layout: layouts/blog
|
||||||
|
date: "2023-07-05"
|
||||||
|
tags:
|
||||||
|
- posts
|
||||||
|
- tailscale
|
||||||
|
- proxy
|
||||||
|
---
|
||||||
|
|
||||||
|
So I just got my new Mac machine last week. It's just a small machine with 8 CPUs and 8 GB of memory. I've been thinking about buying one for a long time and now the time has come!
|
||||||
|
|
||||||
|
I'll be using this tiny PC as my main workstation, but unlike my previous approach of setting up development environments, I now have plans to isolate each existing environment into VMs,
|
||||||
|
like, one for private; one for side projects, one for community stuff, and one for work.
|
||||||
|
|
||||||
|
I want to keep this tiny PC as clean as possible.
|
||||||
|
|
||||||
|
## The dumb way to use Tailscale
|
||||||
|
|
||||||
|
This is the main topic of this post, but I'm doing my best to make this post not feel like a shot tweet. On my laptop I use 4 different identities (accounts) in Tailscale,
|
||||||
|
and I used to use Tailscale's [Fast user switching](https://tailscale.com/blog/fast-user-switching/) feature which was quite useful. Often I forget to switch back
|
||||||
|
to my personal account after I realize I can't SSH into my server at home via its hostname.
|
||||||
|
|
||||||
|
So here's the story: I don't want to "switch" anymore so I don't forget one more time.
|
||||||
|
|
||||||
|
I create every VM I need and use different identities for Tailscale there. The OS is NixOS and I'm using [OrbStack](https://orbstack.dev) to provision VMs on my tiny PC.
|
||||||
|
When I'm not working from home, I can SSH into the VM with this tiny PC as a jumphost while hoping my internet at home is working fine.
|
||||||
|
|
||||||
|
### First, it was DNS
|
||||||
|
|
||||||
|
Every [Tailnet](https://tailscale.com/kb/1136/tailnet/) has its own [unique name](https://tailscale.com/kb/1217/tailnet-name/) with `ts.net` as root domains. My tiny PC's hostname
|
||||||
|
is `mac-mini` (sounds boring) so I can access it via `mac-mini.duck-map.ts.net`, and yes, duck-map.ts.net is my *real* Tailnet name.
|
||||||
|
|
||||||
|
The first problem is that `ts.net` can only be resolved by the "MagicDNS server" which resides on your own device accessed via `100.100.100.100`. This means that
|
||||||
|
when you try to query names for a machine that is outside your tailnet, you will get a bogus NXDOMAIN — which is good.
|
||||||
|
|
||||||
|
The second problem is that you can't route packets to machines outside your tailnet, of course.
|
||||||
|
|
||||||
|
Maybe I could use [Subnet Routers](https://tailscale.com/kb/1019/subnets/) to advertise subnets of the bridge interface used by my VM but that only solves half of the problem (excluding DNS queries).
|
||||||
|
|
||||||
|
And what about the [Exit Node](https://tailscale.com/kb/1103/exit-nodes/) option? Of course not the answer.
|
||||||
|
|
||||||
|
### Then, it was routing tables
|
||||||
|
|
||||||
|
In certain cases I had to access an internal application that was only accessible through Tailscale to troubleshoot (#sysadminlife). I don't use proxies much but when I do my favorite
|
||||||
|
is to `ssh -D 6669 somewhere` and use 127.0.0.1:6669 as SOCKS5 proxy servers.
|
||||||
|
|
||||||
|
![236D9096-4D0A-43A5-9827-6227230FB8FE.jpg](./236D9096-4D0A-43A5-9827-6227230FB8FE.jpg)
|
||||||
|
|
||||||
|
From the screenshot above, `kudxxx.tailnet-xxxxx12.ts.net` is the machine that resides on the tailnet where I work. I can't resolve the name, because, well, I'm using my own personal tailnet.
|
||||||
|
|
||||||
|
Then I can use `ssh -D 6669 delman@orb` where `delman` is the name of the VM to the tailnet at work. `socks5h` indicates that the DNS query is made on SOCKS5 proxy servers.
|
||||||
|
|
||||||
|
If referring to the screenshot above, I think it works.
|
||||||
|
|
||||||
|
### Proxy Auto-Configuration
|
||||||
|
|
||||||
|
What if I need to access different machines on different tailnets like `heavy-rotation.duck-map.ts.net` and `kudxxx.tailnet-xxxxx12.ts.net` at the same time?
|
||||||
|
|
||||||
|
On MacOS (or maybe other OS too) you can only use 1 proxy server on your machine at a time. So `ssh -D 6669 delman@orb` and `ssh -D 4848 heavy-rotation@orb` require extra work when
|
||||||
|
you need to use either one.
|
||||||
|
|
||||||
|
![./AF9155DC-0B3D-4A4C-9F64-7435D6F77738.jpg](./AF9155DC-0B3D-4A4C-9F64-7435D6F77738.jpg)
|
||||||
|
|
||||||
|
And no, using transparent proxies doesn't help.
|
||||||
|
|
||||||
|
And then I just came across an old technology called [Proxy Auto-Configuration](https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file)
|
||||||
|
which is the title of this post. The concept is actually simple: a PAC is just a JavaScript file that calls a `FindProxyForURL` function that returns a single string. The minimal
|
||||||
|
script is something like this:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function FindProxyForURL (url, host) {
|
||||||
|
alert('url ' + url)
|
||||||
|
alert('host ' + host)
|
||||||
|
|
||||||
|
return 'DIRECT'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You don't need to save it using `.js` extensions by the way.
|
||||||
|
|
||||||
|
As far as I know that `alert` function doesn't work in Safari but it works fine in Firefox and Chrome. This is how it looks when "debugging the PAC" using Firefox:
|
||||||
|
|
||||||
|
![./1A5B132F-D2F9-4B1F-9923-434B045CFF60.jpg](./1A5B132F-D2F9-4B1F-9923-434B045CFF60.jpg)
|
||||||
|
|
||||||
|
In many cases, you don't need to do that just to verify if your PAC is working — checking the access.log where you hosted the pac file must be enough.
|
||||||
|
|
||||||
|
### Making it official
|
||||||
|
|
||||||
|
Now, here is the strategy:
|
||||||
|
|
||||||
|
- If I access `.duck-map.ts.net`, proxy the requests to `127.0.0.1:4848`
|
||||||
|
- If I access `.tailnet-xxxxx12.ts.net`, proxy the requests to `127.0.0.1:6669`
|
||||||
|
- Other than that don't proxy the requests to anywhere
|
||||||
|
|
||||||
|
Actually I can use `mac-mini` instead of `127.0.0.1` as the hostname so I can use the PAC file everywhere using the same URL.
|
||||||
|
|
||||||
|
In every VM I use [`gost`](https://github.com/ginuerzh/gost) as SOCKS5 server. I can create a simple systemd service for `gost` like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
systemd.services.gost = {
|
||||||
|
description = "gost";
|
||||||
|
|
||||||
|
after = ["network.target"];
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${pkgs.gost}/bin/gost -L=:6669";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
And then `nixos-rebuild switch` as usual, then check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ systemctl status gost
|
||||||
|
|
||||||
|
● gost.service - gost
|
||||||
|
Loaded: loaded (/etc/systemd/system/gost.service; enabled; preset: enabled)
|
||||||
|
Drop-In: /nix/store/rmhm2f4izkfxkpaix0ca2pxnvyswkxfi-system-units/service.d
|
||||||
|
└─zzz-lxc-service.conf
|
||||||
|
Active: active (running) since Wed 2023-07-05 15:57:51 WIB; 8h ago
|
||||||
|
Main PID: 43857 (gost)
|
||||||
|
IP: 16.1M in, 15.8M out
|
||||||
|
IO: 0B read, 0B written
|
||||||
|
Tasks: 11 (limit: 7106)
|
||||||
|
Memory: 10.5M
|
||||||
|
CPU: 4.504s
|
||||||
|
CGroup: /system.slice/gost.service
|
||||||
|
└─43857 /nix/store/q34c64p4cnxh67yxsqxjpjsgdmg8ilpq-gost-2.11.5/bin/gost -L=:6669
|
||||||
|
```
|
||||||
|
|
||||||
|
For the contents of the PAC file, it could be like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function FindProxyForURL (url, host) {
|
||||||
|
work = "SOCKS5 mac-mini:6669"
|
||||||
|
community = "SOCKS5 mac-mini:4848"
|
||||||
|
|
||||||
|
if (shExpMatch(host, "*.tailnet-xxxxx12.ts.net")) {
|
||||||
|
return work
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shExpMatch(host, "*.duck-map.ts.net")) {
|
||||||
|
return community
|
||||||
|
}
|
||||||
|
|
||||||
|
return "DIRECT"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[`shExpMatch`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file#shexpmatch) is a function to check if the string
|
||||||
|
matches a specified shell glob expression. When one of the conditions is met, it tells the client [how to connect](https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file#return_value_format) to the target.
|
||||||
|
|
||||||
|
[![./F89B89ED-50B6-4A0C-B6C2-078BFE35CCE6.jpg](./F89B89ED-50B6-4A0C-B6C2-078BFE35CCE6.jpg)](./F89B89ED-50B6-4A0C-B6C2-078BFE35CCE6.jpg)
|
||||||
|
|
||||||
|
<small><center><em>\*click image above to enlarge\*</em></center></small>
|
||||||
|
|
||||||
|
As you may see I can access `*.duck-map.ts.net` and `*.tailnet-xxxxx12.ts.net` at the same time without switching accounts. If you check `(1)` and `(2)` in the screenshot above, the value in "remote address" is where the proxy server is running.
|
||||||
|
|
||||||
|
### But why not share nodes?
|
||||||
|
|
||||||
|
I can [share](https://tailscale.com/kb/1084/sharing/) my `mac-mini` devices to every tailnet I want, but why not? I don't know, maybe the answer is the same as why I installed and used a different account on Tailscale, in different VMs.
|
||||||
|
|
||||||
|
Also, "shared devices" are *quarantined* by default. Which means my `mac-mini` can't initiate connections to devices on the "shared network" until they talk to it first — although it's not a big deal.
|
||||||
|
|
||||||
|
### Why not Tailscale Funnel?
|
||||||
|
|
||||||
|
It's a different story. Tailscale Funnel is all about exposing devices to the *wider* internet. This means that even anyone with no Tailscale installed can access
|
||||||
|
(usually a web service) via the boring HTTPS protocol. No MagicDNS. No CGNAT IPs. Just the internet we are used to.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
There is no way to verify the integrity of the PAC file especially if you load it via a remote address using an insecure procotol. Which means MiTM attacks are by no means impossible. Maybe someone
|
||||||
|
is snooping on your network especially if you are on a public network that uses a captive portal. There's nothing to stop anyone from modifying a PAC file if _they_ want and can.
|
||||||
|
|
||||||
|
But it's worth noting that almost all traffic in 2023 uses end-to-end encryption via HTTPS protocols. If you're installing a "CA certificate" because someone out of nowhere
|
||||||
|
asked you to do so, don't do it. If it's too late, maybe consider stopping receiving candy from random people at the bar as well, if that happens to you.
|
||||||
|
|
||||||
|
A simple `python -m http.server` or `caddy:alpine` web server can help serve your PAC files on the machine you control. And since you're in control, you probably already have Tailscale installed on the device and can
|
||||||
|
use a secure transport (such as Wireguard protocols) to load the PAC file.
|
||||||
|
|
||||||
|
The Proxy Auto-Configuration was [introduced](https://developer.mozilla.org/en-US/docs/web/http/proxy_servers_and_tunneling/proxy_auto-configuration_pac_file#history_and_implementation) into Netscape Navigator 2.0 in the late 1990s
|
||||||
|
at the same time when JavaScript was introduced. For years I have wondered why my machine has "Automatic proxy configuration" options and why I would ever need it.
|
||||||
|
|
||||||
|
And now I know.
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
76
src/blog/syncthing-anywhere-with-tailscale/index.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
title: Syncthing Anywhere With Tailscale
|
||||||
|
layout: layouts/blog
|
||||||
|
date: "2022-01-04T13:37:13+07:00"
|
||||||
|
tags:
|
||||||
|
- posts
|
||||||
|
- tailscale
|
||||||
|
- syncthing
|
||||||
|
---
|
||||||
|
|
||||||
|
I don't archive data very often but when I do it must be for a very important one. On the other hand, I somewhat don't trust "cloud" providers and would avoid them as much as I can since my paranoid level is kinda high.
|
||||||
|
|
||||||
|
And just like everyone else, I run servers at home. The servers are not that powerful but sufficient for my needs. I have two servers run but the most important (and data-heavy) is the box with TrueNAS OS.
|
||||||
|
Previously I use NextCloud to store my data plus I could access it anywhere. NextCloud is a powerful platform with rich features, I even can run an ActivityPub-based social network there. But the client app is somewhat heavy and the server consumes more resources than I thought, plus the caching system on the client app is somewhat buggy. I can't depend my life on NextCloud, or maybe someday will.
|
||||||
|
|
||||||
|
I know I can use `rsync(1)` or even `rclone(1)` on my computer but they both do different jobs. I need a system to pull-and-push data not just push.
|
||||||
|
|
||||||
|
And don't ask me why I'm not using BTSync.
|
||||||
|
|
||||||
|
Then I found [Syncthing](https://syncthing.net) somewhere. It only does one thing and probably does it well: **sync data**. Its tagline is _"Syncthing is a **continuous file synchronization program**. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet."_ and it really took my heart.
|
||||||
|
|
||||||
|
And then I installed Syncthing, used it for some time (until now), and that's why we are here.
|
||||||
|
|
||||||
|
## Syncing concepts
|
||||||
|
|
||||||
|
Syncthing will establish direct connections between clients (peer-to-peer) as much as possible, and as we know, p2p connection is never easy. And if that's not possible, traffic is bounced through the "relay" until both computers figured out how to establish a direct connection — once again, if possible.
|
||||||
|
|
||||||
|
Syncthing uses ["Device ID"](https://docs.syncthing.net/dev/device-ids.html) as an identifier so that neither party needs to know each other's IP address thanks to [Global Discovery](https://docs.syncthing.net/users/stdiscosrv.html). I just used the Local Discovery feature and turned the 'Enable Relaying' option off so I know the only connection made to .syncthing.net should be just sending an (anonymous) usage report, and I'm fine with that.
|
||||||
|
|
||||||
|
While this approach only makes my life harder, at least I know what I know even though all packets are encrypted in transit (via TLS) and are every device is authenticated by a cryptographic thing.
|
||||||
|
|
||||||
|
## LAN anywhere
|
||||||
|
|
||||||
|
All my servers at home are almost locked-down. Only traffic to port 80 is allowed in on my WAN. Not even port forwarding because I don't know much about computer and networking security.
|
||||||
|
So I designed my home network to be limited to working only on LAN. I don't know exactly how but I just made TCP listen only to the private network. Like, If I can only visit Syncthing GUI via 192.168.1.56 then I can only visit Syncthing GUI over 192.168.1.0/24 network, on my network.
|
||||||
|
|
||||||
|
Speaking of my Syncthing, it's running on FreeBSD jail with a dedicated IP address from my DHCP server.
|
||||||
|
|
||||||
|
And as always, the only way to connect a private network over public networks is by establishing a VPN, assuming I'm not interested yet in a commercial "Zero Trust Network" solution that maybe tunnels any TCP/UDP packets through a commercial reverse proxy because the packets are not end-to-end encrypted.
|
||||||
|
|
||||||
|
Anyways, I use [Tailscale](https://tailscale.com) and am a huge fan of both the team and the product. Tailscale is built on top Wireguard, and I used to use direct Wireguard until I was overwhelmed with mesh networking.
|
||||||
|
|
||||||
|
Using Tailscale is pretty straight forward, I don't have to manage keys; know each peer's IP address and public key, define DNS and even perform key rotation. It just works.
|
||||||
|
|
||||||
|
The problem is that my Syncthing is running on the FreeBSD jail and can't dial `devd(8)` in the jail whereas Tailscale needs to monitor network state changes (they have a [workaround](https://github.com/tailscale/tailscale/pull/3508) for this! but I haven't tried it yet). Since I'm also running Tailscale on my TrueNAS, I can use ["subnet router"](https://tailscale.com/kb/1019/subnets) which in short acts as a gateway of my physical subnet.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tailscale up --advertise-routes=192.168.1.0/24
|
||||||
|
```
|
||||||
|
|
||||||
|
With the above command, I can access my Syncthing (and other jails too) wherever I am!
|
||||||
|
|
||||||
|
## Throughput
|
||||||
|
|
||||||
|
Although the average latency in transmitting ICMP packets is unnoticeable, in the real world we mostly deal with TCP packets.
|
||||||
|
|
||||||
|
Syncthing transmits packets over TLS on top of TCP (which is great!) but in my needs it adds overhead as packets are already transmitted over secure protocol (Wireguard).
|
||||||
|
|
||||||
|
When I'm on my home network, the throughput is about 44 Mbits/s to my Syncthing jail since my laptop is connected over a WiFi network and on a different subnet. Ideally if on the same subnet (and over wired connection) my router and server can reach up to 900MiB/s.
|
||||||
|
|
||||||
|
What if I'm out of the house?
|
||||||
|
|
||||||
|
On a 50 Mbps network, using iperf3, I get around 6.49 MiB/s to my Syncthing jail and 8.56 MiB/s to direct host (depending on your network) which is… acceptable, kinda.
|
||||||
|
How about transferring 1.2GB files to my Syncthing jail over Tailscale?
|
||||||
|
|
||||||
|
![](./CleanShot-2022-01-04-at-9.18.04@2x.png)
|
||||||
|
|
||||||
|
2.02 MiB/s is not bad enough, I guess?
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Tailscale here is optional as Syncthing does the NAT traversal for you and also uses a secure protocol. Syncthing will do its best to establish a peer to peer connection and that's great!
|
||||||
|
|
||||||
|
However, with Tailscale I can access my "shared directory" via SAMBA on my other devices, anywhere. And also don't need any "Relay Server" only if my device can't talk peer-to-peer as Tailscale will do it for me :))
|
||||||
|
|
||||||
|
As closing, using both Tailscale and Syncthing is the best combination if you don't want to depend on a (cloud) storage providers.
|
10
src/blog/unboxing-the-cloud-intro/index.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: "Unboxing The Cloud (UTC): Intro"
|
||||||
|
layout: layouts/blog
|
||||||
|
eleventyExcludeFromCollections: true # TODO :))
|
||||||
|
tags:
|
||||||
|
- posts
|
||||||
|
- utc
|
||||||
|
---
|
||||||
|
|
||||||
|
WIP
|
10
src/blog/unboxing-the-cloud-utc-vms/index.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: "Unboxing The Cloud (UTC): VMs"
|
||||||
|
layout: layouts/blog
|
||||||
|
eleventyExcludeFromCollections: true # TODO :))
|
||||||
|
tags:
|
||||||
|
- posts
|
||||||
|
- utc
|
||||||
|
---
|
||||||
|
|
||||||
|
WIP
|
24
src/colophon.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: Colophon
|
||||||
|
layout: layouts/base
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="l-fragment">
|
||||||
|
|
||||||
|
## Website colophon
|
||||||
|
|
||||||
|
This page outlines technical details about this website. You can learn more about the purpose and content of the website, as well as the author on the [About](/) page.
|
||||||
|
|
||||||
|
This site was built using a boring [static site generator](https://www.11ty.dev), written in a boring [template](https://daringfireball.net/projects/markdown/) [language](https://shopify.github.io/liquid/) and served by boring [S3 compatible storage](https://min.io) behind a boring [reverse proxy](https://nginx.org/en/).
|
||||||
|
|
||||||
|
The three primary fonts I use are:
|
||||||
|
|
||||||
|
- [Sentient](https://www.fontshare.com/fonts/sentient) (serif)
|
||||||
|
- [Clash Display](https://www.fontshare.com/fonts/clash-display) (sans-serif)
|
||||||
|
- [JetBrains Mono](https://www.jetbrains.com/lp/mono/) (monospace)
|
||||||
|
|
||||||
|
Syntax highlighting is done by the great [Prism.js](https://prismjs.com/), statically generated of course.
|
||||||
|
|
||||||
|
If you enjoy the site, consider to [giving me a word]({{ site.repo }}/discussions/new?category=show-and-tell) or [some love](https://github.com/sponsors/faultables) (my love language is words of affirmation and github sponsors btw).
|
||||||
|
|
||||||
|
</div>
|
14
src/index.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: About
|
||||||
|
layout: layouts/base
|
||||||
|
---
|
||||||
|
|
||||||
|
{% render "fragments/about.md", site: site, contacts: contacts, employer: employer %}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{% render "fragments/writings.md", posts: collections.posts %}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{% render "fragments/services.md", services: collections.services, consulting: consulting %}
|
8
src/services/devops.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: DevOps Engineering
|
||||||
|
layout: layouts/service
|
||||||
|
tags:
|
||||||
|
- services
|
||||||
|
---
|
||||||
|
|
||||||
|
I can help organizations move fast and break less thing by adopting best practices from system administration; release engineering, infrastructure provisioning and management, security, to DevOps advocacy.
|
9
src/services/devrel.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: DevRel
|
||||||
|
layout: layouts/service
|
||||||
|
eleventyExcludeFromCollections: true # TODO :))
|
||||||
|
tags:
|
||||||
|
- services
|
||||||
|
---
|
||||||
|
|
||||||
|
I have no experience in "devrel-ing" profesionally. But I write a lot, and I love writing. Sometimes I give talks too. Some people tend to like my writing by tipping and/or signing up for a service I use with my referral code—maybe that helps.
|
8
src/services/rnd.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: R&D Engineer
|
||||||
|
layout: layouts/service
|
||||||
|
tags:
|
||||||
|
- services
|
||||||
|
---
|
||||||
|
|
||||||
|
Reduce time to market without compromising quality and stability by choosing the right tools for the job. I can help you to choose the right one.
|
8
src/services/sre.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: Site Reliability Engineering
|
||||||
|
layout: layouts/service
|
||||||
|
tags:
|
||||||
|
- services
|
||||||
|
---
|
||||||
|
|
||||||
|
"Hope is not a strategy" as traditional SRE would say. I can help scale from availability; latency, performance, efficiency, change management, monitoring, emergency response, to capacity planning.
|
9
src/services/webapp.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: Web App Development
|
||||||
|
layout: layouts/service
|
||||||
|
eleventyExcludeFromCollections: true # TODO :))
|
||||||
|
tags:
|
||||||
|
- services
|
||||||
|
---
|
||||||
|
|
||||||
|
I don't write web apps professionally anymore but I have a great team to do so.
|